verae-zapier-app/index.js
Andrew Birkholz 220559d257 Bring the Zapier app to parity with the middleware /zapier/v1 contract
Zapier cloud runs this app against verae-middleware over HTTPS with a
customer zmw_ key; the middleware owns keys, plans, job watching, webhooks,
and the api.veraetime.net client. This commit makes the app cover the whole
middleware surface and match its documented behaviour.

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

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

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

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 12:16:47 -06:00

100 lines
3.5 KiB
JavaScript

/**
* 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 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`.
* @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;
}
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.
* @param {object} response
* @param {object} z
*/
const mapMiddlewareErrors = (response, z) => {
if (response.skipThrowForStatus || response.status < 400) {
return response;
}
const { message, code, upgradeUrl } = describeError(response);
if (response.status === 429) {
throw new z.errors.ThrottledError(message);
}
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);
};
module.exports = {
version: require('./package.json').version,
platformVersion: require('zapier-platform-core').version,
authentication,
beforeRequest: [addApiKey],
afterResponse: [mapMiddlewareErrors],
triggers: {
[timestampCompleted.key]: timestampCompleted,
[timestampFailed.key]: timestampFailed,
},
creates: {
[timestampAndWait.key]: timestampAndWait,
[createTimestamp.key]: createTimestamp,
[batchTimestamp.key]: batchTimestamp,
[verifyTimestamp.key]: verifyTimestamp,
[batchVerify.key]: batchVerify,
[batchStatus.key]: batchStatus,
[getReceipt.key]: getReceipt,
},
searches: {
[jobStatus.key]: jobStatus,
[jobVerification.key]: jobVerification,
[hashLookup.key]: hashLookup,
[treeLookup.key]: treeLookup,
[findUsage.key]: findUsage,
},
};