Bind each customer to a Verae userId for hop tracing
Some checks are pending
offline / test (push) Waiting to run

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.
This commit is contained in:
George Lambert 2026-09-11 16:18:06 -04:00
parent 1b199ca4d4
commit 345aeeead9
79 changed files with 703 additions and 95 deletions

View file

@ -25,6 +25,7 @@ All subjects are prefixed with `verae.zapier.` to isolate this platform from oth
| `tenantId` | string | yes | Owning tenant | | `tenantId` | string | yes | Owning tenant |
| `jobId` | string | yes | Verae job id | | `jobId` | string | yes | Verae job id |
| `tokenRef` | string | preferred | Opaque ref to resolve Verae credentials (avoid raw JWT) | | `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 | | `veraeToken` | string | discouraged | Only if tokenRef unavailable; redacted in logs |
| `enqueuedAt` | ISO-8601 | yes | Enqueue time | | `enqueuedAt` | ISO-8601 | yes | Enqueue time |
| `attempt` | number | yes | Delivery attempt (0-based) | | `attempt` | number | yes | Delivery attempt (0-based) |

View file

@ -32,6 +32,7 @@ One MD file per source module.
- [zappier/statement](zappier/statement.md) - [zappier/statement](zappier/statement.md)
- [zappier/upstream](zappier/upstream.md) - [zappier/upstream](zappier/upstream.md)
- [zappier/usage](zappier/usage.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/app](verae-zapier-middleware/app.md)
- [verae-zapier-middleware/clients/veraeClient](verae-zapier-middleware/clients/veraeClient.md) - [verae-zapier-middleware/clients/veraeClient](verae-zapier-middleware/clients/veraeClient.md)
- [verae-zapier-middleware/config](verae-zapier-middleware/config.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/debug/trace](verae-zapier-middleware/debug/trace.md)
- [verae-zapier-middleware/errors](verae-zapier-middleware/errors.md) - [verae-zapier-middleware/errors](verae-zapier-middleware/errors.md)
- [verae-zapier-middleware/index](verae-zapier-middleware/index.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/receiptPdf](verae-zapier-middleware/lib/receiptPdf.md)
- [verae-zapier-middleware/lib/tokens](verae-zapier-middleware/lib/tokens.md) - [verae-zapier-middleware/lib/tokens](verae-zapier-middleware/lib/tokens.md)
- [verae-zapier-middleware/middleware/authenticate](verae-zapier-middleware/middleware/authenticate.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/db](verae-zapier-middleware/store/db.md)
- [verae-zapier-middleware/store/jobWatchers](verae-zapier-middleware/store/jobWatchers.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/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/usage](verae-zapier-middleware/store/usage.md)
- [verae-zapier-middleware/store/webhooks](verae-zapier-middleware/store/webhooks.md) - [verae-zapier-middleware/store/webhooks](verae-zapier-middleware/store/webhooks.md)
- [verae-zapier-middleware/workers/inProcessJobPoller](verae-zapier-middleware/workers/inProcessJobPoller.md) - [verae-zapier-middleware/workers/inProcessJobPoller](verae-zapier-middleware/workers/inProcessJobPoller.md)

View file

@ -2,7 +2,7 @@
**Package:** `verae-access-authz` **Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/policy.js` **Source:** `packages/verae-access-authz/src/policy.js`
**Lines:** 99 **Lines:** 101
## What this module is ## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/clients/veraeClient.js` **Source:** `packages/verae-zapier-middleware/src/clients/veraeClient.js`
**Lines:** 352 **Lines:** 386
## What this module is ## What this module is
@ -24,6 +24,7 @@ _None extracted._
| `delay` | `ms` | ms: `number` | `Promise<void>` | see Call graph | | `delay` | `ms` | ms: `number` | `Promise<void>` | see Call graph |
| `mockLogin` | `{ username, password }` | ms: `number` | `Promise<void>` | see Call graph | | `mockLogin` | `{ username, password }` | ms: `number` | `Promise<void>` | see Call graph |
| `mockValidate` | `token` | — | `unknown` | 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 | | `mockCreateTimestamp` | `{ data, hashAlg, sha256, publicMetadata, privateMetadata }` | — | `unknown` | see Call graph |
| `mockGetStatus` | `jobId` | — | `unknown` | see Call graph | | `mockGetStatus` | `jobId` | — | `unknown` | see Call graph |
| `mockLookupHash` | `sha256` | — | `unknown` | see Call graph | | `mockLookupHash` | `sha256` | — | `unknown` | see Call graph |
@ -38,6 +39,8 @@ _None extracted._
| `setTimeout` | `(` | | `setTimeout` | `(` |
| `login` | `credentials` | | `login` | `credentials` |
| `validate` | `token` | | `validate` | `token` |
| `createUser` | `adminToken, body` |
| `bindUser` | `credentials` |
| `createTimestamp` | `token, body` | | `createTimestamp` | `token, body` |
| `lookupHash` | `token, sha256` | | `lookupHash` | `token, sha256` |
| `createBatchTimestamp` | `token, body` | | `createBatchTimestamp` | `token, body` |
@ -54,10 +57,11 @@ _None extracted._
- `../config.js` - `../config.js`
- `../errors.js` - `../errors.js`
- `../debug/logger.js` - `../debug/logger.js`
- `../lib/identity.js`
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -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 modules 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`.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/middleware/authenticate.js` **Source:** `packages/verae-zapier-middleware/src/middleware/authenticate.js`
**Lines:** 28 **Lines:** 34
## What this module is ## What this module is
@ -35,7 +35,7 @@ _No top-level functions extracted._
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/nats/publishers.js` **Source:** `packages/verae-zapier-middleware/src/nats/publishers.js`
**Lines:** 96 **Lines:** 98
## What this module is ## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/services/authService.js` **Source:** `packages/verae-zapier-middleware/src/services/authService.js`
**Lines:** 142 **Lines:** 154
## What this module is ## What this module is
@ -35,7 +35,7 @@ _None extracted._
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/services/tenantService.js` **Source:** `packages/verae-zapier-middleware/src/services/tenantService.js`
**Lines:** 138 **Lines:** 150
## What this module is ## What this module is
@ -45,7 +45,7 @@ _None extracted._
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/services/timestampService.js` **Source:** `packages/verae-zapier-middleware/src/services/timestampService.js`
**Lines:** 170 **Lines:** 171
## What this module is ## What this module is
@ -53,13 +53,14 @@ _None extracted._
- `../config.js` - `../config.js`
- `../clients/veraeClient.js` - `../clients/veraeClient.js`
- `../store/jobWatchers.js` - `../store/jobWatchers.js`
- `../store/tokenRefs.js`
- `./entitlementService.js` - `./entitlementService.js`
- `../debug/logger.js` - `../debug/logger.js`
- `../debug/trace-context.js` - `../debug/trace-context.js`
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/store/db.js` **Source:** `packages/verae-zapier-middleware/src/store/db.js`
**Lines:** 118 **Lines:** 120
## What this module is ## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/store/tenants.js` **Source:** `packages/verae-zapier-middleware/src/store/tenants.js`
**Lines:** 138 **Lines:** 142
## What this module is ## What this module is
@ -30,8 +30,9 @@ _None extracted._
plan = 'free', plan = 'free',
veraeUsername, veraeUsername,
veraePassword, veraePassword,
veraeUserId,
contract = null, 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` | `{ | `resolveLimits` | `tenant` | tenant: `Tenant` | `{
* timestamps: number|null, * timestamps: number|null,
* verifications: number|null, * verifications: number|null,

View file

@ -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 modules 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`.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/workers/jobPollerWorker.js` **Source:** `packages/verae-zapier-middleware/src/workers/jobPollerWorker.js`
**Lines:** 159 **Lines:** 161
## What this module is ## What this module is
@ -34,11 +34,12 @@ _None extracted._
- `../nats/publishers.js` - `../nats/publishers.js`
- `../clients/veraeClient.js` - `../clients/veraeClient.js`
- `../store/tenants.js` - `../store/tenants.js`
- `../store/tokenRefs.js`
- `../debug/trace.js` - `../debug/trace.js`
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier-account-balance` **Package:** `zappier-account-balance`
**Source:** `packages/zappier-account-balance/src/books.js` **Source:** `packages/zappier-account-balance/src/books.js`
**Lines:** 90 **Lines:** 100
## What this module is ## What this module is
@ -30,8 +30,9 @@ Implementation module in `zappier-account-balance`. The tables below are extract
| Name | Parameters | | Name | Parameters |
|------|------------| |------|------------|
| `constructor` | `(none)` | | `constructor` | `(none)` |
| `remember` | `customerId, veraeUserId` |
| `prepaidCents` | `customerId` | | `prepaidCents` | `customerId` |
| `adjust` | `{ customerId, cents, reason, agent, kind = 'credit' }` | | `adjust` | `{ customerId, cents, reason, agent, kind = 'credit', veraeUserId }` |
| `recordUsage` | `entry` | | `recordUsage` | `entry` |
| `recordPayment` | `entry` | | `recordPayment` | `entry` |
| `statement` | `customerId` | | `statement` | `customerId` |
@ -42,7 +43,7 @@ _No imports detected._
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/admin.ts` **Source:** `packages/zappier/src/admin.ts`
**Lines:** 591 **Lines:** 593
## What this module is ## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/app.ts` **Source:** `packages/zappier/src/app.ts`
**Lines:** 272 **Lines:** 278
## What this module is ## What this module is
@ -52,7 +52,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/auth.ts` **Source:** `packages/zappier/src/auth.ts`
**Lines:** 84 **Lines:** 87
## What this module is ## What this module is
@ -48,7 +48,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/billing-nats.ts` **Source:** `packages/zappier/src/billing-nats.ts`
**Lines:** 130 **Lines:** 138
## What this module is ## 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 | | `decode` | `buf: Uint8Array` | — | `unknown` | see Call graph |
| `authzAllow` | `plane: AccessPlane, | `authzAllow` | `plane: AccessPlane,
subject: string, 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, | `natsStatement` | `customerId: string,
plane: AccessPlane = 'web',` | — | `unknown` | see Call graph | plane: AccessPlane = 'web',
veraeUserId?: string,` | — | `unknown` | see Call graph |
| `natsAdjust` | `payload: { | `natsAdjust` | `payload: {
customerId: string; customerId: string;
veraeUserId?: string;
cents: number; cents: number;
reason: string; reason: string;
agent: string; agent: string;

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/portal.ts` **Source:** `packages/zappier/src/portal.ts`
**Lines:** 328 **Lines:** 337
## What this module is ## What this module is
@ -51,10 +51,11 @@ Implementation module in `zappier`. The tables below are extracted from the sour
- `./credits` - `./credits`
- `./statement` - `./statement`
- `./billing-nats` - `./billing-nats`
- `./verae-bind`
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/statement.ts` **Source:** `packages/zappier/src/statement.ts`
**Lines:** 39 **Lines:** 41
## What this module is ## What this module is
@ -22,6 +22,7 @@ _None extracted._
|------|------------|---------------------|---------|-----------------------------| |------|------------|---------------------|---------|-----------------------------|
| `composeStatement` | `args: { | `composeStatement` | `args: {
customerId: string; customerId: string;
veraeUserId?: string;
prepaidCents: number; prepaidCents: number;
credits: CreditAdjustment[]; credits: CreditAdjustment[];
usage: UsageEntry[]; usage: UsageEntry[];

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/upstream.ts` **Source:** `packages/zappier/src/upstream.ts`
**Lines:** 46 **Lines:** 48
## What this module is ## What this module is

View file

@ -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 modules 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`.

View file

@ -68,6 +68,7 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html
modules/zappier/statement modules/zappier/statement
modules/zappier/upstream modules/zappier/upstream
modules/zappier/usage modules/zappier/usage
modules/zappier/verae-bind
modules/verae-zapier-middleware/app modules/verae-zapier-middleware/app
modules/verae-zapier-middleware/clients/veraeClient modules/verae-zapier-middleware/clients/veraeClient
modules/verae-zapier-middleware/config 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/debug/trace
modules/verae-zapier-middleware/errors modules/verae-zapier-middleware/errors
modules/verae-zapier-middleware/index modules/verae-zapier-middleware/index
modules/verae-zapier-middleware/lib/identity
modules/verae-zapier-middleware/lib/receiptPdf modules/verae-zapier-middleware/lib/receiptPdf
modules/verae-zapier-middleware/lib/tokens modules/verae-zapier-middleware/lib/tokens
modules/verae-zapier-middleware/middleware/authenticate 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/db
modules/verae-zapier-middleware/store/jobWatchers modules/verae-zapier-middleware/store/jobWatchers
modules/verae-zapier-middleware/store/tenants modules/verae-zapier-middleware/store/tenants
modules/verae-zapier-middleware/store/tokenRefs
modules/verae-zapier-middleware/store/usage modules/verae-zapier-middleware/store/usage
modules/verae-zapier-middleware/store/webhooks modules/verae-zapier-middleware/store/webhooks
modules/verae-zapier-middleware/workers/inProcessJobPoller modules/verae-zapier-middleware/workers/inProcessJobPoller

View file

@ -2,7 +2,7 @@
**Package:** `verae-access-authz` **Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/policy.js` **Source:** `packages/verae-access-authz/src/policy.js`
**Lines:** 99 **Lines:** 101
## What this module is ## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/clients/veraeClient.js` **Source:** `packages/verae-zapier-middleware/src/clients/veraeClient.js`
**Lines:** 352 **Lines:** 386
## What this module is ## What this module is
@ -24,6 +24,7 @@ _None extracted._
| `delay` | `ms` | ms: `number` | `Promise<void>` | see Call graph | | `delay` | `ms` | ms: `number` | `Promise<void>` | see Call graph |
| `mockLogin` | `{ username, password }` | ms: `number` | `Promise<void>` | see Call graph | | `mockLogin` | `{ username, password }` | ms: `number` | `Promise<void>` | see Call graph |
| `mockValidate` | `token` | — | `unknown` | 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 | | `mockCreateTimestamp` | `{ data, hashAlg, sha256, publicMetadata, privateMetadata }` | — | `unknown` | see Call graph |
| `mockGetStatus` | `jobId` | — | `unknown` | see Call graph | | `mockGetStatus` | `jobId` | — | `unknown` | see Call graph |
| `mockLookupHash` | `sha256` | — | `unknown` | see Call graph | | `mockLookupHash` | `sha256` | — | `unknown` | see Call graph |
@ -38,6 +39,8 @@ _None extracted._
| `setTimeout` | `(` | | `setTimeout` | `(` |
| `login` | `credentials` | | `login` | `credentials` |
| `validate` | `token` | | `validate` | `token` |
| `createUser` | `adminToken, body` |
| `bindUser` | `credentials` |
| `createTimestamp` | `token, body` | | `createTimestamp` | `token, body` |
| `lookupHash` | `token, sha256` | | `lookupHash` | `token, sha256` |
| `createBatchTimestamp` | `token, body` | | `createBatchTimestamp` | `token, body` |
@ -54,10 +57,11 @@ _None extracted._
- `../config.js` - `../config.js`
- `../errors.js` - `../errors.js`
- `../debug/logger.js` - `../debug/logger.js`
- `../lib/identity.js`
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -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 modules 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`.

View file

@ -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_

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/middleware/authenticate.js` **Source:** `packages/verae-zapier-middleware/src/middleware/authenticate.js`
**Lines:** 28 **Lines:** 34
## What this module is ## What this module is
@ -35,7 +35,7 @@ _No top-level functions extracted._
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/nats/publishers.js` **Source:** `packages/verae-zapier-middleware/src/nats/publishers.js`
**Lines:** 96 **Lines:** 98
## What this module is ## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/services/authService.js` **Source:** `packages/verae-zapier-middleware/src/services/authService.js`
**Lines:** 142 **Lines:** 154
## What this module is ## What this module is
@ -35,7 +35,7 @@ _None extracted._
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/services/tenantService.js` **Source:** `packages/verae-zapier-middleware/src/services/tenantService.js`
**Lines:** 138 **Lines:** 150
## What this module is ## What this module is
@ -45,7 +45,7 @@ _None extracted._
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/services/timestampService.js` **Source:** `packages/verae-zapier-middleware/src/services/timestampService.js`
**Lines:** 170 **Lines:** 171
## What this module is ## What this module is
@ -53,13 +53,14 @@ _None extracted._
- `../config.js` - `../config.js`
- `../clients/veraeClient.js` - `../clients/veraeClient.js`
- `../store/jobWatchers.js` - `../store/jobWatchers.js`
- `../store/tokenRefs.js`
- `./entitlementService.js` - `./entitlementService.js`
- `../debug/logger.js` - `../debug/logger.js`
- `../debug/trace-context.js` - `../debug/trace-context.js`
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/store/db.js` **Source:** `packages/verae-zapier-middleware/src/store/db.js`
**Lines:** 118 **Lines:** 120
## What this module is ## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/store/tenants.js` **Source:** `packages/verae-zapier-middleware/src/store/tenants.js`
**Lines:** 138 **Lines:** 142
## What this module is ## What this module is
@ -30,8 +30,9 @@ _None extracted._
plan = 'free', plan = 'free',
veraeUsername, veraeUsername,
veraePassword, veraePassword,
veraeUserId,
contract = null, 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` | `{ | `resolveLimits` | `tenant` | tenant: `Tenant` | `{
* timestamps: number|null, * timestamps: number|null,
* verifications: number|null, * verifications: number|null,

View file

@ -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 modules 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`.

View file

@ -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_

View file

@ -2,7 +2,7 @@
**Package:** `verae-zapier-middleware` **Package:** `verae-zapier-middleware`
**Source:** `packages/verae-zapier-middleware/src/workers/jobPollerWorker.js` **Source:** `packages/verae-zapier-middleware/src/workers/jobPollerWorker.js`
**Lines:** 159 **Lines:** 161
## What this module is ## What this module is
@ -34,11 +34,12 @@ _None extracted._
- `../nats/publishers.js` - `../nats/publishers.js`
- `../clients/veraeClient.js` - `../clients/veraeClient.js`
- `../store/tenants.js` - `../store/tenants.js`
- `../store/tokenRefs.js`
- `../debug/trace.js` - `../debug/trace.js`
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier-account-balance` **Package:** `zappier-account-balance`
**Source:** `packages/zappier-account-balance/src/books.js` **Source:** `packages/zappier-account-balance/src/books.js`
**Lines:** 90 **Lines:** 100
## What this module is ## What this module is
@ -30,8 +30,9 @@ Implementation module in `zappier-account-balance`. The tables below are extract
| Name | Parameters | | Name | Parameters |
|------|------------| |------|------------|
| `constructor` | `(none)` | | `constructor` | `(none)` |
| `remember` | `customerId, veraeUserId` |
| `prepaidCents` | `customerId` | | `prepaidCents` | `customerId` |
| `adjust` | `{ customerId, cents, reason, agent, kind = 'credit' }` | | `adjust` | `{ customerId, cents, reason, agent, kind = 'credit', veraeUserId }` |
| `recordUsage` | `entry` | | `recordUsage` | `entry` |
| `recordPayment` | `entry` | | `recordPayment` | `entry` |
| `statement` | `customerId` | | `statement` | `customerId` |
@ -42,7 +43,7 @@ _No imports detected._
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/admin.ts` **Source:** `packages/zappier/src/admin.ts`
**Lines:** 591 **Lines:** 593
## What this module is ## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/app.ts` **Source:** `packages/zappier/src/app.ts`
**Lines:** 272 **Lines:** 278
## What this module is ## What this module is
@ -52,7 +52,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/auth.ts` **Source:** `packages/zappier/src/auth.ts`
**Lines:** 84 **Lines:** 87
## What this module is ## What this module is
@ -48,7 +48,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/billing-nats.ts` **Source:** `packages/zappier/src/billing-nats.ts`
**Lines:** 130 **Lines:** 138
## What this module is ## 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 | | `decode` | `buf: Uint8Array` | — | `unknown` | see Call graph |
| `authzAllow` | `plane: AccessPlane, | `authzAllow` | `plane: AccessPlane,
subject: string, 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, | `natsStatement` | `customerId: string,
plane: AccessPlane = 'web',` | — | `unknown` | see Call graph | plane: AccessPlane = 'web',
veraeUserId?: string,` | — | `unknown` | see Call graph |
| `natsAdjust` | `payload: { | `natsAdjust` | `payload: {
customerId: string; customerId: string;
veraeUserId?: string;
cents: number; cents: number;
reason: string; reason: string;
agent: string; agent: string;

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/portal.ts` **Source:** `packages/zappier/src/portal.ts`
**Lines:** 328 **Lines:** 337
## What this module is ## What this module is
@ -51,10 +51,11 @@ Implementation module in `zappier`. The tables below are extracted from the sour
- `./credits` - `./credits`
- `./statement` - `./statement`
- `./billing-nats` - `./billing-nats`
- `./verae-bind`
## Call graph (identifiers invoked) ## 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 modules MD for parameter and return types. Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/statement.ts` **Source:** `packages/zappier/src/statement.ts`
**Lines:** 39 **Lines:** 41
## What this module is ## What this module is
@ -22,6 +22,7 @@ _None extracted._
|------|------------|---------------------|---------|-----------------------------| |------|------------|---------------------|---------|-----------------------------|
| `composeStatement` | `args: { | `composeStatement` | `args: {
customerId: string; customerId: string;
veraeUserId?: string;
prepaidCents: number; prepaidCents: number;
credits: CreditAdjustment[]; credits: CreditAdjustment[];
usage: UsageEntry[]; usage: UsageEntry[];

View file

@ -2,7 +2,7 @@
**Package:** `zappier` **Package:** `zappier`
**Source:** `packages/zappier/src/upstream.ts` **Source:** `packages/zappier/src/upstream.ts`
**Lines:** 46 **Lines:** 48
## What this module is ## What this module is

View file

@ -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 modules 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`.

View file

@ -0,0 +1,7 @@
zappier.verae-bind
==================
Generated API sheet for ``zappier/verae-bind``.
.. include:: verae-bind.md
:parser: myst_parser.sphinx_

View file

@ -59,7 +59,7 @@ Zapier cloud **never** connects to NATS. Internal services do: middleware worker
| Address | Kind | Payload (required fields) | | 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.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.webhooks.deliver` | JetStream work queue | `hookId`, `tenantId`, `targetUrl`, `event`, `payload`, `attempt`, `traceId` |
| `verae.zapier.usage` | optional | `tenantId`, `action`, `amount`, `at` | | `verae.zapier.usage` | optional | `tenantId`, `action`, `amount`, `at` |

View file

@ -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` | | IN HTTPS | `/v1/*` | Zapier Platform app | JSON + `x-api-key` |
| OUT HTTPS | middleware `/zapier/v1/*` | verae-middleware | same tenant request | | 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.payment.recorded` | account-balance / others | portal reload |
| OUT NATS | `verae.billing.credit.applied` | account-balance / others | admin/CS credit | | OUT NATS | `verae.billing.credit.applied` | account-balance / others | admin/CS credit |
| OUT NATS | `verae.billing.statement.get` | account-balance | portal/admin review | | OUT NATS | `verae.billing.statement.get` | account-balance | portal/admin review |

View file

@ -89,6 +89,8 @@ export function authorize(req) {
plane, plane,
subject: internal, subject: internal,
principal: req?.principal || null, principal: req?.principal || null,
veraeUserId: req?.veraeUserId || req?.payload?.veraeUserId || null,
traceId: req?.traceId || req?.payload?.traceId || null,
reason: 'ok', reason: 'ok',
}; };
} }

View file

@ -60,6 +60,19 @@ test('access-prefixed address is mapped to internal', () => {
assert.equal(authorize({ plane: 'web', subject: addr }).allow, false); 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', () => { test('unknown plane and authz subjects denied', () => {
assert.equal(authorize({ plane: 'partner', subject: 'verae.archive.put' }).allow, false); assert.equal(authorize({ plane: 'partner', subject: 'verae.archive.put' }).allow, false);
assert.equal(authorize({ plane: 'web', subject: 'verae.access.authz.check' }).allow, false); assert.equal(authorize({ plane: 'web', subject: 'verae.access.authz.check' }).allow, false);

View file

@ -7,6 +7,7 @@ import { createHash, randomUUID } from 'node:crypto';
import { config } from '../config.js'; import { config } from '../config.js';
import { AppError } from '../errors.js'; import { AppError } from '../errors.js';
import { createDebugger } from '../debug/logger.js'; import { createDebugger } from '../debug/logger.js';
import { stableVeraeUserId } from '../lib/identity.js';
const log = createDebugger('http'); const log = createDebugger('http');
@ -39,7 +40,7 @@ async function mockLogin({ username, password }) {
token: `mock-jwt-${username}`, token: `mock-jwt-${username}`,
expiresAt, expiresAt,
user: { user: {
id: randomUUID(), id: stableVeraeUserId(username),
username, username,
role: username.includes('admin') ? 'admin' : 'user', role: username.includes('admin') ? 'admin' : 'user',
}, },
@ -53,13 +54,24 @@ async function mockValidate(token) {
const username = token.replace('mock-jwt-', ''); const username = token.replace('mock-jwt-', '');
return { return {
valid: true, valid: true,
userId: randomUUID(), userId: stableVeraeUserId(username),
username, username,
role: 'user', role: 'user',
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), 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 }) { async function mockCreateTimestamp({ data, hashAlg, sha256, publicMetadata, privateMetadata }) {
if (!data && !sha256) { if (!data && !sha256) {
throw new AppError('Invalid input data', { status: 400, code: 'VALIDATION_ERROR' }); throw new AppError('Invalid input data', { status: 400, code: 'VALIDATION_ERROR' });
@ -228,6 +240,28 @@ export const veraeClient = {
return request('/auth/validate', { token }); 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 {string} token
* @param {{ data: string, hashAlg?: string }} body * @param {{ data: string, hashAlg?: string }} body

View file

@ -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)}`;
}

View file

@ -18,8 +18,14 @@ export const authenticate = asyncHandler(async (req, res, next) => {
extractBearerToken(req.headers.authorization) ?? req.headers['x-api-key'] ?? null; extractBearerToken(req.headers.authorization) ?? req.headers['x-api-key'] ?? null;
req.auth = await resolveAuthContext(rawToken); 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', { log.debug('authenticated', {
tenantId: req.auth.tenantId, tenantId: req.auth.tenantId,
veraeUserId: req.auth.veraeUserId,
method: req.auth.authMethod, method: req.auth.authMethod,
plan: req.auth.tenant?.plan, plan: req.auth.tenant?.plan,
}); });

View file

@ -37,9 +37,11 @@ export async function enqueueWatch(partial) {
log.debug('enqueueWatch', { log.debug('enqueueWatch', {
subject: SUBJECTS.JOBS_WATCH, subject: SUBJECTS.JOBS_WATCH,
tenantId: msg.tenantId, tenantId: msg.tenantId,
veraeUserId: msg.veraeUserId,
jobId: msg.jobId, jobId: msg.jobId,
attempt: msg.attempt, attempt: msg.attempt,
traceId: msg.traceId, traceId: msg.traceId,
hasTokenRef: Boolean(msg.tokenRef),
}); });
const js = await requireJs(); const js = await requireJs();

View file

@ -4,7 +4,7 @@
*/ */
import { veraeClient } from '../clients/veraeClient.js'; 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 { issueSessionToken, parseSessionToken, isApiKey } from '../lib/tokens.js';
import { AppError } from '../errors.js'; import { AppError } from '../errors.js';
import { createDebugger } from '../debug/logger.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 tenantId = tenant?.id ?? `user:${verae.user.username}`;
const veraeUserId = verae.user?.id;
if (tenant && veraeUserId && tenant.veraeUserId !== veraeUserId) {
upsertTenant({ ...tenant, veraeUserId });
}
const accessToken = issueSessionToken({ const accessToken = issueSessionToken({
tenantId, tenantId,
veraeToken: verae.token, veraeToken: verae.token,
@ -43,9 +47,10 @@ export async function loginWithCredentials({ username, password, tenant }) {
accessToken, accessToken,
expiresAt: verae.expiresAt, expiresAt: verae.expiresAt,
tenant: tenant tenant: tenant
? { id: tenant.id, name: tenant.name, plan: tenant.plan } ? { id: tenant.id, name: tenant.name, plan: tenant.plan, veraeUserId: veraeUserId || tenant.veraeUserId }
: { id: tenantId, name: verae.user.username, plan: 'free' }, : { id: tenantId, name: verae.user.username, plan: 'free', veraeUserId },
user: verae.user, user: verae.user,
veraeUserId,
}; };
} }
@ -93,8 +98,14 @@ export async function resolveAuthContext(rawToken) {
const tenant = getTenant(session.tenant.id) ?? session.tenant; const tenant = getTenant(session.tenant.id) ?? session.tenant;
return { return {
tenantId: session.tenant.id, 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, veraeToken: parsed.veraeToken,
veraeUserId: session.veraeUserId || tenant?.veraeUserId,
authMethod: 'api_key', authMethod: 'api_key',
fullTenant: tenant, fullTenant: tenant,
}; };
@ -114,9 +125,10 @@ export async function resolveAuthContext(rawToken) {
return { return {
tenantId: parsed.tenantId, tenantId: parsed.tenantId,
tenant: tenant 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' }, : { id: parsed.tenantId, plan: 'free' },
veraeToken: parsed.veraeToken, veraeToken: parsed.veraeToken,
veraeUserId: tenant?.veraeUserId,
authMethod: 'session', authMethod: 'session',
fullTenant: tenant, fullTenant: tenant,
}; };

View file

@ -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 id = `tenant-${slugify(email)}-${randomUUID().slice(0, 8)}`;
const { tenant, apiKey } = createTenant({ const { tenant, apiKey } = createTenant({
@ -54,14 +54,21 @@ export async function selfServeSignup({ email, name, veraeUsername, veraePasswor
plan: 'free', plan: 'free',
veraeUsername, veraeUsername,
veraePassword, veraePassword,
veraeUserId: bound.veraeUserId,
contract: null, contract: null,
metadata: { email, audience: 'self-serve', createdVia: 'signup' }, 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 { 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, apiKey,
zapierSetup: { zapierSetup: {
authType: 'custom', 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)}`; const tenantId = id ?? `tenant-${slugify(name)}-${randomUUID().slice(0, 8)}`;
if (getTenant(tenantId)) { if (getTenant(tenantId)) {
@ -115,12 +122,16 @@ export async function provisionTenant({
plan, plan,
veraeUsername, veraeUsername,
veraePassword, veraePassword,
veraeUserId: bound.veraeUserId,
contract, contract,
metadata: { ...metadata, audience, createdVia: 'provision' }, metadata: { ...metadata, audience, createdVia: 'provision' },
}); });
log.info('tenant provisioned', { tenantId: tenant.id, plan, audience }); log.info('tenant provisioned', { tenantId: tenant.id, plan, audience, veraeUserId: bound.veraeUserId });
return { tenant: { id: tenant.id, name: tenant.name, plan: tenant.plan }, apiKey }; 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, name: tenant.name,
plan: tenant.plan, plan: tenant.plan,
audience: tenant.metadata?.audience ?? 'unknown', audience: tenant.metadata?.audience ?? 'unknown',
veraeUserId: tenant.veraeUserId,
createdAt: tenant.createdAt, createdAt: tenant.createdAt,
})); }));
} }

View file

@ -6,6 +6,7 @@
import { config } from '../config.js'; import { config } from '../config.js';
import { veraeClient } from '../clients/veraeClient.js'; import { veraeClient } from '../clients/veraeClient.js';
import { enqueueJob } from '../store/jobWatchers.js'; import { enqueueJob } from '../store/jobWatchers.js';
import { issueTokenRef } from '../store/tokenRefs.js';
import { checkEntitlement, recordUsage } from './entitlementService.js'; import { checkEntitlement, recordUsage } from './entitlementService.js';
import { createDebugger } from '../debug/logger.js'; import { createDebugger } from '../debug/logger.js';
import { getTraceId } from '../debug/trace-context.js'; import { getTraceId } from '../debug/trace-context.js';
@ -25,8 +26,8 @@ async function enqueueWatchForJob(ctx, jobId) {
await enqueueWatch({ await enqueueWatch({
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
jobId, jobId,
// Prefer re-login in worker; include token for MVP simplicity when mock tokenRef: issueTokenRef(ctx.tenantId),
veraeToken: ctx.veraeToken, veraeUserId: ctx.veraeUserId || ctx.tenant?.veraeUserId,
maxAttempts: config.jobPollMaxAttempts, maxAttempts: config.jobPollMaxAttempts,
intervalMs: config.jobPollIntervalMs, intervalMs: config.jobPollIntervalMs,
traceId, traceId,

View file

@ -33,6 +33,7 @@ export function emptyStore() {
usage: {}, usage: {},
webhooks: [], webhooks: [],
jobWatchers: [], jobWatchers: [],
tokenRefs: {},
blobs: {}, blobs: {},
shares: {}, shares: {},
trees: {}, trees: {},
@ -59,6 +60,7 @@ export function loadStore(path = config.storePath) {
usage: parsed.usage ?? {}, usage: parsed.usage ?? {},
webhooks: Array.isArray(parsed.webhooks) ? parsed.webhooks : [], webhooks: Array.isArray(parsed.webhooks) ? parsed.webhooks : [],
jobWatchers: Array.isArray(parsed.jobWatchers) ? parsed.jobWatchers : [], jobWatchers: Array.isArray(parsed.jobWatchers) ? parsed.jobWatchers : [],
tokenRefs: parsed.tokenRefs ?? {},
}; };
log.debug('store loaded', { path, tenants: Object.keys(store.tenants).length }); log.debug('store loaded', { path, tenants: Object.keys(store.tenants).length });
} catch (err) { } catch (err) {

View file

@ -17,6 +17,7 @@ const log = createDebugger('billing');
* @property {string} plan * @property {string} plan
* @property {string} veraeUsername * @property {string} veraeUsername
* @property {string} veraePassword * @property {string} veraePassword
* @property {string} [veraeUserId]
* @property {object|null} contract * @property {object|null} contract
* @property {object} [metadata] * @property {object} [metadata]
* @property {string} createdAt * @property {string} createdAt
@ -68,6 +69,7 @@ export function upsertTenant(tenant) {
* @param {string} [params.plan='free'] * @param {string} [params.plan='free']
* @param {string} params.veraeUsername * @param {string} params.veraeUsername
* @param {string} params.veraePassword * @param {string} params.veraePassword
* @param {string} [params.veraeUserId]
* @param {object|null} [params.contract=null] * @param {object|null} [params.contract=null]
* @param {string} [params.apiKey] * @param {string} [params.apiKey]
* @param {object} [params.metadata] * @param {object} [params.metadata]
@ -79,6 +81,7 @@ export function createTenant({
plan = 'free', plan = 'free',
veraeUsername, veraeUsername,
veraePassword, veraePassword,
veraeUserId,
contract = null, contract = null,
apiKey = generateApiKey(), apiKey = generateApiKey(),
metadata = {}, metadata = {},
@ -91,6 +94,7 @@ export function createTenant({
plan, plan,
veraeUsername, veraeUsername,
veraePassword, veraePassword,
veraeUserId,
contract, contract,
metadata, metadata,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),

View file

@ -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;
}

View file

@ -10,6 +10,7 @@ import { connectNats, ensureStreams } from '../nats/connection.js';
import { publishJobEvent } from '../nats/publishers.js'; import { publishJobEvent } from '../nats/publishers.js';
import { veraeClient } from '../clients/veraeClient.js'; import { veraeClient } from '../clients/veraeClient.js';
import { getTenant } from '../store/tenants.js'; import { getTenant } from '../store/tenants.js';
import { resolveTokenRef } from '../store/tokenRefs.js';
import { withTrace } from '../debug/trace.js'; import { withTrace } from '../debug/trace.js';
const log = createDebugger('jobs'); const log = createDebugger('jobs');
@ -26,9 +27,10 @@ let abort = null;
async function resolveVeraeToken(msg) { async function resolveVeraeToken(msg) {
if (msg.veraeToken) return msg.veraeToken; 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) { 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({ const login = await veraeClient.login({
username: tenant.veraeUsername, username: tenant.veraeUsername,

View file

@ -44,6 +44,8 @@ describe('tenancy', () => {
const body = await res.json(); const body = await res.json();
assert.equal(body.tenant.plan, 'free'); assert.equal(body.tenant.plan, 'free');
assert.ok(body.apiKey.startsWith('zmw_')); 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 () => { it('enterprise without contract rejected', async () => {

View file

@ -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();
}
});
});

View file

@ -18,6 +18,9 @@ describe('veraeClient mock', () => {
it('create → wait → completed', async () => { it('create → wait → completed', async () => {
const login = await veraeClient.login({ username: 'u', password: 'p' }); const login = await veraeClient.login({ username: 'u', password: 'p' });
assert.ok(login.token.startsWith('mock-jwt-')); 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, { const { jobId } = await veraeClient.createTimestamp(login.token, {
data: 'hello', data: 'hello',

View file

@ -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. 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**. 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. 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. 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.

View file

@ -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. - 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. - 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. - 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. - 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. 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.

View file

@ -5,16 +5,23 @@ export class AccountBooks {
constructor() { constructor() {
/** @type {Record<string, number>} */ /** @type {Record<string, number>} */
this.prepaid = {}; this.prepaid = {};
/** @type {Record<string, string>} */
this.veraeUserIds = {};
this.credits = []; this.credits = [];
this.usage = []; this.usage = [];
this.payments = []; this.payments = [];
} }
remember(customerId, veraeUserId) {
if (customerId && veraeUserId) this.veraeUserIds[customerId] = veraeUserId;
}
prepaidCents(customerId) { prepaidCents(customerId) {
return this.prepaid[customerId] ?? 0; 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 delta = Math.trunc(Number(cents) || 0);
const next = this.prepaidCents(customerId) + delta; const next = this.prepaidCents(customerId) + delta;
this.prepaid[customerId] = next; this.prepaid[customerId] = next;
@ -34,6 +41,7 @@ export class AccountBooks {
} }
recordUsage(entry) { recordUsage(entry) {
this.remember(entry.customerId, entry.veraeUserId);
const cents = Number(entry.cents) || 0; const cents = Number(entry.cents) || 0;
const customerId = entry.customerId; const customerId = entry.customerId;
const next = this.prepaidCents(customerId) - cents; 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); const match = (rows) => rows.filter((r) => r.customerId === customerId).slice(0, 100);
return { return {
customerId, customerId,
veraeUserId: this.veraeUserIds[customerId],
prepaidCents: this.prepaidCents(customerId), prepaidCents: this.prepaidCents(customerId),
credits: match(this.credits), credits: match(this.credits),
usage: match(this.usage), usage: match(this.usage),
@ -74,6 +83,7 @@ export class AccountBooks {
export function handle(subject, payload, books) { export function handle(subject, payload, books) {
const p = payload || {}; const p = payload || {};
if (subject.endsWith('balance.get') || subject.endsWith('statement.get')) { if (subject.endsWith('balance.get') || subject.endsWith('statement.get')) {
books.remember(p.customerId, p.veraeUserId);
return books.statement(p.customerId); return books.statement(p.customerId);
} }
if (subject.endsWith('balance.adjust') || subject.endsWith('credit.applied')) { if (subject.endsWith('balance.adjust') || subject.endsWith('credit.applied')) {

View file

@ -5,11 +5,16 @@ import { SUBJECTS } from '../src/subjects.js';
test('adjust credits prepaid and statement lists credits usage payments', () => { test('adjust credits prepaid and statement lists credits usage payments', () => {
const books = new AccountBooks(); 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.USAGE_RECORDED, { customerId: 'c1', endpointId: 'timestamp', cents: 4 }, books);
handle(SUBJECTS.PAYMENT_RECORDED, { customerId: 'c1', cents: 1000, reason: 'reload' }, books); handle(SUBJECTS.PAYMENT_RECORDED, { customerId: 'c1', cents: 1000, reason: 'reload' }, books);
const st = handle(SUBJECTS.STATEMENT_GET, { customerId: 'c1' }, books); const st = handle(SUBJECTS.STATEMENT_GET, { customerId: 'c1' }, books);
assert.equal(st.prepaidCents, 1496); assert.equal(st.prepaidCents, 1496);
assert.equal(st.veraeUserId, 'vu_abc');
assert.equal(st.credits[0].cents, 500); assert.equal(st.credits[0].cents, 500);
assert.equal(st.usage[0].endpointId, 'timestamp'); assert.equal(st.usage[0].endpointId, 'timestamp');
assert.equal(st.payments[0].kind, 'payment'); assert.equal(st.payments[0].kind, 'payment');

View file

@ -248,10 +248,11 @@ export function adminRouter(
agent: typeof agent === 'string' ? agent : 'admin', agent: typeof agent === 'string' ? agent : 'admin',
}); });
customers.save({ ...customer, balanceCents: (customer.balanceCents ?? 0) + delta }); 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( void natsAdjust(
{ {
customerId, customerId,
veraeUserId: customer.veraeUserId,
cents: delta, cents: delta,
reason: rec.reason, reason: rec.reason,
agent: rec.agent, agent: rec.agent,
@ -273,7 +274,7 @@ export function adminRouter(
res.status(404).json({ error: 'customer not found' }); res.status(404).json({ error: 'customer not found' });
return; return;
} }
const fromNats = await natsStatement(customer.id, 'staff'); const fromNats = await natsStatement(customer.id, 'staff', customer.veraeUserId);
if (fromNats) { if (fromNats) {
res.json({ ...fromNats, name: customer.name, tierId: customer.tierId, source: 'nats' }); res.json({ ...fromNats, name: customer.name, tierId: customer.tierId, source: 'nats' });
return; return;
@ -281,6 +282,7 @@ export function adminRouter(
res.json({ res.json({
...composeStatement({ ...composeStatement({
customerId: customer.id, customerId: customer.id,
veraeUserId: customer.veraeUserId,
prepaidCents: customer.balanceCents ?? 0, prepaidCents: customer.balanceCents ?? 0,
credits: credits.list(customer.id), credits: credits.list(customer.id),
usage: accounting.usage.listFor(customer.id), usage: accounting.usage.listFor(customer.id),

View file

@ -102,8 +102,14 @@ export function buildApp(deps: AppDeps = {}): {
}; };
const items: StoredItem[] = []; const items: StoredItem[] = [];
const credits = new CreditLedger(); const credits = new CreditLedger();
const onUsage = (e: { customerId: string; endpointId: string; cents: number }) => const onUsage = (e: { customerId: string; endpointId: string; cents: number }) => {
natsPublish(BILLING_SUBJECTS.USAGE_RECORDED, { ...e, at: new Date().toISOString() }, 'api'); 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< const hashIndex = new Map<
string, string,
{ jobId: string; sha256: string; data?: string; timestamp: string } { jobId: string; sha256: string; data?: string; timestamp: string }

View file

@ -12,6 +12,9 @@ export interface Customer {
/** How this customer is billed. Absent means 'stripe' (the default). */ /** How this customer is billed. Absent means 'stripe' (the default). */
billingType?: BillingType; billingType?: BillingType;
email?: string; 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. */ /** scrypt hash for portal login. Absent = no portal password set yet. */
passwordHash?: string; passwordHash?: string;
/** Base32 TOTP secret. Present once 2FA setup begins. */ /** Base32 TOTP secret. Present once 2FA setup begins. */

View file

@ -15,6 +15,7 @@ export type AccessPlane = 'zapier' | 'web' | 'api' | 'leaf' | 'staff';
export type BillingStatement = { export type BillingStatement = {
customerId: string; customerId: string;
veraeUserId?: string;
prepaidCents: number; prepaidCents: number;
credits: unknown[]; credits: unknown[];
usage: unknown[]; usage: unknown[];
@ -54,7 +55,7 @@ function decode(buf: Uint8Array): unknown {
async function authzAllow( async function authzAllow(
plane: AccessPlane, plane: AccessPlane,
subject: string, subject: string,
extra: { principal?: string; kind?: string } = {}, extra: { principal?: string; kind?: string; veraeUserId?: string } = {},
): Promise<boolean> { ): Promise<boolean> {
const http = process.env.AUTHZ_URL; const http = process.env.AUTHZ_URL;
if (!http && !(await nc())) return true; if (!http && !(await nc())) return true;
@ -83,13 +84,18 @@ async function authzAllow(
export async function natsStatement( export async function natsStatement(
customerId: string, customerId: string,
plane: AccessPlane = 'web', plane: AccessPlane = 'web',
veraeUserId?: string,
): Promise<BillingStatement | null> { ): Promise<BillingStatement | null> {
const c = await nc(); const c = await nc();
if (!c) return null; 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( const m = await c.request(
BILLING_SUBJECTS.STATEMENT_GET, BILLING_SUBJECTS.STATEMENT_GET,
encode({ customerId, plane }), encode({ customerId, plane, veraeUserId }),
{ timeout: 2000 }, { timeout: 2000 },
); );
return decode(m.data) as BillingStatement; return decode(m.data) as BillingStatement;
@ -98,6 +104,7 @@ export async function natsStatement(
export async function natsAdjust( export async function natsAdjust(
payload: { payload: {
customerId: string; customerId: string;
veraeUserId?: string;
cents: number; cents: number;
reason: string; reason: string;
agent: string; agent: string;
@ -111,6 +118,7 @@ export async function natsAdjust(
!(await authzAllow(plane, BILLING_SUBJECTS.BALANCE_ADJUST, { !(await authzAllow(plane, BILLING_SUBJECTS.BALANCE_ADJUST, {
principal: payload.agent, principal: payload.agent,
kind: payload.kind, kind: payload.kind,
veraeUserId: payload.veraeUserId,
})) }))
) { ) {
return null; return null;

View file

@ -17,6 +17,7 @@ import { UsageRepo } from './usage';
import { CreditLedger } from './credits'; import { CreditLedger } from './credits';
import { composeStatement } from './statement'; import { composeStatement } from './statement';
import { BILLING_SUBJECTS, natsPublish, natsStatement, natsAdjust } from './billing-nats'; 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, * Customer portal API (/portal/api): signup, login with optional TOTP 2FA,
@ -63,6 +64,7 @@ function publicProfile(c: Customer) {
tierId: c.tierId, tierId: c.tierId,
billingType: c.billingType ?? 'stripe', billingType: c.billingType ?? 'stripe',
apiKey: c.apiKey, apiKey: c.apiKey,
veraeUserId: c.veraeUserId,
balanceCents: c.balanceCents ?? 0, balanceCents: c.balanceCents ?? 0,
totpEnabled: c.totpEnabled ?? false, totpEnabled: c.totpEnabled ?? false,
emailInvoicing: c.emailInvoicing ?? false, emailInvoicing: c.emailInvoicing ?? false,
@ -97,7 +99,7 @@ export function portalRouter(deps: PortalDeps): Router {
/* ---------------- auth ---------------- */ /* ---------------- auth ---------------- */
router.post('/signup', (req, res) => { router.post('/signup', async (req, res) => {
const { name, email, password } = req.body ?? {}; const { name, email, password } = req.body ?? {};
if (typeof name !== 'string' || name.trim().length === 0) { if (typeof name !== 'string' || name.trim().length === 0) {
res.status(400).json({ error: 'name is required' }); res.status(400).json({ error: 'name is required' });
@ -133,6 +135,10 @@ export function portalRouter(deps: PortalDeps): Router {
balanceCents: 0, balanceCents: 0,
}; };
} }
if (!customer.veraeUserId) {
const bind = await bindVeraeUser(email);
customer = { ...customer, veraeUserId: bind.veraeUserId, veraeUsername: bind.veraeUsername };
}
save(deps, customer); save(deps, customer);
const session = deps.sessions.create(customer.id, ttl); const session = deps.sessions.create(customer.id, ttl);
res.status(201).json({ token: session.token, customer: publicProfile(customer) }); 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) => { 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) { if (fromNats) {
res.json({ ...fromNats, source: 'nats' }); res.json({ ...fromNats, source: 'nats' });
return; return;
@ -211,6 +217,7 @@ export function portalRouter(deps: PortalDeps): Router {
res.json({ res.json({
...composeStatement({ ...composeStatement({
customerId: req.customer!.id, customerId: req.customer!.id,
veraeUserId: req.customer!.veraeUserId,
prepaidCents: req.customer!.balanceCents ?? 0, prepaidCents: req.customer!.balanceCents ?? 0,
credits: (deps.credits || new CreditLedger()).list(req.customer!.id), credits: (deps.credits || new CreditLedger()).list(req.customer!.id),
usage: deps.usage.listFor(req.customer!.id), usage: deps.usage.listFor(req.customer!.id),
@ -290,6 +297,7 @@ export function portalRouter(deps: PortalDeps): Router {
BILLING_SUBJECTS.PAYMENT_RECORDED, BILLING_SUBJECTS.PAYMENT_RECORDED,
{ {
customerId: customer.id, customerId: customer.id,
veraeUserId: customer.veraeUserId,
cents: result.creditedCents, cents: result.creditedCents,
reason: 'reload', reason: 'reload',
}, },
@ -298,6 +306,7 @@ export function portalRouter(deps: PortalDeps): Router {
void natsAdjust( void natsAdjust(
{ {
customerId: customer.id, customerId: customer.id,
veraeUserId: customer.veraeUserId,
cents: result.creditedCents, cents: result.creditedCents,
reason: 'reload', reason: 'reload',
agent: 'portal', agent: 'portal',

View file

@ -4,6 +4,7 @@ import { UsageEntry } from './usage';
export function composeStatement(args: { export function composeStatement(args: {
customerId: string; customerId: string;
veraeUserId?: string;
prepaidCents: number; prepaidCents: number;
credits: CreditAdjustment[]; credits: CreditAdjustment[];
usage: UsageEntry[]; usage: UsageEntry[];
@ -21,6 +22,7 @@ export function composeStatement(args: {
})); }));
return { return {
customerId: args.customerId, customerId: args.customerId,
veraeUserId: args.veraeUserId,
prepaidCents: args.prepaidCents, prepaidCents: args.prepaidCents,
credits: args.credits.filter((c) => c.customerId === args.customerId), credits: args.credits.filter((c) => c.customerId === args.customerId),
usage: args.usage usage: args.usage

View file

@ -23,6 +23,8 @@ export async function proxyVerae(
if (key) headers['x-api-key'] = key; if (key) headers['x-api-key'] = key;
const auth = req.header('authorization'); const auth = req.header('authorization');
if (auth) headers.authorization = auth; if (auth) headers.authorization = auth;
const veraeUserId = req.customer?.veraeUserId;
if (veraeUserId) headers['x-verae-user-id'] = veraeUserId;
const method = req.method.toUpperCase(); const method = req.method.toUpperCase();
const init: RequestInit = { method, headers }; const init: RequestInit = { method, headers };
if (method !== 'GET' && method !== 'HEAD') { if (method !== 'GET' && method !== 'HEAD') {

View file

@ -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<VeraeBind> {
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);
}
}

View file

@ -19,7 +19,7 @@ async function signup(app: ReturnType<typeof buildApp>['app'], email = 'ada@exam
.post('/portal/api/signup') .post('/portal/api/signup')
.send({ name: 'Ada', email, password: 'super-secret-1' }); .send({ name: 'Ada', email, password: 'super-secret-1' });
expect(res.status).toBe(201); 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', () => { describe('portal signup + login', () => {
@ -34,6 +34,9 @@ describe('portal signup + login', () => {
expect(me.status).toBe(200); expect(me.status).toBe(200);
expect(me.body.tierId).toBe('free'); expect(me.body.tierId).toBe('free');
expect(me.body.balanceCents).toBe(0); 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 () => { it('never leaks passwordHash or totpSecret through the API', async () => {