Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
Andrew Birkholz
d42aa4e455 README: proposed Verae chain API changes for hash lookup, receipts, metadata
Answers the developer question "how do I query my original hash, get a
receipt on submission and on lookup, see all timestamp metadata, and store
metadata or links in a timestamp". Lists the required additions to
api.veraetime.net (hash lookup, idempotent create with digest input, full
status on lookup, metadata field), recommended extras (certified retrieval
receipt, JSON errors, completion callback), spec rules for public vs
private metadata, and what the middleware can cover without API changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 12:34:00 -06:00
Andrew Birkholz
220559d257 Bring the Zapier app to parity with the middleware /zapier/v1 contract
Zapier cloud runs this app against verae-middleware over HTTPS with a
customer zmw_ key; the middleware owns keys, plans, job watching, webhooks,
and the api.veraetime.net client. This commit makes the app cover the whole
middleware surface and match its documented behaviour.

New operations
- creates: batch_verify (POST /verify/batch), batch_status
  (POST /status/batch), get_receipt (GET /receipts/{jobId}, optional PDF via
  z.stashFile)
- searches: job_verification (GET /status/{jobId}/verification), usage
  (GET /auth/me -> plan and month-to-date usage vs limits)
- triggers: timestamp_failed REST Hook alongside timestamp_completed

Behaviour changes
- Create Timestamp / Create Timestamp and Wait accept Data or a 64-hex
  SHA256, plus optional public/private metadata dicts, per the user guide.
- Searches return [] on 404 instead of failing the Zap; other errors carry
  the middleware message.
- Hook items get id = jobId so Zapier can dedupe; subscribeData is
  { id, event }; performUnsubscribe accepts the legacy bare id.
- The API key is sent as Authorization: Bearer and as x-api-key so the app
  works through the middleware today and the access planes / billing edge
  that forward only x-api-key.
- afterResponse maps every middleware error body; QUOTA_EXCEEDED,
  PLAN_UPGRADE_REQUIRED and BATCH_LIMIT_EXCEEDED include the upgrade URL;
  429 becomes a ThrottledError.
- MIDDLEWARE_PATH_PREFIX (default /zapier/v1) for a fronting edge.

Shared code
- lib/middleware.js: base URL, error parsing, 404-as-no-match search
  helper, timestamp input/body builder, shared samples and output fields.
- lib/hookTrigger.js: REST Hook factory.

Repo health
- Remove creates/add_numbers.js, which required a file from the sibling
  verae-activate package and prevented the app from loading.
- Fix the npm test glob for Node 22; add .gitignore and package-lock.json.
- Version 1.1.0.

Tests and docs
- test/app.test.js rewritten: 25 unit tests with a fake z, no network.
- test/e2e.test.js: opt-in run through zapier-platform-core against a live
  middleware (MIDDLEWARE_BASE_URL + MIDDLEWARE_API_KEY); plan-aware and
  E2E_SKIP_HOOK_DELIVERY for the lab. Passes on pro and free tenants.
- README: module table, live-lab origins and which are usable, env, and an
  "open items" status section; SUMMARY and NATS updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 12:16:47 -06:00
27 changed files with 2458 additions and 298 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
node_modules/
build/
.env
.env.*
.zapierapprc
*.log
.DS_Store

11
NATS.md
View file

@ -5,6 +5,13 @@ This Zapier Platform app **does not use NATS**.
| Direction | Address | Peer | Body |
|-----------|---------|------|------|
| IN | Zapier runtime | user Zap | create/search/trigger fields |
| OUT HTTPS | middleware `/zapier/v1/timestamp`, `/wait`, `/verify`, `/status/{id}`, `/hashes/{sha256}`, `/hashes/{sha256}?includeTree=true`, REST Hook subscribe | verae-middleware | Bearer `zmw_` |
| OUT HTTPS | middleware `/zapier/v1/auth/me` | verae-middleware | Bearer `zmw_` |
| OUT HTTPS | `/zapier/v1/timestamp`, `/timestamp/wait`, `/timestamp/batch` | verae-middleware | Bearer `zmw_` |
| OUT HTTPS | `/zapier/v1/verify`, `/verify/batch` | verae-middleware | Bearer `zmw_` |
| OUT HTTPS | `/zapier/v1/status/{id}`, `/status/{id}/verification`, `/status/batch` | verae-middleware | Bearer `zmw_` |
| OUT HTTPS | `/zapier/v1/hashes/{sha256}`, `/hashes/{sha256}?includeAttached=true&includeTree=true` | verae-middleware | Bearer `zmw_` |
| OUT HTTPS | `/zapier/v1/receipts/{id}` | verae-middleware | Bearer `zmw_` |
| OUT HTTPS | `/zapier/v1/webhooks/subscribe`, `/webhooks/unsubscribe` | verae-middleware | Bearer `zmw_` |
| IN HTTPS | Zapier hook URL | webhook-deliver / in-process poller | `{ event, jobId, tenantId, status }` |
Job completion is a REST Hook HTTP POST from webhook-deliver, not a NATS client in Zapier.
Job completion is a REST Hook HTTP POST from the middleware's webhook path, not a NATS client in Zapier.

197
README.md
View file

