diff --git a/.github/workflows/offline.yml b/.github/workflows/offline.yml new file mode 100644 index 0000000..aaed9ff --- /dev/null +++ b/.github/workflows/offline.yml @@ -0,0 +1,20 @@ +name: offline +on: + push: + branches: [main] + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npm --prefix packages/verae-zapier-middleware install + - run: npm --prefix packages/zappier install + - run: npm --prefix packages/verae-activate install + - run: npm run test:offline + env: + MOCK_VERAE: 'true' + NATS_ENABLED: 'false' diff --git a/docs/04-activate/SETUP-ZAPIER-DEVELOPER.md b/docs/04-activate/SETUP-ZAPIER-DEVELOPER.md index da6d7a9..1dd3bcd 100644 --- a/docs/04-activate/SETUP-ZAPIER-DEVELOPER.md +++ b/docs/04-activate/SETUP-ZAPIER-DEVELOPER.md @@ -103,7 +103,7 @@ If `register` says the name is taken, use `"Verae Time Dev"` or `"VeraeTime"`. - **API Key** — leave **blank**. - **API base URL** — leave **blank**. - Test connection → should succeed (*Verae Time (Add Numbers)* / local mode). -5. Choose action **Add Numbers** (or **Echo Text**). +5. Choose action **Add Numbers**, **Echo Text**, **SHA256 Hash Text**, or **Create Timestamp** (local mock if API fields blank). 6. Set **Number 1** = `2`, **Number 2** = `3`. 7. **Test step**. You want: diff --git a/docs/OPEN.md b/docs/OPEN.md index ab9bbb7..9d2688a 100644 --- a/docs/OPEN.md +++ b/docs/OPEN.md @@ -5,7 +5,7 @@ These cannot finish in this session: 1. **`zapier-platform login` + `register` + `push`** — browser OAuth to your Zapier account. Follow [docs/04-activate/SETUP-ZAPIER-DEVELOPER.md](04-activate/SETUP-ZAPIER-DEVELOPER.md). After push, commit `.zapierapprc`. 2. **Live `api.veraetime.net`** — hash lookup, metadata, receipts, LTS, share are **not** in the public OpenAPI. Mocks exist; live client refuses unknown paths. 3. **PDF receipts** — JSON extra-seal works; PDF is generated locally (`?format=pdf`). Legal copy/seal artwork still TBD. -4. **Encrypted LTS + share** — mock AES-256-GCM blobs + share tokens exist (`/zapier/v1/objects`, `/zapier/v1/shares/:token`). Not live Verae/Peergos. +4. **Encrypted LTS + share** — file **and directory tree** mocks: `/zapier/v1/objects`, `/objects/trees`, `/shares/:token`. Not live Verae/Peergos. 5. **NS1 NATS public bind** — left loopback-only; `scripts/nats-tunnel.sh` maps `14222`. Everything else for morning Add Numbers is ready. diff --git a/docs/api/middleware-openapi.yaml b/docs/api/middleware-openapi.yaml index f2d5291..1f6fc51 100644 --- a/docs/api/middleware-openapi.yaml +++ b/docs/api/middleware-openapi.yaml @@ -72,6 +72,63 @@ paths: responses: '200': description: Unsubscribed + /zapier/v1/hashes/{sha256}: + get: + summary: Lookup timestamp by SHA256 (mock) + operationId: lookupHash + security: + - BearerAuth: [] + parameters: + - name: sha256 + in: path + required: true + schema: { type: string } + responses: + '200': { description: Found } + '404': { description: Missing } + /zapier/v1/receipts/{jobId}: + get: + summary: Certified retrieval receipt (json or pdf) + operationId: getReceipt + security: + - BearerAuth: [] + parameters: + - name: jobId + in: path + required: true + schema: { type: string } + - name: format + in: query + schema: { type: string, enum: [json, pdf] } + responses: + '200': { description: Receipt } + /zapier/v1/objects: + post: + summary: Store encrypted blob and timestamp + operationId: putObject + security: + - BearerAuth: [] + responses: + '201': { description: Stored } + /zapier/v1/objects/trees: + post: + summary: Store encrypted directory tree + operationId: putTree + security: + - BearerAuth: [] + responses: + '201': { description: Stored } + /zapier/v1/shares/{token}: + get: + summary: Redeem a share token + operationId: redeemShare + parameters: + - name: token + in: path + required: true + schema: { type: string } + responses: + '200': { description: Ciphertext + dek (mock) } components: securitySchemes: BearerAuth: diff --git a/docs/modules/README.md b/docs/modules/README.md index 93f9b25..019da50 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -74,6 +74,7 @@ One MD file per source module. - [verae-activate/creates/add_numbers](verae-activate/creates/add_numbers.md) - [verae-activate/creates/echo](verae-activate/creates/echo.md) - [verae-activate/creates/sha256](verae-activate/creates/sha256.md) +- [verae-activate/creates/timestamp](verae-activate/creates/timestamp.md) - [verae-activate/index](verae-activate/index.md) - [verae-zapier/authentication](verae-zapier/authentication.md) - [verae-zapier/creates/add_numbers](verae-zapier/creates/add_numbers.md) diff --git a/docs/modules/verae-activate/creates/timestamp.md b/docs/modules/verae-activate/creates/timestamp.md new file mode 100644 index 0000000..162a9c9 --- /dev/null +++ b/docs/modules/verae-activate/creates/timestamp.md @@ -0,0 +1,40 @@ +# `verae-activate/creates/timestamp` + +**Package:** `verae-activate` +**Source:** `packages/verae-activate/creates/timestamp.js` +**Lines:** 55 + +## What this module is + +Implementation module in `verae-activate`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`key`, `noun`, `display`, `label`, `description` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `perform` | `z, bundle` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:crypto` + +## Call graph (identifiers invoked) + +`async`, `replace`, `request`, `randomUUID` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-activate/index.md b/docs/modules/verae-activate/index.md index 25a0a61..5e5b869 100644 --- a/docs/modules/verae-activate/index.md +++ b/docs/modules/verae-activate/index.md @@ -2,7 +2,7 @@ **Package:** `verae-activate` **Source:** `packages/verae-activate/index.js` -**Lines:** 25 +**Lines:** 27 ## What this module is @@ -10,7 +10,7 @@ Implementation module in `verae-activate`. The tables below are extracted from t ## Exports -`version`, `platformVersion`, `authentication`, `creates`, `addNumbers`, `echo`, `sha256Hash` +`version`, `platformVersion`, `authentication`, `creates`, `addNumbers`, `echo`, `sha256Hash`, `createTimestamp` ## Types / interfaces / classes @@ -26,6 +26,7 @@ _No top-level functions extracted._ - `./creates/add_numbers` - `./creates/echo` - `./creates/sha256` +- `./creates/timestamp` - `./package.json` - `zapier-platform-core` diff --git a/docs/modules/verae-zapier-middleware/app.md b/docs/modules/verae-zapier-middleware/app.md index bcc4784..bccb200 100644 --- a/docs/modules/verae-zapier-middleware/app.md +++ b/docs/modules/verae-zapier-middleware/app.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/app.js` -**Lines:** 51 +**Lines:** 56 ## What this module is @@ -30,10 +30,12 @@ _None extracted._ - `./debug/trace.js` - `./routes/index.js` - `./store/db.js` +- `./config.js` +- `./nats/connection.js` ## Call graph (identifiers invoked) -`createDebugger`, `app`, `import`, `createApp`, `loadStore`, `express`, `disable`, `use`, `json`, `get`, `sendError`, `info` +`createDebugger`, `app`, `import`, `createApp`, `loadStore`, `express`, `disable`, `use`, `json`, `get`, `isNatsConnected`, `sendError`, `info` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-zapier-middleware/routes/objectRoutes.md b/docs/modules/verae-zapier-middleware/routes/objectRoutes.md index 5a053d7..40598a9 100644 --- a/docs/modules/verae-zapier-middleware/routes/objectRoutes.md +++ b/docs/modules/verae-zapier-middleware/routes/objectRoutes.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/routes/objectRoutes.js` -**Lines:** 91 +**Lines:** 126 ## What this module is @@ -28,6 +28,8 @@ _No top-level functions extracted._ | `asyncHandler` | `async (req, res` | | `asyncHandler` | `async (req, res` | | `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | ## What it imports / requires @@ -38,7 +40,7 @@ _No top-level functions extracted._ ## Call graph (identifiers invoked) -`post`, `asyncHandler`, `async`, `from`, `createTimestamp`, `toString`, `slice`, `putBlob`, `status`, `json`, `get`, `readBlobPlaintext`, `createShare`, `redeemShare` +`post`, `asyncHandler`, `async`, `from`, `createTimestamp`, `toString`, `slice`, `putBlob`, `status`, `json`, `get`, `readBlobPlaintext`, `isArray`, `putTree`, `map`, `createTreeShare`, `createShare`, `redeemShare` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-zapier-middleware/store/blobs.md b/docs/modules/verae-zapier-middleware/store/blobs.md index 6378b66..72d0ddc 100644 --- a/docs/modules/verae-zapier-middleware/store/blobs.md +++ b/docs/modules/verae-zapier-middleware/store/blobs.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/store/blobs.js` -**Lines:** 137 +**Lines:** 209 ## What this module is @@ -10,7 +10,7 @@ Implementation module in `verae-zapier-middleware`. The tables below are extract ## Exports -`decryptBlob`, `putBlob`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare` +`decryptBlob`, `putBlob`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare`, `putTree`, `getTree`, `createTreeShare` ## Types / interfaces / classes @@ -28,6 +28,9 @@ _None extracted._ | `readBlobPlaintext` | `id, tenantId` | id: `string`, tenantId: `string` | `unknown` | see Call graph | | `createShare` | `{ blobId, fromTenantId, label }` | — | `unknown` | see Call graph | | `redeemShare` | `token` | token: `string` | `unknown` | see Call graph | +| `putTree` | `{ tenantId, prefix, files }` | — | `unknown` | see Call graph | +| `getTree` | `id` | — | `unknown` | see Call graph | +| `createTreeShare` | `{ treeId, fromTenantId, label }` | — | `unknown` | see Call graph | ## Methods (class / object) @@ -35,6 +38,8 @@ _None extracted._ |------|------------| | `persist` | `(none)` | | `persist` | `(none)` | +| `persist` | `(none)` | +| `persist` | `(none)` | ## What it imports / requires @@ -43,7 +48,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`ensure`, `getStore`, `encrypt`, `randomBytes`, `createCipheriv`, `concat`, `update`, `final`, `getAuthTag`, `toString`, `decryptBlob`, `createDecipheriv`, `from`, `setAuthTag`, `putBlob`, `randomUUID`, `toISOString`, `persist`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare` +`ensure`, `getStore`, `encrypt`, `randomBytes`, `createCipheriv`, `concat`, `update`, `final`, `getAuthTag`, `toString`, `decryptBlob`, `createDecipheriv`, `from`, `setAuthTag`, `putBlob`, `randomUUID`, `toISOString`, `persist`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare`, `map`, `putTree`, `split`, `pop`, `push`, `getTree`, `createTreeShare` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-zapier-middleware/store/db.md b/docs/modules/verae-zapier-middleware/store/db.md index 869e8c5..328acea 100644 --- a/docs/modules/verae-zapier-middleware/store/db.md +++ b/docs/modules/verae-zapier-middleware/store/db.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/store/db.js` -**Lines:** 117 +**Lines:** 118 ## What this module is diff --git a/docs/modules/zappier/app.md b/docs/modules/zappier/app.md index fe64428..052f90b 100644 --- a/docs/modules/zappier/app.md +++ b/docs/modules/zappier/app.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/app.ts` -**Lines:** 237 +**Lines:** 257 ## What this module is diff --git a/docs/modules/zappier/pricing.md b/docs/modules/zappier/pricing.md index ed264d7..210891f 100644 --- a/docs/modules/zappier/pricing.md +++ b/docs/modules/zappier/pricing.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/pricing.ts` -**Lines:** 173 +**Lines:** 174 ## What this module is diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index c282c34..5a0fde9 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -77,6 +77,7 @@ Verae × Zapier module reference modules/verae-activate/creates/add_numbers modules/verae-activate/creates/echo modules/verae-activate/creates/sha256 + modules/verae-activate/creates/timestamp modules/verae-activate/index modules/verae-zapier/authentication modules/verae-zapier/creates/add_numbers diff --git a/docs/sphinx/modules/verae-activate/creates/timestamp.md b/docs/sphinx/modules/verae-activate/creates/timestamp.md new file mode 100644 index 0000000..162a9c9 --- /dev/null +++ b/docs/sphinx/modules/verae-activate/creates/timestamp.md @@ -0,0 +1,40 @@ +# `verae-activate/creates/timestamp` + +**Package:** `verae-activate` +**Source:** `packages/verae-activate/creates/timestamp.js` +**Lines:** 55 + +## What this module is + +Implementation module in `verae-activate`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`key`, `noun`, `display`, `label`, `description` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `perform` | `z, bundle` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:crypto` + +## Call graph (identifiers invoked) + +`async`, `replace`, `request`, `randomUUID` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-activate/creates/timestamp.rst b/docs/sphinx/modules/verae-activate/creates/timestamp.rst new file mode 100644 index 0000000..59ec60b --- /dev/null +++ b/docs/sphinx/modules/verae-activate/creates/timestamp.rst @@ -0,0 +1,7 @@ +verae-activate.creates.timestamp +================================ + +Generated API sheet for ``verae-activate/creates/timestamp``. + +.. include:: timestamp.md + :literal: diff --git a/docs/sphinx/modules/verae-activate/index.md b/docs/sphinx/modules/verae-activate/index.md index 25a0a61..5e5b869 100644 --- a/docs/sphinx/modules/verae-activate/index.md +++ b/docs/sphinx/modules/verae-activate/index.md @@ -2,7 +2,7 @@ **Package:** `verae-activate` **Source:** `packages/verae-activate/index.js` -**Lines:** 25 +**Lines:** 27 ## What this module is @@ -10,7 +10,7 @@ Implementation module in `verae-activate`. The tables below are extracted from t ## Exports -`version`, `platformVersion`, `authentication`, `creates`, `addNumbers`, `echo`, `sha256Hash` +`version`, `platformVersion`, `authentication`, `creates`, `addNumbers`, `echo`, `sha256Hash`, `createTimestamp` ## Types / interfaces / classes @@ -26,6 +26,7 @@ _No top-level functions extracted._ - `./creates/add_numbers` - `./creates/echo` - `./creates/sha256` +- `./creates/timestamp` - `./package.json` - `zapier-platform-core` diff --git a/docs/sphinx/modules/verae-zapier-middleware/app.md b/docs/sphinx/modules/verae-zapier-middleware/app.md index bcc4784..bccb200 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/app.md +++ b/docs/sphinx/modules/verae-zapier-middleware/app.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/app.js` -**Lines:** 51 +**Lines:** 56 ## What this module is @@ -30,10 +30,12 @@ _None extracted._ - `./debug/trace.js` - `./routes/index.js` - `./store/db.js` +- `./config.js` +- `./nats/connection.js` ## Call graph (identifiers invoked) -`createDebugger`, `app`, `import`, `createApp`, `loadStore`, `express`, `disable`, `use`, `json`, `get`, `sendError`, `info` +`createDebugger`, `app`, `import`, `createApp`, `loadStore`, `express`, `disable`, `use`, `json`, `get`, `isNatsConnected`, `sendError`, `info` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.md b/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.md index 5a053d7..40598a9 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.md +++ b/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/routes/objectRoutes.js` -**Lines:** 91 +**Lines:** 126 ## What this module is @@ -28,6 +28,8 @@ _No top-level functions extracted._ | `asyncHandler` | `async (req, res` | | `asyncHandler` | `async (req, res` | | `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | ## What it imports / requires @@ -38,7 +40,7 @@ _No top-level functions extracted._ ## Call graph (identifiers invoked) -`post`, `asyncHandler`, `async`, `from`, `createTimestamp`, `toString`, `slice`, `putBlob`, `status`, `json`, `get`, `readBlobPlaintext`, `createShare`, `redeemShare` +`post`, `asyncHandler`, `async`, `from`, `createTimestamp`, `toString`, `slice`, `putBlob`, `status`, `json`, `get`, `readBlobPlaintext`, `isArray`, `putTree`, `map`, `createTreeShare`, `createShare`, `redeemShare` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-zapier-middleware/store/blobs.md b/docs/sphinx/modules/verae-zapier-middleware/store/blobs.md index 6378b66..72d0ddc 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/store/blobs.md +++ b/docs/sphinx/modules/verae-zapier-middleware/store/blobs.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/store/blobs.js` -**Lines:** 137 +**Lines:** 209 ## What this module is @@ -10,7 +10,7 @@ Implementation module in `verae-zapier-middleware`. The tables below are extract ## Exports -`decryptBlob`, `putBlob`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare` +`decryptBlob`, `putBlob`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare`, `putTree`, `getTree`, `createTreeShare` ## Types / interfaces / classes @@ -28,6 +28,9 @@ _None extracted._ | `readBlobPlaintext` | `id, tenantId` | id: `string`, tenantId: `string` | `unknown` | see Call graph | | `createShare` | `{ blobId, fromTenantId, label }` | — | `unknown` | see Call graph | | `redeemShare` | `token` | token: `string` | `unknown` | see Call graph | +| `putTree` | `{ tenantId, prefix, files }` | — | `unknown` | see Call graph | +| `getTree` | `id` | — | `unknown` | see Call graph | +| `createTreeShare` | `{ treeId, fromTenantId, label }` | — | `unknown` | see Call graph | ## Methods (class / object) @@ -35,6 +38,8 @@ _None extracted._ |------|------------| | `persist` | `(none)` | | `persist` | `(none)` | +| `persist` | `(none)` | +| `persist` | `(none)` | ## What it imports / requires @@ -43,7 +48,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`ensure`, `getStore`, `encrypt`, `randomBytes`, `createCipheriv`, `concat`, `update`, `final`, `getAuthTag`, `toString`, `decryptBlob`, `createDecipheriv`, `from`, `setAuthTag`, `putBlob`, `randomUUID`, `toISOString`, `persist`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare` +`ensure`, `getStore`, `encrypt`, `randomBytes`, `createCipheriv`, `concat`, `update`, `final`, `getAuthTag`, `toString`, `decryptBlob`, `createDecipheriv`, `from`, `setAuthTag`, `putBlob`, `randomUUID`, `toISOString`, `persist`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare`, `map`, `putTree`, `split`, `pop`, `push`, `getTree`, `createTreeShare` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-zapier-middleware/store/db.md b/docs/sphinx/modules/verae-zapier-middleware/store/db.md index 869e8c5..328acea 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/store/db.md +++ b/docs/sphinx/modules/verae-zapier-middleware/store/db.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/store/db.js` -**Lines:** 117 +**Lines:** 118 ## What this module is diff --git a/docs/sphinx/modules/zappier/app.md b/docs/sphinx/modules/zappier/app.md index fe64428..052f90b 100644 --- a/docs/sphinx/modules/zappier/app.md +++ b/docs/sphinx/modules/zappier/app.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/app.ts` -**Lines:** 237 +**Lines:** 257 ## What this module is diff --git a/docs/sphinx/modules/zappier/pricing.md b/docs/sphinx/modules/zappier/pricing.md index ed264d7..210891f 100644 --- a/docs/sphinx/modules/zappier/pricing.md +++ b/docs/sphinx/modules/zappier/pricing.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/pricing.ts` -**Lines:** 173 +**Lines:** 174 ## What this module is diff --git a/harness/compose.yaml b/harness/compose.yaml index f307447..7d2ad86 100644 --- a/harness/compose.yaml +++ b/harness/compose.yaml @@ -25,16 +25,18 @@ services: - nats zappier: - image: node:20 - working_dir: /app - volumes: - - ../packages/zappier:/app - command: ["npx", "ts-node", "src/index.ts"] + build: ../packages/zappier ports: - "3000:3000" environment: PORT: "3000" - ZAPPIER_DB: /app/data/zappier.db + ZAPPIER_DB: /data/zappier.db ADMIN_KEY: ${ADMIN_KEY:-admin-dev-key} + VERAE_MIDDLEWARE_URL: http://middleware:3100 + volumes: + - zappier-data:/data depends_on: - middleware + +volumes: + zappier-data: diff --git a/packages/verae-activate/creates/timestamp.js b/packages/verae-activate/creates/timestamp.js new file mode 100644 index 0000000..edf6cb9 --- /dev/null +++ b/packages/verae-activate/creates/timestamp.js @@ -0,0 +1,54 @@ +/** + * Create Timestamp — hosted if api_base+api_key, else local mock job. + * @module creates/timestamp + */ + +const { randomUUID } = require('node:crypto'); + +const perform = async (z, bundle) => { + const data = String(bundle.inputData.data ?? ''); + const sha256 = bundle.inputData.sha256; + const base = (bundle.authData.api_base || process.env.ZAPPIER_API_BASE || '').replace(/\/$/, ''); + const key = bundle.authData.api_key || ''; + if (base && key) { + const response = await z.request({ + method: 'POST', + url: `${base}/v1/timestamp`, + headers: { 'x-api-key': key, 'content-type': 'application/json' }, + body: { data, sha256, hashAlg: bundle.inputData.hashAlg }, + }); + return { ...(response.data || {}), mode: 'hosted' }; + } + return { + jobId: randomUUID(), + sha256: sha256 || null, + existing: false, + mode: 'local-mock', + note: 'No API configured — mock jobId only. Set API base + key to hit zappier /v1/timestamp.', + }; +}; + +module.exports = { + key: 'create_timestamp', + noun: 'Timestamp', + display: { + label: 'Create Timestamp', + description: 'Registers data/SHA256. Local mock if no API; hosted zappier when connected.', + }, + operation: { + cleanInputData: false, + inputFields: [ + { key: 'data', label: 'Data', type: 'text', required: false }, + { key: 'sha256', label: 'SHA256 hex', type: 'string', required: false }, + { key: 'hashAlg', label: 'Hash algorithm', type: 'string', required: false, default: 'SHA256' }, + ], + perform, + sample: { jobId: '00000000-0000-4000-8000-000000000000', existing: false, mode: 'local-mock' }, + outputFields: [ + { key: 'jobId', type: 'string' }, + { key: 'sha256', type: 'string' }, + { key: 'existing', type: 'boolean' }, + { key: 'mode', type: 'string' }, + ], + }, +}; diff --git a/packages/verae-activate/index.js b/packages/verae-activate/index.js index a5c27c7..0767dc0 100644 --- a/packages/verae-activate/index.js +++ b/packages/verae-activate/index.js @@ -11,6 +11,7 @@ const authentication = require('./authentication'); const addNumbers = require('./creates/add_numbers'); const echo = require('./creates/echo'); const sha256Hash = require('./creates/sha256'); +const createTimestamp = require('./creates/timestamp'); module.exports = { version: require('./package.json').version, @@ -20,5 +21,6 @@ module.exports = { [addNumbers.key]: addNumbers, [echo.key]: echo, [sha256Hash.key]: sha256Hash, + [createTimestamp.key]: createTimestamp, }, }; diff --git a/packages/verae-activate/test/add_numbers.test.js b/packages/verae-activate/test/add_numbers.test.js index e0ff825..f9fe70b 100644 --- a/packages/verae-activate/test/add_numbers.test.js +++ b/packages/verae-activate/test/add_numbers.test.js @@ -15,6 +15,7 @@ describe('verae-activate app', () => { assert.ok(App.creates.add_numbers); assert.ok(App.creates.echo); assert.ok(App.creates.sha256_hash); + assert.ok(App.creates.create_timestamp); assert.equal(addNumbers.key, 'add_numbers'); assert.ok(addNumbers.operation.inputFields.some((f) => f.key === 'number1')); assert.ok(addNumbers.operation.inputFields.some((f) => f.key === 'number2')); @@ -65,6 +66,16 @@ describe('verae-activate app', () => { assert.equal(result.sum, 5); }); + it('create timestamp local mock without API', async () => { + const ts = require('../creates/timestamp'); + const out = await ts.operation.perform( + { request: async () => { throw new Error('no http'); } }, + { inputData: { data: 'x' }, authData: {} }, + ); + assert.equal(out.mode, 'local-mock'); + assert.ok(out.jobId); + }); + it('sha256 hashes hello', async () => { const sha = require('../creates/sha256'); const out = await sha.operation.perform({}, { inputData: { text: 'hello' }, authData: {} }); diff --git a/packages/verae-zapier-middleware/src/app.js b/packages/verae-zapier-middleware/src/app.js index aca14f8..2048b9b 100644 --- a/packages/verae-zapier-middleware/src/app.js +++ b/packages/verae-zapier-middleware/src/app.js @@ -9,6 +9,8 @@ import { createDebugger } from './debug/logger.js'; import { traceMiddleware } from './debug/trace.js'; import { apiRoutes } from './routes/index.js'; import { loadStore } from './store/db.js'; +import { config } from './config.js'; +import { isNatsConnected } from './nats/connection.js'; const log = createDebugger('app'); @@ -36,6 +38,9 @@ export function createApp(options = {}) { res.json({ status: 'ok', service: 'verae-zapier-middleware', + mockVerae: config.mockVerae, + natsEnabled: config.natsEnabled, + natsConnected: isNatsConnected(), }); }); diff --git a/packages/verae-zapier-middleware/src/routes/objectRoutes.js b/packages/verae-zapier-middleware/src/routes/objectRoutes.js index 4e84feb..35a7b9a 100644 --- a/packages/verae-zapier-middleware/src/routes/objectRoutes.js +++ b/packages/verae-zapier-middleware/src/routes/objectRoutes.js @@ -10,6 +10,8 @@ import { readBlobPlaintext, createShare, redeemShare, + putTree, + createTreeShare, } from '../store/blobs.js'; import { createTimestamp } from '../services/timestampService.js'; @@ -65,6 +67,39 @@ objectRoutes.get( }), ); +objectRoutes.post( + '/trees', + asyncHandler(async (req, res) => { + const { prefix, files } = req.body ?? {}; + if (!Array.isArray(files) || files.length === 0) { + throw new AppError('files array is required', { status: 400, code: 'VALIDATION_ERROR' }); + } + const tree = putTree({ + tenantId: req.auth.tenantId, + prefix: prefix || '/', + files: files.map((f) => ({ + path: String(f.path || f.filename || 'file.bin'), + bytes: Buffer.from(String(f.contentBase64 || f.data || ''), f.contentBase64 ? 'base64' : 'utf8'), + sha256: f.sha256, + })), + }); + res.status(201).json(tree); + }), +); + +objectRoutes.post( + '/trees/:id/share', + asyncHandler(async (req, res) => { + const share = createTreeShare({ + treeId: req.params.id, + fromTenantId: req.auth.tenantId, + label: req.body?.label, + }); + if (!share) throw new AppError('Tree not found', { status: 404, code: 'NOT_FOUND' }); + res.status(201).json(share); + }), +); + objectRoutes.post( '/:id/share', asyncHandler(async (req, res) => { diff --git a/packages/verae-zapier-middleware/src/store/blobs.js b/packages/verae-zapier-middleware/src/store/blobs.js index 29cf799..d24dfe0 100644 --- a/packages/verae-zapier-middleware/src/store/blobs.js +++ b/packages/verae-zapier-middleware/src/store/blobs.js @@ -12,6 +12,7 @@ function ensure() { const store = getStore(); store.blobs ??= {}; store.shares ??= {}; + store.trees ??= {}; return store; } @@ -103,6 +104,7 @@ export function createShare({ blobId, fromTenantId, label }) { const store = ensure(); store.shares[token] = { token, + kind: 'file', blobId, fromTenantId, label: label ?? rec.filename, @@ -120,10 +122,35 @@ export function redeemShare(token) { const store = ensure(); const share = store.shares[token]; if (!share) return null; + if (share.kind === 'tree') { + const tree = store.trees[share.treeId]; + if (!tree) return null; + return { + token, + kind: 'tree', + prefix: tree.prefix, + files: tree.entries.map((e) => { + const rec = store.blobs[e.blobId]; + return rec + ? { + path: e.path, + blobId: rec.id, + filename: rec.filename, + size: rec.size, + sha256: rec.sha256, + enc: rec.enc, + dek: rec.dek, + } + : { path: e.path, missing: true }; + }), + note: 'Mock tree share: each file includes dek. Production must wrap per-file deks to recipient pubkey.', + }; + } const rec = store.blobs[share.blobId]; if (!rec) return null; return { token, + kind: 'file', blobId: rec.id, filename: rec.filename, size: rec.size, @@ -134,3 +161,48 @@ export function redeemShare(token) { note: 'Mock share: dek is included so the recipient can decrypt. Production must wrap dek to recipient pubkey.', }; } + +/** + * @param {{ tenantId: string, prefix: string, files: Array<{ path: string, bytes: Buffer, sha256?: string }> }} params + */ +export function putTree({ tenantId, prefix, files }) { + const store = ensure(); + const id = randomUUID(); + const entries = []; + for (const file of files) { + const stored = putBlob({ + tenantId, + sha256: file.sha256, + filename: file.path.split('/').pop(), + bytes: file.bytes, + }); + entries.push({ path: file.path, blobId: stored.id }); + } + store.trees[id] = { id, tenantId, prefix: prefix || '/', entries, createdAt: new Date().toISOString() }; + persist(); + return { id, prefix: store.trees[id].prefix, fileCount: entries.length }; +} + +export function getTree(id) { + return ensure().trees[id] ?? null; +} + +/** + * @param {{ treeId: string, fromTenantId: string, label?: string }} params + */ +export function createTreeShare({ treeId, fromTenantId, label }) { + const tree = getTree(treeId); + if (!tree || tree.tenantId !== fromTenantId) return null; + const token = `shr_${randomBytes(16).toString('hex')}`; + const store = ensure(); + store.shares[token] = { + token, + kind: 'tree', + treeId, + fromTenantId, + label: label ?? tree.prefix, + createdAt: new Date().toISOString(), + }; + persist(); + return { token, kind: 'tree', treeId, label: store.shares[token].label, fileCount: tree.entries.length }; +} diff --git a/packages/verae-zapier-middleware/src/store/db.js b/packages/verae-zapier-middleware/src/store/db.js index a73f94d..3f1fd3a 100644 --- a/packages/verae-zapier-middleware/src/store/db.js +++ b/packages/verae-zapier-middleware/src/store/db.js @@ -35,6 +35,7 @@ export function emptyStore() { jobWatchers: [], blobs: {}, shares: {}, + trees: {}, }; } diff --git a/packages/verae-zapier-middleware/test/integration/objects.test.js b/packages/verae-zapier-middleware/test/integration/objects.test.js index 9613a5e..76a6d4d 100644 --- a/packages/verae-zapier-middleware/test/integration/objects.test.js +++ b/packages/verae-zapier-middleware/test/integration/objects.test.js @@ -62,6 +62,41 @@ describe('encrypted objects + share + pdf receipt', () => { assert.equal(redeemed.body.publicMetadata.title, 'pub'); }); + it('shares a directory tree of encrypted files', async () => { + const tree = await call(port, 'POST', '/zapier/v1/objects/trees', ownerKey, { + prefix: 'docs/', + files: [ + { path: 'docs/a.txt', data: 'alpha' }, + { path: 'docs/b.txt', data: 'beta' }, + ], + }); + assert.equal(tree.status, 201); + assert.equal(tree.body.fileCount, 2); + const share = await call(port, 'POST', `/zapier/v1/objects/trees/${tree.body.id}/share`, ownerKey, {}); + assert.equal(share.status, 201); + assert.equal(share.body.kind, 'tree'); + const redeemed = await call(port, 'GET', `/zapier/v1/shares/${share.body.token}`, otherKey); + assert.equal(redeemed.status, 200); + assert.equal(redeemed.body.kind, 'tree'); + assert.equal(redeemed.body.files.length, 2); + assert.ok(redeemed.body.files[0].dek); + }); + + it('hash lookup never includes privateMetadata', async () => { + const created = await call(port, 'POST', '/zapier/v1/timestamp', ownerKey, { + data: 'meta-private', + publicMetadata: { vis: 'yes' }, + privateMetadata: { secret: 'nope' }, + }); + const sha = created.body.sha256; + const look = await call(port, 'GET', `/zapier/v1/hashes/${sha}`, ownerKey); + assert.equal(look.status, 200); + assert.equal(look.body.publicMetadata.vis, 'yes'); + assert.equal(look.body.privateMetadata, undefined); + const st = await call(port, 'GET', `/zapier/v1/status/${created.body.jobId}`, ownerKey); + assert.equal(st.body.privateMetadata.secret, 'nope'); + }); + it('returns a PDF receipt starting with %PDF', async () => { const wait = await call(port, 'POST', '/zapier/v1/timestamp/wait', ownerKey, { data: 'pdf-receipt' }); const jobId = wait.body.id; diff --git a/packages/verae-zapier-middleware/test/unit/app.test.js b/packages/verae-zapier-middleware/test/unit/app.test.js index c4dc6f0..48000d7 100644 --- a/packages/verae-zapier-middleware/test/unit/app.test.js +++ b/packages/verae-zapier-middleware/test/unit/app.test.js @@ -30,6 +30,9 @@ describe('createApp', () => { const body = await res.json(); assert.equal(body.status, 'ok'); assert.equal(body.service, 'verae-zapier-middleware'); + assert.equal(typeof body.mockVerae, 'boolean'); + assert.equal(typeof body.natsEnabled, 'boolean'); + assert.equal(typeof body.natsConnected, 'boolean'); assert.ok(res.headers.get('x-trace-id')); }); diff --git a/packages/zappier/Dockerfile b/packages/zappier/Dockerfile new file mode 100644 index 0000000..48a5d4e --- /dev/null +++ b/packages/zappier/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-bookworm-slim +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src ./src +COPY admin ./admin +COPY portal ./portal +COPY openapi.yaml ./ +ENV PORT=3000 +EXPOSE 3000 +CMD ["npx", "ts-node", "src/index.ts"] diff --git a/packages/zappier/openapi.yaml b/packages/zappier/openapi.yaml index d6cddab..f1ac451 100644 --- a/packages/zappier/openapi.yaml +++ b/packages/zappier/openapi.yaml @@ -93,6 +93,21 @@ paths: description: OK '404': description: Missing + /v1/receipts/{jobId}: + get: + operationId: receipt + summary: Retrieval receipt JSON (mock job ids from /v1/timestamp) + parameters: + - name: jobId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '404': + description: Missing /v1/add: post: operationId: add diff --git a/packages/zappier/src/app.ts b/packages/zappier/src/app.ts index 9a5c5a1..c6cc916 100644 --- a/packages/zappier/src/app.ts +++ b/packages/zappier/src/app.ts @@ -101,6 +101,7 @@ export function buildApp(deps: AppDeps = {}): { string, { jobId: string; sha256: string; data?: string; timestamp: string } >(); + const jobIndex = new Map(); const app = express(); app.use(express.json()); @@ -177,10 +178,29 @@ export function buildApp(deps: AppDeps = {}): { } const jobId = randomUUID(); const timestamp = new Date().toISOString(); - hashIndex.set(sha256, { jobId, sha256, data, timestamp }); + const rec = { jobId, sha256, data, timestamp }; + hashIndex.set(sha256, rec); + jobIndex.set(jobId, rec); res.status(202).json({ jobId, sha256, existing: false, timestamp }); }); + app.get('/v1/receipts/:jobId', meter('receipt', usage, pricing), (req, res) => { + const rec = jobIndex.get(String(req.params.jobId)); + if (!rec) { + res.status(404).json({ error: 'Job not found' }); + return; + } + res.json({ + type: 'verae.retrieval-receipt', + format: 'json', + jobId: rec.jobId, + sha256: rec.sha256, + timestamp: rec.timestamp, + extraSeal: { event: 'document.retrieved', retrievedAt: new Date().toISOString() }, + quote: res.locals.quote, + }); + }); + app.get('/v1/hashes/:sha256', meter('hash-lookup', usage, pricing), (req, res) => { const sha256 = String(req.params.sha256 || '').toLowerCase(); const rec = hashIndex.get(sha256); diff --git a/packages/zappier/src/pricing.ts b/packages/zappier/src/pricing.ts index 5512e56..32bae74 100644 --- a/packages/zappier/src/pricing.ts +++ b/packages/zappier/src/pricing.ts @@ -66,6 +66,7 @@ export const DEFAULT_RATE_CARD: RateCard = { add: { kind: 'free' }, timestamp: { kind: 'fixed', fixedCents: 4 }, 'hash-lookup': { kind: 'free' }, + receipt: { kind: 'free' }, storage: { kind: 'variable', baseCents: 10, perKbCents: 1, perMbCents: 50 }, }, }; diff --git a/packages/zappier/tests/app.test.ts b/packages/zappier/tests/app.test.ts index e6cef14..80d5626 100644 --- a/packages/zappier/tests/app.test.ts +++ b/packages/zappier/tests/app.test.ts @@ -36,6 +36,11 @@ describe('Zappier API', () => { .set('x-api-key', KEY); expect(look.status).toBe(200); expect(look.body.jobId).toBe(a.body.jobId); + const receipt = await request(app) + .get(`/v1/receipts/${a.body.jobId}`) + .set('x-api-key', KEY); + expect(receipt.status).toBe(200); + expect(receipt.body.type).toBe('verae.retrieval-receipt'); }); it('POST /v1/add returns the sum and is free', async () => { diff --git a/scripts/seed-demo.mjs b/scripts/seed-demo.mjs new file mode 100644 index 0000000..3e7b3ea --- /dev/null +++ b/scripts/seed-demo.mjs @@ -0,0 +1,22 @@ +#!/usr/bin/env node +/** + * Seed a demo tenant on local middleware (MOCK_VERAE). + */ +const base = process.env.MIDDLEWARE_URL || 'http://127.0.0.1:3100'; + +const res = await fetch(`${base}/zapier/v1/signup`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + email: 'demo@veraetime.local', + name: 'Demo tenant', + veraeUsername: 'demouser', + veraePassword: 'demopass', + }), +}); +const body = await res.json(); +if (!res.ok) { + console.error(body); + process.exit(1); +} +console.log(JSON.stringify({ apiKey: body.apiKey, tenant: body.tenant, middleware: base }, null, 2));