From 345aeeead95a4e830753545b73b01187770f915e Mon Sep 17 00:00:00 2001 From: George Lambert Date: Fri, 11 Sep 2026 16:18:06 -0400 Subject: [PATCH] Bind each customer to a Verae userId for hop tracing Signup registers/binds a Verae central user and stores veraeUserId. Public access stays the zappier API key. Chain JWTs stay server-side behind tokenRef. Authz, billing, and jobs.watch carry veraeUserId. --- docs/02-architecture/nats-subjects.md | 1 + docs/modules/README.md | 3 + docs/modules/verae-access-authz/policy.md | 2 +- .../clients/veraeClient.md | 8 +- .../verae-zapier-middleware/lib/identity.md | 41 ++++++++++ .../middleware/authenticate.md | 4 +- .../nats/publishers.md | 2 +- .../services/authService.md | 4 +- .../services/tenantService.md | 4 +- .../services/timestampService.md | 5 +- .../verae-zapier-middleware/store/db.md | 2 +- .../verae-zapier-middleware/store/tenants.md | 5 +- .../store/tokenRefs.md | 48 ++++++++++++ .../workers/jobPollerWorker.md | 5 +- docs/modules/zappier-account-balance/books.md | 7 +- docs/modules/zappier/admin.md | 2 +- docs/modules/zappier/app.md | 4 +- docs/modules/zappier/auth.md | 4 +- docs/modules/zappier/billing-nats.md | 8 +- docs/modules/zappier/portal.md | 5 +- docs/modules/zappier/statement.md | 3 +- docs/modules/zappier/upstream.md | 2 +- docs/modules/zappier/verae-bind.md | 46 +++++++++++ docs/sphinx/index.rst | 3 + .../modules/verae-access-authz/policy.md | 2 +- .../clients/veraeClient.md | 8 +- .../verae-zapier-middleware/lib/identity.md | 41 ++++++++++ .../verae-zapier-middleware/lib/identity.rst | 7 ++ .../middleware/authenticate.md | 4 +- .../nats/publishers.md | 2 +- .../services/authService.md | 4 +- .../services/tenantService.md | 4 +- .../services/timestampService.md | 5 +- .../verae-zapier-middleware/store/db.md | 2 +- .../verae-zapier-middleware/store/tenants.md | 5 +- .../store/tokenRefs.md | 48 ++++++++++++ .../store/tokenRefs.rst | 7 ++ .../workers/jobPollerWorker.md | 5 +- .../modules/zappier-account-balance/books.md | 7 +- docs/sphinx/modules/zappier/admin.md | 2 +- docs/sphinx/modules/zappier/app.md | 4 +- docs/sphinx/modules/zappier/auth.md | 4 +- docs/sphinx/modules/zappier/billing-nats.md | 8 +- docs/sphinx/modules/zappier/portal.md | 5 +- docs/sphinx/modules/zappier/statement.md | 3 +- docs/sphinx/modules/zappier/upstream.md | 2 +- docs/sphinx/modules/zappier/verae-bind.md | 46 +++++++++++ docs/sphinx/modules/zappier/verae-bind.rst | 7 ++ packages/docs-master/modules-and-nats.md | 2 +- packages/docs-master/modules/zappier/NATS.md | 2 +- packages/verae-access-authz/src/policy.js | 2 + .../verae-access-authz/test/policy.test.js | 13 ++++ .../src/clients/veraeClient.js | 38 +++++++++- .../src/lib/identity.js | 16 ++++ .../src/middleware/authenticate.js | 6 ++ .../src/nats/publishers.js | 2 + .../src/services/authService.js | 22 ++++-- .../src/services/tenantService.js | 24 ++++-- .../src/services/timestampService.js | 5 +- .../verae-zapier-middleware/src/store/db.js | 2 + .../src/store/tenants.js | 4 + .../src/store/tokenRefs.js | 20 +++++ .../src/workers/jobPollerWorker.js | 6 +- .../test/integration/tenants.test.js | 2 + .../test/unit/identity.test.js | 27 +++++++ .../test/unit/veraeClient.test.js | 3 + .../02-signup-zappier-portal.md | 1 + packages/zapier-user-docs/12-security.md | 1 + packages/zappier-account-balance/src/books.js | 12 ++- .../test/books.test.js | 7 +- packages/zappier/src/admin.ts | 6 +- packages/zappier/src/app.ts | 10 ++- packages/zappier/src/auth.ts | 3 + packages/zappier/src/billing-nats.ts | 14 +++- packages/zappier/src/portal.ts | 13 +++- packages/zappier/src/statement.ts | 2 + packages/zappier/src/upstream.ts | 2 + packages/zappier/src/verae-bind.ts | 76 +++++++++++++++++++ packages/zappier/tests/portal.test.ts | 5 +- 79 files changed, 703 insertions(+), 95 deletions(-) create mode 100644 docs/modules/verae-zapier-middleware/lib/identity.md create mode 100644 docs/modules/verae-zapier-middleware/store/tokenRefs.md create mode 100644 docs/modules/zappier/verae-bind.md create mode 100644 docs/sphinx/modules/verae-zapier-middleware/lib/identity.md create mode 100644 docs/sphinx/modules/verae-zapier-middleware/lib/identity.rst create mode 100644 docs/sphinx/modules/verae-zapier-middleware/store/tokenRefs.md create mode 100644 docs/sphinx/modules/verae-zapier-middleware/store/tokenRefs.rst create mode 100644 docs/sphinx/modules/zappier/verae-bind.md create mode 100644 docs/sphinx/modules/zappier/verae-bind.rst create mode 100644 packages/verae-zapier-middleware/src/lib/identity.js create mode 100644 packages/verae-zapier-middleware/src/store/tokenRefs.js create mode 100644 packages/verae-zapier-middleware/test/unit/identity.test.js create mode 100644 packages/zappier/src/verae-bind.ts diff --git a/docs/02-architecture/nats-subjects.md b/docs/02-architecture/nats-subjects.md index 99332e1..2cacd4b 100644 --- a/docs/02-architecture/nats-subjects.md +++ b/docs/02-architecture/nats-subjects.md @@ -25,6 +25,7 @@ All subjects are prefixed with `verae.zapier.` to isolate this platform from oth | `tenantId` | string | yes | Owning tenant | | `jobId` | string | yes | Verae job id | | `tokenRef` | string | preferred | Opaque ref to resolve Verae credentials (avoid raw JWT) | +| `veraeUserId` | string | preferred | Stable Verae central user id (`vu_…`); not a JWT | | `veraeToken` | string | discouraged | Only if tokenRef unavailable; redacted in logs | | `enqueuedAt` | ISO-8601 | yes | Enqueue time | | `attempt` | number | yes | Delivery attempt (0-based) | diff --git a/docs/modules/README.md b/docs/modules/README.md index 75c84d7..f0b689e 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -32,6 +32,7 @@ One MD file per source module. - [zappier/statement](zappier/statement.md) - [zappier/upstream](zappier/upstream.md) - [zappier/usage](zappier/usage.md) +- [zappier/verae-bind](zappier/verae-bind.md) - [verae-zapier-middleware/app](verae-zapier-middleware/app.md) - [verae-zapier-middleware/clients/veraeClient](verae-zapier-middleware/clients/veraeClient.md) - [verae-zapier-middleware/config](verae-zapier-middleware/config.md) @@ -43,6 +44,7 @@ One MD file per source module. - [verae-zapier-middleware/debug/trace](verae-zapier-middleware/debug/trace.md) - [verae-zapier-middleware/errors](verae-zapier-middleware/errors.md) - [verae-zapier-middleware/index](verae-zapier-middleware/index.md) +- [verae-zapier-middleware/lib/identity](verae-zapier-middleware/lib/identity.md) - [verae-zapier-middleware/lib/receiptPdf](verae-zapier-middleware/lib/receiptPdf.md) - [verae-zapier-middleware/lib/tokens](verae-zapier-middleware/lib/tokens.md) - [verae-zapier-middleware/middleware/authenticate](verae-zapier-middleware/middleware/authenticate.md) @@ -71,6 +73,7 @@ One MD file per source module. - [verae-zapier-middleware/store/db](verae-zapier-middleware/store/db.md) - [verae-zapier-middleware/store/jobWatchers](verae-zapier-middleware/store/jobWatchers.md) - [verae-zapier-middleware/store/tenants](verae-zapier-middleware/store/tenants.md) +- [verae-zapier-middleware/store/tokenRefs](verae-zapier-middleware/store/tokenRefs.md) - [verae-zapier-middleware/store/usage](verae-zapier-middleware/store/usage.md) - [verae-zapier-middleware/store/webhooks](verae-zapier-middleware/store/webhooks.md) - [verae-zapier-middleware/workers/inProcessJobPoller](verae-zapier-middleware/workers/inProcessJobPoller.md) diff --git a/docs/modules/verae-access-authz/policy.md b/docs/modules/verae-access-authz/policy.md index ddeee18..b6e8c8a 100644 --- a/docs/modules/verae-access-authz/policy.md +++ b/docs/modules/verae-access-authz/policy.md @@ -2,7 +2,7 @@ **Package:** `verae-access-authz` **Source:** `packages/verae-access-authz/src/policy.js` -**Lines:** 99 +**Lines:** 101 ## What this module is diff --git a/docs/modules/verae-zapier-middleware/clients/veraeClient.md b/docs/modules/verae-zapier-middleware/clients/veraeClient.md index 8d75562..8963d25 100644 --- a/docs/modules/verae-zapier-middleware/clients/veraeClient.md +++ b/docs/modules/verae-zapier-middleware/clients/veraeClient.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/clients/veraeClient.js` -**Lines:** 352 +**Lines:** 386 ## What this module is @@ -24,6 +24,7 @@ _None extracted._ | `delay` | `ms` | ms: `number` | `Promise` | see Call graph | | `mockLogin` | `{ username, password }` | ms: `number` | `Promise` | see Call graph | | `mockValidate` | `token` | — | `unknown` | see Call graph | +| `mockCreateUser` | `{ username, password, role = 'user' }` | — | `unknown` | see Call graph | | `mockCreateTimestamp` | `{ data, hashAlg, sha256, publicMetadata, privateMetadata }` | — | `unknown` | see Call graph | | `mockGetStatus` | `jobId` | — | `unknown` | see Call graph | | `mockLookupHash` | `sha256` | — | `unknown` | see Call graph | @@ -38,6 +39,8 @@ _None extracted._ | `setTimeout` | `(` | | `login` | `credentials` | | `validate` | `token` | +| `createUser` | `adminToken, body` | +| `bindUser` | `credentials` | | `createTimestamp` | `token, body` | | `lookupHash` | `token, sha256` | | `createBatchTimestamp` | `token, body` | @@ -54,10 +57,11 @@ _None extracted._ - `../config.js` - `../errors.js` - `../debug/logger.js` +- `../lib/identity.js` ## Call graph (identifiers invoked) -`net`, `createDebugger`, `sha256Hex`, `createHash`, `update`, `digest`, `delay`, `setTimeout`, `mockLogin`, `now`, `toISOString`, `randomUUID`, `includes`, `mockValidate`, `startsWith`, `replace`, `mockCreateTimestamp`, `toLowerCase`, `get`, `set`, `mockGetStatus`, `mockLookupHash`, `mockVerify`, `request`, `debug`, `fetch`, `stringify`, `text`, `parse`, `error`, `client`, `login`, `validate`, `createTimestamp`, `lookupHash`, `createBatchTimestamp`, `push`, `getStatus`, `encodeURIComponent`, `getBatchStatus`, `verify`, `verifyBatch`, `getJobVerification`, `waitForJob`, `jobs`, `clearMockJobs`, `clear` +`net`, `createDebugger`, `sha256Hex`, `createHash`, `update`, `digest`, `delay`, `setTimeout`, `mockLogin`, `now`, `toISOString`, `stableVeraeUserId`, `includes`, `mockValidate`, `startsWith`, `replace`, `mockCreateUser`, `mockCreateTimestamp`, `toLowerCase`, `get`, `randomUUID`, `set`, `mockGetStatus`, `mockLookupHash`, `mockVerify`, `request`, `debug`, `fetch`, `stringify`, `text`, `parse`, `error`, `client`, `login`, `validate`, `createUser`, `bindUser`, `createTimestamp`, `lookupHash`, `createBatchTimestamp`, `push`, `getStatus`, `encodeURIComponent`, `getBatchStatus`, `verify`, `verifyBatch`, `getJobVerification`, `waitForJob`, `jobs`, `clearMockJobs`, `clear` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-zapier-middleware/lib/identity.md b/docs/modules/verae-zapier-middleware/lib/identity.md new file mode 100644 index 0000000..77090c2 --- /dev/null +++ b/docs/modules/verae-zapier-middleware/lib/identity.md @@ -0,0 +1,41 @@ +# `verae-zapier-middleware/lib/identity` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/lib/identity.js` +**Lines:** 17 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`normalizeVeraeUsername`, `stableVeraeUserId` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `normalizeVeraeUsername` | `username` | — | `unknown` | see Call graph | +| `stableVeraeUserId` | `username` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:crypto` + +## Call graph (identifiers invoked) + +`normalizeVeraeUsername`, `trim`, `toLowerCase`, `stableVeraeUserId`, `createHash`, `update`, `digest`, `slice` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-zapier-middleware/middleware/authenticate.md b/docs/modules/verae-zapier-middleware/middleware/authenticate.md index 46e1205..82ce03f 100644 --- a/docs/modules/verae-zapier-middleware/middleware/authenticate.md +++ b/docs/modules/verae-zapier-middleware/middleware/authenticate.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/middleware/authenticate.js` -**Lines:** 28 +**Lines:** 34 ## What this module is @@ -35,7 +35,7 @@ _No top-level functions extracted._ ## Call graph (identifiers invoked) -`createDebugger`, `asyncHandler`, `async`, `extractBearerToken`, `resolveAuthContext`, `debug`, `next` +`createDebugger`, `asyncHandler`, `async`, `extractBearerToken`, `resolveAuthContext`, `startsWith`, `debug`, `next` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-zapier-middleware/nats/publishers.md b/docs/modules/verae-zapier-middleware/nats/publishers.md index 38b368d..46f3c76 100644 --- a/docs/modules/verae-zapier-middleware/nats/publishers.md +++ b/docs/modules/verae-zapier-middleware/nats/publishers.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/nats/publishers.js` -**Lines:** 96 +**Lines:** 98 ## What this module is diff --git a/docs/modules/verae-zapier-middleware/services/authService.md b/docs/modules/verae-zapier-middleware/services/authService.md index 887c141..5b8b1f1 100644 --- a/docs/modules/verae-zapier-middleware/services/authService.md +++ b/docs/modules/verae-zapier-middleware/services/authService.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/services/authService.js` -**Lines:** 142 +**Lines:** 154 ## What this module is @@ -35,7 +35,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`createDebugger`, `loginWithCredentials`, `debug`, `login`, `issueSessionToken`, `loginWithApiKey`, `getTenantByApiKey`, `resolveAuthContext`, `isApiKey`, `parseSessionToken`, `getTenant`, `parse`, `now`, `validateSession`, `validate` +`createDebugger`, `loginWithCredentials`, `debug`, `login`, `upsertTenant`, `issueSessionToken`, `loginWithApiKey`, `getTenantByApiKey`, `resolveAuthContext`, `isApiKey`, `parseSessionToken`, `getTenant`, `parse`, `now`, `validateSession`, `validate` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-zapier-middleware/services/tenantService.md b/docs/modules/verae-zapier-middleware/services/tenantService.md index 85c65c3..09a541e 100644 --- a/docs/modules/verae-zapier-middleware/services/tenantService.md +++ b/docs/modules/verae-zapier-middleware/services/tenantService.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/services/tenantService.js` -**Lines:** 138 +**Lines:** 150 ## What this module is @@ -45,7 +45,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`createDebugger`, `slugify`, `toLowerCase`, `replace`, `slice`, `validateVeraeCredentials`, `login`, `selfServeSignup`, `randomUUID`, `createTenant`, `info`, `provision`, `provisionTenant`, `has`, `getTenant`, `listProvisionedTenants`, `listTenants`, `map` +`createDebugger`, `slugify`, `toLowerCase`, `replace`, `slice`, `validateVeraeCredentials`, `login`, `selfServeSignup`, `bindUser`, `randomUUID`, `createTenant`, `info`, `provision`, `provisionTenant`, `has`, `getTenant`, `listProvisionedTenants`, `listTenants`, `map` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-zapier-middleware/services/timestampService.md b/docs/modules/verae-zapier-middleware/services/timestampService.md index 925a3f9..461f5b9 100644 --- a/docs/modules/verae-zapier-middleware/services/timestampService.md +++ b/docs/modules/verae-zapier-middleware/services/timestampService.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/services/timestampService.js` -**Lines:** 170 +**Lines:** 171 ## What this module is @@ -53,13 +53,14 @@ _None extracted._ - `../config.js` - `../clients/veraeClient.js` - `../store/jobWatchers.js` +- `../store/tokenRefs.js` - `./entitlementService.js` - `../debug/logger.js` - `../debug/trace-context.js` ## Call graph (identifiers invoked) -`createDebugger`, `enqueueWatchForJob`, `getTraceId`, `import`, `enqueueWatch`, `debug`, `enqueueJob`, `createTimestamp`, `checkEntitlement`, `recordUsage`, `createTimestampAndWait`, `waitForJobEvent`, `getStatus`, `waitForJob`, `createBatchTimestamp`, `getJobStatus`, `getBatchJobStatus`, `getBatchStatus`, `getJobVerification`, `lookupHash` +`createDebugger`, `enqueueWatchForJob`, `getTraceId`, `import`, `enqueueWatch`, `issueTokenRef`, `debug`, `enqueueJob`, `createTimestamp`, `checkEntitlement`, `recordUsage`, `createTimestampAndWait`, `waitForJobEvent`, `getStatus`, `waitForJob`, `createBatchTimestamp`, `getJobStatus`, `getBatchJobStatus`, `getBatchStatus`, `getJobVerification`, `lookupHash` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-zapier-middleware/store/db.md b/docs/modules/verae-zapier-middleware/store/db.md index 328acea..c89b430 100644 --- a/docs/modules/verae-zapier-middleware/store/db.md +++ b/docs/modules/verae-zapier-middleware/store/db.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/store/db.js` -**Lines:** 118 +**Lines:** 120 ## What this module is diff --git a/docs/modules/verae-zapier-middleware/store/tenants.md b/docs/modules/verae-zapier-middleware/store/tenants.md index a30761c..32a535c 100644 --- a/docs/modules/verae-zapier-middleware/store/tenants.md +++ b/docs/modules/verae-zapier-middleware/store/tenants.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/store/tenants.js` -**Lines:** 138 +**Lines:** 142 ## What this module is @@ -30,8 +30,9 @@ _None extracted._ plan = 'free', veraeUsername, veraePassword, + veraeUserId, contract = null, - apiKey = generateApiKey(` | params: `object`, params.id: `string`, params.name: `string`, params.plan: `string`, params.veraeUsername: `string`, params.veraePassword: `string`, params.contract: `object|null`, params.apiKey: `string`, params.metadata: `object` | `{ tenant: Tenant, apiKey: string ` — } | see Call graph | + apiKey = generateApiKey(` | params: `object`, params.id: `string`, params.name: `string`, params.plan: `string`, params.veraeUsername: `string`, params.veraePassword: `string`, params.veraeUserId: `string`, params.contract: `object|null`, params.apiKey: `string`, params.metadata: `object` | `{ tenant: Tenant, apiKey: string ` — } | see Call graph | | `resolveLimits` | `tenant` | tenant: `Tenant` | `{ * timestamps: number|null, * verifications: number|null, diff --git a/docs/modules/verae-zapier-middleware/store/tokenRefs.md b/docs/modules/verae-zapier-middleware/store/tokenRefs.md new file mode 100644 index 0000000..2cb564b --- /dev/null +++ b/docs/modules/verae-zapier-middleware/store/tokenRefs.md @@ -0,0 +1,48 @@ +# `verae-zapier-middleware/store/tokenRefs` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/store/tokenRefs.js` +**Lines:** 21 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`issueTokenRef`, `resolveTokenRef` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `issueTokenRef` | `tenantId` | — | `unknown` | see Call graph | +| `resolveTokenRef` | `ref` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `persist` | `(none)` | + +## What it imports / requires + +- `node:crypto` +- `./db.js` + +## Call graph (identifiers invoked) + +`issueTokenRef`, `randomBytes`, `toString`, `getStore`, `toISOString`, `persist`, `resolveTokenRef` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-zapier-middleware/workers/jobPollerWorker.md b/docs/modules/verae-zapier-middleware/workers/jobPollerWorker.md index 21aabe9..5544d47 100644 --- a/docs/modules/verae-zapier-middleware/workers/jobPollerWorker.md +++ b/docs/modules/verae-zapier-middleware/workers/jobPollerWorker.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/workers/jobPollerWorker.js` -**Lines:** 159 +**Lines:** 161 ## What this module is @@ -34,11 +34,12 @@ _None extracted._ - `../nats/publishers.js` - `../clients/veraeClient.js` - `../store/tenants.js` +- `../store/tokenRefs.js` - `../debug/trace.js` ## Call graph (identifiers invoked) -`createDebugger`, `polling`, `resolveVeraeToken`, `getTenant`, `login`, `handleWatch`, `withTrace`, `async`, `publishJobEvent`, `ack`, `getStatus`, `debug`, `nak`, `startJobPollerWorker`, `stopJobPollerWorker`, `connectNats`, `ensureStreams`, `consumer`, `add`, `get`, `info`, `consume`, `parse`, `string`, `error`, `abort` +`createDebugger`, `polling`, `resolveVeraeToken`, `resolveTokenRef`, `getTenant`, `login`, `handleWatch`, `withTrace`, `async`, `publishJobEvent`, `ack`, `getStatus`, `debug`, `nak`, `startJobPollerWorker`, `stopJobPollerWorker`, `connectNats`, `ensureStreams`, `consumer`, `add`, `get`, `info`, `consume`, `parse`, `string`, `error`, `abort` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/zappier-account-balance/books.md b/docs/modules/zappier-account-balance/books.md index 421a2d3..49dc16d 100644 --- a/docs/modules/zappier-account-balance/books.md +++ b/docs/modules/zappier-account-balance/books.md @@ -2,7 +2,7 @@ **Package:** `zappier-account-balance` **Source:** `packages/zappier-account-balance/src/books.js` -**Lines:** 90 +**Lines:** 100 ## What this module is @@ -30,8 +30,9 @@ Implementation module in `zappier-account-balance`. The tables below are extract | Name | Parameters | |------|------------| | `constructor` | `(none)` | +| `remember` | `customerId, veraeUserId` | | `prepaidCents` | `customerId` | -| `adjust` | `{ customerId, cents, reason, agent, kind = 'credit' }` | +| `adjust` | `{ customerId, cents, reason, agent, kind = 'credit', veraeUserId }` | | `recordUsage` | `entry` | | `recordPayment` | `entry` | | `statement` | `customerId` | @@ -42,7 +43,7 @@ _No imports detected._ ## Call graph (identifiers invoked) -`constructor`, `prepaidCents`, `adjust`, `trunc`, `now`, `toString`, `random`, `slice`, `toISOString`, `unshift`, `recordUsage`, `recordPayment`, `statement`, `filter`, `match`, `handle`, `endsWith` +`constructor`, `remember`, `prepaidCents`, `adjust`, `trunc`, `now`, `toString`, `random`, `slice`, `toISOString`, `unshift`, `recordUsage`, `recordPayment`, `statement`, `filter`, `match`, `handle`, `endsWith` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/zappier/admin.md b/docs/modules/zappier/admin.md index f783eb0..96e67cd 100644 --- a/docs/modules/zappier/admin.md +++ b/docs/modules/zappier/admin.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/admin.ts` -**Lines:** 591 +**Lines:** 593 ## What this module is diff --git a/docs/modules/zappier/app.md b/docs/modules/zappier/app.md index 6ac6afe..467f61a 100644 --- a/docs/modules/zappier/app.md +++ b/docs/modules/zappier/app.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/app.ts` -**Lines:** 272 +**Lines:** 278 ## What this module is @@ -52,7 +52,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour ## Call graph (identifiers invoked) -`join`, `next`, `parse`, `status`, `json`, `buildApp`, `seeded`, `seedAdminUsersFromEnv`, `rateCard`, `getRateCard`, `tiers`, `getTiers`, `natsPublish`, `toISOString`, `express`, `use`, `load`, `setup`, `adminLoginRouter`, `adminAuth`, `adminRouter`, `static`, `async`, `import`, `toDataURL`, `portalRouter`, `get`, `apiKeyAuth`, `middleware`, `meter`, `post`, `toUpperCase`, `proxyVerae`, `toLowerCase`, `createHash`, `update`, `digest`, `randomUUID`, `set`, `isFinite`, `map`, `unshift`, `filter`, `setUTCDate`, `setUTCHours`, `summaryFor`, `find`, `applyMonthlyCredit` +`join`, `next`, `parse`, `status`, `json`, `buildApp`, `seeded`, `seedAdminUsersFromEnv`, `rateCard`, `getRateCard`, `tiers`, `getTiers`, `list`, `find`, `natsPublish`, `toISOString`, `express`, `use`, `load`, `setup`, `adminLoginRouter`, `adminAuth`, `adminRouter`, `static`, `async`, `import`, `toDataURL`, `portalRouter`, `get`, `apiKeyAuth`, `middleware`, `meter`, `post`, `toUpperCase`, `proxyVerae`, `toLowerCase`, `createHash`, `update`, `digest`, `randomUUID`, `set`, `isFinite`, `map`, `unshift`, `filter`, `setUTCDate`, `setUTCHours`, `summaryFor`, `applyMonthlyCredit` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/zappier/auth.md b/docs/modules/zappier/auth.md index b6a5831..958828c 100644 --- a/docs/modules/zappier/auth.md +++ b/docs/modules/zappier/auth.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/auth.ts` -**Lines:** 84 +**Lines:** 87 ## What this module is @@ -48,7 +48,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour ## Call graph (identifiers invoked) -`findByApiKey`, `findByEmail`, `list`, `save`, `constructor`, `arrays`, `map`, `find`, `toLowerCase`, `findIndex`, `push`, `apiKeyAuth`, `header`, `status`, `json`, `next` +`id`, `findByApiKey`, `findByEmail`, `list`, `save`, `constructor`, `arrays`, `map`, `find`, `toLowerCase`, `findIndex`, `push`, `apiKeyAuth`, `header`, `status`, `json`, `next` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/zappier/billing-nats.md b/docs/modules/zappier/billing-nats.md index f4ec548..b12e0bc 100644 --- a/docs/modules/zappier/billing-nats.md +++ b/docs/modules/zappier/billing-nats.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/billing-nats.ts` -**Lines:** 130 +**Lines:** 138 ## What this module is @@ -29,11 +29,13 @@ Implementation module in `zappier`. The tables below are extracted from the sour | `decode` | `buf: Uint8Array` | — | `unknown` | see Call graph | | `authzAllow` | `plane: AccessPlane, subject: string, - extra: { principal?: string; kind?: string } = {},` | — | `unknown` | see Call graph | + extra: { principal?: string; kind?: string; veraeUserId?: string } = {},` | — | `unknown` | see Call graph | | `natsStatement` | `customerId: string, - plane: AccessPlane = 'web',` | — | `unknown` | see Call graph | + plane: AccessPlane = 'web', + veraeUserId?: string,` | — | `unknown` | see Call graph | | `natsAdjust` | `payload: { customerId: string; + veraeUserId?: string; cents: number; reason: string; agent: string; diff --git a/docs/modules/zappier/portal.md b/docs/modules/zappier/portal.md index c3123d1..4c31a9c 100644 --- a/docs/modules/zappier/portal.md +++ b/docs/modules/zappier/portal.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/portal.ts` -**Lines:** 328 +**Lines:** 337 ## What this module is @@ -51,10 +51,11 @@ Implementation module in `zappier`. The tables below are extracted from the sour - `./credits` - `./statement` - `./billing-nats` +- `./verae-bind` ## Call graph (identifiers invoked) -`now`, `reload`, `customer`, `publicProfile`, `sessionAuth`, `header`, `startsWith`, `slice`, `get`, `list`, `find`, `status`, `json`, `next`, `save`, `portalRouter`, `post`, `trim`, `test`, `hashPassword`, `findByEmail`, `randomBytes`, `toString`, `create`, `verifyPassword`, `verifyTotp`, `use`, `delete`, `setUTCDate`, `setUTCHours`, `summaryFor`, `tiers`, `applyMonthlyCredit`, `rateCard`, `async`, `natsStatement`, `composeStatement`, `listFor`, `type`, `send`, `renderInvoiceHtml`, `generateTotpSecret`, `totpUri`, `qr`, `isInteger`, `natsPublish`, `natsAdjust`, `put` +`now`, `reload`, `customer`, `publicProfile`, `sessionAuth`, `header`, `startsWith`, `slice`, `get`, `list`, `find`, `status`, `json`, `next`, `save`, `portalRouter`, `post`, `async`, `trim`, `test`, `hashPassword`, `findByEmail`, `randomBytes`, `toString`, `bindVeraeUser`, `create`, `verifyPassword`, `verifyTotp`, `use`, `delete`, `setUTCDate`, `setUTCHours`, `summaryFor`, `tiers`, `applyMonthlyCredit`, `rateCard`, `natsStatement`, `composeStatement`, `listFor`, `type`, `send`, `renderInvoiceHtml`, `generateTotpSecret`, `totpUri`, `qr`, `isInteger`, `natsPublish`, `natsAdjust`, `put` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/zappier/statement.md b/docs/modules/zappier/statement.md index c0709a5..53bce5f 100644 --- a/docs/modules/zappier/statement.md +++ b/docs/modules/zappier/statement.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/statement.ts` -**Lines:** 39 +**Lines:** 41 ## What this module is @@ -22,6 +22,7 @@ _None extracted._ |------|------------|---------------------|---------|-----------------------------| | `composeStatement` | `args: { customerId: string; + veraeUserId?: string; prepaidCents: number; credits: CreditAdjustment[]; usage: UsageEntry[]; diff --git a/docs/modules/zappier/upstream.md b/docs/modules/zappier/upstream.md index 00a5a19..ef2c4d1 100644 --- a/docs/modules/zappier/upstream.md +++ b/docs/modules/zappier/upstream.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/upstream.ts` -**Lines:** 46 +**Lines:** 48 ## What this module is diff --git a/docs/modules/zappier/verae-bind.md b/docs/modules/zappier/verae-bind.md new file mode 100644 index 0000000..30f3320 --- /dev/null +++ b/docs/modules/zappier/verae-bind.md @@ -0,0 +1,46 @@ +# `zappier/verae-bind` + +**Package:** `zappier` +**Source:** `packages/zappier/src/verae-bind.ts` +**Lines:** 77 + +## What this module is + +Implementation module in `zappier`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`normalizeVeraeUsername`, `stableVeraeUserId`, `VeraeBind`, `bindVeraeUser` + +## Types / interfaces / classes + +| Kind | Name | +|------|------| +| type | `VeraeBind` | + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `normalizeVeraeUsername` | `username: string` | — | `unknown` | see Call graph | +| `stableVeraeUserId` | `username: string` | — | `unknown` | see Call graph | +| `mockBind` | `email: string` | — | `unknown` | see Call graph | +| `bindVeraeUser` | `email: string` | — | `unknown` | see Call graph | +| `login` | `username: string, pass: string` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `crypto` + +## Call graph (identifiers invoked) + +`normalizeVeraeUsername`, `trim`, `toLowerCase`, `stableVeraeUserId`, `createHash`, `update`, `digest`, `slice`, `mockBind`, `bindVeraeUser`, `replace`, `randomBytes`, `toString`, `async`, `fetch`, `stringify`, `json`, `login` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index c8bf17b..47e4264 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -68,6 +68,7 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html modules/zappier/statement modules/zappier/upstream modules/zappier/usage + modules/zappier/verae-bind modules/verae-zapier-middleware/app modules/verae-zapier-middleware/clients/veraeClient modules/verae-zapier-middleware/config @@ -79,6 +80,7 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html modules/verae-zapier-middleware/debug/trace modules/verae-zapier-middleware/errors modules/verae-zapier-middleware/index + modules/verae-zapier-middleware/lib/identity modules/verae-zapier-middleware/lib/receiptPdf modules/verae-zapier-middleware/lib/tokens modules/verae-zapier-middleware/middleware/authenticate @@ -107,6 +109,7 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html modules/verae-zapier-middleware/store/db modules/verae-zapier-middleware/store/jobWatchers modules/verae-zapier-middleware/store/tenants + modules/verae-zapier-middleware/store/tokenRefs modules/verae-zapier-middleware/store/usage modules/verae-zapier-middleware/store/webhooks modules/verae-zapier-middleware/workers/inProcessJobPoller diff --git a/docs/sphinx/modules/verae-access-authz/policy.md b/docs/sphinx/modules/verae-access-authz/policy.md index ddeee18..b6e8c8a 100644 --- a/docs/sphinx/modules/verae-access-authz/policy.md +++ b/docs/sphinx/modules/verae-access-authz/policy.md @@ -2,7 +2,7 @@ **Package:** `verae-access-authz` **Source:** `packages/verae-access-authz/src/policy.js` -**Lines:** 99 +**Lines:** 101 ## What this module is diff --git a/docs/sphinx/modules/verae-zapier-middleware/clients/veraeClient.md b/docs/sphinx/modules/verae-zapier-middleware/clients/veraeClient.md index 8d75562..8963d25 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/clients/veraeClient.md +++ b/docs/sphinx/modules/verae-zapier-middleware/clients/veraeClient.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/clients/veraeClient.js` -**Lines:** 352 +**Lines:** 386 ## What this module is @@ -24,6 +24,7 @@ _None extracted._ | `delay` | `ms` | ms: `number` | `Promise` | see Call graph | | `mockLogin` | `{ username, password }` | ms: `number` | `Promise` | see Call graph | | `mockValidate` | `token` | — | `unknown` | see Call graph | +| `mockCreateUser` | `{ username, password, role = 'user' }` | — | `unknown` | see Call graph | | `mockCreateTimestamp` | `{ data, hashAlg, sha256, publicMetadata, privateMetadata }` | — | `unknown` | see Call graph | | `mockGetStatus` | `jobId` | — | `unknown` | see Call graph | | `mockLookupHash` | `sha256` | — | `unknown` | see Call graph | @@ -38,6 +39,8 @@ _None extracted._ | `setTimeout` | `(` | | `login` | `credentials` | | `validate` | `token` | +| `createUser` | `adminToken, body` | +| `bindUser` | `credentials` | | `createTimestamp` | `token, body` | | `lookupHash` | `token, sha256` | | `createBatchTimestamp` | `token, body` | @@ -54,10 +57,11 @@ _None extracted._ - `../config.js` - `../errors.js` - `../debug/logger.js` +- `../lib/identity.js` ## Call graph (identifiers invoked) -`net`, `createDebugger`, `sha256Hex`, `createHash`, `update`, `digest`, `delay`, `setTimeout`, `mockLogin`, `now`, `toISOString`, `randomUUID`, `includes`, `mockValidate`, `startsWith`, `replace`, `mockCreateTimestamp`, `toLowerCase`, `get`, `set`, `mockGetStatus`, `mockLookupHash`, `mockVerify`, `request`, `debug`, `fetch`, `stringify`, `text`, `parse`, `error`, `client`, `login`, `validate`, `createTimestamp`, `lookupHash`, `createBatchTimestamp`, `push`, `getStatus`, `encodeURIComponent`, `getBatchStatus`, `verify`, `verifyBatch`, `getJobVerification`, `waitForJob`, `jobs`, `clearMockJobs`, `clear` +`net`, `createDebugger`, `sha256Hex`, `createHash`, `update`, `digest`, `delay`, `setTimeout`, `mockLogin`, `now`, `toISOString`, `stableVeraeUserId`, `includes`, `mockValidate`, `startsWith`, `replace`, `mockCreateUser`, `mockCreateTimestamp`, `toLowerCase`, `get`, `randomUUID`, `set`, `mockGetStatus`, `mockLookupHash`, `mockVerify`, `request`, `debug`, `fetch`, `stringify`, `text`, `parse`, `error`, `client`, `login`, `validate`, `createUser`, `bindUser`, `createTimestamp`, `lookupHash`, `createBatchTimestamp`, `push`, `getStatus`, `encodeURIComponent`, `getBatchStatus`, `verify`, `verifyBatch`, `getJobVerification`, `waitForJob`, `jobs`, `clearMockJobs`, `clear` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-zapier-middleware/lib/identity.md b/docs/sphinx/modules/verae-zapier-middleware/lib/identity.md new file mode 100644 index 0000000..77090c2 --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/lib/identity.md @@ -0,0 +1,41 @@ +# `verae-zapier-middleware/lib/identity` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/lib/identity.js` +**Lines:** 17 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`normalizeVeraeUsername`, `stableVeraeUserId` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `normalizeVeraeUsername` | `username` | — | `unknown` | see Call graph | +| `stableVeraeUserId` | `username` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:crypto` + +## Call graph (identifiers invoked) + +`normalizeVeraeUsername`, `trim`, `toLowerCase`, `stableVeraeUserId`, `createHash`, `update`, `digest`, `slice` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-zapier-middleware/lib/identity.rst b/docs/sphinx/modules/verae-zapier-middleware/lib/identity.rst new file mode 100644 index 0000000..8c75ae0 --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/lib/identity.rst @@ -0,0 +1,7 @@ +verae-zapier-middleware.lib.identity +==================================== + +Generated API sheet for ``verae-zapier-middleware/lib/identity``. + +.. include:: identity.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-zapier-middleware/middleware/authenticate.md b/docs/sphinx/modules/verae-zapier-middleware/middleware/authenticate.md index 46e1205..82ce03f 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/middleware/authenticate.md +++ b/docs/sphinx/modules/verae-zapier-middleware/middleware/authenticate.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/middleware/authenticate.js` -**Lines:** 28 +**Lines:** 34 ## What this module is @@ -35,7 +35,7 @@ _No top-level functions extracted._ ## Call graph (identifiers invoked) -`createDebugger`, `asyncHandler`, `async`, `extractBearerToken`, `resolveAuthContext`, `debug`, `next` +`createDebugger`, `asyncHandler`, `async`, `extractBearerToken`, `resolveAuthContext`, `startsWith`, `debug`, `next` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-zapier-middleware/nats/publishers.md b/docs/sphinx/modules/verae-zapier-middleware/nats/publishers.md index 38b368d..46f3c76 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/nats/publishers.md +++ b/docs/sphinx/modules/verae-zapier-middleware/nats/publishers.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/nats/publishers.js` -**Lines:** 96 +**Lines:** 98 ## What this module is diff --git a/docs/sphinx/modules/verae-zapier-middleware/services/authService.md b/docs/sphinx/modules/verae-zapier-middleware/services/authService.md index 887c141..5b8b1f1 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/services/authService.md +++ b/docs/sphinx/modules/verae-zapier-middleware/services/authService.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/services/authService.js` -**Lines:** 142 +**Lines:** 154 ## What this module is @@ -35,7 +35,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`createDebugger`, `loginWithCredentials`, `debug`, `login`, `issueSessionToken`, `loginWithApiKey`, `getTenantByApiKey`, `resolveAuthContext`, `isApiKey`, `parseSessionToken`, `getTenant`, `parse`, `now`, `validateSession`, `validate` +`createDebugger`, `loginWithCredentials`, `debug`, `login`, `upsertTenant`, `issueSessionToken`, `loginWithApiKey`, `getTenantByApiKey`, `resolveAuthContext`, `isApiKey`, `parseSessionToken`, `getTenant`, `parse`, `now`, `validateSession`, `validate` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-zapier-middleware/services/tenantService.md b/docs/sphinx/modules/verae-zapier-middleware/services/tenantService.md index 85c65c3..09a541e 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/services/tenantService.md +++ b/docs/sphinx/modules/verae-zapier-middleware/services/tenantService.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/services/tenantService.js` -**Lines:** 138 +**Lines:** 150 ## What this module is @@ -45,7 +45,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`createDebugger`, `slugify`, `toLowerCase`, `replace`, `slice`, `validateVeraeCredentials`, `login`, `selfServeSignup`, `randomUUID`, `createTenant`, `info`, `provision`, `provisionTenant`, `has`, `getTenant`, `listProvisionedTenants`, `listTenants`, `map` +`createDebugger`, `slugify`, `toLowerCase`, `replace`, `slice`, `validateVeraeCredentials`, `login`, `selfServeSignup`, `bindUser`, `randomUUID`, `createTenant`, `info`, `provision`, `provisionTenant`, `has`, `getTenant`, `listProvisionedTenants`, `listTenants`, `map` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-zapier-middleware/services/timestampService.md b/docs/sphinx/modules/verae-zapier-middleware/services/timestampService.md index 925a3f9..461f5b9 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/services/timestampService.md +++ b/docs/sphinx/modules/verae-zapier-middleware/services/timestampService.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/services/timestampService.js` -**Lines:** 170 +**Lines:** 171 ## What this module is @@ -53,13 +53,14 @@ _None extracted._ - `../config.js` - `../clients/veraeClient.js` - `../store/jobWatchers.js` +- `../store/tokenRefs.js` - `./entitlementService.js` - `../debug/logger.js` - `../debug/trace-context.js` ## Call graph (identifiers invoked) -`createDebugger`, `enqueueWatchForJob`, `getTraceId`, `import`, `enqueueWatch`, `debug`, `enqueueJob`, `createTimestamp`, `checkEntitlement`, `recordUsage`, `createTimestampAndWait`, `waitForJobEvent`, `getStatus`, `waitForJob`, `createBatchTimestamp`, `getJobStatus`, `getBatchJobStatus`, `getBatchStatus`, `getJobVerification`, `lookupHash` +`createDebugger`, `enqueueWatchForJob`, `getTraceId`, `import`, `enqueueWatch`, `issueTokenRef`, `debug`, `enqueueJob`, `createTimestamp`, `checkEntitlement`, `recordUsage`, `createTimestampAndWait`, `waitForJobEvent`, `getStatus`, `waitForJob`, `createBatchTimestamp`, `getJobStatus`, `getBatchJobStatus`, `getBatchStatus`, `getJobVerification`, `lookupHash` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-zapier-middleware/store/db.md b/docs/sphinx/modules/verae-zapier-middleware/store/db.md index 328acea..c89b430 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/store/db.md +++ b/docs/sphinx/modules/verae-zapier-middleware/store/db.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/store/db.js` -**Lines:** 118 +**Lines:** 120 ## What this module is diff --git a/docs/sphinx/modules/verae-zapier-middleware/store/tenants.md b/docs/sphinx/modules/verae-zapier-middleware/store/tenants.md index a30761c..32a535c 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/store/tenants.md +++ b/docs/sphinx/modules/verae-zapier-middleware/store/tenants.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/store/tenants.js` -**Lines:** 138 +**Lines:** 142 ## What this module is @@ -30,8 +30,9 @@ _None extracted._ plan = 'free', veraeUsername, veraePassword, + veraeUserId, contract = null, - apiKey = generateApiKey(` | params: `object`, params.id: `string`, params.name: `string`, params.plan: `string`, params.veraeUsername: `string`, params.veraePassword: `string`, params.contract: `object|null`, params.apiKey: `string`, params.metadata: `object` | `{ tenant: Tenant, apiKey: string ` — } | see Call graph | + apiKey = generateApiKey(` | params: `object`, params.id: `string`, params.name: `string`, params.plan: `string`, params.veraeUsername: `string`, params.veraePassword: `string`, params.veraeUserId: `string`, params.contract: `object|null`, params.apiKey: `string`, params.metadata: `object` | `{ tenant: Tenant, apiKey: string ` — } | see Call graph | | `resolveLimits` | `tenant` | tenant: `Tenant` | `{ * timestamps: number|null, * verifications: number|null, diff --git a/docs/sphinx/modules/verae-zapier-middleware/store/tokenRefs.md b/docs/sphinx/modules/verae-zapier-middleware/store/tokenRefs.md new file mode 100644 index 0000000..2cb564b --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/store/tokenRefs.md @@ -0,0 +1,48 @@ +# `verae-zapier-middleware/store/tokenRefs` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/store/tokenRefs.js` +**Lines:** 21 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`issueTokenRef`, `resolveTokenRef` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `issueTokenRef` | `tenantId` | — | `unknown` | see Call graph | +| `resolveTokenRef` | `ref` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `persist` | `(none)` | + +## What it imports / requires + +- `node:crypto` +- `./db.js` + +## Call graph (identifiers invoked) + +`issueTokenRef`, `randomBytes`, `toString`, `getStore`, `toISOString`, `persist`, `resolveTokenRef` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-zapier-middleware/store/tokenRefs.rst b/docs/sphinx/modules/verae-zapier-middleware/store/tokenRefs.rst new file mode 100644 index 0000000..59b7ae5 --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/store/tokenRefs.rst @@ -0,0 +1,7 @@ +verae-zapier-middleware.store.tokenRefs +======================================= + +Generated API sheet for ``verae-zapier-middleware/store/tokenRefs``. + +.. include:: tokenRefs.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-zapier-middleware/workers/jobPollerWorker.md b/docs/sphinx/modules/verae-zapier-middleware/workers/jobPollerWorker.md index 21aabe9..5544d47 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/workers/jobPollerWorker.md +++ b/docs/sphinx/modules/verae-zapier-middleware/workers/jobPollerWorker.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/workers/jobPollerWorker.js` -**Lines:** 159 +**Lines:** 161 ## What this module is @@ -34,11 +34,12 @@ _None extracted._ - `../nats/publishers.js` - `../clients/veraeClient.js` - `../store/tenants.js` +- `../store/tokenRefs.js` - `../debug/trace.js` ## Call graph (identifiers invoked) -`createDebugger`, `polling`, `resolveVeraeToken`, `getTenant`, `login`, `handleWatch`, `withTrace`, `async`, `publishJobEvent`, `ack`, `getStatus`, `debug`, `nak`, `startJobPollerWorker`, `stopJobPollerWorker`, `connectNats`, `ensureStreams`, `consumer`, `add`, `get`, `info`, `consume`, `parse`, `string`, `error`, `abort` +`createDebugger`, `polling`, `resolveVeraeToken`, `resolveTokenRef`, `getTenant`, `login`, `handleWatch`, `withTrace`, `async`, `publishJobEvent`, `ack`, `getStatus`, `debug`, `nak`, `startJobPollerWorker`, `stopJobPollerWorker`, `connectNats`, `ensureStreams`, `consumer`, `add`, `get`, `info`, `consume`, `parse`, `string`, `error`, `abort` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier-account-balance/books.md b/docs/sphinx/modules/zappier-account-balance/books.md index 421a2d3..49dc16d 100644 --- a/docs/sphinx/modules/zappier-account-balance/books.md +++ b/docs/sphinx/modules/zappier-account-balance/books.md @@ -2,7 +2,7 @@ **Package:** `zappier-account-balance` **Source:** `packages/zappier-account-balance/src/books.js` -**Lines:** 90 +**Lines:** 100 ## What this module is @@ -30,8 +30,9 @@ Implementation module in `zappier-account-balance`. The tables below are extract | Name | Parameters | |------|------------| | `constructor` | `(none)` | +| `remember` | `customerId, veraeUserId` | | `prepaidCents` | `customerId` | -| `adjust` | `{ customerId, cents, reason, agent, kind = 'credit' }` | +| `adjust` | `{ customerId, cents, reason, agent, kind = 'credit', veraeUserId }` | | `recordUsage` | `entry` | | `recordPayment` | `entry` | | `statement` | `customerId` | @@ -42,7 +43,7 @@ _No imports detected._ ## Call graph (identifiers invoked) -`constructor`, `prepaidCents`, `adjust`, `trunc`, `now`, `toString`, `random`, `slice`, `toISOString`, `unshift`, `recordUsage`, `recordPayment`, `statement`, `filter`, `match`, `handle`, `endsWith` +`constructor`, `remember`, `prepaidCents`, `adjust`, `trunc`, `now`, `toString`, `random`, `slice`, `toISOString`, `unshift`, `recordUsage`, `recordPayment`, `statement`, `filter`, `match`, `handle`, `endsWith` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier/admin.md b/docs/sphinx/modules/zappier/admin.md index f783eb0..96e67cd 100644 --- a/docs/sphinx/modules/zappier/admin.md +++ b/docs/sphinx/modules/zappier/admin.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/admin.ts` -**Lines:** 591 +**Lines:** 593 ## What this module is diff --git a/docs/sphinx/modules/zappier/app.md b/docs/sphinx/modules/zappier/app.md index 6ac6afe..467f61a 100644 --- a/docs/sphinx/modules/zappier/app.md +++ b/docs/sphinx/modules/zappier/app.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/app.ts` -**Lines:** 272 +**Lines:** 278 ## What this module is @@ -52,7 +52,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour ## Call graph (identifiers invoked) -`join`, `next`, `parse`, `status`, `json`, `buildApp`, `seeded`, `seedAdminUsersFromEnv`, `rateCard`, `getRateCard`, `tiers`, `getTiers`, `natsPublish`, `toISOString`, `express`, `use`, `load`, `setup`, `adminLoginRouter`, `adminAuth`, `adminRouter`, `static`, `async`, `import`, `toDataURL`, `portalRouter`, `get`, `apiKeyAuth`, `middleware`, `meter`, `post`, `toUpperCase`, `proxyVerae`, `toLowerCase`, `createHash`, `update`, `digest`, `randomUUID`, `set`, `isFinite`, `map`, `unshift`, `filter`, `setUTCDate`, `setUTCHours`, `summaryFor`, `find`, `applyMonthlyCredit` +`join`, `next`, `parse`, `status`, `json`, `buildApp`, `seeded`, `seedAdminUsersFromEnv`, `rateCard`, `getRateCard`, `tiers`, `getTiers`, `list`, `find`, `natsPublish`, `toISOString`, `express`, `use`, `load`, `setup`, `adminLoginRouter`, `adminAuth`, `adminRouter`, `static`, `async`, `import`, `toDataURL`, `portalRouter`, `get`, `apiKeyAuth`, `middleware`, `meter`, `post`, `toUpperCase`, `proxyVerae`, `toLowerCase`, `createHash`, `update`, `digest`, `randomUUID`, `set`, `isFinite`, `map`, `unshift`, `filter`, `setUTCDate`, `setUTCHours`, `summaryFor`, `applyMonthlyCredit` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier/auth.md b/docs/sphinx/modules/zappier/auth.md index b6a5831..958828c 100644 --- a/docs/sphinx/modules/zappier/auth.md +++ b/docs/sphinx/modules/zappier/auth.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/auth.ts` -**Lines:** 84 +**Lines:** 87 ## What this module is @@ -48,7 +48,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour ## Call graph (identifiers invoked) -`findByApiKey`, `findByEmail`, `list`, `save`, `constructor`, `arrays`, `map`, `find`, `toLowerCase`, `findIndex`, `push`, `apiKeyAuth`, `header`, `status`, `json`, `next` +`id`, `findByApiKey`, `findByEmail`, `list`, `save`, `constructor`, `arrays`, `map`, `find`, `toLowerCase`, `findIndex`, `push`, `apiKeyAuth`, `header`, `status`, `json`, `next` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier/billing-nats.md b/docs/sphinx/modules/zappier/billing-nats.md index f4ec548..b12e0bc 100644 --- a/docs/sphinx/modules/zappier/billing-nats.md +++ b/docs/sphinx/modules/zappier/billing-nats.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/billing-nats.ts` -**Lines:** 130 +**Lines:** 138 ## What this module is @@ -29,11 +29,13 @@ Implementation module in `zappier`. The tables below are extracted from the sour | `decode` | `buf: Uint8Array` | — | `unknown` | see Call graph | | `authzAllow` | `plane: AccessPlane, subject: string, - extra: { principal?: string; kind?: string } = {},` | — | `unknown` | see Call graph | + extra: { principal?: string; kind?: string; veraeUserId?: string } = {},` | — | `unknown` | see Call graph | | `natsStatement` | `customerId: string, - plane: AccessPlane = 'web',` | — | `unknown` | see Call graph | + plane: AccessPlane = 'web', + veraeUserId?: string,` | — | `unknown` | see Call graph | | `natsAdjust` | `payload: { customerId: string; + veraeUserId?: string; cents: number; reason: string; agent: string; diff --git a/docs/sphinx/modules/zappier/portal.md b/docs/sphinx/modules/zappier/portal.md index c3123d1..4c31a9c 100644 --- a/docs/sphinx/modules/zappier/portal.md +++ b/docs/sphinx/modules/zappier/portal.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/portal.ts` -**Lines:** 328 +**Lines:** 337 ## What this module is @@ -51,10 +51,11 @@ Implementation module in `zappier`. The tables below are extracted from the sour - `./credits` - `./statement` - `./billing-nats` +- `./verae-bind` ## Call graph (identifiers invoked) -`now`, `reload`, `customer`, `publicProfile`, `sessionAuth`, `header`, `startsWith`, `slice`, `get`, `list`, `find`, `status`, `json`, `next`, `save`, `portalRouter`, `post`, `trim`, `test`, `hashPassword`, `findByEmail`, `randomBytes`, `toString`, `create`, `verifyPassword`, `verifyTotp`, `use`, `delete`, `setUTCDate`, `setUTCHours`, `summaryFor`, `tiers`, `applyMonthlyCredit`, `rateCard`, `async`, `natsStatement`, `composeStatement`, `listFor`, `type`, `send`, `renderInvoiceHtml`, `generateTotpSecret`, `totpUri`, `qr`, `isInteger`, `natsPublish`, `natsAdjust`, `put` +`now`, `reload`, `customer`, `publicProfile`, `sessionAuth`, `header`, `startsWith`, `slice`, `get`, `list`, `find`, `status`, `json`, `next`, `save`, `portalRouter`, `post`, `async`, `trim`, `test`, `hashPassword`, `findByEmail`, `randomBytes`, `toString`, `bindVeraeUser`, `create`, `verifyPassword`, `verifyTotp`, `use`, `delete`, `setUTCDate`, `setUTCHours`, `summaryFor`, `tiers`, `applyMonthlyCredit`, `rateCard`, `natsStatement`, `composeStatement`, `listFor`, `type`, `send`, `renderInvoiceHtml`, `generateTotpSecret`, `totpUri`, `qr`, `isInteger`, `natsPublish`, `natsAdjust`, `put` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier/statement.md b/docs/sphinx/modules/zappier/statement.md index c0709a5..53bce5f 100644 --- a/docs/sphinx/modules/zappier/statement.md +++ b/docs/sphinx/modules/zappier/statement.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/statement.ts` -**Lines:** 39 +**Lines:** 41 ## What this module is @@ -22,6 +22,7 @@ _None extracted._ |------|------------|---------------------|---------|-----------------------------| | `composeStatement` | `args: { customerId: string; + veraeUserId?: string; prepaidCents: number; credits: CreditAdjustment[]; usage: UsageEntry[]; diff --git a/docs/sphinx/modules/zappier/upstream.md b/docs/sphinx/modules/zappier/upstream.md index 00a5a19..ef2c4d1 100644 --- a/docs/sphinx/modules/zappier/upstream.md +++ b/docs/sphinx/modules/zappier/upstream.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/upstream.ts` -**Lines:** 46 +**Lines:** 48 ## What this module is diff --git a/docs/sphinx/modules/zappier/verae-bind.md b/docs/sphinx/modules/zappier/verae-bind.md new file mode 100644 index 0000000..30f3320 --- /dev/null +++ b/docs/sphinx/modules/zappier/verae-bind.md @@ -0,0 +1,46 @@ +# `zappier/verae-bind` + +**Package:** `zappier` +**Source:** `packages/zappier/src/verae-bind.ts` +**Lines:** 77 + +## What this module is + +Implementation module in `zappier`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`normalizeVeraeUsername`, `stableVeraeUserId`, `VeraeBind`, `bindVeraeUser` + +## Types / interfaces / classes + +| Kind | Name | +|------|------| +| type | `VeraeBind` | + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `normalizeVeraeUsername` | `username: string` | — | `unknown` | see Call graph | +| `stableVeraeUserId` | `username: string` | — | `unknown` | see Call graph | +| `mockBind` | `email: string` | — | `unknown` | see Call graph | +| `bindVeraeUser` | `email: string` | — | `unknown` | see Call graph | +| `login` | `username: string, pass: string` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `crypto` + +## Call graph (identifiers invoked) + +`normalizeVeraeUsername`, `trim`, `toLowerCase`, `stableVeraeUserId`, `createHash`, `update`, `digest`, `slice`, `mockBind`, `bindVeraeUser`, `replace`, `randomBytes`, `toString`, `async`, `fetch`, `stringify`, `json`, `login` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/zappier/verae-bind.rst b/docs/sphinx/modules/zappier/verae-bind.rst new file mode 100644 index 0000000..0194721 --- /dev/null +++ b/docs/sphinx/modules/zappier/verae-bind.rst @@ -0,0 +1,7 @@ +zappier.verae-bind +================== + +Generated API sheet for ``zappier/verae-bind``. + +.. include:: verae-bind.md + :parser: myst_parser.sphinx_ diff --git a/packages/docs-master/modules-and-nats.md b/packages/docs-master/modules-and-nats.md index 6451653..b536e3d 100644 --- a/packages/docs-master/modules-and-nats.md +++ b/packages/docs-master/modules-and-nats.md @@ -59,7 +59,7 @@ Zapier cloud **never** connects to NATS. Internal services do: middleware worker | Address | Kind | Payload (required fields) | |---------|------|---------------------------| -| `verae.zapier.jobs.watch` | JetStream work queue | `tenantId`, `jobId`, `tokenRef`, `enqueuedAt`, `attempt`, `maxAttempts`, `intervalMs`, `traceId` | +| `verae.zapier.jobs.watch` | JetStream work queue | `tenantId`, `veraeUserId`, `jobId`, `tokenRef`, `enqueuedAt`, `attempt`, `maxAttempts`, `intervalMs`, `traceId` | | `verae.zapier.jobs.events` | JetStream events | `event` (`timestamp.completed\|failed\|timeout`), `tenantId`, `jobId`, `status`, `traceId`, `emittedAt` | | `verae.zapier.webhooks.deliver` | JetStream work queue | `hookId`, `tenantId`, `targetUrl`, `event`, `payload`, `attempt`, `traceId` | | `verae.zapier.usage` | optional | `tenantId`, `action`, `amount`, `at` | diff --git a/packages/docs-master/modules/zappier/NATS.md b/packages/docs-master/modules/zappier/NATS.md index 0573c73..f216c3b 100644 --- a/packages/docs-master/modules/zappier/NATS.md +++ b/packages/docs-master/modules/zappier/NATS.md @@ -6,7 +6,7 @@ Zapier cloud still never connects to NATS. The edge process **does** publish and |-----------|---------|------|------| | IN HTTPS | `/v1/*` | Zapier Platform app | JSON + `x-api-key` | | OUT HTTPS | middleware `/zapier/v1/*` | verae-middleware | same tenant request | -| OUT NATS | `verae.billing.usage.recorded` | account-balance | meter event | +| OUT NATS | `verae.billing.usage.recorded` | account-balance | meter event (`customerId`, `veraeUserId`) | | OUT NATS | `verae.billing.payment.recorded` | account-balance / others | portal reload | | OUT NATS | `verae.billing.credit.applied` | account-balance / others | admin/CS credit | | OUT NATS | `verae.billing.statement.get` | account-balance | portal/admin review | diff --git a/packages/verae-access-authz/src/policy.js b/packages/verae-access-authz/src/policy.js index d8f2149..1971275 100644 --- a/packages/verae-access-authz/src/policy.js +++ b/packages/verae-access-authz/src/policy.js @@ -89,6 +89,8 @@ export function authorize(req) { plane, subject: internal, principal: req?.principal || null, + veraeUserId: req?.veraeUserId || req?.payload?.veraeUserId || null, + traceId: req?.traceId || req?.payload?.traceId || null, reason: 'ok', }; } diff --git a/packages/verae-access-authz/test/policy.test.js b/packages/verae-access-authz/test/policy.test.js index 30cee3b..f0592d5 100644 --- a/packages/verae-access-authz/test/policy.test.js +++ b/packages/verae-access-authz/test/policy.test.js @@ -60,6 +60,19 @@ test('access-prefixed address is mapped to internal', () => { assert.equal(authorize({ plane: 'web', subject: addr }).allow, false); }); +test('allow echoes veraeUserId for hop tracing', () => { + const out = authorize({ + plane: 'web', + subject: 'verae.billing.statement.get', + principal: 'cust_1', + veraeUserId: 'vu_deadbeefdeadbeef', + traceId: 'tr-1', + }); + assert.equal(out.allow, true); + assert.equal(out.veraeUserId, 'vu_deadbeefdeadbeef'); + assert.equal(out.traceId, 'tr-1'); +}); + test('unknown plane and authz subjects denied', () => { assert.equal(authorize({ plane: 'partner', subject: 'verae.archive.put' }).allow, false); assert.equal(authorize({ plane: 'web', subject: 'verae.access.authz.check' }).allow, false); diff --git a/packages/verae-zapier-middleware/src/clients/veraeClient.js b/packages/verae-zapier-middleware/src/clients/veraeClient.js index b27c644..31aff54 100644 --- a/packages/verae-zapier-middleware/src/clients/veraeClient.js +++ b/packages/verae-zapier-middleware/src/clients/veraeClient.js @@ -7,6 +7,7 @@ import { createHash, randomUUID } from 'node:crypto'; import { config } from '../config.js'; import { AppError } from '../errors.js'; import { createDebugger } from '../debug/logger.js'; +import { stableVeraeUserId } from '../lib/identity.js'; const log = createDebugger('http'); @@ -39,7 +40,7 @@ async function mockLogin({ username, password }) { token: `mock-jwt-${username}`, expiresAt, user: { - id: randomUUID(), + id: stableVeraeUserId(username), username, role: username.includes('admin') ? 'admin' : 'user', }, @@ -53,13 +54,24 @@ async function mockValidate(token) { const username = token.replace('mock-jwt-', ''); return { valid: true, - userId: randomUUID(), + userId: stableVeraeUserId(username), username, role: 'user', expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), }; } +async function mockCreateUser({ username, password, role = 'user' }) { + if (!username || !password) { + throw new AppError('Invalid input', { status: 400, code: 'VALIDATION_ERROR' }); + } + return { + id: stableVeraeUserId(username), + username, + role, + }; +} + async function mockCreateTimestamp({ data, hashAlg, sha256, publicMetadata, privateMetadata }) { if (!data && !sha256) { throw new AppError('Invalid input data', { status: 400, code: 'VALIDATION_ERROR' }); @@ -228,6 +240,28 @@ export const veraeClient = { return request('/auth/validate', { token }); }, + /** + * Admin-only Verae user register. JWT is not returned to callers of bind. + * @param {string} adminToken + * @param {{ username: string, password: string, role?: string }} body + */ + async createUser(adminToken, body) { + if (config.mockVerae) return mockCreateUser(body); + return request('/auth/users', { method: 'POST', token: adminToken, body }); + }, + + /** + * Login (or create via admin) and return the stable user id — never the JWT. + * @param {{ username: string, password: string }} credentials + */ + async bindUser(credentials) { + const login = await this.login(credentials); + return { + veraeUserId: login.user?.id || stableVeraeUserId(credentials.username), + veraeUsername: login.user?.username || credentials.username, + }; + }, + /** * @param {string} token * @param {{ data: string, hashAlg?: string }} body diff --git a/packages/verae-zapier-middleware/src/lib/identity.js b/packages/verae-zapier-middleware/src/lib/identity.js new file mode 100644 index 0000000..732f378 --- /dev/null +++ b/packages/verae-zapier-middleware/src/lib/identity.js @@ -0,0 +1,16 @@ +/** + * Stable Verae user id derived from username/email. + * Live /auth/login.user.id wins when present; this is the mock/offline id. + */ +import { createHash } from 'node:crypto'; + +export function normalizeVeraeUsername(username) { + return String(username || '') + .trim() + .toLowerCase(); +} + +export function stableVeraeUserId(username) { + const n = normalizeVeraeUsername(username); + return `vu_${createHash('sha256').update(n).digest('hex').slice(0, 16)}`; +} diff --git a/packages/verae-zapier-middleware/src/middleware/authenticate.js b/packages/verae-zapier-middleware/src/middleware/authenticate.js index 87ee129..83c9775 100644 --- a/packages/verae-zapier-middleware/src/middleware/authenticate.js +++ b/packages/verae-zapier-middleware/src/middleware/authenticate.js @@ -18,8 +18,14 @@ export const authenticate = asyncHandler(async (req, res, next) => { extractBearerToken(req.headers.authorization) ?? req.headers['x-api-key'] ?? null; req.auth = await resolveAuthContext(rawToken); + const headerId = req.headers['x-verae-user-id']; + if (typeof headerId === 'string' && headerId.startsWith('vu_') && !req.auth.veraeUserId) { + req.auth.veraeUserId = headerId; + if (req.auth.tenant) req.auth.tenant.veraeUserId = headerId; + } log.debug('authenticated', { tenantId: req.auth.tenantId, + veraeUserId: req.auth.veraeUserId, method: req.auth.authMethod, plan: req.auth.tenant?.plan, }); diff --git a/packages/verae-zapier-middleware/src/nats/publishers.js b/packages/verae-zapier-middleware/src/nats/publishers.js index 0d1ac26..06148a3 100644 --- a/packages/verae-zapier-middleware/src/nats/publishers.js +++ b/packages/verae-zapier-middleware/src/nats/publishers.js @@ -37,9 +37,11 @@ export async function enqueueWatch(partial) { log.debug('enqueueWatch', { subject: SUBJECTS.JOBS_WATCH, tenantId: msg.tenantId, + veraeUserId: msg.veraeUserId, jobId: msg.jobId, attempt: msg.attempt, traceId: msg.traceId, + hasTokenRef: Boolean(msg.tokenRef), }); const js = await requireJs(); diff --git a/packages/verae-zapier-middleware/src/services/authService.js b/packages/verae-zapier-middleware/src/services/authService.js index 5e3a6e2..dcf6c38 100644 --- a/packages/verae-zapier-middleware/src/services/authService.js +++ b/packages/verae-zapier-middleware/src/services/authService.js @@ -4,7 +4,7 @@ */ import { veraeClient } from '../clients/veraeClient.js'; -import { getTenantByApiKey, getTenant } from '../store/tenants.js'; +import { getTenantByApiKey, getTenant, upsertTenant } from '../store/tenants.js'; import { issueSessionToken, parseSessionToken, isApiKey } from '../lib/tokens.js'; import { AppError } from '../errors.js'; import { createDebugger } from '../debug/logger.js'; @@ -33,6 +33,10 @@ export async function loginWithCredentials({ username, password, tenant }) { } const tenantId = tenant?.id ?? `user:${verae.user.username}`; + const veraeUserId = verae.user?.id; + if (tenant && veraeUserId && tenant.veraeUserId !== veraeUserId) { + upsertTenant({ ...tenant, veraeUserId }); + } const accessToken = issueSessionToken({ tenantId, veraeToken: verae.token, @@ -43,9 +47,10 @@ export async function loginWithCredentials({ username, password, tenant }) { accessToken, expiresAt: verae.expiresAt, tenant: tenant - ? { id: tenant.id, name: tenant.name, plan: tenant.plan } - : { id: tenantId, name: verae.user.username, plan: 'free' }, + ? { id: tenant.id, name: tenant.name, plan: tenant.plan, veraeUserId: veraeUserId || tenant.veraeUserId } + : { id: tenantId, name: verae.user.username, plan: 'free', veraeUserId }, user: verae.user, + veraeUserId, }; } @@ -93,8 +98,14 @@ export async function resolveAuthContext(rawToken) { const tenant = getTenant(session.tenant.id) ?? session.tenant; return { tenantId: session.tenant.id, - tenant: { id: session.tenant.id, name: session.tenant.name, plan: session.tenant.plan }, + tenant: { + id: session.tenant.id, + name: session.tenant.name, + plan: session.tenant.plan, + veraeUserId: session.veraeUserId || tenant?.veraeUserId, + }, veraeToken: parsed.veraeToken, + veraeUserId: session.veraeUserId || tenant?.veraeUserId, authMethod: 'api_key', fullTenant: tenant, }; @@ -114,9 +125,10 @@ export async function resolveAuthContext(rawToken) { return { tenantId: parsed.tenantId, tenant: tenant - ? { id: tenant.id, name: tenant.name, plan: tenant.plan } + ? { id: tenant.id, name: tenant.name, plan: tenant.plan, veraeUserId: tenant.veraeUserId } : { id: parsed.tenantId, plan: 'free' }, veraeToken: parsed.veraeToken, + veraeUserId: tenant?.veraeUserId, authMethod: 'session', fullTenant: tenant, }; diff --git a/packages/verae-zapier-middleware/src/services/tenantService.js b/packages/verae-zapier-middleware/src/services/tenantService.js index 5e96b69..e4879f1 100644 --- a/packages/verae-zapier-middleware/src/services/tenantService.js +++ b/packages/verae-zapier-middleware/src/services/tenantService.js @@ -45,7 +45,7 @@ export async function selfServeSignup({ email, name, veraeUsername, veraePasswor }); } - await validateVeraeCredentials(veraeUsername, veraePassword); + const bound = await veraeClient.bindUser({ username: veraeUsername, password: veraePassword }); const id = `tenant-${slugify(email)}-${randomUUID().slice(0, 8)}`; const { tenant, apiKey } = createTenant({ @@ -54,14 +54,21 @@ export async function selfServeSignup({ email, name, veraeUsername, veraePasswor plan: 'free', veraeUsername, veraePassword, + veraeUserId: bound.veraeUserId, contract: null, metadata: { email, audience: 'self-serve', createdVia: 'signup' }, }); - log.info('self-serve signup', { tenantId: tenant.id }); + log.info('self-serve signup', { tenantId: tenant.id, veraeUserId: bound.veraeUserId }); return { - tenant: { id: tenant.id, name: tenant.name, plan: tenant.plan, email }, + tenant: { + id: tenant.id, + name: tenant.name, + plan: tenant.plan, + email, + veraeUserId: bound.veraeUserId, + }, apiKey, zapierSetup: { authType: 'custom', @@ -102,7 +109,7 @@ export async function provisionTenant({ }); } - await validateVeraeCredentials(veraeUsername, veraePassword); + const bound = await veraeClient.bindUser({ username: veraeUsername, password: veraePassword }); const tenantId = id ?? `tenant-${slugify(name)}-${randomUUID().slice(0, 8)}`; if (getTenant(tenantId)) { @@ -115,12 +122,16 @@ export async function provisionTenant({ plan, veraeUsername, veraePassword, + veraeUserId: bound.veraeUserId, contract, metadata: { ...metadata, audience, createdVia: 'provision' }, }); - log.info('tenant provisioned', { tenantId: tenant.id, plan, audience }); - return { tenant: { id: tenant.id, name: tenant.name, plan: tenant.plan }, apiKey }; + log.info('tenant provisioned', { tenantId: tenant.id, plan, audience, veraeUserId: bound.veraeUserId }); + return { + tenant: { id: tenant.id, name: tenant.name, plan: tenant.plan, veraeUserId: bound.veraeUserId }, + apiKey, + }; } /** @@ -132,6 +143,7 @@ export function listProvisionedTenants() { name: tenant.name, plan: tenant.plan, audience: tenant.metadata?.audience ?? 'unknown', + veraeUserId: tenant.veraeUserId, createdAt: tenant.createdAt, })); } diff --git a/packages/verae-zapier-middleware/src/services/timestampService.js b/packages/verae-zapier-middleware/src/services/timestampService.js index bf110af..a8a9588 100644 --- a/packages/verae-zapier-middleware/src/services/timestampService.js +++ b/packages/verae-zapier-middleware/src/services/timestampService.js @@ -6,6 +6,7 @@ import { config } from '../config.js'; import { veraeClient } from '../clients/veraeClient.js'; import { enqueueJob } from '../store/jobWatchers.js'; +import { issueTokenRef } from '../store/tokenRefs.js'; import { checkEntitlement, recordUsage } from './entitlementService.js'; import { createDebugger } from '../debug/logger.js'; import { getTraceId } from '../debug/trace-context.js'; @@ -25,8 +26,8 @@ async function enqueueWatchForJob(ctx, jobId) { await enqueueWatch({ tenantId: ctx.tenantId, jobId, - // Prefer re-login in worker; include token for MVP simplicity when mock - veraeToken: ctx.veraeToken, + tokenRef: issueTokenRef(ctx.tenantId), + veraeUserId: ctx.veraeUserId || ctx.tenant?.veraeUserId, maxAttempts: config.jobPollMaxAttempts, intervalMs: config.jobPollIntervalMs, traceId, diff --git a/packages/verae-zapier-middleware/src/store/db.js b/packages/verae-zapier-middleware/src/store/db.js index 3f1fd3a..1beaf3b 100644 --- a/packages/verae-zapier-middleware/src/store/db.js +++ b/packages/verae-zapier-middleware/src/store/db.js @@ -33,6 +33,7 @@ export function emptyStore() { usage: {}, webhooks: [], jobWatchers: [], + tokenRefs: {}, blobs: {}, shares: {}, trees: {}, @@ -59,6 +60,7 @@ export function loadStore(path = config.storePath) { usage: parsed.usage ?? {}, webhooks: Array.isArray(parsed.webhooks) ? parsed.webhooks : [], jobWatchers: Array.isArray(parsed.jobWatchers) ? parsed.jobWatchers : [], + tokenRefs: parsed.tokenRefs ?? {}, }; log.debug('store loaded', { path, tenants: Object.keys(store.tenants).length }); } catch (err) { diff --git a/packages/verae-zapier-middleware/src/store/tenants.js b/packages/verae-zapier-middleware/src/store/tenants.js index 09558bc..0b60e56 100644 --- a/packages/verae-zapier-middleware/src/store/tenants.js +++ b/packages/verae-zapier-middleware/src/store/tenants.js @@ -17,6 +17,7 @@ const log = createDebugger('billing'); * @property {string} plan * @property {string} veraeUsername * @property {string} veraePassword + * @property {string} [veraeUserId] * @property {object|null} contract * @property {object} [metadata] * @property {string} createdAt @@ -68,6 +69,7 @@ export function upsertTenant(tenant) { * @param {string} [params.plan='free'] * @param {string} params.veraeUsername * @param {string} params.veraePassword + * @param {string} [params.veraeUserId] * @param {object|null} [params.contract=null] * @param {string} [params.apiKey] * @param {object} [params.metadata] @@ -79,6 +81,7 @@ export function createTenant({ plan = 'free', veraeUsername, veraePassword, + veraeUserId, contract = null, apiKey = generateApiKey(), metadata = {}, @@ -91,6 +94,7 @@ export function createTenant({ plan, veraeUsername, veraePassword, + veraeUserId, contract, metadata, createdAt: new Date().toISOString(), diff --git a/packages/verae-zapier-middleware/src/store/tokenRefs.js b/packages/verae-zapier-middleware/src/store/tokenRefs.js new file mode 100644 index 0000000..2850056 --- /dev/null +++ b/packages/verae-zapier-middleware/src/store/tokenRefs.js @@ -0,0 +1,20 @@ +/** + * Opaque tokenRef → tenantId. Workers resolve a fresh Verae JWT; NATS never carries it. + */ +import { randomBytes } from 'node:crypto'; +import { getStore, persist } from './db.js'; + +export function issueTokenRef(tenantId) { + const ref = `tref_${randomBytes(12).toString('hex')}`; + const store = getStore(); + if (!store.tokenRefs) store.tokenRefs = {}; + store.tokenRefs[ref] = { tenantId, createdAt: new Date().toISOString() }; + persist(); + return ref; +} + +export function resolveTokenRef(ref) { + if (!ref) return null; + const row = getStore().tokenRefs?.[ref]; + return row?.tenantId ?? null; +} diff --git a/packages/verae-zapier-middleware/src/workers/jobPollerWorker.js b/packages/verae-zapier-middleware/src/workers/jobPollerWorker.js index 108c640..6fd682d 100644 --- a/packages/verae-zapier-middleware/src/workers/jobPollerWorker.js +++ b/packages/verae-zapier-middleware/src/workers/jobPollerWorker.js @@ -10,6 +10,7 @@ import { connectNats, ensureStreams } from '../nats/connection.js'; import { publishJobEvent } from '../nats/publishers.js'; import { veraeClient } from '../clients/veraeClient.js'; import { getTenant } from '../store/tenants.js'; +import { resolveTokenRef } from '../store/tokenRefs.js'; import { withTrace } from '../debug/trace.js'; const log = createDebugger('jobs'); @@ -26,9 +27,10 @@ let abort = null; async function resolveVeraeToken(msg) { if (msg.veraeToken) return msg.veraeToken; - const tenant = getTenant(msg.tenantId); + const tenantId = msg.tokenRef ? resolveTokenRef(msg.tokenRef) || msg.tenantId : msg.tenantId; + const tenant = getTenant(tenantId); if (!tenant?.veraeUsername) { - throw new Error(`Cannot resolve token for tenant ${msg.tenantId}`); + throw new Error(`Cannot resolve token for tenant ${tenantId}`); } const login = await veraeClient.login({ username: tenant.veraeUsername, diff --git a/packages/verae-zapier-middleware/test/integration/tenants.test.js b/packages/verae-zapier-middleware/test/integration/tenants.test.js index c825885..84e0620 100644 --- a/packages/verae-zapier-middleware/test/integration/tenants.test.js +++ b/packages/verae-zapier-middleware/test/integration/tenants.test.js @@ -44,6 +44,8 @@ describe('tenancy', () => { const body = await res.json(); assert.equal(body.tenant.plan, 'free'); assert.ok(body.apiKey.startsWith('zmw_')); + assert.match(body.tenant.veraeUserId, /^vu_[0-9a-f]{16}$/); + assert.doesNotMatch(JSON.stringify(body), /mock-jwt|eyJhbGci/); }); it('enterprise without contract rejected', async () => { diff --git a/packages/verae-zapier-middleware/test/unit/identity.test.js b/packages/verae-zapier-middleware/test/unit/identity.test.js new file mode 100644 index 0000000..cc1bd85 --- /dev/null +++ b/packages/verae-zapier-middleware/test/unit/identity.test.js @@ -0,0 +1,27 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { stableVeraeUserId } from '../../src/lib/identity.js'; +import { issueTokenRef, resolveTokenRef } from '../../src/store/tokenRefs.js'; +import { useTempStore } from '../helpers.js'; + +describe('verae identity', () => { + it('stableVeraeUserId is deterministic and not a JWT', () => { + const a = stableVeraeUserId('Ada@Example.com'); + const b = stableVeraeUserId('ada@example.com'); + assert.equal(a, b); + assert.match(a, /^vu_[0-9a-f]{16}$/); + assert.doesNotMatch(a, /eyJ/); + }); + + it('tokenRef resolves tenant and is not a Verae JWT', () => { + const ctx = useTempStore(); + try { + const ref = issueTokenRef('tenant-1'); + assert.match(ref, /^tref_/); + assert.equal(resolveTokenRef(ref), 'tenant-1'); + assert.doesNotMatch(ref, /mock-jwt|eyJ/); + } finally { + ctx.cleanup(); + } + }); +}); diff --git a/packages/verae-zapier-middleware/test/unit/veraeClient.test.js b/packages/verae-zapier-middleware/test/unit/veraeClient.test.js index 2dd2373..a14ddc1 100644 --- a/packages/verae-zapier-middleware/test/unit/veraeClient.test.js +++ b/packages/verae-zapier-middleware/test/unit/veraeClient.test.js @@ -18,6 +18,9 @@ describe('veraeClient mock', () => { it('create → wait → completed', async () => { const login = await veraeClient.login({ username: 'u', password: 'p' }); assert.ok(login.token.startsWith('mock-jwt-')); + const again = await veraeClient.login({ username: 'u', password: 'p' }); + assert.equal(login.user.id, again.user.id); + assert.match(login.user.id, /^vu_[0-9a-f]{16}$/); const { jobId } = await veraeClient.createTimestamp(login.token, { data: 'hello', diff --git a/packages/zapier-user-docs/02-signup-zappier-portal.md b/packages/zapier-user-docs/02-signup-zappier-portal.md index 2d98c43..ec8d8b4 100644 --- a/packages/zapier-user-docs/02-signup-zappier-portal.md +++ b/packages/zapier-user-docs/02-signup-zappier-portal.md @@ -7,6 +7,7 @@ You need an API key **before** you can connect Zapier. 3. Enter name, email, and a password of at least 8 characters. 4. You start on the **Free** plan and receive an **API key immediately**. 5. Copy the key. It is sent as `x-api-key` on every Zapier request. +6. Signup also **registers you on Verae central** and stores a stable `veraeUserId` on your account. That id rides every internal hop (billing, authz, job watch) so CS/sales and the chain can see the same person. You never paste the Verae JWT into Zapier — that token stays on the server (`tokenRef` on NATS). If your company already created an account (you received a key by email), sign up with the **same email**. The existing plan and key are kept; your password is attached to that account. diff --git a/packages/zapier-user-docs/12-security.md b/packages/zapier-user-docs/12-security.md index fe3af6d..f403402 100644 --- a/packages/zapier-user-docs/12-security.md +++ b/packages/zapier-user-docs/12-security.md @@ -4,6 +4,7 @@ - NS1 `nats-server` stays on **127.0.0.1:4222**. Operators use `scripts/nats-tunnel.sh`; it is not a public bind. - File bytes and private metadata never go on chain. Private metadata is only on authenticated archive replies. - API keys are `x-api-key` / Bearer tokens on HTTPS. Treat them like passwords; regenerating kills old Zaps. +- The Verae central JWT from `/auth/login` is **not** your Zapier/portal token. Middleware holds it (or re-logins via `tokenRef`). `veraeUserId` is the public correlation id (`vu_…`). - Bloom filters are **not** an access-control list. On a hit, middleware still checks tenant/share before returning private records. If a trace (simulator or `DEBUG_VERAE`) ever shows a `zapier-platform-app` hop with a `verae.*` subject, that is a bug — do not push the app. diff --git a/packages/zappier-account-balance/src/books.js b/packages/zappier-account-balance/src/books.js index ed4623b..e715911 100644 --- a/packages/zappier-account-balance/src/books.js +++ b/packages/zappier-account-balance/src/books.js @@ -5,16 +5,23 @@ export class AccountBooks { constructor() { /** @type {Record} */ this.prepaid = {}; + /** @type {Record} */ + this.veraeUserIds = {}; this.credits = []; this.usage = []; this.payments = []; } + remember(customerId, veraeUserId) { + if (customerId && veraeUserId) this.veraeUserIds[customerId] = veraeUserId; + } + prepaidCents(customerId) { return this.prepaid[customerId] ?? 0; } - adjust({ customerId, cents, reason, agent, kind = 'credit' }) { + adjust({ customerId, cents, reason, agent, kind = 'credit', veraeUserId }) { + this.remember(customerId, veraeUserId); const delta = Math.trunc(Number(cents) || 0); const next = this.prepaidCents(customerId) + delta; this.prepaid[customerId] = next; @@ -34,6 +41,7 @@ export class AccountBooks { } recordUsage(entry) { + this.remember(entry.customerId, entry.veraeUserId); const cents = Number(entry.cents) || 0; const customerId = entry.customerId; const next = this.prepaidCents(customerId) - cents; @@ -63,6 +71,7 @@ export class AccountBooks { const match = (rows) => rows.filter((r) => r.customerId === customerId).slice(0, 100); return { customerId, + veraeUserId: this.veraeUserIds[customerId], prepaidCents: this.prepaidCents(customerId), credits: match(this.credits), usage: match(this.usage), @@ -74,6 +83,7 @@ export class AccountBooks { export function handle(subject, payload, books) { const p = payload || {}; if (subject.endsWith('balance.get') || subject.endsWith('statement.get')) { + books.remember(p.customerId, p.veraeUserId); return books.statement(p.customerId); } if (subject.endsWith('balance.adjust') || subject.endsWith('credit.applied')) { diff --git a/packages/zappier-account-balance/test/books.test.js b/packages/zappier-account-balance/test/books.test.js index 7d581b6..48e0583 100644 --- a/packages/zappier-account-balance/test/books.test.js +++ b/packages/zappier-account-balance/test/books.test.js @@ -5,11 +5,16 @@ import { SUBJECTS } from '../src/subjects.js'; test('adjust credits prepaid and statement lists credits usage payments', () => { const books = new AccountBooks(); - handle(SUBJECTS.BALANCE_ADJUST, { customerId: 'c1', cents: 500, reason: 'goodwill', agent: 'cs' }, books); + handle( + SUBJECTS.BALANCE_ADJUST, + { customerId: 'c1', veraeUserId: 'vu_abc', cents: 500, reason: 'goodwill', agent: 'cs' }, + books, + ); handle(SUBJECTS.USAGE_RECORDED, { customerId: 'c1', endpointId: 'timestamp', cents: 4 }, books); handle(SUBJECTS.PAYMENT_RECORDED, { customerId: 'c1', cents: 1000, reason: 'reload' }, books); const st = handle(SUBJECTS.STATEMENT_GET, { customerId: 'c1' }, books); assert.equal(st.prepaidCents, 1496); + assert.equal(st.veraeUserId, 'vu_abc'); assert.equal(st.credits[0].cents, 500); assert.equal(st.usage[0].endpointId, 'timestamp'); assert.equal(st.payments[0].kind, 'payment'); diff --git a/packages/zappier/src/admin.ts b/packages/zappier/src/admin.ts index 6024e0d..03264c6 100644 --- a/packages/zappier/src/admin.ts +++ b/packages/zappier/src/admin.ts @@ -248,10 +248,11 @@ export function adminRouter( agent: typeof agent === 'string' ? agent : 'admin', }); customers.save({ ...customer, balanceCents: (customer.balanceCents ?? 0) + delta }); - natsPublish(BILLING_SUBJECTS.CREDIT_APPLIED, rec, 'staff'); + natsPublish(BILLING_SUBJECTS.CREDIT_APPLIED, { ...rec, veraeUserId: customer.veraeUserId }, 'staff'); void natsAdjust( { customerId, + veraeUserId: customer.veraeUserId, cents: delta, reason: rec.reason, agent: rec.agent, @@ -273,7 +274,7 @@ export function adminRouter( res.status(404).json({ error: 'customer not found' }); return; } - const fromNats = await natsStatement(customer.id, 'staff'); + const fromNats = await natsStatement(customer.id, 'staff', customer.veraeUserId); if (fromNats) { res.json({ ...fromNats, name: customer.name, tierId: customer.tierId, source: 'nats' }); return; @@ -281,6 +282,7 @@ export function adminRouter( res.json({ ...composeStatement({ customerId: customer.id, + veraeUserId: customer.veraeUserId, prepaidCents: customer.balanceCents ?? 0, credits: credits.list(customer.id), usage: accounting.usage.listFor(customer.id), diff --git a/packages/zappier/src/app.ts b/packages/zappier/src/app.ts index c244f86..a6e9efa 100644 --- a/packages/zappier/src/app.ts +++ b/packages/zappier/src/app.ts @@ -102,8 +102,14 @@ export function buildApp(deps: AppDeps = {}): { }; const items: StoredItem[] = []; const credits = new CreditLedger(); - const onUsage = (e: { customerId: string; endpointId: string; cents: number }) => - natsPublish(BILLING_SUBJECTS.USAGE_RECORDED, { ...e, at: new Date().toISOString() }, 'api'); + const onUsage = (e: { customerId: string; endpointId: string; cents: number }) => { + const c = customers.list().find((row) => row.id === e.customerId); + natsPublish( + BILLING_SUBJECTS.USAGE_RECORDED, + { ...e, veraeUserId: c?.veraeUserId, at: new Date().toISOString() }, + 'api', + ); + }; const hashIndex = new Map< string, { jobId: string; sha256: string; data?: string; timestamp: string } diff --git a/packages/zappier/src/auth.ts b/packages/zappier/src/auth.ts index 0512b42..c747eb0 100644 --- a/packages/zappier/src/auth.ts +++ b/packages/zappier/src/auth.ts @@ -12,6 +12,9 @@ export interface Customer { /** How this customer is billed. Absent means 'stripe' (the default). */ billingType?: BillingType; email?: string; + /** Stable Verae central user id (not a JWT). */ + veraeUserId?: string; + veraeUsername?: string; /** scrypt hash for portal login. Absent = no portal password set yet. */ passwordHash?: string; /** Base32 TOTP secret. Present once 2FA setup begins. */ diff --git a/packages/zappier/src/billing-nats.ts b/packages/zappier/src/billing-nats.ts index b05782a..a22a1f9 100644 --- a/packages/zappier/src/billing-nats.ts +++ b/packages/zappier/src/billing-nats.ts @@ -15,6 +15,7 @@ export type AccessPlane = 'zapier' | 'web' | 'api' | 'leaf' | 'staff'; export type BillingStatement = { customerId: string; + veraeUserId?: string; prepaidCents: number; credits: unknown[]; usage: unknown[]; @@ -54,7 +55,7 @@ function decode(buf: Uint8Array): unknown { async function authzAllow( plane: AccessPlane, subject: string, - extra: { principal?: string; kind?: string } = {}, + extra: { principal?: string; kind?: string; veraeUserId?: string } = {}, ): Promise { const http = process.env.AUTHZ_URL; if (!http && !(await nc())) return true; @@ -83,13 +84,18 @@ async function authzAllow( export async function natsStatement( customerId: string, plane: AccessPlane = 'web', + veraeUserId?: string, ): Promise { const c = await nc(); if (!c) return null; - if (!(await authzAllow(plane, BILLING_SUBJECTS.STATEMENT_GET, { principal: customerId }))) return null; + if ( + !(await authzAllow(plane, BILLING_SUBJECTS.STATEMENT_GET, { principal: customerId, veraeUserId })) + ) { + return null; + } const m = await c.request( BILLING_SUBJECTS.STATEMENT_GET, - encode({ customerId, plane }), + encode({ customerId, plane, veraeUserId }), { timeout: 2000 }, ); return decode(m.data) as BillingStatement; @@ -98,6 +104,7 @@ export async function natsStatement( export async function natsAdjust( payload: { customerId: string; + veraeUserId?: string; cents: number; reason: string; agent: string; @@ -111,6 +118,7 @@ export async function natsAdjust( !(await authzAllow(plane, BILLING_SUBJECTS.BALANCE_ADJUST, { principal: payload.agent, kind: payload.kind, + veraeUserId: payload.veraeUserId, })) ) { return null; diff --git a/packages/zappier/src/portal.ts b/packages/zappier/src/portal.ts index f2ebb19..b78bd95 100644 --- a/packages/zappier/src/portal.ts +++ b/packages/zappier/src/portal.ts @@ -17,6 +17,7 @@ import { UsageRepo } from './usage'; import { CreditLedger } from './credits'; import { composeStatement } from './statement'; import { BILLING_SUBJECTS, natsPublish, natsStatement, natsAdjust } from './billing-nats'; +import { bindVeraeUser } from './verae-bind'; /** * Customer portal API (/portal/api): signup, login with optional TOTP 2FA, @@ -63,6 +64,7 @@ function publicProfile(c: Customer) { tierId: c.tierId, billingType: c.billingType ?? 'stripe', apiKey: c.apiKey, + veraeUserId: c.veraeUserId, balanceCents: c.balanceCents ?? 0, totpEnabled: c.totpEnabled ?? false, emailInvoicing: c.emailInvoicing ?? false, @@ -97,7 +99,7 @@ export function portalRouter(deps: PortalDeps): Router { /* ---------------- auth ---------------- */ - router.post('/signup', (req, res) => { + router.post('/signup', async (req, res) => { const { name, email, password } = req.body ?? {}; if (typeof name !== 'string' || name.trim().length === 0) { res.status(400).json({ error: 'name is required' }); @@ -133,6 +135,10 @@ export function portalRouter(deps: PortalDeps): Router { balanceCents: 0, }; } + if (!customer.veraeUserId) { + const bind = await bindVeraeUser(email); + customer = { ...customer, veraeUserId: bind.veraeUserId, veraeUsername: bind.veraeUsername }; + } save(deps, customer); const session = deps.sessions.create(customer.id, ttl); res.status(201).json({ token: session.token, customer: publicProfile(customer) }); @@ -203,7 +209,7 @@ export function portalRouter(deps: PortalDeps): Router { }); router.get('/statement', async (req, res) => { - const fromNats = await natsStatement(req.customer!.id, 'web'); + const fromNats = await natsStatement(req.customer!.id, 'web', req.customer!.veraeUserId); if (fromNats) { res.json({ ...fromNats, source: 'nats' }); return; @@ -211,6 +217,7 @@ export function portalRouter(deps: PortalDeps): Router { res.json({ ...composeStatement({ customerId: req.customer!.id, + veraeUserId: req.customer!.veraeUserId, prepaidCents: req.customer!.balanceCents ?? 0, credits: (deps.credits || new CreditLedger()).list(req.customer!.id), usage: deps.usage.listFor(req.customer!.id), @@ -290,6 +297,7 @@ export function portalRouter(deps: PortalDeps): Router { BILLING_SUBJECTS.PAYMENT_RECORDED, { customerId: customer.id, + veraeUserId: customer.veraeUserId, cents: result.creditedCents, reason: 'reload', }, @@ -298,6 +306,7 @@ export function portalRouter(deps: PortalDeps): Router { void natsAdjust( { customerId: customer.id, + veraeUserId: customer.veraeUserId, cents: result.creditedCents, reason: 'reload', agent: 'portal', diff --git a/packages/zappier/src/statement.ts b/packages/zappier/src/statement.ts index ad533c9..d3d29a3 100644 --- a/packages/zappier/src/statement.ts +++ b/packages/zappier/src/statement.ts @@ -4,6 +4,7 @@ import { UsageEntry } from './usage'; export function composeStatement(args: { customerId: string; + veraeUserId?: string; prepaidCents: number; credits: CreditAdjustment[]; usage: UsageEntry[]; @@ -21,6 +22,7 @@ export function composeStatement(args: { })); return { customerId: args.customerId, + veraeUserId: args.veraeUserId, prepaidCents: args.prepaidCents, credits: args.credits.filter((c) => c.customerId === args.customerId), usage: args.usage diff --git a/packages/zappier/src/upstream.ts b/packages/zappier/src/upstream.ts index f29616b..29eaeb3 100644 --- a/packages/zappier/src/upstream.ts +++ b/packages/zappier/src/upstream.ts @@ -23,6 +23,8 @@ export async function proxyVerae( if (key) headers['x-api-key'] = key; const auth = req.header('authorization'); if (auth) headers.authorization = auth; + const veraeUserId = req.customer?.veraeUserId; + if (veraeUserId) headers['x-verae-user-id'] = veraeUserId; const method = req.method.toUpperCase(); const init: RequestInit = { method, headers }; if (method !== 'GET' && method !== 'HEAD') { diff --git a/packages/zappier/src/verae-bind.ts b/packages/zappier/src/verae-bind.ts new file mode 100644 index 0000000..04dffb7 --- /dev/null +++ b/packages/zappier/src/verae-bind.ts @@ -0,0 +1,76 @@ +/** + * Bind a portal customer to a Verae central user id. + * The public credential stays the zappier API key. The Verae JWT never leaves the server. + */ +import { createHash, randomBytes } from 'crypto'; + +export function normalizeVeraeUsername(username: string): string { + return username.trim().toLowerCase(); +} + +export function stableVeraeUserId(username: string): string { + const n = normalizeVeraeUsername(username); + return `vu_${createHash('sha256').update(n).digest('hex').slice(0, 16)}`; +} + +export type VeraeBind = { + veraeUserId: string; + veraeUsername: string; + bound: boolean; +}; + +function mockBind(email: string): VeraeBind { + const veraeUsername = normalizeVeraeUsername(email); + return { veraeUserId: stableVeraeUserId(veraeUsername), veraeUsername, bound: true }; +} + +/** + * Register or look up the customer on api.veraetime.net. + * MOCK_VERAE (default) or missing VERAE_API_BASE_URL → stable id, no network. + */ +export async function bindVeraeUser(email: string): Promise { + const mock = process.env.MOCK_VERAE !== 'false'; + const base = (process.env.VERAE_API_BASE_URL || '').replace(/\/$/, ''); + if (mock || !base) return mockBind(email); + + const veraeUsername = normalizeVeraeUsername(email); + const adminUser = process.env.VERAE_ADMIN_USER; + const adminPass = process.env.VERAE_ADMIN_PASSWORD; + const password = `vt_${randomBytes(18).toString('base64url')}`; + + const login = async (username: string, pass: string) => { + const r = await fetch(`${base}/auth/login`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ username, password: pass }), + }); + if (!r.ok) throw new Error(`verae login ${r.status}`); + return r.json() as Promise<{ token: string; user?: { id?: string; username?: string } }>; + }; + + if (!adminUser || !adminPass) return mockBind(email); + + try { + const admin = await login(adminUser, adminPass); + const created = await fetch(`${base}/auth/users`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + authorization: `Bearer ${admin.token}`, + }, + body: JSON.stringify({ username: veraeUsername, password, role: 'user' }), + }); + if (!created.ok && created.status !== 409) { + return mockBind(email); + } + const user = (await created.json().catch(() => ({}))) as { id?: string; username?: string }; + const id = user.id || (await login(veraeUsername, password)).user?.id; + return { + veraeUserId: id || stableVeraeUserId(veraeUsername), + veraeUsername, + bound: true, + }; + } catch { + return mockBind(email); + } +} diff --git a/packages/zappier/tests/portal.test.ts b/packages/zappier/tests/portal.test.ts index 66ca55c..763802a 100644 --- a/packages/zappier/tests/portal.test.ts +++ b/packages/zappier/tests/portal.test.ts @@ -19,7 +19,7 @@ async function signup(app: ReturnType['app'], email = 'ada@exam .post('/portal/api/signup') .send({ name: 'Ada', email, password: 'super-secret-1' }); expect(res.status).toBe(201); - return res.body as { token: string; customer: { id: string; apiKey: string } }; + return res.body as { token: string; customer: { id: string; apiKey: string; veraeUserId?: string } }; } describe('portal signup + login', () => { @@ -34,6 +34,9 @@ describe('portal signup + login', () => { expect(me.status).toBe(200); expect(me.body.tierId).toBe('free'); expect(me.body.balanceCents).toBe(0); + expect(customer.veraeUserId).toMatch(/^vu_[0-9a-f]{16}$/); + expect(me.body.veraeUserId).toBe(customer.veraeUserId); + expect(JSON.stringify(me.body)).not.toMatch(/eyJ|mock-jwt|veraeToken/); }); it('never leaks passwordHash or totpSecret through the API', async () => {