@ -1,34 +1,109 @@
# verae-zapier
Zapier Platform CLI app for Verae. Runs on **Zapier’s servers**. Calls only middleware HTTPS (`MIDDLEWARE_BASE_URL`), never NATS and never `api.veraetime.net`.
Zapier Platform CLI app for Verae. Runs on **Zapier's servers**. Calls only middleware HTTPS (`MIDDLEWARE_BASE_URL`, `/zapier/v1/*`), never NATS and never `api.veraetime.net`.
**Forgejo:** https://git.georgelambert.org/marchon/verae-zapier-app
**Middleware:** https://git.georgelambert.org/marchon/verae-middleware
**Catalog:** https://zapier.georgelambert.org/packages/verae-zapier/README.pdf
**Sphinx API sheets:** https://zapier.georgelambert.org/sphinx/index.html
## Role
Runs on **Zapier’s servers**. Calls only the middleware HTTPS API (`MIDDLEWARE_BASE_URL`), never NATS and never `api.veraetime.net` directly.
Runs on **Zapier's servers**. Calls only the middleware HTTPS API (`MIDDLEWARE_BASE_URL`), never
NATS and never `api.veraetime.net` directly. The middleware owns customer API keys (`zmw_…`),
plans and quotas, job watching, REST Hook delivery, and the upstream Verae chain client
(`POST /auth/login`, `/api/timestamp`, `/api/status`, `/api/verify`, … on api.veraetime.net).
## Planned modules
## Modules
| File | Purpose |
|------|---------|
| `authentication.js` | Custom API key auth → `GET /zapier/v1/auth/me` |
| `index.js` | App definition, beforeRequest, afterResponse error mapping |
| `creates/timestamp_and_wait.js` | Primary action |
| `creates/create_timestamp.js` | Async jobId action |
| `creates/verify_timestamp.js` | Verify certificate |
| `creates/batch_timestamp.js` | Batch create |
| `searches/job_status.js` | Lookup by jobId |
| `triggers/timestamp_completed.js` | REST Hook |
| File | Zapier type | Middleware call |
|------|-------------|-----------------|
| `authentication.js` | custom API key | `GET /zapier/v1/auth/me` → connection label `tenantId (plan)` |
| `index.js` | app definition | `beforeRequest` sends the key as `Authorization: Bearer` **and** `x-api-key`; `afterResponse` maps `{ error, code, details }` (402/403 billing → upgrade URL, 429 → ThrottledError) |
| `lib/middleware.js` | helpers | base URL, error parsing, 404-as-no-match search helper, shared samples |
| `lib/hookTrigger.js` | helpers | REST Hook factory (`/webhooks/subscribe`, `/webhooks/unsubscribe`) |
| `creates/timestamp_and_wait.js` | action | `POST /timestamp/wait` (primary) |
| `creates/create_timestamp.js` | action | `POST /timestamp` → 202 `{ jobId }` |
| `creates/batch_timestamp.js` | action | `POST /timestamp/batch` (paid plans) |
| `creates/verify_timestamp.js` | action | `POST /verify` |
| `creates/batch_verify.js` | action | `POST /verify/batch` |
| `creates/batch_status.js` | action | `POST /status/batch` |
| `creates/get_receipt.js` | action | `GET /receipts/{jobId}` (JSON; optional PDF via `z.stashFile`) |
| `searches/job_status.js` | search | `GET /status/{jobId}` (404 → no match) |
| `searches/job_verification.js` | search | `GET /status/{jobId}/verification` (404 → no match) |
| `searches/hash_lookup.js` | search | `GET /hashes/{sha256}` (404 → no match) |
| `searches/tree_lookup.js` | search | `GET /hashes/{sha256}?includeAttached=true&includeTree=true` |
| `searches/usage.js` | search | `GET /auth/me` → plan + month-to-date usage vs limits (Find Usage) |
| `triggers/timestamp_completed.js` | REST Hook | event `timestamp.completed` |
| `triggers/timestamp_failed.js` | REST Hook | event `timestamp.failed` |
Hook payload delivered by the middleware: `{ event, jobId, tenantId, status }`; the trigger
adds `id = jobId` so Zapier can dedupe.
Create Timestamp / Create Timestamp and Wait take **Data or SHA256** (64 hex; hash large files
outside Zapier and send only the digest, per the user guide), plus optional public / private
metadata dictionaries. A digest that was sealed before returns the original job with
`existing: true` and no second chain write.
I/O contracts: https://zapier.georgelambert.org/docs/developer/modules/function-reference.pdf
### Known gaps upstream (middleware / chain), not in this app
- `GET /hashes/{sha256}` returns **501 NOT_IMPLEMENTED** when the middleware runs against the
live chain (`MOCK_VERAE=false`); the Verae OpenAPI has no hash lookup. The two hash searches
surface that message verbatim.
- `publicMetadata` / `privateMetadata` and `sha256`-only creates are accepted by the middleware
but only honoured by its mock chain, so this app does not expose them.
- The middleware has no "list recent events" endpoint, so `performList` on the hook triggers
returns nothing and Zapier uses the static `sample`.
## Env
| Variable | Default | Purpose |
|----------|---------|---------|
| `MIDDLEWARE_BASE_URL` | `http://127.0.0.1:3100` | Origin the app calls. Set per Zapier version: `zapier env:set 1.1.0 MIDDLEWARE_BASE_URL=https://mw.zapier.georgelambert.org` |
| `MIDDLEWARE_PATH_PREFIX` | `/zapier/v1` | Route prefix at that origin. Leave the default for the middleware or the access planes; change it only if a fronting edge mounts the same routes elsewhere. |
## Where to point it (lab, 2026-09-14)
The catalog at https://zapier.georgelambert.org/ documents the live lab on NS1:
| Origin | What it is | Usable by this app today? |
|--------|------------|---------------------------|
| `https://mw.zapier.georgelambert.org` | verae-middleware (`MOCK_VERAE=true`, NATS on) | **Yes** — full `/zapier/v1/*`; accepts `Bearer` or `x-api-key`. Use this until the Zap plane proxies every route. |
| `https://zap.zapier.georgelambert.org` | verae-access-zapier, the intended Zapier door (STATUS.md / dev-overview) | **Not yet** — it only proxies `POST /v1/timestamp` and does not forward the API key, so `auth/me`, wait, verify, status, hooks all 404/401. |
| `https://api.zapier.georgelambert.org` | verae-access-api (direct customer API plane) | Forwards `x-api-key` for `POST /v1/timestamp` only; not for Zapier. |
| zappier-edge `/v1/*` | billing edge (composition target; loopback :13000, tunnel only) | Not yet — covers timestamp/hashes/receipts/usage only, no `/zapier/v1` prefix. |
Keys: connect with a middleware **`zmw_…`** key (portal / `POST /zapier/v1/signup` /
admin provisioning). Never a Verae username, password, or JWT. After the zappier
composition lands, the same field takes the portal `x-api-key`; the app already sends that
header.
## Develop
```bash
export MIDDLEWARE_BASE_URL=https://your-middleware.example.com
npm install
npm test # unit tests, no network
npx zapier-platform-cli validate --without-style
```
End-to-end against a running middleware (mock chain shown; a live-chain middleware works the same).
Against the lab middleware add `E2E_SKIP_HOOK_DELIVERY=1` (it cannot POST back to your laptop):
```bash
# in verae-middleware
PORT=3100 MOCK_VERAE=true NATS_ENABLED=false ADMIN_SECRET=dev-admin npm start
curl -s -X POST http://127.0.0.1:3100/zapier/v1/admin/tenants \
-H 'x-admin-secret: dev-admin' -H 'content-type: application/json' \
-d '{"name":"E2E","plan":"pro","veraeUsername":"e2e","veraePassword":"x"}' # → apiKey
# in verae-zapier-app
MIDDLEWARE_BASE_URL=http://127.0.0.1:3100 MIDDLEWARE_API_KEY=zmw_... npm run test:e2e
# against the live lab (key from dev-overview/05-developer-credentials or /zapier/v1/signup)
MIDDLEWARE_BASE_URL=https://mw.zapier.georgelambert.org MIDDLEWARE_API_KEY=zmw_... \
E2E_SKIP_HOOK_DELIVERY=1 npm run test:e2e
```
## Gate
@ -36,3 +111,97 @@ export MIDDLEWARE_BASE_URL=https://your-middleware.example.com
```bash
npm run gate:11 # from monorepo root, after Phase 11 implementation
```
## Status of the API integration (2026-09-15)
Where the Zapier ↔ Verae path stands, and what is still open. Sources: this repo,
`verae-middleware`, and the catalog (`zapier-decisions/STATUS`, `dev-overview/03…05`).
### Done
- This app covers the whole middleware `/zapier/v1` surface that Zapier needs: async / wait /
batch create, single and batch verify, job status / verification / batch status, hash and
tree-node lookup, usage, retrieval receipt, and REST Hooks for `timestamp.completed` and
`timestamp.failed`.
- The middleware already has the live chain client (`src/clients/veraeClient.js`) for
`POST /api/timestamp`, `/api/batch/timestamp`, `/api/verify`, `/api/batch/verify`,
`GET /api/status/{jobId}`, `GET /api/verify/{jobId}`, `POST /api/batch/status`, and
`POST /auth/login` on https://api.veraetime.net. No Zapier-app change is needed to go live.
- Verified end to end through `zapier-platform-core` against a mock-chain middleware
(`npm run test:e2e`), on both a pro tenant (batch, hook delivery) and a free tenant
(402/403 upgrade-URL mapping).
### Open — blocked on people, not code
| Item | Owner | What is needed |
|------|-------|----------------|
| Zapier Platform push | operator | `zapier login`, `zapier register` (or link), `zapier env:set <version> MIDDLEWARE_BASE_URL=…`, `zapier push` as a private app, then test Create Timestamp and Wait, Create Timestamp, Find Job Status, Timestamp Completed. |
| Live chain (`MOCK_VERAE=false`) | Verae / operator | A Verae username + password for the middleware (`VERAE_API_BASE_URL=https://api.veraetime.net`). Then prove one real seal, confirm `202 { jobId }` → status → verify, and map the live status JSON into the receipt shape this app samples (`certificate`, `timestamp`, `blockIndex`, `sha256`). |
| Live-lab e2e run | anyone with the lab key | `MIDDLEWARE_BASE_URL=https://mw.zapier.georgelambert.org MIDDLEWARE_API_KEY=zmw_… E2E_SKIP_HOOK_DELIVERY=1 npm run test:e2e`. Exercises the NATS-enabled wait / poller path that the local run does not. |
### Open — code in other repos
| Item | Repo | Notes |
|------|------|-------|
| Zapier plane does not proxy the app | `verae-access-zapier` | `zap.zapier.georgelambert.org` handles only `POST /v1/timestamp` and drops the API key. STATUS says to point the app there; until it proxies all `/zapier/v1/*` with the key intact, use `mw.zapier.georgelambert.org`. |
| Hash lookup on live chain | Verae API or middleware store | `GET /hashes/{sha256}` is mock-only; live returns `501 NOT_IMPLEMENTED`. Needs a Verae API gap fill or a middleware-side index. |
| `sha256`-only create and metadata on live chain | `verae-middleware` | Accepted by the middleware and honoured by the mock; the live client sends only `data` + `hashAlg` (adapter rule). Decide how the live path handles digest-only registration and public/private metadata. |
| Tree-leaf receipts on wait/status | `verae-middleware` / `verae-archive-aggregator` | The user guide describes `receipts[]`, `itemizedOnMainChain`, `merkleRoot`, `proofOk`; the live wait path does not call the aggregator yet, so those fields are absent. This app's `tree_lookup` sample already names them. |
| Billing-edge composition | `zappier-edge` | Target is Zapier → zappier `/v1/*` (`x-api-key`, metered) → middleware. The edge is loopback-only today and covers only timestamp / hashes / receipts / usage without the `/zapier/v1` prefix. This app already sends `x-api-key` and has `MIDDLEWARE_PATH_PREFIX` for when it lands. |
| Webhook `performList` | `verae-middleware` | No "recent events" endpoint, so the hook triggers return `[]` from `performList` and Zapier uses the static sample. A small list route would give real sample data in the editor. |
### Not planned in this app
- Verae admin HTML, user CRUD, and get-block-by-hash (`/admin/*`, `/auth/users*`).
- Encrypted object storage and shares (`/objects*`, `/shares/*`) — mock-only today; add creates
here once the middleware contract is final.
- Anything that talks to NATS or api.veraetime.net directly.
## Proposed changes to the Verae chain API
Context: a developer asked how to query an original hash, get a receipt back from both the
original submission and later lookups, see every timestamp field and its metadata, and store
metadata (or links to it) in a blockchain timestamp. Against the live OpenAPI
(`docs/00-sources/veraetime-openapi.yaml`) none of that is possible today: lookups are by job ID
only, `POST /api/timestamp` returns just `{ jobId }`, and the request has no metadata field.
The middleware mocks all of it (`MOCK_VERAE=true`) but its live client may only call paths in
the snapshot spec.
### Required on api.veraetime.net
| Ask | Change to the Verae API | Why the chain, not the middleware |
|-----|-------------------------|-----------------------------------|
| Query by original hash | New `GET /api/hashes/{sha256}` → `{ exists, jobId, sha256, timestamp, blockIndex, certificate }`. The chain already records `dataHash` per transaction; it needs an index on it. | Only the chain can find hashes sealed by any submitter. A middleware index only knows what passed through it. |
| Receipt on original submission | Make `POST /api/timestamp` idempotent on the digest: if the hash already exists return `200 { jobId, existing: true, certificate, timestamp, blockIndex }` instead of `202`. Accept a pre-hashed input `{ sha256 }` alongside `{ data }` so clients never ship the document. | Idempotency has to live where the seal lives. Accepting a digest also stops raw data landing in blocks. |
| Receipt on lookup | Have the hash lookup and `GET /api/verify/{jobId}` return the full `StatusResponse` (certificate + block metadata). The shape exists; the lookup just needs to reuse it. | No new data, a consistent response. |
| Metadata in the timestamp | Optional `metadata` object on `TimestampRequest` with a documented size cap, stored with the transaction and returned on status, verify, and hash lookup. Add `metadataHash` as an alternative for callers who want to commit to metadata without publishing it. | Anything the seal must prove existed at that time has to be inside what the chain hashes. Everything else stays off chain. |
### Strongly recommended
- **Certified retrieval receipt** — `GET /api/receipts/{jobId}?format=json|pdf`, where the chain
seals a `document.retrieved` event referencing the original job and returns both. The
middleware mocks this; a chain-signed receipt is what makes it certified.
- **JSON error bodies everywhere** — the spec promises `{ error, code }` but the live service
returns plain text on 401 (`Unauthorized: token is missing`). Every client special-cases it.
- **Completion callback or job list** — an optional `callbackUrl` on create, or
`GET /api/jobs?since=…`. Today every consumer polls `GET /api/status/{jobId}` per job.
### Rules to write into the spec
- Public metadata on chain is immutable and readable by anyone. Private metadata never goes on
chain; only a hash commitment does.
- Define `data` as "bytes to hash" and state whether blocks retain it. The `/admin/*` examples
show blocks carrying a base64 `data` field that decodes to the sample input, which suggests
they do — that matters for privacy and should be confirmed on a live seal.
### What can be done without touching the API
The middleware can index SHA-256 → job ID at submission, store public/private metadata, and
merge both back into `/status/{jobId}` and `/hashes/{sha256}` on the live path (it already does
so in mock). That covers the developer's flow for anything submitted through Verae's own
middleware. It cannot deliver chain-wide lookup, chain-level idempotency, or a chain-signed
receipt — those are the real API asks in the table above.
This Zapier app already sends `sha256`, `publicMetadata`, and `privateMetadata` to the
middleware and exposes Find Timestamp by SHA256, Find Job Verification Details, and Get
Retrieval Receipt, so no app change is needed once either side lands.

