diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0f49048..0000000 --- a/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/ -build/ -.env -.env.* -.zapierapprc -*.log -.DS_Store diff --git a/NATS.md b/NATS.md index 38749c8..3254376 100644 --- a/NATS.md +++ b/NATS.md @@ -5,13 +5,6 @@ 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/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 }` | +| OUT HTTPS | middleware `/zapier/v1/timestamp`, `/wait`, `/verify`, `/status/{id}`, `/hashes/{sha256}`, `/hashes/{sha256}?includeTree=true`, REST Hook subscribe | verae-middleware | Bearer `zmw_` | -Job completion is a REST Hook HTTP POST from the middleware's webhook path, not a NATS client in Zapier. +Job completion is a REST Hook HTTP POST from webhook-deliver, not a NATS client in Zapier. diff --git a/README.md b/README.md index df9821b..3cb834c 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,36 @@ # verae-zapier -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`. +[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) + +Zapier Platform CLI app for Verae. Runs on **Zapier’s servers**. Calls only middleware HTTPS... **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. 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). +Runs on **Zapier’s servers**. Calls only the middleware HTTPS API (`MIDDLEWARE_BASE_URL`), never NATS and never `api.veraetime.net` directly. -## Modules +## Planned modules -| 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. +| 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 | 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 -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 +export MIDDLEWARE_BASE_URL=https://your-middleware.example.com ``` ## Gate @@ -112,96 +39,24 @@ MIDDLEWARE_BASE_URL=https://mw.zapier.georgelambert.org MIDDLEWARE_API_KEY=zmw_. npm run gate:11 # from monorepo root, after Phase 11 implementation ``` -## Status of the API integration (2026-09-15) +## Install -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`). +```sh +git clone ssh://git@git.georgelambert.org:2223/marchon/verae-zapier-app.git +cd verae-zapier-app +``` -### Done +## Usage -- 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). +```sh +# No single entrypoint is declared in this tree. +ls +``` -### Open — blocked on people, not code +## Contributing -| Item | Owner | What is needed | -|------|-------|----------------| -| Zapier Platform push | operator | `zapier login`, `zapier register` (or link), `zapier env:set 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. | +Questions: open an issue at https://git.georgelambert.org/marchon/verae-zapier-app/issues. Pull requests are accepted. -### Open — code in other repos +## License -| 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. +UNLICENSED © George Lambert diff --git a/SUMMARY.md b/SUMMARY.md index a3420d8..94b22b3 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,9 +1,9 @@ # verae-zapier-app -**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`. +**Job:** Full Zapier CLI app: timestamp, wait, verify, batch, job status, hash lookup, REST Hook. -**Expects:** `zmw_` middleware key (today) or zappier portal key after composition; sent as `Authorization: Bearer` and `x-api-key`. +**Expects:** `zmw_` middleware key (today) or zappier key after composition. -**Sends:** HTTPS to middleware `/zapier/v1/*` only. +**Sends:** HTTPS to middleware `/zapier/v1/*`. -**Does not** speak NATS or call `api.veraetime.net`. +**Does not** speak NATS. diff --git a/authentication.js b/authentication.js index 47a5afd..0a9b5e3 100644 --- a/authentication.js +++ b/authentication.js @@ -1,25 +1,21 @@ /** - * 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. + * Custom API key auth against Verae middleware. * @module authentication */ -const { url } = require('./lib/middleware'); +const middlewareBase = () => + process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100'; /** - * Zapier connection test — `GET /zapier/v1/auth/me`. Its fields feed - * `connectionLabel`. + * Zapier connection test — validates API key. * @param {object} z * @param {object} bundle */ const testAuth = async (z, bundle) => { const response = await z.request({ - url: url('/auth/me'), + url: `${middlewareBase()}/zapier/v1/auth/me`, headers: { Authorization: `Bearer ${bundle.authData.api_key}`, - 'x-api-key': bundle.authData.api_key, }, }); return response.data; @@ -33,7 +29,8 @@ module.exports = { type: 'string', required: true, label: 'API Key', - helpText: 'Your Verae API key (zmw_…) from portal signup or admin provisioning. Never a Verae username, password, or JWT.', + helpText: + 'Your Verae API key (zmw_…) from signup or admin provisioning.', }, ], test: testAuth, diff --git a/creates/add_numbers.js b/creates/add_numbers.js new file mode 100644 index 0000000..c2dfb5a --- /dev/null +++ b/creates/add_numbers.js @@ -0,0 +1,2 @@ +/** Re-export activate-now Add Numbers so this package stays self-contained. */ +module.exports = require('../../verae-activate/creates/add_numbers'); diff --git a/creates/batch_status.js b/creates/batch_status.js deleted file mode 100644 index b3d34d7..0000000 --- a/creates/batch_status.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * `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' }, - ], - }, -}; diff --git a/creates/batch_timestamp.js b/creates/batch_timestamp.js index 9899371..5b8a279 100644 --- a/creates/batch_timestamp.js +++ b/creates/batch_timestamp.js @@ -1,24 +1,18 @@ -/** - * `POST /zapier/v1/timestamp/batch` — several items in one request (paid plans). - * @module creates/batch_timestamp - */ - -const { url, hashAlgField, toStringList } = require('../lib/middleware'); +const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100'; const perform = async (z, bundle) => { - 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 items = (bundle.inputData.items || '') + .split('\n') + .map((s) => s.trim()) + .filter(Boolean) + .map((data) => ({ data })); const response = await z.request({ method: 'POST', - url: url('/timestamp/batch'), + url: `${base()}/zapier/v1/timestamp/batch`, body: { items }, }); - const jobIds = (response.data && response.data.jobIds) || []; - return { ...response.data, jobIds, count: jobIds.length }; + return response.data; }; module.exports = { @@ -26,28 +20,20 @@ module.exports = { noun: 'Timestamp', display: { label: 'Create Batch Timestamps', - description: 'Submits multiple data items for timestamping in one request (paid plans).', + description: 'Submit multiple data lines for timestamping (paid plans).', }, operation: { inputFields: [ { key: 'items', - label: 'Data Items', + label: 'Data Lines', type: 'text', required: true, - list: true, - helpText: 'One item per entry (or one per line). Each item becomes its own timestamp job.', + helpText: 'One payload per line.', }, - hashAlgField, ], perform, - 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' }, - ], + sample: { jobIds: ['id-1', 'id-2'] }, + outputFields: [{ key: 'jobIds', label: 'Job IDs' }], }, }; diff --git a/creates/batch_verify.js b/creates/batch_verify.js deleted file mode 100644 index 637e582..0000000 --- a/creates/batch_verify.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * `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' }, - ], - }, -}; diff --git a/creates/create_timestamp.js b/creates/create_timestamp.js index 5db8347..c1921c3 100644 --- a/creates/create_timestamp.js +++ b/creates/create_timestamp.js @@ -1,16 +1,13 @@ -/** - * `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 base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100'; const perform = async (z, bundle) => { const response = await z.request({ method: 'POST', - url: url('/timestamp'), - body: timestampBody(z, bundle.inputData), + url: `${base()}/zapier/v1/timestamp`, + body: { + data: bundle.inputData.data, + hashAlg: bundle.inputData.hashAlg || undefined, + }, }); return response.data; }; @@ -20,21 +17,21 @@ module.exports = { noun: 'Timestamp', display: { label: 'Create Timestamp (Async)', - 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.', + description: 'Submits data for timestamping and returns a job ID.', }, operation: { - inputFields: timestampInputFields(), - perform, - 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' }, + inputFields: [ + { key: 'data', label: 'Data', type: 'string', required: true }, + { + key: 'hashAlg', + label: 'Hash Algorithm', + type: 'string', + required: false, + default: 'SHA256', + }, ], + perform, + sample: { jobId: '550e8400-e29b-41d4-a716-446655440000' }, + outputFields: [{ key: 'jobId', label: 'Job ID' }], }, }; diff --git a/creates/get_receipt.js b/creates/get_receipt.js deleted file mode 100644 index 2ce11ce..0000000 --- a/creates/get_receipt.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * `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' }, - ], - }, -}; diff --git a/creates/timestamp_and_wait.js b/creates/timestamp_and_wait.js index 046900c..2fe1f5b 100644 --- a/creates/timestamp_and_wait.js +++ b/creates/timestamp_and_wait.js @@ -1,27 +1,15 @@ -/** - * `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 base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100'; const perform = async (z, bundle) => { const response = await z.request({ method: 'POST', - url: url('/timestamp/wait'), - body: timestampBody(z, bundle.inputData), + url: `${base()}/zapier/v1/timestamp/wait`, + body: { + data: bundle.inputData.data, + hashAlg: bundle.inputData.hashAlg || undefined, + }, }); - const status = response.data || {}; - const id = status.id || status.jobId; - return { ...status, id, jobId: status.jobId || id }; + return response.data; }; module.exports = { @@ -30,12 +18,37 @@ module.exports = { display: { label: 'Create Timestamp and Wait', description: - '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.', + 'Submits data for blockchain timestamping and waits for the certificate.', }, operation: { - inputFields: timestampInputFields(), + 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', + }, + ], perform, - sample: statusSample, - outputFields: [{ key: 'jobId', label: 'Job ID' }, ...statusOutputFields.slice(1)], + 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' }, + ], }, }; diff --git a/creates/verify_timestamp.js b/creates/verify_timestamp.js index 360afbc..510926a 100644 --- a/creates/verify_timestamp.js +++ b/creates/verify_timestamp.js @@ -1,14 +1,9 @@ -/** - * `POST /zapier/v1/verify` — verify one certificate. - * @module creates/verify_timestamp - */ - -const { url, verifyOutputFields, verifySample } = require('../lib/middleware'); +const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100'; const perform = async (z, bundle) => { const response = await z.request({ method: 'POST', - url: url('/verify'), + url: `${base()}/zapier/v1/verify`, body: { certificate: bundle.inputData.certificate }, }); return response.data; @@ -19,7 +14,7 @@ module.exports = { noun: 'Verification', display: { label: 'Verify Timestamp', - description: 'Verifies the authenticity of a timestamp certificate.', + description: 'Verify a timestamp certificate.', }, operation: { inputFields: [ @@ -28,11 +23,14 @@ module.exports = { label: 'Certificate', type: 'text', required: true, - helpText: 'The certificate returned when a timestamp job completed.', }, ], perform, - sample: verifySample, - outputFields: verifyOutputFields, + 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' }, + ], }, }; diff --git a/index.js b/index.js index dfdc196..8727990 100644 --- a/index.js +++ b/index.js @@ -1,100 +1,82 @@ /** * 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 batchTimestamp = require('./creates/batch_timestamp'); 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 batchTimestamp = require('./creates/batch_timestamp'); +const addNumbers = require('./creates/add_numbers'); 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 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`. + * Attach middleware API key to every outbound request. * @param {object} request * @param {object} _z * @param {object} bundle */ const addApiKey = (request, _z, bundle) => { request.headers = request.headers || {}; - 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; - } + request.headers.Authorization = `Bearer ${bundle.authData.api_key}`; return request; }; /** - * 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. + * Map middleware billing errors to Zapier errors. * @param {object} response * @param {object} z */ const mapMiddlewareErrors = (response, z) => { - if (response.skipThrowForStatus || response.status < 400) { - return response; + 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, + ); } - const { message, code, upgradeUrl } = describeError(response); - - if (response.status === 429) { - throw new z.errors.ThrottledError(message); + 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 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); + return response; }; +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: require('zapier-platform-core').version, + platformVersion, authentication, beforeRequest: [addApiKey], afterResponse: [mapMiddlewareErrors], triggers: { [timestampCompleted.key]: timestampCompleted, - [timestampFailed.key]: timestampFailed, }, creates: { + [addNumbers.key]: addNumbers, [timestampAndWait.key]: timestampAndWait, [createTimestamp.key]: createTimestamp, - [batchTimestamp.key]: batchTimestamp, [verifyTimestamp.key]: verifyTimestamp, - [batchVerify.key]: batchVerify, - [batchStatus.key]: batchStatus, - [getReceipt.key]: getReceipt, + [batchTimestamp.key]: batchTimestamp, }, searches: { [jobStatus.key]: jobStatus, - [jobVerification.key]: jobVerification, [hashLookup.key]: hashLookup, [treeLookup.key]: treeLookup, - [findUsage.key]: findUsage, }, }; diff --git a/lib/hookTrigger.js b/lib/hookTrigger.js deleted file mode 100644 index 0650e8b..0000000 --- a/lib/hookTrigger.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * 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; diff --git a/lib/middleware.js b/lib/middleware.js deleted file mode 100644 index ee5bd72..0000000 --- a/lib/middleware.js +++ /dev/null @@ -1,233 +0,0 @@ -/** - * 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 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} - */ -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, -}; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index fe11a63..0000000 --- a/package-lock.json +++ /dev/null @@ -1,819 +0,0 @@ -{ - "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" - } - } - } -} diff --git a/package.json b/package.json index 1acf24f..dcabbfb 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,10 @@ { "name": "verae-zapier", - "version": "1.1.0", + "version": "1.0.0", "description": "Zapier CLI app for Verae Timestamping via middleware", "main": "index.js", "scripts": { - "test": "node --test test/*.test.js", - "test:e2e": "node --test test/e2e.test.js" + "test": "node --test test/**/*.test.js" }, "engines": { "node": ">=18", diff --git a/searches/hash_lookup.js b/searches/hash_lookup.js index d41974d..cc60ac4 100644 --- a/searches/hash_lookup.js +++ b/searches/hash_lookup.js @@ -1,45 +1,34 @@ -/** - * `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 base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100'; -const { url, searchOne } = require('../lib/middleware'); - -const perform = (z, bundle) => - searchOne(z, { url: url(`/hashes/${encodeURIComponent(bundle.inputData.sha256)}`) }); +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; + } +}; module.exports = { key: 'hash_lookup', noun: 'Timestamp', display: { label: 'Find Timestamp by SHA256', - description: 'Looks up an existing timestamp on the main Verae chain by the SHA256 hex digest of its data.', + description: 'Looks up an existing timestamp for a SHA256 hex digest (mock/middleware).', }, operation: { inputFields: [ - { - key: 'sha256', - label: 'SHA256', - type: 'string', - required: true, - helpText: '64 hex characters.', - }, + { key: 'sha256', label: 'SHA256', type: 'string', required: true }, ], 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' }, - ], }, }; diff --git a/searches/job_status.js b/searches/job_status.js index 294ecb7..b39fc8c 100644 --- a/searches/job_status.js +++ b/searches/job_status.js @@ -1,26 +1,33 @@ -/** - * `GET /zapier/v1/status/{jobId}` — 404 means no such job (empty result). - * @module searches/job_status - */ +const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100'; -const { url, searchOne, statusOutputFields, statusSample } = require('../lib/middleware'); - -const perform = (z, bundle) => - searchOne(z, { url: url(`/status/${encodeURIComponent(bundle.inputData.jobId)}`) }); +const perform = async (z, bundle) => { + const response = await z.request({ + url: `${base()}/zapier/v1/status/${encodeURIComponent(bundle.inputData.jobId)}`, + }); + return [response.data]; +}; module.exports = { key: 'job_status', noun: 'Job', display: { label: 'Find Job Status', - description: 'Looks up a timestamp job by its job ID.', + description: 'Look up a timestamp job by ID.', }, operation: { inputFields: [ { key: 'jobId', label: 'Job ID', type: 'string', required: true }, ], perform, - sample: statusSample, - outputFields: statusOutputFields, + 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' }, + ], }, }; diff --git a/searches/job_verification.js b/searches/job_verification.js deleted file mode 100644 index a9cff83..0000000 --- a/searches/job_verification.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * `GET /zapier/v1/status/{jobId}/verification` — block details and - * verification info for a job. 404 means no such job. - * @module searches/job_verification - */ - -const { url, searchOne, statusOutputFields, statusSample } = require('../lib/middleware'); - -const perform = (z, bundle) => - searchOne(z, { - url: url(`/status/${encodeURIComponent(bundle.inputData.jobId)}/verification`), - }); - -module.exports = { - key: 'job_verification', - noun: 'Verification', - display: { - label: 'Find Job Verification Details', - description: 'Retrieves block details and verification information for a timestamp job.', - }, - operation: { - inputFields: [ - { key: 'jobId', label: 'Job ID', type: 'string', required: true }, - ], - perform, - sample: statusSample, - outputFields: statusOutputFields, - }, -}; diff --git a/searches/tree_lookup.js b/searches/tree_lookup.js index 7b4c9bb..a1ba223 100644 --- a/searches/tree_lookup.js +++ b/searches/tree_lookup.js @@ -1,17 +1,18 @@ -/** - * `GET /zapier/v1/hashes/{sha256}?includeAttached=true&includeTree=true` — - * main chain first, then tree-node archives for leaves sealed in a batch - * Merkle summary. 404 means no match anywhere. - * @module searches/tree_lookup - */ +const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100'; -const { url, searchOne } = require('../lib/middleware'); - -const perform = (z, bundle) => - searchOne(z, { - url: url(`/hashes/${encodeURIComponent(bundle.inputData.sha256)}`), - params: { includeAttached: 'true', includeTree: 'true' }, - }); +const perform = async (z, bundle) => { + try { + const sha = encodeURIComponent(bundle.inputData.sha256); + const response = await z.request({ + method: 'GET', + url: `${base()}/zapier/v1/hashes/${sha}?includeAttached=true&includeTree=true`, + }); + return [response.data]; + } catch (err) { + if (err.status === 404) return []; + throw err; + } +}; module.exports = { key: 'tree_lookup', @@ -36,20 +37,9 @@ module.exports = { sample: { sha256: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', exists: true, - jobId: '550e8400-e29b-41d4-a716-446655440000', - includeAttached: true, - includeTree: true, itemizedOnMainChain: false, merkleRoot: 'ab'.repeat(32), proofOk: true, }, - outputFields: [ - { key: 'sha256', label: 'SHA256' }, - { key: 'exists', label: 'Exists', type: 'boolean' }, - { key: 'jobId', label: 'Job ID' }, - { key: 'itemizedOnMainChain', label: 'Itemized On Main Chain', type: 'boolean' }, - { key: 'merkleRoot', label: 'Merkle Root' }, - { key: 'proofOk', label: 'Proof OK', type: 'boolean' }, - ], }, }; diff --git a/searches/usage.js b/searches/usage.js deleted file mode 100644 index 7b2b24f..0000000 --- a/searches/usage.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * `GET /zapier/v1/auth/me` — the connection's tenant, plan, and month-to-date - * usage against plan limits. Lets a Zap branch before it hits a quota. - * @module searches/usage - */ - -const { url } = require('../lib/middleware'); - -const perform = async (z) => { - const response = await z.request({ url: url('/auth/me') }); - const me = response.data || {}; - const usage = me.usage || {}; - const limits = usage.limits || {}; - return [ - { - id: me.tenantId, - tenantId: me.tenantId, - plan: me.plan, - period: usage.period, - timestamps: usage.timestamps, - verifications: usage.verifications, - statusChecks: usage.statusChecks, - batchTimestamps: usage.batchTimestamps, - timestampsLimit: limits.timestamps ?? null, - verificationsLimit: limits.verifications ?? null, - batchAllowed: Boolean(limits.batch), - batchMaxItems: limits.batchMaxItems ?? null, - username: me.user && me.user.username, - }, - ]; -}; - -module.exports = { - key: 'usage', - noun: 'Usage', - display: { - label: 'Find Usage', - description: 'Returns your plan and month-to-date usage against its limits.', - }, - operation: { - inputFields: [], - perform, - sample: { - id: 'tenant-example', - tenantId: 'tenant-example', - plan: 'pro', - period: '2026-09', - timestamps: 12, - verifications: 3, - statusChecks: 20, - batchTimestamps: 0, - timestampsLimit: 5000, - verificationsLimit: 5000, - batchAllowed: true, - batchMaxItems: 100, - username: 'alice', - }, - outputFields: [ - { key: 'tenantId', label: 'Tenant ID' }, - { key: 'plan', label: 'Plan' }, - { key: 'period', label: 'Period' }, - { key: 'timestamps', label: 'Timestamps Used', type: 'integer' }, - { key: 'verifications', label: 'Verifications Used', type: 'integer' }, - { key: 'statusChecks', label: 'Status Checks', type: 'integer' }, - { key: 'batchTimestamps', label: 'Batch Timestamps', type: 'integer' }, - { key: 'timestampsLimit', label: 'Timestamp Limit', type: 'integer' }, - { key: 'verificationsLimit', label: 'Verification Limit', type: 'integer' }, - { key: 'batchAllowed', label: 'Batch Allowed', type: 'boolean' }, - { key: 'batchMaxItems', label: 'Batch Max Items', type: 'integer' }, - { key: 'username', label: 'Verae Username' }, - ], - }, -}; diff --git a/test/app.test.js b/test/app.test.js index 4359963..0c160d1 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,392 +1,95 @@ /** - * Unit tests for the Verae Zapier app — no network, no Zapier CLI. - * Every operation is exercised with a fake `z` whose request() answers from - * a route table keyed "METHOD /zapier/v1/path". + * GATE 11 — Zapier package shape tests (no live Zapier CLI required) */ const { describe, it } = require('node:test'); const assert = require('node:assert/strict'); -process.env.MIDDLEWARE_BASE_URL = 'https://mw.example.com/'; - const App = require('../index'); const authentication = require('../authentication'); -const mw = require('../lib/middleware'); -const createTimestamp = require('../creates/create_timestamp'); const timestampAndWait = require('../creates/timestamp_and_wait'); -const batchTimestamp = require('../creates/batch_timestamp'); +const createTimestamp = require('../creates/create_timestamp'); 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 batchTimestamp = require('../creates/batch_timestamp'); 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 timestampCompleted = require('../triggers/timestamp_completed'); -const timestampFailed = require('../triggers/timestamp_failed'); -const findUsage = require('../searches/usage'); -const BASE = 'https://mw.example.com/zapier/v1'; - -class ZError extends Error { - constructor(message, code, status) { - super(message); - this.name = 'Error'; - this.code = code; - this.status = status; - } -} -class ThrottledError extends Error { - constructor(message) { - super(message); - this.name = 'ThrottledError'; - } -} - -/** - * @param {Record} routes "METHOD /path" -> response or (opts) => response - */ -const makeZ = (routes) => { - const calls = []; - return { - calls, - errors: { Error: ZError, ThrottledError }, - stashFile: async (_stream, _len, filename) => `https://zapier-stash.example/${filename}`, - request: async (opts) => { - const method = (opts.method || 'GET').toUpperCase(); - assert.ok(opts.url.startsWith(BASE), `unexpected base in ${opts.url}`); - const path = opts.url.slice(BASE.length); - calls.push({ ...opts, method, path }); - const handler = routes[`${method} ${path}`]; - if (!handler) throw new Error(`unexpected request ${method} ${path}`); - const res = typeof handler === 'function' ? handler(opts) : handler; - return { status: 200, data: {}, ...res }; - }, - }; -}; - -const bundle = (inputData = {}, extra = {}) => ({ - authData: { api_key: 'zmw_test' }, - inputData, - ...extra, -}); - -describe('app definition', () => { - it('uses custom API key auth against /auth/me', async () => { +describe('verae-zapier app definition', () => { + it('exports authentication with api_key field', () => { assert.equal(authentication.type, 'custom'); assert.ok(authentication.fields.some((f) => f.key === 'api_key')); - const z = makeZ({ 'GET /auth/me': { data: { valid: true, tenantId: 't1', plan: 'pro' } } }); - const me = await authentication.test(z, bundle()); - assert.equal(me.tenantId, 't1'); - assert.equal(authentication.connectionLabel, '{{tenantId}} ({{plan}})'); }); - it('wires every trigger, create, and search', () => { - assert.deepEqual(Object.keys(App.triggers).sort(), ['timestamp_completed', 'timestamp_failed']); - assert.deepEqual( - Object.keys(App.creates).sort(), - ['batch_status', 'batch_timestamp', 'batch_verify', 'create_timestamp', 'get_receipt', 'timestamp_and_wait', 'verify_timestamp'], - ); - assert.deepEqual(Object.keys(App.searches).sort(), ['hash_lookup', 'job_status', 'job_verification', 'tree_lookup', 'usage']); + it('wires creates, searches, triggers', () => { + assert.ok(App.creates.add_numbers); + assert.ok(App.creates.timestamp_and_wait); + assert.ok(App.creates.create_timestamp); + assert.ok(App.creates.verify_timestamp); + assert.ok(App.creates.batch_timestamp); + assert.ok(App.searches.job_status); + assert.ok(App.searches.hash_lookup); + assert.ok(App.searches.tree_lookup); + assert.ok(App.triggers.timestamp_completed); }); - it('builds /zapier/v1 URLs from MIDDLEWARE_BASE_URL (trailing slash tolerated)', () => { - assert.equal(mw.url('/timestamp/wait'), 'https://mw.example.com/zapier/v1/timestamp/wait'); - }); - - it('honours MIDDLEWARE_PATH_PREFIX for a fronting edge', () => { - const saved = process.env.MIDDLEWARE_PATH_PREFIX; - try { - process.env.MIDDLEWARE_PATH_PREFIX = 'v1/'; - assert.equal(mw.url('/timestamp'), 'https://mw.example.com/v1/timestamp'); - process.env.MIDDLEWARE_PATH_PREFIX = ''; - assert.equal(mw.url('/timestamp'), 'https://mw.example.com/timestamp'); - } finally { - if (saved === undefined) delete process.env.MIDDLEWARE_PATH_PREFIX; - else process.env.MIDDLEWARE_PATH_PREFIX = saved; - } - }); -}); - -describe('request middleware', () => { - const addApiKey = App.beforeRequest[0]; - const mapErrors = App.afterResponse[0]; - const z = makeZ({}); - - it('adds the API key as both a bearer token and x-api-key', () => { - const req = addApiKey({}, z, { authData: { api_key: 'zmw_abc' } }); - assert.equal(req.headers.Authorization, 'Bearer zmw_abc'); - assert.equal(req.headers['x-api-key'], 'zmw_abc'); - const untouched = addApiKey({ headers: { Authorization: 'Bearer other' } }, z, { authData: { api_key: 'zmw_abc' } }); - assert.equal(untouched.headers.Authorization, 'Bearer other'); - assert.equal(addApiKey({}, z, { authData: {} }).headers['x-api-key'], undefined); - }); - - it('maps 402 quota errors with the upgrade URL from details', () => { - const response = { - status: 402, - data: { error: 'Monthly timestamp quota exceeded', code: 'QUOTA_EXCEEDED', details: { upgradeUrl: 'https://veraetime.net/billing' } }, + it('create actions target middleware paths', () => { + process.env.MIDDLEWARE_BASE_URL = 'https://mw.example.com'; + const urls = []; + const z = { + request: async (opts) => { + urls.push(opts.url); + return { data: { ok: true } }; + }, }; - assert.throws( - () => mapErrors(response, z), - (err) => err instanceof ZError && err.code === 'QUOTA_EXCEEDED' && err.status === 402 - && err.message === 'Monthly timestamp quota exceeded. Upgrade at https://veraetime.net/billing.', - ); + const bundle = { + authData: { api_key: 'zmw_test' }, + inputData: { data: 'x', certificate: 'c', items: 'a\nb', jobId: 'j1' }, + }; + + return Promise.all([ + timestampAndWait.operation.perform(z, bundle), + createTimestamp.operation.perform(z, bundle), + verifyTimestamp.operation.perform(z, bundle), + batchTimestamp.operation.perform(z, bundle), + jobStatus.operation.perform(z, bundle), + ]).then(() => { + assert.ok(urls.some((u) => u.endsWith('/zapier/v1/timestamp/wait'))); + assert.ok(urls.some((u) => u.endsWith('/zapier/v1/timestamp'))); + assert.ok(urls.some((u) => u.endsWith('/zapier/v1/verify'))); + assert.ok(urls.some((u) => u.endsWith('/zapier/v1/timestamp/batch'))); + assert.ok(urls.some((u) => u.includes('/zapier/v1/status/'))); + }); }); - it('maps 403 plan and batch-limit errors as billing errors', () => { - for (const code of ['PLAN_UPGRADE_REQUIRED', 'BATCH_LIMIT_EXCEEDED']) { - assert.throws( - () => mapErrors({ status: 403, data: { error: 'nope', code, details: { upgradeUrl: 'https://u' } } }, z), - (err) => err instanceof ZError && err.code === code && /Upgrade at https:\/\/u\./.test(err.message), - ); - } + it('trigger subscribe/unsubscribe shapes', async () => { + process.env.MIDDLEWARE_BASE_URL = 'https://mw.example.com'; + const calls = []; + const z = { + request: async (opts) => { + calls.push(opts); + return { data: { id: 'hook-1' } }; + }, + }; + + const id = await timestampCompleted.operation.performSubscribe(z, { + targetUrl: 'https://hooks.zapier.com/x', + authData: { api_key: 'k' }, + }); + assert.equal(id, 'hook-1'); + assert.equal(calls[0].method, 'POST'); + assert.match(calls[0].url, /webhooks\/subscribe$/); + assert.equal(calls[0].body.event, 'timestamp.completed'); + + await timestampCompleted.operation.performUnsubscribe(z, { + subscribeData: 'hook-1', + authData: { api_key: 'k' }, + }); + assert.equal(calls[1].method, 'DELETE'); + assert.match(calls[1].url, /webhooks\/unsubscribe$/); }); - it('maps other JSON errors, plain-text errors, 429, and leaves skipThrowForStatus alone', () => { - assert.throws( - () => mapErrors({ status: 401, data: { error: 'Invalid API key', code: 'UNAUTHORIZED' } }, z), - (err) => err instanceof ZError && err.message === 'Invalid API key' && err.code === 'UNAUTHORIZED' && err.status === 401, - ); - assert.throws( - () => mapErrors({ status: 502, data: 'Bad Gateway', content: 'Bad Gateway' }, z), - (err) => err instanceof ZError && err.message === 'Bad Gateway' && err.code === 'HTTP_502', - ); - assert.throws(() => mapErrors({ status: 429, data: { error: 'Rate limit exceeded', code: 'RATE_LIMITED' } }, z), ThrottledError); - const skipped = { status: 404, skipThrowForStatus: true }; - assert.equal(mapErrors(skipped, z), skipped); - const ok = { status: 202, data: {} }; - assert.equal(mapErrors(ok, z), ok); - }); -}); - -describe('creates', () => { - it('create_timestamp posts data/hashAlg to /timestamp', async () => { - const z = makeZ({ - 'POST /timestamp': (opts) => { - assert.deepEqual(opts.body, { data: 'hello', hashAlg: 'SHA256' }); - return { status: 202, data: { jobId: 'job-1', sha256: 'abc', existing: false } }; - }, - }); - const out = await createTimestamp.operation.perform(z, bundle({ data: 'hello', hashAlg: 'SHA256' })); - assert.equal(out.jobId, 'job-1'); - }); - - it('create_timestamp accepts a SHA256 instead of data, plus metadata, and validates input', async () => { - const sha = 'AB'.repeat(32); - const z = makeZ({ - 'POST /timestamp': (opts) => { - assert.deepEqual(opts.body, { - sha256: sha.toLowerCase(), - hashAlg: 'SHA256', - publicMetadata: { title: 'Doc' }, - privateMetadata: { owner: 'me' }, - }); - return { status: 202, data: { jobId: 'job-5', sha256: sha.toLowerCase(), existing: true } }; - }, - }); - const out = await createTimestamp.operation.perform(z, bundle({ - sha256: ` ${sha} `, hashAlg: 'SHA256', publicMetadata: { title: 'Doc' }, privateMetadata: { owner: 'me' }, - })); - assert.equal(out.existing, true); - - await assert.rejects(createTimestamp.operation.perform(z, bundle({})), (err) => /Data or a SHA256/.test(err.message) && err.status === 400); - await assert.rejects(createTimestamp.operation.perform(z, bundle({ sha256: 'not-hex' })), (err) => /64 hexadecimal/.test(err.message)); - // Empty metadata dicts are dropped, not sent. - const z2 = makeZ({ 'POST /timestamp': (opts) => { assert.deepEqual(opts.body, { data: 'x', hashAlg: undefined }); return { status: 202, data: { jobId: 'j' } }; } }); - await createTimestamp.operation.perform(z2, bundle({ data: 'x', publicMetadata: {}, privateMetadata: null })); - }); - - it('timestamp_and_wait posts to /timestamp/wait and normalises id/jobId', async () => { - const z = makeZ({ - 'POST /timestamp/wait': { data: { id: 'job-2', status: 'completed', result: 'cert' } }, - }); - const out = await timestampAndWait.operation.perform(z, bundle({ data: 'x' })); - assert.equal(out.id, 'job-2'); - assert.equal(out.jobId, 'job-2'); - assert.equal(out.result, 'cert'); - - const pending = makeZ({ 'POST /timestamp/wait': { data: { id: 'job-3', jobId: 'job-3', status: 'pending' } } }); - const late = await timestampAndWait.operation.perform(pending, bundle({ data: 'x' })); - assert.equal(late.status, 'pending'); - assert.equal(late.id, 'job-3'); - }); - - it('batch_timestamp accepts list input or newline text and rejects empty input', async () => { - const z = makeZ({ - 'POST /timestamp/batch': (opts) => { - assert.deepEqual(opts.body.items, [{ data: 'a', hashAlg: 'SHA256' }, { data: 'b', hashAlg: 'SHA256' }]); - return { status: 202, data: { jobIds: ['j1', 'j2'] } }; - }, - }); - assert.deepEqual(await batchTimestamp.operation.perform(z, bundle({ items: ['a', 'b'], hashAlg: 'SHA256' })), { jobIds: ['j1', 'j2'], count: 2 }); - assert.deepEqual(await batchTimestamp.operation.perform(z, bundle({ items: 'a\n b \n\n', hashAlg: 'SHA256' })), { jobIds: ['j1', 'j2'], count: 2 }); - await assert.rejects(batchTimestamp.operation.perform(z, bundle({ items: '' })), ZError); - }); - - it('verify_timestamp posts the certificate to /verify', async () => { - const z = makeZ({ - 'POST /verify': (opts) => { - assert.deepEqual(opts.body, { certificate: 'cert' }); - return { data: { valid: true, timestamp: '2023-01-01T12:00:00Z', blockIndex: 42 } }; - }, - }); - const out = await verifyTimestamp.operation.perform(z, bundle({ certificate: 'cert' })); - assert.equal(out.valid, true); - }); - - it('batch_verify posts to /verify/batch and summarises', async () => { - const z = makeZ({ - 'POST /verify/batch': (opts) => { - assert.deepEqual(opts.body, { certificates: ['c1', 'c2'] }); - return { data: { results: [{ valid: true, blockIndex: 1 }, { valid: false }] } }; - }, - }); - const out = await batchVerify.operation.perform(z, bundle({ certificates: ['c1', 'c2'] })); - assert.deepEqual([out.total, out.validCount, out.invalidCount, out.allValid], [2, 1, 1, false]); - }); - - it('batch_status posts to /status/batch and preserves input order', async () => { - const z = makeZ({ - 'POST /status/batch': (opts) => { - assert.deepEqual(opts.body, { jobIds: ['j1', 'j2', 'j3'] }); - return { data: { results: { j2: { id: 'j2', status: 'pending' }, j1: { id: 'j1', status: 'completed' } } } }; - }, - }); - const out = await batchStatus.operation.perform(z, bundle({ jobIds: ['j1', 'j2', 'j3'] })); - assert.deepEqual(out.results.map((r) => [r.id, r.status]), [['j1', 'completed'], ['j2', 'pending'], ['j3', 'unknown']]); - assert.deepEqual([out.completedCount, out.pendingCount, out.failedCount, out.allCompleted], [1, 1, 0, false]); - }); - - it('get_receipt fetches JSON and optionally stashes the PDF', async () => { - const z = makeZ({ - 'GET /receipts/job-1': (opts) => ( - opts.params.format === 'pdf' - ? { data: 'pdf-bytes' } - : { data: { type: 'verae.retrieval-receipt', jobId: 'job-1', certificate: 'cert' } } - ), - }); - const plain = await getReceipt.operation.perform(z, bundle({ jobId: 'job-1' })); - assert.equal(plain.id, 'job-1'); - assert.equal(plain.certificate, 'cert'); - assert.equal(plain.pdf, undefined); - - const withPdf = await getReceipt.operation.perform(z, bundle({ jobId: 'job-1', includePdf: true })); - assert.equal(withPdf.pdf, 'https://zapier-stash.example/verae-receipt-job-1.pdf'); - const pdfCall = z.calls.find((c) => c.params && c.params.format === 'pdf'); - assert.equal(pdfCall.raw, true); - }); -}); - -describe('searches', () => { - it('job_status wraps a hit, returns [] on 404, and raises the middleware message otherwise', async () => { - const z = makeZ({ - 'GET /status/job-1': { data: { id: 'job-1', status: 'completed' } }, - 'GET /status/missing': { status: 404, data: { error: 'Job not found', code: 'NOT_FOUND' } }, - 'GET /status/broken': { status: 500, data: { error: 'chain down', code: 'INTERNAL_ERROR' } }, - }); - assert.deepEqual(await jobStatus.operation.perform(z, bundle({ jobId: 'job-1' })), [{ id: 'job-1', status: 'completed' }]); - assert.deepEqual(await jobStatus.operation.perform(z, bundle({ jobId: 'missing' })), []); - await assert.rejects(jobStatus.operation.perform(z, bundle({ jobId: 'broken' })), (err) => err.message === 'chain down' && err.status === 500); - assert.ok(z.calls.every((c) => c.skipThrowForStatus === true), 'searches must opt out of throwForStatus'); - }); - - it('job_verification hits /status/{jobId}/verification', async () => { - const z = makeZ({ 'GET /status/job-1/verification': { data: { id: 'job-1', metadata: { blockIndex: 42 } } } }); - const [out] = await jobVerification.operation.perform(z, bundle({ jobId: 'job-1' })); - assert.equal(out.metadata.blockIndex, 42); - }); - - it('hash_lookup and tree_lookup hit /hashes/{sha256}, tree adds the include flags, 501 is surfaced', async () => { - const sha = 'a'.repeat(64); - const z = makeZ({ - [`GET /hashes/${sha}`]: (opts) => ( - opts.params - ? { data: { sha256: sha, exists: true, includeTree: true } } - : { data: { sha256: sha, exists: true, jobId: 'job-1' } } - ), - [`GET /hashes/${'b'.repeat(64)}`]: { status: 501, data: { error: 'Hash lookup is not on the live Verae OpenAPI', code: 'NOT_IMPLEMENTED' } }, - }); - const [hit] = await hashLookup.operation.perform(z, bundle({ sha256: sha })); - assert.equal(hit.jobId, 'job-1'); - const [tree] = await treeLookup.operation.perform(z, bundle({ sha256: sha })); - assert.equal(tree.includeTree, true); - assert.deepEqual(z.calls[1].params, { includeAttached: 'true', includeTree: 'true' }); - await assert.rejects(hashLookup.operation.perform(z, bundle({ sha256: 'b'.repeat(64) })), (err) => err.code === 'NOT_IMPLEMENTED' && err.status === 501); - }); - - it('usage search maps /auth/me into one flat record', async () => { - const z = makeZ({ - 'GET /auth/me': { - data: { - valid: true, tenantId: 't1', plan: 'free', user: { username: 'ada' }, - usage: { period: '2026-09', timestamps: 3, verifications: 1, statusChecks: 4, batchTimestamps: 0, limits: { timestamps: 50, verifications: 50, batch: false, batchMaxItems: 0 } }, - }, - }, - }); - const [u] = await findUsage.operation.perform(z, bundle()); - assert.equal(u.id, 't1'); - assert.equal(u.plan, 'free'); - assert.equal(u.timestamps, 3); - assert.equal(u.timestampsLimit, 50); - assert.equal(u.batchAllowed, false); - assert.equal(u.username, 'ada'); - }); - - it('every search sample has fields Zapier can key on', () => { - for (const s of [jobStatus, jobVerification, hashLookup, treeLookup, findUsage]) { - assert.ok(s.operation.sample && Object.keys(s.operation.sample).length > 0, `${s.key} needs a sample`); - } - }); -}); - -describe('triggers', () => { - it('subscribe stores hook id + event; unsubscribe sends hookId', async () => { - const z = makeZ({ - 'POST /webhooks/subscribe': (opts) => { - assert.equal(opts.body.targetUrl, 'https://hooks.zapier.com/x'); - return { status: 201, data: { id: 'hook-1', event: opts.body.event } }; - }, - 'DELETE /webhooks/unsubscribe': (opts) => { - assert.equal(opts.body.hookId, 'hook-1'); - return { data: { removed: true } }; - }, - }); - const sub = await timestampCompleted.operation.performSubscribe(z, bundle({}, { targetUrl: 'https://hooks.zapier.com/x' })); - assert.deepEqual(sub, { id: 'hook-1', event: 'timestamp.completed' }); - assert.equal(z.calls[0].body.event, 'timestamp.completed'); - - await timestampCompleted.operation.performUnsubscribe(z, bundle({}, { subscribeData: sub, targetUrl: 'https://hooks.zapier.com/x' })); - // Legacy subscribeData (a bare string) still works. - await timestampCompleted.operation.performUnsubscribe(z, bundle({}, { subscribeData: 'hook-1' })); - assert.equal(z.calls.filter((c) => c.method === 'DELETE').length, 2); - }); - - it('timestamp_failed subscribes to timestamp.failed', async () => { - const z = makeZ({ 'POST /webhooks/subscribe': { status: 201, data: { id: 'hook-2' } } }); - const sub = await timestampFailed.operation.performSubscribe(z, bundle({}, { targetUrl: 'https://hooks.zapier.com/y' })); - assert.equal(sub.event, 'timestamp.failed'); - assert.equal(z.calls[0].body.event, 'timestamp.failed'); - }); - - it('perform gives each delivered payload an id (the jobId) for dedupe', async () => { - const payload = { event: 'timestamp.completed', jobId: 'job-9', tenantId: 't', status: { id: 'job-9', status: 'completed' } }; - const [item] = await timestampCompleted.operation.perform(makeZ({}), bundle({}, { cleanedRequest: payload })); - assert.equal(item.id, 'job-9'); - assert.equal(item.status.status, 'completed'); - assert.deepEqual(await timestampCompleted.operation.performList(makeZ({}), bundle()), []); - assert.equal(timestampFailed.operation.sample.status.status, 'failed'); - }); -}); - -describe('lib/middleware.describeError', () => { - it('prefers JSON bodies (with upgradeUrl), then text, then a generic message', () => { - assert.deepEqual( - mw.describeError({ status: 402, data: { error: 'quota', code: 'QUOTA_EXCEEDED', details: { upgradeUrl: 'https://u', limit: 50 } } }), - { message: 'quota', code: 'QUOTA_EXCEEDED', upgradeUrl: 'https://u', details: { upgradeUrl: 'https://u', limit: 50 } }, - ); - assert.deepEqual(mw.describeError({ status: 504, data: 'upstream timeout', content: 'upstream timeout' }), { message: 'upstream timeout', code: 'HTTP_504' }); - assert.deepEqual(mw.describeError({ status: 502, content: 'bad gateway' }), { message: 'Verae middleware request failed with HTTP 502', code: 'HTTP_502' }); + it('beforeRequest adds Authorization bearer', () => { + const req = App.beforeRequest[0]({}, null, { authData: { api_key: 'zmw_abc' } }); + assert.equal(req.headers.Authorization, 'Bearer zmw_abc'); }); }); diff --git a/test/e2e.test.js b/test/e2e.test.js deleted file mode 100644 index 0880f7a..0000000 --- a/test/e2e.test.js +++ /dev/null @@ -1,202 +0,0 @@ -/** - * End-to-end test: drives the real app definition through zapier-platform-core - * (beforeRequest / afterResponse / throwForStatus) against a running - * verae-middleware. - * - * Opt in with an API key for a tenant on that middleware: - * - * MIDDLEWARE_BASE_URL=http://127.0.0.1:3100 MIDDLEWARE_API_KEY=zmw_... npm run test:e2e - * - * Works for any plan: on a free tenant the batch step asserts the 403 - * PLAN_UPGRADE_REQUIRED mapping instead of a successful batch. The hook - * delivery test needs the middleware to reach this machine; set - * E2E_SKIP_HOOK_DELIVERY=1 when it cannot (e.g. the live lab middleware). - * The middleware may run in mock-chain mode or against api.veraetime.net. - */ - -const { describe, it, before, after } = require('node:test'); -const assert = require('node:assert/strict'); -const http = require('node:http'); -const { createHash } = require('node:crypto'); - -const apiKey = process.env.MIDDLEWARE_API_KEY; -const enabled = Boolean(apiKey); - -describe('e2e against verae-middleware', { skip: !enabled && 'set MIDDLEWARE_API_KEY (and MIDDLEWARE_BASE_URL) to run' }, () => { - const zapier = require('zapier-platform-core'); - const App = require('../index'); - const appTester = zapier.createAppTester(App); - const authData = { api_key: apiKey }; - const run = (op, inputData = {}, extra = {}) => appTester(op, { authData, inputData, ...extra }); - - /** Local HTTP sink that plays the role of hooks.zapier.com. */ - const deliveries = []; - let hookServer; - let hookUrl; - - before(async () => { - await new Promise((resolve) => { - hookServer = http.createServer((req, res) => { - let body = ''; - req.on('data', (c) => { body += c; }); - req.on('end', () => { - deliveries.push(JSON.parse(body || '{}')); - res.writeHead(200); - res.end('ok'); - }); - }); - hookServer.listen(0, '127.0.0.1', () => { - hookUrl = `http://127.0.0.1:${hookServer.address().port}/hook`; - resolve(); - }); - }); - }); - - after(async () => { - await new Promise((resolve) => hookServer.close(resolve)); - }); - - const waitFor = async (predicate, ms = 15000) => { - const deadline = Date.now() + ms; - while (Date.now() < deadline) { - const value = await predicate(); - if (value) return value; - await new Promise((r) => setTimeout(r, 200)); - } - throw new Error('timed out waiting'); - }; - - let tenantId; - let plan; - let completed; // StatusResponse of a completed job - const skipHookDelivery = process.env.E2E_SKIP_HOOK_DELIVERY === '1'; - const data = `zapier-e2e-${Date.now()}`; - const sha256 = createHash('sha256').update(data).digest('hex'); - - it('authenticates and labels the connection', async () => { - const me = await run(App.authentication.test); - assert.equal(me.valid, true); - assert.ok(me.tenantId); - assert.ok(me.plan); - tenantId = me.tenantId; - plan = me.plan; - }); - - it('rejects a bad API key with the middleware message', async () => { - await assert.rejects( - appTester(App.authentication.test, { authData: { api_key: 'zmw_definitely-not-a-key' } }), - (err) => /Invalid API key/.test(err.message), - ); - }); - - it('subscribes and unsubscribes a hook, creates a timestamp, and sees it complete', async () => { - const sub = await run(App.triggers.timestamp_completed.operation.performSubscribe, {}, { targetUrl: hookUrl }); - assert.ok(sub.id); - - const created = await run(App.creates.create_timestamp.operation.perform, { data }); - assert.ok(created.jobId); - - let status; - if (skipHookDelivery) { - status = await waitFor(async () => { - const [s] = await run(App.searches.job_status.operation.perform, { jobId: created.jobId }); - return s && s.status === 'completed' ? s : null; - }); - } else { - const delivered = await waitFor(async () => deliveries.find((d) => d.jobId === created.jobId)); - assert.equal(delivered.event, 'timestamp.completed'); - assert.equal(delivered.tenantId, tenantId); - assert.equal(delivered.status.status, 'completed'); - const [item] = await run(App.triggers.timestamp_completed.operation.perform, {}, { cleanedRequest: delivered }); - assert.equal(item.id, created.jobId); - status = delivered.status; - } - - await run(App.triggers.timestamp_completed.operation.performUnsubscribe, {}, { subscribeData: sub, targetUrl: hookUrl }); - completed = { id: created.jobId, ...status }; - }); - - it('re-registering the same SHA-256 is idempotent (existing: true)', async () => { - const again = await run(App.creates.create_timestamp.operation.perform, { sha256 }); - assert.equal(again.jobId, completed.id); - assert.equal(again.existing, true); - }); - - it('reports usage for the connection', async () => { - const [usage] = await run(App.searches.usage.operation.perform); - assert.equal(usage.tenantId, tenantId); - assert.equal(usage.plan, plan); - assert.ok(usage.timestamps >= 1); - }); - - it('finds the job by id and by hash; a missing job is no match', async () => { - const [status] = await run(App.searches.job_status.operation.perform, { jobId: completed.id }); - assert.equal(status.status, 'completed'); - assert.ok(status.result); - - const [details] = await run(App.searches.job_verification.operation.perform, { jobId: completed.id }); - assert.equal(details.id, completed.id); - - const [byHash] = await run(App.searches.hash_lookup.operation.perform, { sha256 }); - assert.equal(byHash.exists, true); - assert.equal(byHash.jobId, completed.id); - - const [tree] = await run(App.searches.tree_lookup.operation.perform, { sha256 }); - assert.equal(tree.includeTree, true); - - assert.deepEqual(await run(App.searches.job_status.operation.perform, { jobId: '00000000-0000-0000-0000-000000000000' }), []); - assert.deepEqual(await run(App.searches.hash_lookup.operation.perform, { sha256: 'f'.repeat(64) }), []); - }); - - it('verifies the certificate, singly and in batch', async () => { - const single = await run(App.creates.verify_timestamp.operation.perform, { certificate: completed.result }); - assert.equal(single.valid, true); - - const batch = await run(App.creates.batch_verify.operation.perform, { certificates: [completed.result, 'not-a-certificate'] }); - assert.equal(batch.total, 2); - assert.equal(batch.results[0].valid, true); - assert.equal(batch.results[1].valid, false); - assert.equal(batch.allValid, false); - }); - - it('timestamp-and-wait returns a terminal or pending status with a job id', async () => { - const out = await run(App.creates.timestamp_and_wait.operation.perform, { data: `${data}-wait` }); - assert.ok(out.id); - assert.ok(['completed', 'pending'].includes(out.status), out.status); - }); - - it('batch timestamps then batch status (paid plan), or a clear upgrade error (free plan)', async () => { - const items = [`${data}-b1`, `${data}-b2`]; - if (plan === 'free') { - await assert.rejects( - run(App.creates.batch_timestamp.operation.perform, { items }), - (err) => /paid plan/i.test(err.message) && /Upgrade at https?:\/\//.test(err.message), - ); - return; - } - const batch = await run(App.creates.batch_timestamp.operation.perform, { items }); - assert.equal(batch.count, 2); - - const statuses = await waitFor(async () => { - const out = await run(App.creates.batch_status.operation.perform, { jobIds: batch.jobIds }); - return out.allCompleted ? out : null; - }); - assert.equal(statuses.completedCount, 2); - assert.deepEqual(statuses.results.map((r) => r.id), batch.jobIds); - }); - - it('fetches a retrieval receipt for the completed job', async () => { - const receipt = await run(App.creates.get_receipt.operation.perform, { jobId: completed.id }); - assert.equal(receipt.type, 'verae.retrieval-receipt'); - assert.equal(receipt.jobId, completed.id); - assert.equal(receipt.certificate, completed.result); - assert.equal(receipt.extraSeal.event, 'document.retrieved'); - }); - - it('refuses a receipt for a job that is not complete', async () => { - await assert.rejects( - run(App.creates.get_receipt.operation.perform, { jobId: '00000000-0000-0000-0000-000000000000' }), - (err) => /not found|NOT_COMPLETE|Receipt only/i.test(`${err.message} ${err.code || ''}`), - ); - }); -}); diff --git a/triggers/timestamp_completed.js b/triggers/timestamp_completed.js index 70a5a10..300c36a 100644 --- a/triggers/timestamp_completed.js +++ b/triggers/timestamp_completed.js @@ -1,13 +1,53 @@ -/** - * REST Hook: fires when a timestamp job completes (`timestamp.completed`). - * @module triggers/timestamp_completed - */ +const base = () => process.env.MIDDLEWARE_BASE_URL || 'http://127.0.0.1:3100'; -const hookTrigger = require('../lib/hookTrigger'); +const subscribeHook = async (z, bundle) => { + const response = await z.request({ + method: 'POST', + url: `${base()}/zapier/v1/webhooks/subscribe`, + body: { + targetUrl: bundle.targetUrl, + event: 'timestamp.completed', + }, + }); + return response.data.id; +}; -module.exports = hookTrigger({ +const unsubscribeHook = async (z, bundle) => { + await z.request({ + method: 'DELETE', + url: `${base()}/zapier/v1/webhooks/unsubscribe`, + body: { + hookId: bundle.subscribeData, + }, + }); + return {}; +}; + +const perform = async (z, bundle) => [bundle.cleanedRequest]; + +const performList = async () => []; + +module.exports = { key: 'timestamp_completed', - event: 'timestamp.completed', - label: 'Timestamp Completed', - description: 'Triggers when a blockchain timestamp job completes and its certificate is ready.', -}); + noun: 'Timestamp', + display: { + label: 'Timestamp Completed', + description: 'Triggers when a blockchain timestamp job completes.', + }, + operation: { + type: 'hook', + perform, + performList, + performSubscribe: subscribeHook, + performUnsubscribe: unsubscribeHook, + sample: { + event: 'timestamp.completed', + jobId: '550e8400-e29b-41d4-a716-446655440000', + status: { id: '550e8400-e29b-41d4-a716-446655440000', status: 'completed' }, + }, + outputFields: [ + { key: 'event', label: 'Event' }, + { key: 'jobId', label: 'Job ID' }, + ], + }, +}; diff --git a/triggers/timestamp_failed.js b/triggers/timestamp_failed.js deleted file mode 100644 index 66277fb..0000000 --- a/triggers/timestamp_failed.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * REST Hook: fires when a timestamp job fails (`timestamp.failed`). - * @module triggers/timestamp_failed - */ - -const hookTrigger = require('../lib/hookTrigger'); -const { statusSample } = require('../lib/middleware'); - -module.exports = hookTrigger({ - key: 'timestamp_failed', - event: 'timestamp.failed', - label: 'Timestamp Failed', - description: 'Triggers when a blockchain timestamp job fails, so you can alert or retry.', - sampleStatus: { - id: statusSample.id, - status: 'failed', - error: 'chain rejected payload', - completedAt: statusSample.completedAt, - sha256: statusSample.sha256, - }, -});