View file

@ -1,9 +1,9 @@
# verae-zapier-app
**Job:** Full Zapier CLI app: timestamp, wait, verify, batch, job status, hash lookup, REST Hook.
**Job:** Full Zapier CLI app: timestamp (async, wait, batch), verify (single, batch), job status (single, batch, verification details), hash / tree-node lookup, usage, retrieval receipt, REST Hooks for `timestamp.completed` and `timestamp.failed`.
**Expects:** `zmw_` middleware key (today) or zappier key after composition.
**Expects:** `zmw_` middleware key (today) or zappier portal key after composition; sent as `Authorization: Bearer` and `x-api-key`.
**Sends:** HTTPS to middleware `/zapier/v1/*`.
**Sends:** HTTPS to middleware `/zapier/v1/*` only.
**Does not** speak NATS.
**Does not** speak NATS or call `api.veraetime.net`.

View file

@ -1,21 +1,25 @@
/**
* Custom API key auth against Verae middleware.
* Custom API key auth against the Verae middleware.
*
* Customers get a `zmw_…` key from signup / provisioning. The middleware
* resolves it to a tenant (plan, quotas) and logs in to Verae on their behalf.
* @module authentication
*/
const middlewareBase = () =>
process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100';
const { url } = require('./lib/middleware');
/**
* Zapier connection test — validates API key.
* Zapier connection test — `GET /zapier/v1/auth/me`. Its fields feed
* `connectionLabel`.
* @param {object} z
* @param {object} bundle
*/
const testAuth = async (z, bundle) => {
const response = await z.request({
url: `${middlewareBase()}/zapier/v1/auth/me`,
url: url('/auth/me'),
headers: {
Authorization: `Bearer ${bundle.authData.api_key}`,
'x-api-key': bundle.authData.api_key,
},
});
return response.data;
@ -29,8 +33,7 @@ module.exports = {
type: 'string',
required: true,
label: 'API Key',
helpText:
'Your Verae API key (zmw_…) from signup or admin provisioning.',
helpText: 'Your Verae API key (zmw_…) from portal signup or admin provisioning. Never a Verae username, password, or JWT.',
},
],
test: testAuth,

View file

@ -1,2 +0,0 @@
/** Re-export activate-now Add Numbers so this package stays self-contained. */
module.exports = require('../../verae-activate/creates/add_numbers');

73
creates/batch_status.js Normal file
View file

@ -0,0 +1,73 @@
/**
* `POST /zapier/v1/status/batch` — status of several jobs at once.
* Modelled as an action because it returns one aggregate result.
* @module creates/batch_status
*/
const { url, statusSample, toStringList } = require('../lib/middleware');
const perform = async (z, bundle) => {
const jobIds = toStringList(bundle.inputData.jobIds);
if (jobIds.length === 0) {
throw new z.errors.Error('Provide at least one job ID.', 'VALIDATION_ERROR', 400);
}
const response = await z.request({
method: 'POST',
url: url('/status/batch'),
body: { jobIds },
});
const map = (response.data && response.data.results) || {};
const results = jobIds.map((id) => ({ id, ...(map[id] || { status: 'unknown' }) }));
const count = (s) => results.filter((r) => r.status === s).length;
return {
jobIds,
results,
total: results.length,
completedCount: count('completed'),
pendingCount: count('pending'),
failedCount: count('failed'),
allCompleted: results.length > 0 && count('completed') === results.length,
};
};
module.exports = {
key: 'batch_status',
noun: 'Job',
display: {
label: 'Get Batch Job Status',
description: 'Retrieves the current status of multiple timestamp jobs in one request.',
},
operation: {
inputFields: [
{
key: 'jobIds',
label: 'Job IDs',
type: 'string',
required: true,
list: true,
helpText: 'One job ID per entry (or one per line), e.g. the Job IDs from "Create Batch Timestamps".',
},
],
perform,
sample: {
jobIds: [statusSample.id, '650e8400-e29b-41d4-a716-446655440001'],
results: [statusSample, { id: '650e8400-e29b-41d4-a716-446655440001', status: 'pending' }],
total: 2,
completedCount: 1,
pendingCount: 1,
failedCount: 0,
allCompleted: false,
},
outputFields: [
{ key: 'jobIds', label: 'Job IDs' },
{ key: 'results', label: 'Results' },
{ key: 'total', label: 'Total', type: 'integer' },
{ key: 'completedCount', label: 'Completed Count', type: 'integer' },
{ key: 'pendingCount', label: 'Pending Count', type: 'integer' },
{ key: 'failedCount', label: 'Failed Count', type: 'integer' },
{ key: 'allCompleted', label: 'All Completed', type: 'boolean' },
],
},
};

View file

@ -1,18 +1,24 @@
const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100';
/**
* `POST /zapier/v1/timestamp/batch` — several items in one request (paid plans).
* @module creates/batch_timestamp
*/
const { url, hashAlgField, toStringList } = require('../lib/middleware');
const perform = async (z, bundle) => {
const items = (bundle.inputData.items || '')
.split('\n')
.map((s) => s.trim())
.filter(Boolean)
.map((data) => ({ data }));
const hashAlg = bundle.inputData.hashAlg || undefined;
const items = toStringList(bundle.inputData.items).map((data) => ({ data, hashAlg }));
if (items.length === 0) {
throw new z.errors.Error('Provide at least one data item to timestamp.', 'VALIDATION_ERROR', 400);
}
const response = await z.request({
method: 'POST',
url: `${base()}/zapier/v1/timestamp/batch`,
url: url('/timestamp/batch'),
body: { items },
});
return response.data;
const jobIds = (response.data && response.data.jobIds) || [];
return { ...response.data, jobIds, count: jobIds.length };
};
module.exports = {
@ -20,20 +26,28 @@ module.exports = {
noun: 'Timestamp',
display: {
label: 'Create Batch Timestamps',
description: 'Submit multiple data lines for timestamping (paid plans).',
description: 'Submits multiple data items for timestamping in one request (paid plans).',
},
operation: {
inputFields: [
{
key: 'items',
label: 'Data Lines',
label: 'Data Items',
type: 'text',
required: true,
helpText: 'One payload per line.',
list: true,
helpText: 'One item per entry (or one per line). Each item becomes its own timestamp job.',
},
hashAlgField,
],
perform,
sample: { jobIds: ['id-1', 'id-2'] },
outputFields: [{ key: 'jobIds', label: 'Job IDs' }],
sample: {
jobIds: ['550e8400-e29b-41d4-a716-446655440000', '650e8400-e29b-41d4-a716-446655440001'],
count: 2,
},
outputFields: [
{ key: 'jobIds', label: 'Job IDs' },
{ key: 'count', label: 'Job Count', type: 'integer' },
],
},
};

67
creates/batch_verify.js Normal file
View file

@ -0,0 +1,67 @@
/**
* `POST /zapier/v1/verify/batch` — verify several certificates at once.
* @module creates/batch_verify
*/
const { url, toStringList } = require('../lib/middleware');
const perform = async (z, bundle) => {
const certificates = toStringList(bundle.inputData.certificates);
if (certificates.length === 0) {
throw new z.errors.Error('Provide at least one certificate to verify.', 'VALIDATION_ERROR', 400);
}
const response = await z.request({
method: 'POST',
url: url('/verify/batch'),
body: { certificates },
});
const results = (response.data && response.data.results) || [];
const validCount = results.filter((r) => r && r.valid === true).length;
return {
results,
total: results.length,
validCount,
invalidCount: results.length - validCount,
allValid: results.length > 0 && validCount === results.length,
};
};
module.exports = {
key: 'batch_verify',
noun: 'Verification',
display: {
label: 'Verify Batch Timestamps',
description: 'Verifies multiple timestamp certificates in one request.',
},
operation: {
inputFields: [
{
key: 'certificates',
label: 'Certificates',
type: 'text',
required: true,
list: true,
helpText: 'One certificate per entry (or one per line). Results come back in the same order.',
},
],
perform,
sample: {
results: [
{ valid: true, timestamp: '2023-01-01T12:00:00Z', blockIndex: 42 },
{ valid: false },
],
total: 2,
validCount: 1,
invalidCount: 1,
allValid: false,
},
outputFields: [
{ key: 'results', label: 'Results' },
{ key: 'total', label: 'Total', type: 'integer' },
{ key: 'validCount', label: 'Valid Count', type: 'integer' },
{ key: 'invalidCount', label: 'Invalid Count', type: 'integer' },
{ key: 'allValid', label: 'All Valid', type: 'boolean' },
],
},
};

View file

@ -1,13 +1,16 @@
const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100';
/**
* `POST /zapier/v1/timestamp` — submit data or a SHA-256, get a job ID (202).
* A digest that was sealed before returns the original job with `existing: true`.
* @module creates/create_timestamp
*/
const { url, timestampBody, timestampInputFields } = require('../lib/middleware');
const perform = async (z, bundle) => {
const response = await z.request({
method: 'POST',
url: `${base()}/zapier/v1/timestamp`,
body: {
data: bundle.inputData.data,
hashAlg: bundle.inputData.hashAlg || undefined,
},
url: url('/timestamp'),
body: timestampBody(z, bundle.inputData),
});
return response.data;
};
@ -17,21 +20,21 @@ module.exports = {
noun: 'Timestamp',
display: {
label: 'Create Timestamp (Async)',
description: 'Submits data for timestamping and returns a job ID.',
description:
'Registers data or a SHA-256 digest for blockchain timestamping and returns a job ID right away. Pair with "Timestamp Completed" or "Find Job Status" to get the certificate.',
},
operation: {
inputFields: [
{ key: 'data', label: 'Data', type: 'string', required: true },
{
key: 'hashAlg',
label: 'Hash Algorithm',
type: 'string',
required: false,
default: 'SHA256',
},
],
inputFields: timestampInputFields(),
perform,
sample: { jobId: '550e8400-e29b-41d4-a716-446655440000' },
outputFields: [{ key: 'jobId', label: 'Job ID' }],
sample: {
jobId: '550e8400-e29b-41d4-a716-446655440000',
sha256: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
existing: false,
},
outputFields: [
{ key: 'jobId', label: 'Job ID' },
{ key: 'sha256', label: 'SHA256' },
{ key: 'existing', label: 'Already Timestamped', type: 'boolean' },
],
},
};

76
creates/get_receipt.js Normal file
View file

@ -0,0 +1,76 @@
/**
* `GET /zapier/v1/receipts/{jobId}` — certified retrieval receipt for a
* completed job (JSON, optionally the PDF as a Zapier file). The middleware
* seals a `document.retrieved` event on each call, so this is an action.
* @module creates/get_receipt
*/
const { url } = require('../lib/middleware');
const perform = async (z, bundle) => {
const jobId = bundle.inputData.jobId;
const receiptUrl = url(`/receipts/${encodeURIComponent(jobId)}`);
const response = await z.request({ url: receiptUrl, params: { format: 'json' } });
const receipt = { id: jobId, ...(response.data || {}) };
if (bundle.inputData.includePdf) {
const pdfRequest = z.request({ url: receiptUrl, params: { format: 'pdf' }, raw: true });
receipt.pdf = await z.stashFile(
pdfRequest,
undefined,
`verae-receipt-${jobId}.pdf`,
'application/pdf',
);
}
return receipt;
};
module.exports = {
key: 'get_receipt',
noun: 'Receipt',
display: {
label: 'Get Retrieval Receipt',
description:
'Fetches the certified retrieval receipt for a completed timestamp job. Fails with NOT_COMPLETE while the job is still pending.',
},
operation: {
inputFields: [
{ key: 'jobId', label: 'Job ID', type: 'string', required: true },
{
key: 'includePdf',
label: 'Include PDF',
type: 'boolean',
required: false,
default: 'false',
helpText: 'Also attach the receipt as a PDF file.',
},
],
perform,
sample: {
id: '550e8400-e29b-41d4-a716-446655440000',
type: 'verae.retrieval-receipt',
format: 'json',
jobId: '550e8400-e29b-41d4-a716-446655440000',
sha256: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
timestamp: '2023-01-01T12:05:00Z',
certificate: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
extraSeal: {
event: 'document.retrieved',
retrievedAt: '2023-01-02T09:00:00Z',
tenantId: 'tenant-example',
},
},
outputFields: [
{ key: 'jobId', label: 'Job ID' },
{ key: 'type', label: 'Receipt Type' },
{ key: 'sha256', label: 'SHA256' },
{ key: 'timestamp', label: 'Timestamp', type: 'datetime' },
{ key: 'certificate', label: 'Certificate' },
{ key: 'extraSeal__event', label: 'Seal Event' },
{ key: 'extraSeal__retrievedAt', label: 'Retrieved At', type: 'datetime' },
{ key: 'extraSeal__tenantId', label: 'Tenant ID' },
{ key: 'pdf', label: 'Receipt PDF', type: 'file' },
],
},
};

View file

@ -1,15 +1,27 @@
const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100';
/**
* `POST /zapier/v1/timestamp/wait` — submit data or a SHA-256 and hold until
* the job is terminal or the middleware's WAIT_TIMEOUT_MS elapses
* (then `status: pending` with the job ID).
* @module creates/timestamp_and_wait
*/
const {
url,
statusOutputFields,
statusSample,
timestampBody,
timestampInputFields,
} = require('../lib/middleware');
const perform = async (z, bundle) => {
const response = await z.request({
method: 'POST',
url: `${base()}/zapier/v1/timestamp/wait`,
body: {
data: bundle.inputData.data,
hashAlg: bundle.inputData.hashAlg || undefined,
},
url: url('/timestamp/wait'),
body: timestampBody(z, bundle.inputData),
});
return response.data;
const status = response.data || {};
const id = status.id || status.jobId;
return { ...status, id, jobId: status.jobId || id };
};
module.exports = {
@ -18,37 +30,12 @@ module.exports = {
display: {
label: 'Create Timestamp and Wait',
description:
'Submits data for blockchain timestamping and waits for the certificate.',
'Registers data or a SHA-256 digest and holds the step until the certificate exists. If the chain is slow the step returns status "pending" with the job ID so a later step (or the Timestamp Completed trigger) can finish the Zap.',
},
operation: {
inputFields: [
{
key: 'data',
label: 'Data',
type: 'string',
required: true,
helpText: 'Content to timestamp on the blockchain.',
},
{
key: 'hashAlg',
label: 'Hash Algorithm',
type: 'string',
required: false,
default: 'SHA256',
},
],
inputFields: timestampInputFields(),
perform,
sample: {
id: '550e8400-e29b-41d4-a716-446655440000',
status: 'completed',
result: 'mock-cert-example',
completedAt: '2023-01-01T12:05:00Z',
},
outputFields: [
{ key: 'id', label: 'Job ID' },
{ key: 'status', label: 'Status' },
{ key: 'result', label: 'Certificate' },
{ key: 'completedAt', label: 'Completed At', type: 'datetime' },
],
sample: statusSample,
outputFields: [{ key: 'jobId', label: 'Job ID' }, ...statusOutputFields.slice(1)],
},
};

View file

@ -1,9 +1,14 @@
const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100';
/**
* `POST /zapier/v1/verify` — verify one certificate.
* @module creates/verify_timestamp
*/
const { url, verifyOutputFields, verifySample } = require('../lib/middleware');
const perform = async (z, bundle) => {
const response = await z.request({
method: 'POST',
url: `${base()}/zapier/v1/verify`,
url: url('/verify'),
body: { certificate: bundle.inputData.certificate },
});
return response.data;
@ -14,7 +19,7 @@ module.exports = {
noun: 'Verification',
display: {
label: 'Verify Timestamp',
description: 'Verify a timestamp certificate.',
description: 'Verifies the authenticity of a timestamp certificate.',
},
operation: {
inputFields: [
@ -23,14 +28,11 @@ module.exports = {
label: 'Certificate',
type: 'text',
required: true,
helpText: 'The certificate returned when a timestamp job completed.',
},
],
perform,
sample: { valid: true, timestamp: '2023-01-01T12:00:00Z', blockIndex: 42 },
outputFields: [
{ key: 'valid', label: 'Valid', type: 'boolean' },
{ key: 'timestamp', label: 'Timestamp', type: 'datetime' },
{ key: 'blockIndex', label: 'Block Index', type: 'integer' },
],
sample: verifySample,
outputFields: verifyOutputFields,
},
};

View file

@ -1,82 +1,100 @@
/**
* Verae Zapier Platform app definition.
*
* Every operation calls the Verae middleware (`MIDDLEWARE_BASE_URL`,
* `/zapier/v1/*`). The middleware talks to api.veraetime.net; this app never does.
* @module index
*/
const authentication = require('./authentication');
const { describeError } = require('./lib/middleware');
const timestampAndWait = require('./creates/timestamp_and_wait');
const createTimestamp = require('./creates/create_timestamp');
const verifyTimestamp = require('./creates/verify_timestamp');
const batchTimestamp = require('./creates/batch_timestamp');
const addNumbers = require('./creates/add_numbers');
const verifyTimestamp = require('./creates/verify_timestamp');
const batchVerify = require('./creates/batch_verify');
const batchStatus = require('./creates/batch_status');
const getReceipt = require('./creates/get_receipt');
const jobStatus = require('./searches/job_status');
const jobVerification = require('./searches/job_verification');
const hashLookup = require('./searches/hash_lookup');
const treeLookup = require('./searches/tree_lookup');
const findUsage = require('./searches/usage');
const timestampCompleted = require('./triggers/timestamp_completed');
const timestampFailed = require('./triggers/timestamp_failed');
/**
* Attach middleware API key to every outbound request.
* Attach the API key to every outbound request, both ways the stack accepts
* it: the middleware reads `Authorization: Bearer` or `x-api-key`; the
* access planes and the zappier billing edge forward only `x-api-key`.
* @param {object} request
* @param {object} _z
* @param {object} bundle
*/
const addApiKey = (request, _z, bundle) => {
request.headers = request.headers || {};
request.headers.Authorization = `Bearer ${bundle.authData.api_key}`;
const key = bundle && bundle.authData && bundle.authData.api_key;
if (key) {
if (!request.headers.Authorization) request.headers.Authorization = `Bearer ${key}`;
if (!request.headers['x-api-key']) request.headers['x-api-key'] = key;
}
return request;
};
/**
* Map middleware billing errors to Zapier errors.
* Map middleware errors (`{ error, code, details }`) to Zapier errors.
* Billing errors carry the upgrade URL; 429 becomes a ThrottledError so
* Zapier retries later. Requests made with `skipThrowForStatus` are left alone.
* @param {object} response
* @param {object} z
*/
const mapMiddlewareErrors = (response, z) => {
if (response.status === 402) {
throw new z.errors.Error(
`${response.data?.error ?? 'Quota exceeded'}. Upgrade at ${response.data?.details?.upgradeUrl ?? response.data?.upgradeUrl ?? 'your billing portal'}.`,
'QuotaExceeded',
402,
);
if (response.skipThrowForStatus || response.status < 400) {
return response;
}
if (response.status === 403 && response.data?.code === 'PLAN_UPGRADE_REQUIRED') {
throw new z.errors.Error(
response.data?.error ?? 'This action requires a paid plan.',
'PlanUpgradeRequired',
403,
);
const { message, code, upgradeUrl } = describeError(response);
if (response.status === 429) {
throw new z.errors.ThrottledError(message);
}
return response;
const billingCodes = new Set(['QUOTA_EXCEEDED', 'PLAN_UPGRADE_REQUIRED', 'BATCH_LIMIT_EXCEEDED']);
if (response.status === 402 || billingCodes.has(code)) {
const where = upgradeUrl || 'your billing portal';
throw new z.errors.Error(`${message}. Upgrade at ${where}.`, code, response.status);
}
throw new z.errors.Error(message, code, response.status);
};
let platformVersion = '15.19.0';
try {
platformVersion = require('zapier-platform-core').version;
} catch {
// optional for unit tests without full install
}
module.exports = {
version: require('./package.json').version,
platformVersion,
platformVersion: require('zapier-platform-core').version,
authentication,
beforeRequest: [addApiKey],
afterResponse: [mapMiddlewareErrors],
triggers: {
[timestampCompleted.key]: timestampCompleted,
[timestampFailed.key]: timestampFailed,
},
creates: {
[addNumbers.key]: addNumbers,
[timestampAndWait.key]: timestampAndWait,
[createTimestamp.key]: createTimestamp,
[verifyTimestamp.key]: verifyTimestamp,
[batchTimestamp.key]: batchTimestamp,
[verifyTimestamp.key]: verifyTimestamp,
[batchVerify.key]: batchVerify,
[batchStatus.key]: batchStatus,
[getReceipt.key]: getReceipt,
},
searches: {
[jobStatus.key]: jobStatus,
[jobVerification.key]: jobVerification,
[hashLookup.key]: hashLookup,
[treeLookup.key]: treeLookup,
[findUsage.key]: findUsage,
},
};

87
lib/hookTrigger.js Normal file
View file

@ -0,0 +1,87 @@
/**
* Factory for REST Hook triggers backed by the middleware's
* `/webhooks/subscribe` and `/webhooks/unsubscribe` endpoints.
*
* Delivered payload (see verae-middleware inProcessJobPoller / webhookWorker):
* `{ event, jobId, tenantId, status: StatusResponse }`.
* @module lib/hookTrigger
*/
const { url, statusSample } = require('./middleware');
/**
* @param {object} spec
* @param {string} spec.key Zapier trigger key
* @param {string} spec.event middleware event name (timestamp.completed | timestamp.failed)
* @param {string} spec.label
* @param {string} spec.description
* @param {object} [spec.sampleStatus] StatusResponse used in the sample payload
* @returns {object} Zapier trigger definition
*/
const hookTrigger = ({ key, event, label, description, sampleStatus = statusSample }) => {
const performSubscribe = async (z, bundle) => {
const response = await z.request({
method: 'POST',
url: url('/webhooks/subscribe'),
body: { targetUrl: bundle.targetUrl, event },
});
return { id: response.data.id, event };
};
const performUnsubscribe = async (z, bundle) => {
const hookId =
bundle.subscribeData && typeof bundle.subscribeData === 'object'
? bundle.subscribeData.id
: bundle.subscribeData;
await z.request({
method: 'DELETE',
url: url('/webhooks/unsubscribe'),
body: { hookId, targetUrl: bundle.targetUrl },
});
return {};
};
/** Zapier dedupes hook items on `id`; the payload only carries `jobId`. */
const perform = async (_z, bundle) => {
const payload = bundle.cleanedRequest || {};
const id = payload.jobId || (payload.status && payload.status.id);
return [{ id, ...payload }];
};
/** The middleware has no "list recent events" endpoint; Zapier falls back to `sample`. */
const performList = async () => [];
return {
key,
noun: 'Timestamp',
display: { label, description },
operation: {
type: 'hook',
perform,
performList,
performSubscribe,
performUnsubscribe,
sample: {
id: sampleStatus.id,
event,
jobId: sampleStatus.id,
tenantId: 'tenant-example',
status: sampleStatus,
},
outputFields: [
{ key: 'id', label: 'Job ID' },
{ key: 'event', label: 'Event' },
{ key: 'jobId', label: 'Job ID' },
{ key: 'tenantId', label: 'Tenant ID' },
{ key: 'status__status', label: 'Status' },
{ key: 'status__result', label: 'Certificate' },
{ key: 'status__error', label: 'Error' },
{ key: 'status__completedAt', label: 'Completed At', type: 'datetime' },
{ key: 'status__sha256', label: 'SHA256' },
{ key: 'status__metadata__blockIndex', label: 'Block Index', type: 'integer' },
],
},
};
};
module.exports = hookTrigger;

233
lib/middleware.js Normal file
View file

@ -0,0 +1,233 @@
/**
* Shared helpers for calling the Verae middleware (`/zapier/v1/*`).
*
* The Zapier app never talks to api.veraetime.net or NATS; the middleware
* (verae-middleware) owns API keys, plans/quotas, job watching, webhooks,
* and the upstream Verae chain client.
* @module lib/middleware
*/
const DEFAULT_BASE_URL = 'http://127.0.0.1:3100';
const DEFAULT_PATH_PREFIX = '/zapier/v1';
/**
* Middleware origin, from MIDDLEWARE_BASE_URL (set per Zapier version with
* `zapier env:set <version> MIDDLEWARE_BASE_URL=https://...`).
* @returns {string} origin without a trailing slash
*/
const baseUrl = () =>
(process.env.MIDDLEWARE_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, '');
/**
* Path prefix in front of every route. The middleware serves `/zapier/v1`;
* a fronting edge may mount the same routes elsewhere, so it is overridable
* with MIDDLEWARE_PATH_PREFIX.
* @returns {string} prefix starting with "/" and without a trailing slash
*/
const pathPrefix = () => {
const raw = process.env.MIDDLEWARE_PATH_PREFIX;
if (raw === undefined || raw === null) return DEFAULT_PATH_PREFIX;
const trimmed = String(raw).trim().replace(/\/+$/, '');
if (!trimmed) return '';
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
};
/**
* Build a middleware URL.
* @param {string} path path starting with "/", e.g. "/timestamp/wait"
* @returns {string}
*/
const url = (path) => `${baseUrl()}${pathPrefix()}${path}`;
/** 64 lowercase/uppercase hex characters. */
const SHA256_RE = /^[A-Fa-f0-9]{64}$/;
/**
* Build the body for `/timestamp` and `/timestamp/wait` from Zapier input.
* Either `data` or a 64-hex `sha256` is required (the middleware hashes data
* itself; sending only the digest is recommended for large files).
* Optional public/private metadata dictionaries are passed through.
* @param {object} z
* @param {object} inputData
* @returns {{ data?: string, sha256?: string, hashAlg?: string, publicMetadata?: object, privateMetadata?: object }}
*/
const timestampBody = (z, inputData) => {
const data = inputData.data === undefined || inputData.data === null ? '' : String(inputData.data);
const sha256 = String(inputData.sha256 || '').trim().toLowerCase();
if (!data && !sha256) {
throw new z.errors.Error('Provide either Data or a SHA256 digest.', 'VALIDATION_ERROR', 400);
}
if (sha256 && !SHA256_RE.test(sha256)) {
throw new z.errors.Error('SHA256 must be 64 hexadecimal characters.', 'VALIDATION_ERROR', 400);
}
const body = { hashAlg: inputData.hashAlg || undefined };
if (data) body.data = data;
if (sha256) body.sha256 = sha256;
const isDict = (v) => v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length > 0;
if (isDict(inputData.publicMetadata)) body.publicMetadata = inputData.publicMetadata;
if (isDict(inputData.privateMetadata)) body.privateMetadata = inputData.privateMetadata;
return body;
};
/** Input fields shared by Create Timestamp and Create Timestamp and Wait. */
const timestampInputFields = () => [
{
key: 'data',
label: 'Data',
type: 'text',
required: false,
helpText: 'Content to timestamp. The middleware hashes it with SHA-256; only the digest is sealed on chain. Leave blank if you supply a SHA256 below.',
},
{
key: 'sha256',
label: 'SHA256',
type: 'string',
required: false,
helpText: '64 hex characters if you already hashed the file outside Zapier (recommended for large files). Either Data or SHA256 is required.',
},
hashAlgField,
{
key: 'publicMetadata',
label: 'Public Metadata',
dict: true,
required: false,
helpText: 'Key/value pairs stored with the seal and returned on lookups.',
},
{
key: 'privateMetadata',
label: 'Private Metadata',
dict: true,
required: false,
helpText: 'Key/value pairs stored privately with the job (only visible to your account).',
},
];
/**
* Read the middleware's structured error body (`{ error, code, details }`),
* falling back to plain text or a generic message.
* @param {object} response zapier-platform-core response
* @returns {{ message: string, code: string, upgradeUrl?: string, details?: object }}
*/
const describeError = (response) => {
const data = response.data;
if (data && typeof data === 'object' && typeof data.error === 'string') {
const details = data.details && typeof data.details === 'object' ? data.details : undefined;
return {
message: data.error,
code: typeof data.code === 'string' ? data.code : `HTTP_${response.status}`,
upgradeUrl: details?.upgradeUrl ?? data.upgradeUrl,
details,
};
}
let text = '';
try {
text = typeof response.content === 'string' ? response.content.trim() : '';
} catch {
text = '';
}
if (text && text.length <= 500 && !text.startsWith('<')) {
return { message: text, code: `HTTP_${response.status}` };
}
return {
message: `Verae middleware request failed with HTTP ${response.status}`,
code: `HTTP_${response.status}`,
};
};
/**
* Run a lookup request for a Zapier search: 404 means "no match" (empty
* array), other errors are raised with the middleware's message, and a hit
* is returned as a one-element array.
* @param {object} z
* @param {object} requestOptions passed to z.request
* @returns {Promise<object[]>}
*/
const searchOne = async (z, requestOptions) => {
const response = await z.request({ ...requestOptions, skipThrowForStatus: true });
if (response.status === 404) return [];
if (response.status >= 400) {
const { message, code } = describeError(response);
throw new z.errors.Error(message, code, response.status);
}
return [response.data];
};
/**
* Normalise a Zapier `list: true` field or newline-separated text into an
* array of non-empty trimmed strings.
* @param {string[]|string|undefined} value
* @returns {string[]}
*/
const toStringList = (value) => {
const raw = Array.isArray(value) ? value : String(value ?? '').split('\n');
return raw.map((s) => String(s).trim()).filter(Boolean);
};
/** Sample job status as returned by `/status/{jobId}` and `/timestamp/wait`. */
const statusSample = {
id: '550e8400-e29b-41d4-a716-446655440000',
jobId: '550e8400-e29b-41d4-a716-446655440000',
status: 'completed',
result: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
completedAt: '2023-01-01T12:05:00Z',
sha256: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
metadata: {
blockIndex: 42,
timestamp: '2023-01-01T12:00:00Z',
certificate: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
sha256: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
},
};
/** Output fields for a job status (nested keys use Zapier's `__` notation). */
const statusOutputFields = [
{ key: 'id', label: 'Job ID' },
{ key: 'status', label: 'Status' },
{ key: 'error', label: 'Error' },
{ key: 'result', label: 'Certificate' },
{ key: 'completedAt', label: 'Completed At', type: 'datetime' },
{ key: 'sha256', label: 'SHA256' },
{ key: 'metadata__blockIndex', label: 'Block Index', type: 'integer' },
{ key: 'metadata__timestamp', label: 'Block Timestamp', type: 'datetime' },
{ key: 'metadata__certificate', label: 'Certificate (metadata)' },
];
/** Sample verification result from `/verify`. */
const verifySample = { valid: true, timestamp: '2023-01-01T12:00:00Z', blockIndex: 42 };
/** Output fields for a verification result. */
const verifyOutputFields = [
{ key: 'valid', label: 'Valid', type: 'boolean' },
{ key: 'timestamp', label: 'Timestamp', type: 'datetime' },
{ key: 'blockIndex', label: 'Block Index', type: 'integer' },
];
/** Hash-algorithm input shared by the timestamp actions. */
const hashAlgField = {
key: 'hashAlg',
label: 'Hash Algorithm',
type: 'string',
required: false,
default: 'SHA256',
helpText: 'Hash algorithm for the chain. Defaults to SHA256.',
};
module.exports = {
DEFAULT_BASE_URL,
DEFAULT_PATH_PREFIX,
SHA256_RE,
baseUrl,
describeError,
hashAlgField,
pathPrefix,
searchOne,
statusOutputFields,
statusSample,
timestampBody,
timestampInputFields,
toStringList,
url,
verifyOutputFields,
verifySample,
};

819
package-lock.json generated Normal file
View file

@ -0,0 +1,819 @@
{
"name": "verae-zapier",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "verae-zapier",
"version": "1.0.0",
"dependencies": {
"zapier-platform-core": "15.19.0"
},
"engines": {
"node": ">=18",
"npm": ">=5.6.0"
}
},
"node_modules/@types/node": {
"version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"license": "MIT",
"optional": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@zapier/secret-scrubber": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@zapier/secret-scrubber/-/secret-scrubber-1.1.6.tgz",
"integrity": "sha512-ETblIxh6lnkVHjVqQ8XVB2Ymeo7RudR7kIiZylXtYXAfpmGLXIzbk1h9fjF/SuO21k7jdxaMBaBdqUCBHv8+og==",
"license": "ISC",
"dependencies": {
"create-hash": "1.2.0",
"lodash.isplainobject": "4.0.6"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"license": "MIT"
},
"node_modules/call-bind": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
"integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"get-intrinsic": "^1.3.0",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/cipher-base": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
"integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.2"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"license": "MIT",
"dependencies": {
"cipher-base": "^1.0.1",
"inherits": "^2.0.1",
"md5.js": "^1.3.4",
"ripemd160": "^2.0.1",
"sha.js": "^2.4.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dotenv": {
"version": "12.0.4",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-12.0.4.tgz",
"integrity": "sha512-oWdqbSywffzH1l4WXKPHWA0TWYpqp7IyLfqjipT4upoIFS0HPMqtNotykQpD4iIg0BqtNmdgPCh2WMvMt7yTiw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash-base": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz",
"integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"readable-stream": "^2.3.8",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jsonschema": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.2.tgz",
"integrity": "sha512-iX5OFQ6yx9NgbHCwse51ohhKgLuLL7Z5cNOeZOPIlDUtAMrxlruHLzVZxbltdHE5mEDXN+75oFOwq6Gn0MZwsA==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"license": "MIT",
"dependencies": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
"safe-buffer": "^5.1.2"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/node-abort-controller": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/ripemd160": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
"integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==",
"license": "MIT",
"dependencies": {
"hash-base": "^3.1.2",
"inherits": "^2.0.4"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/sha.js": {
"version": "2.4.12",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
"license": "(MIT AND BSD-3-Clause)",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.0"
},
"bin": {
"sha.js": "bin.js"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/to-buffer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
"integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
"license": "MIT",
"dependencies": {
"isarray": "^2.0.5",
"safe-buffer": "^5.2.1",
"typed-array-buffer": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/to-buffer/node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"license": "MIT"
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
"integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"es-errors": "^1.3.0",
"is-typed-array": "^1.1.14"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT",
"optional": true
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which-typed-array": {
"version": "1.1.22",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz",
"integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==",
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.9",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/zapier-platform-core": {
"version": "15.19.0",
"resolved": "https://registry.npmjs.org/zapier-platform-core/-/zapier-platform-core-15.19.0.tgz",
"integrity": "sha512-KWyvuZ+QnooOknFl3AW6HObbXdz1FZYclN46EwUeQEd1xIjHwr+WdQwOYPJJM6jwSBkh0Fb7MMVhZQQ0VmPlCg==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@zapier/secret-scrubber": "^1.1.1",
"bluebird": "3.7.2",
"content-disposition": "0.5.4",
"dotenv": "12.0.4 ",
"form-data": "4.0.0",
"lodash": "4.17.21",
"mime-types": "2.1.35",
"node-abort-controller": "3.1.1",
"node-fetch": "2.6.7",
"oauth-sign": "0.9.0",
"semver": "7.5.2",
"zapier-platform-schema": "15.19.0"
},
"engines": {
"node": ">=16",
"npm": ">=5.6.0"
},
"optionalDependencies": {
"@types/node": "^20.3.1"
}
},
"node_modules/zapier-platform-schema": {
"version": "15.19.0",
"resolved": "https://registry.npmjs.org/zapier-platform-schema/-/zapier-platform-schema-15.19.0.tgz",
"integrity": "sha512-c3lkrIm3/KaAOXGgayhQtXiH7ODtn1lxBTK5marU30yuyO+7WMCpxW+H79yVrfeiDyR7Dhy7LyKtoKw7pEWq+Q==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"jsonschema": "1.2.2",
"lodash": "4.17.21"
}
}
}
}

View file

@ -1,10 +1,11 @@
{
"name": "verae-zapier",
"version": "1.0.0",
"version": "1.1.0",
"description": "Zapier CLI app for Verae Timestamping via middleware",
"main": "index.js",
"scripts": {
"test": "node --test test/**/*.test.js"
"test": "node --test test/*.test.js",
"test:e2e": "node --test test/e2e.test.js"
},
"engines": {
"node": ">=18",

View file

@ -1,34 +1,45 @@
const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100';
/**
* `GET /zapier/v1/hashes/{sha256}` — find an existing timestamp by the
* SHA-256 of its data. 404 means no match.
* @module searches/hash_lookup
*/
const perform = async (z, bundle) => {
try {
const response = await z.request({
method: 'GET',
url: `${base()}/zapier/v1/hashes/${encodeURIComponent(bundle.inputData.sha256)}`,
});
return [response.data];
} catch (err) {
if (err.status === 404) return [];
throw err;
}
};
const { url, searchOne } = require('../lib/middleware');
const perform = (z, bundle) =>
searchOne(z, { url: url(`/hashes/${encodeURIComponent(bundle.inputData.sha256)}`) });
module.exports = {
key: 'hash_lookup',
noun: 'Timestamp',
display: {
label: 'Find Timestamp by SHA256',
description: 'Looks up an existing timestamp for a SHA256 hex digest (mock/middleware).',
description: 'Looks up an existing timestamp on the main Verae chain by the SHA256 hex digest of its data.',
},
operation: {
inputFields: [
{ key: 'sha256', label: 'SHA256', type: 'string', required: true },
{
key: 'sha256',
label: 'SHA256',
type: 'string',
required: true,
helpText: '64 hex characters.',
},
],
perform,
sample: {
sha256: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
exists: true,
jobId: '550e8400-e29b-41d4-a716-446655440000',
timestamp: '2023-01-01T12:05:00Z',
status: 'completed',
},
outputFields: [
{ key: 'sha256', label: 'SHA256' },
{ key: 'exists', label: 'Exists', type: 'boolean' },
{ key: 'jobId', label: 'Job ID' },
{ key: 'timestamp', label: 'Timestamp', type: 'datetime' },
{ key: 'status', label: 'Status' },
],
},
};

View file

@ -1,33 +1,26 @@
const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100';
/**
* `GET /zapier/v1/status/{jobId}` — 404 means no such job (empty result).
* @module searches/job_status
*/
const perform = async (z, bundle) => {
const response = await z.request({
url: `${base()}/zapier/v1/status/${encodeURIComponent(bundle.inputData.jobId)}`,
});
return [response.data];
};
const { url, searchOne, statusOutputFields, statusSample } = require('../lib/middleware');
const perform = (z, bundle) =>
searchOne(z, { url: url(`/status/${encodeURIComponent(bundle.inputData.jobId)}`) });
module.exports = {
key: 'job_status',
noun: 'Job',
display: {
label: 'Find Job Status',
description: 'Look up a timestamp job by ID.',
description: 'Looks up a timestamp job by its job ID.',
},
operation: {
inputFields: [
{ key: 'jobId', label: 'Job ID', type: 'string', required: true },
],
perform,
sample: {
id: '550e8400-e29b-41d4-a716-446655440000',
status: 'completed',
result: 'mock-cert',
},
outputFields: [
{ key: 'id', label: 'Job ID' },
{ key: 'status', label: 'Status' },
{ key: 'result', label: 'Result' },
],
sample: statusSample,
outputFields: statusOutputFields,
},
};

Some files were not shown because too many files have changed in this diff Show more