diff --git a/docs/01-product/features-a-m.md b/docs/01-product/features-a-m.md index c89a982..e7fde12 100644 --- a/docs/01-product/features-a-m.md +++ b/docs/01-product/features-a-m.md @@ -9,10 +9,10 @@ Source: [workspace-brief.md](../00-sources/workspace-brief.md). Owners after com | c | Register SHA256 or return original timestamp + ref | verae middleware mock + zappier `/v1/timestamp` | **Mock done** — second create returns original `jobId` + `existing: true` | Live OpenAPI still `data` only | | d | Lookup by SHA256 | `GET /zapier/v1/hashes/{sha256}` and zappier `/v1/hashes/{sha256}` | **Mock done** | No live Verae route | | e | Timestamp + public/private metadata | verae mock | Missing | No | -| f | e + encrypted LTS binary | verae mock | Missing. Do not use zappier demo `/v1/storage` | No | -| g | Share file/dir + decryption key | verae mock | Missing | No | +| f | e + encrypted LTS binary | `POST/GET /zapier/v1/objects` AES-256-GCM mock | **Mock done** | No | +| g | Share file/dir + decryption key | `POST .../share` + `GET /zapier/v1/shares/:token` | **File mock done**; directory tree still TBD | No | | h | Retrieve SHA256 + metadata | verae status/verify | Partial by `jobId` | `GET /api/status/{jobId}`, `GET /api/verify/{jobId}` | -| i | Certified retrieval receipt (PDF/JSON) extra seal | `GET /zapier/v1/receipts/{jobId}?format=json` | **JSON mock done**; PDF 501 | No | +| i | Certified retrieval receipt (PDF/JSON) extra seal | `GET /zapier/v1/receipts/{jobId}?format=json\|pdf` | **JSON + PDF mock done** | No | | j | Setup payment method | zappier portal reload (Stripe) | Built (prepaid reload; card-on-file TBD) | No | | k | Get invoices | zappier portal + admin | Built | No | | l | Get payment receipts | zappier invoice HTML/print | Built as invoices | No | diff --git a/docs/OPEN.md b/docs/OPEN.md index 4163234..ab9bbb7 100644 --- a/docs/OPEN.md +++ b/docs/OPEN.md @@ -4,8 +4,8 @@ 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 501. -4. **Encrypted LTS + directory share (f, g)** — not implemented beyond mock timestamp metadata. -5. **NS1 NATS public bind** — left loopback-only; use SSH tunnel (`14222`). +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. +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/modules/README.md b/docs/modules/README.md index 73f7506..93f9b25 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -37,6 +37,7 @@ One MD file per source module. - [verae-zapier-middleware/debug/trace](verae-zapier-middleware/debug/trace.md) - [verae-zapier-middleware/errors](verae-zapier-middleware/errors.md) - [verae-zapier-middleware/index](verae-zapier-middleware/index.md) +- [verae-zapier-middleware/lib/receiptPdf](verae-zapier-middleware/lib/receiptPdf.md) - [verae-zapier-middleware/lib/tokens](verae-zapier-middleware/lib/tokens.md) - [verae-zapier-middleware/middleware/authenticate](verae-zapier-middleware/middleware/authenticate.md) - [verae-zapier-middleware/middleware/rateLimit](verae-zapier-middleware/middleware/rateLimit.md) @@ -47,6 +48,8 @@ One MD file per source module. - [verae-zapier-middleware/routes/authRoutes](verae-zapier-middleware/routes/authRoutes.md) - [verae-zapier-middleware/routes/hashRoutes](verae-zapier-middleware/routes/hashRoutes.md) - [verae-zapier-middleware/routes/index](verae-zapier-middleware/routes/index.md) +- [verae-zapier-middleware/routes/objectRoutes](verae-zapier-middleware/routes/objectRoutes.md) +- [verae-zapier-middleware/routes/receiptRoutes](verae-zapier-middleware/routes/receiptRoutes.md) - [verae-zapier-middleware/routes/statusRoutes](verae-zapier-middleware/routes/statusRoutes.md) - [verae-zapier-middleware/routes/tenantRoutes](verae-zapier-middleware/routes/tenantRoutes.md) - [verae-zapier-middleware/routes/timestampRoutes](verae-zapier-middleware/routes/timestampRoutes.md) @@ -58,6 +61,7 @@ One MD file per source module. - [verae-zapier-middleware/services/timestampService](verae-zapier-middleware/services/timestampService.md) - [verae-zapier-middleware/services/verifyService](verae-zapier-middleware/services/verifyService.md) - [verae-zapier-middleware/services/webhookService](verae-zapier-middleware/services/webhookService.md) +- [verae-zapier-middleware/store/blobs](verae-zapier-middleware/store/blobs.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/tenants](verae-zapier-middleware/store/tenants.md) @@ -69,6 +73,7 @@ One MD file per source module. - [verae-activate/authentication](verae-activate/authentication.md) - [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/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/sha256.md b/docs/modules/verae-activate/creates/sha256.md new file mode 100644 index 0000000..d754181 --- /dev/null +++ b/docs/modules/verae-activate/creates/sha256.md @@ -0,0 +1,40 @@ +# `verae-activate/creates/sha256` + +**Package:** `verae-activate` +**Source:** `packages/verae-activate/creates/sha256.js` +**Lines:** 37 + +## 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`, `createHash`, `update`, `digest` + +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 5303bf9..25a0a61 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:** 23 +**Lines:** 25 ## 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` +`version`, `platformVersion`, `authentication`, `creates`, `addNumbers`, `echo`, `sha256Hash` ## Types / interfaces / classes @@ -25,6 +25,7 @@ _No top-level functions extracted._ - `./authentication` - `./creates/add_numbers` - `./creates/echo` +- `./creates/sha256` - `./package.json` - `zapier-platform-core` diff --git a/docs/modules/verae-zapier-middleware/clients/veraeClient.md b/docs/modules/verae-zapier-middleware/clients/veraeClient.md index 00ec894..8d75562 100644 --- a/docs/modules/verae-zapier-middleware/clients/veraeClient.md +++ b/docs/modules/verae-zapier-middleware/clients/veraeClient.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/clients/veraeClient.js` -**Lines:** 351 +**Lines:** 352 ## What this module is diff --git a/docs/modules/verae-zapier-middleware/lib/receiptPdf.md b/docs/modules/verae-zapier-middleware/lib/receiptPdf.md new file mode 100644 index 0000000..60ef9b1 --- /dev/null +++ b/docs/modules/verae-zapier-middleware/lib/receiptPdf.md @@ -0,0 +1,41 @@ +# `verae-zapier-middleware/lib/receiptPdf` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/lib/receiptPdf.js` +**Lines:** 65 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`buildReceiptPdf` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `pdfEscape` | `s` | s: `string` | `string` | see Call graph | +| `buildReceiptPdf` | `receipt` | receipt: `object` | `Buffer` | see Call graph | + +## What it imports / requires + +_No imports detected._ + +## Call graph (identifiers invoked) + +`receipts`, `pdfEscape`, `replace`, `buildReceiptPdf`, `slice`, `map`, `join`, `byteLength`, `push`, `padStart`, `from` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-zapier-middleware/routes/index.md b/docs/modules/verae-zapier-middleware/routes/index.md index a01ba88..af46ee1 100644 --- a/docs/modules/verae-zapier-middleware/routes/index.md +++ b/docs/modules/verae-zapier-middleware/routes/index.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/routes/index.js` -**Lines:** 36 +**Lines:** 41 ## What this module is @@ -30,6 +30,8 @@ _No top-level functions extracted._ - `./webhookRoutes.js` - `./tenantRoutes.js` - `./hashRoutes.js` +- `./receiptRoutes.js` +- `./objectRoutes.js` - `../middleware/authenticate.js` - `../middleware/rateLimit.js` diff --git a/docs/modules/verae-zapier-middleware/routes/objectRoutes.md b/docs/modules/verae-zapier-middleware/routes/objectRoutes.md new file mode 100644 index 0000000..5a053d7 --- /dev/null +++ b/docs/modules/verae-zapier-middleware/routes/objectRoutes.md @@ -0,0 +1,50 @@ +# `verae-zapier-middleware/routes/objectRoutes` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/routes/objectRoutes.js` +**Lines:** 91 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`objectRoutes`, `publicShareRoutes` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +_No top-level functions extracted._ + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | + +## What it imports / requires + +- `express` +- `../errors.js` +- `../store/blobs.js` +- `../services/timestampService.js` + +## Call graph (identifiers invoked) + +`post`, `asyncHandler`, `async`, `from`, `createTimestamp`, `toString`, `slice`, `putBlob`, `status`, `json`, `get`, `readBlobPlaintext`, `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. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-zapier-middleware/routes/receiptRoutes.md b/docs/modules/verae-zapier-middleware/routes/receiptRoutes.md new file mode 100644 index 0000000..80ff9ae --- /dev/null +++ b/docs/modules/verae-zapier-middleware/routes/receiptRoutes.md @@ -0,0 +1,47 @@ +# `verae-zapier-middleware/routes/receiptRoutes` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/routes/receiptRoutes.js` +**Lines:** 46 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`receiptRoutes` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +_No top-level functions extracted._ + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `asyncHandler` | `async (req, res` | + +## What it imports / requires + +- `express` +- `../errors.js` +- `../services/timestampService.js` +- `../lib/receiptPdf.js` + +## Call graph (identifiers invoked) + +`receipt`, `get`, `asyncHandler`, `async`, `toLowerCase`, `getJobStatus`, `toISOString`, `buildReceiptPdf`, `setHeader`, `send`, `json` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-zapier-middleware/routes/statusRoutes.md b/docs/modules/verae-zapier-middleware/routes/statusRoutes.md index 6b5ec8a..9145436 100644 --- a/docs/modules/verae-zapier-middleware/routes/statusRoutes.md +++ b/docs/modules/verae-zapier-middleware/routes/statusRoutes.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/routes/statusRoutes.js` -**Lines:** 47 +**Lines:** 52 ## What this module is diff --git a/docs/modules/verae-zapier-middleware/store/blobs.md b/docs/modules/verae-zapier-middleware/store/blobs.md new file mode 100644 index 0000000..6378b66 --- /dev/null +++ b/docs/modules/verae-zapier-middleware/store/blobs.md @@ -0,0 +1,55 @@ +# `verae-zapier-middleware/store/blobs` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/store/blobs.js` +**Lines:** 137 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`decryptBlob`, `putBlob`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `ensure` | `(none)` | — | `unknown` | see Call graph | +| `encrypt` | `plaintext, key` | plaintext: `Buffer`, key: `Buffer` | `unknown` | see Call graph | +| `decryptBlob` | `enc, key` | key: `Buffer` | `unknown` | see Call graph | +| `putBlob` | `{ tenantId, sha256, filename, bytes, publicMetadata = {}, privateMetadata = {} }` | params: `object`, params.tenantId: `string`, params.sha256: `string`, params.filename: `string`, params.bytes: `Buffer`, params.publicMetadata: `object`, params.privateMetadata: `object` | `unknown` | see Call graph | +| `getBlobRecord` | `id` | — | `unknown` | see Call graph | +| `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 | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `persist` | `(none)` | +| `persist` | `(none)` | + +## What it imports / requires + +- `node:crypto` +- `./db.js` + +## 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` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-zapier-middleware/store/db.md b/docs/modules/verae-zapier-middleware/store/db.md index 46d0d86..869e8c5 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:** 115 +**Lines:** 117 ## What this module is diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index 38d5777..c282c34 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -40,6 +40,7 @@ Verae × Zapier module reference modules/verae-zapier-middleware/debug/trace modules/verae-zapier-middleware/errors modules/verae-zapier-middleware/index + modules/verae-zapier-middleware/lib/receiptPdf modules/verae-zapier-middleware/lib/tokens modules/verae-zapier-middleware/middleware/authenticate modules/verae-zapier-middleware/middleware/rateLimit @@ -50,6 +51,8 @@ Verae × Zapier module reference modules/verae-zapier-middleware/routes/authRoutes modules/verae-zapier-middleware/routes/hashRoutes modules/verae-zapier-middleware/routes/index + modules/verae-zapier-middleware/routes/objectRoutes + modules/verae-zapier-middleware/routes/receiptRoutes modules/verae-zapier-middleware/routes/statusRoutes modules/verae-zapier-middleware/routes/tenantRoutes modules/verae-zapier-middleware/routes/timestampRoutes @@ -61,6 +64,7 @@ Verae × Zapier module reference modules/verae-zapier-middleware/services/timestampService modules/verae-zapier-middleware/services/verifyService modules/verae-zapier-middleware/services/webhookService + modules/verae-zapier-middleware/store/blobs modules/verae-zapier-middleware/store/db modules/verae-zapier-middleware/store/jobWatchers modules/verae-zapier-middleware/store/tenants @@ -72,6 +76,7 @@ Verae × Zapier module reference modules/verae-activate/authentication modules/verae-activate/creates/add_numbers modules/verae-activate/creates/echo + modules/verae-activate/creates/sha256 modules/verae-activate/index modules/verae-zapier/authentication modules/verae-zapier/creates/add_numbers diff --git a/docs/sphinx/modules/verae-activate/creates/sha256.md b/docs/sphinx/modules/verae-activate/creates/sha256.md new file mode 100644 index 0000000..d754181 --- /dev/null +++ b/docs/sphinx/modules/verae-activate/creates/sha256.md @@ -0,0 +1,40 @@ +# `verae-activate/creates/sha256` + +**Package:** `verae-activate` +**Source:** `packages/verae-activate/creates/sha256.js` +**Lines:** 37 + +## 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`, `createHash`, `update`, `digest` + +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/sha256.rst b/docs/sphinx/modules/verae-activate/creates/sha256.rst new file mode 100644 index 0000000..a28edef --- /dev/null +++ b/docs/sphinx/modules/verae-activate/creates/sha256.rst @@ -0,0 +1,7 @@ +verae-activate.creates.sha256 +============================= + +Generated API sheet for ``verae-activate/creates/sha256``. + +.. include:: sha256.md + :literal: diff --git a/docs/sphinx/modules/verae-activate/index.md b/docs/sphinx/modules/verae-activate/index.md index 5303bf9..25a0a61 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:** 23 +**Lines:** 25 ## 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` +`version`, `platformVersion`, `authentication`, `creates`, `addNumbers`, `echo`, `sha256Hash` ## Types / interfaces / classes @@ -25,6 +25,7 @@ _No top-level functions extracted._ - `./authentication` - `./creates/add_numbers` - `./creates/echo` +- `./creates/sha256` - `./package.json` - `zapier-platform-core` diff --git a/docs/sphinx/modules/verae-zapier-middleware/clients/veraeClient.md b/docs/sphinx/modules/verae-zapier-middleware/clients/veraeClient.md index 00ec894..8d75562 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/clients/veraeClient.md +++ b/docs/sphinx/modules/verae-zapier-middleware/clients/veraeClient.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/clients/veraeClient.js` -**Lines:** 351 +**Lines:** 352 ## What this module is diff --git a/docs/sphinx/modules/verae-zapier-middleware/lib/receiptPdf.md b/docs/sphinx/modules/verae-zapier-middleware/lib/receiptPdf.md new file mode 100644 index 0000000..60ef9b1 --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/lib/receiptPdf.md @@ -0,0 +1,41 @@ +# `verae-zapier-middleware/lib/receiptPdf` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/lib/receiptPdf.js` +**Lines:** 65 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`buildReceiptPdf` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `pdfEscape` | `s` | s: `string` | `string` | see Call graph | +| `buildReceiptPdf` | `receipt` | receipt: `object` | `Buffer` | see Call graph | + +## What it imports / requires + +_No imports detected._ + +## Call graph (identifiers invoked) + +`receipts`, `pdfEscape`, `replace`, `buildReceiptPdf`, `slice`, `map`, `join`, `byteLength`, `push`, `padStart`, `from` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-zapier-middleware/lib/receiptPdf.rst b/docs/sphinx/modules/verae-zapier-middleware/lib/receiptPdf.rst new file mode 100644 index 0000000..012a299 --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/lib/receiptPdf.rst @@ -0,0 +1,7 @@ +verae-zapier-middleware.lib.receiptPdf +====================================== + +Generated API sheet for ``verae-zapier-middleware/lib/receiptPdf``. + +.. include:: receiptPdf.md + :literal: diff --git a/docs/sphinx/modules/verae-zapier-middleware/routes/index.md b/docs/sphinx/modules/verae-zapier-middleware/routes/index.md index a01ba88..af46ee1 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/routes/index.md +++ b/docs/sphinx/modules/verae-zapier-middleware/routes/index.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/routes/index.js` -**Lines:** 36 +**Lines:** 41 ## What this module is @@ -30,6 +30,8 @@ _No top-level functions extracted._ - `./webhookRoutes.js` - `./tenantRoutes.js` - `./hashRoutes.js` +- `./receiptRoutes.js` +- `./objectRoutes.js` - `../middleware/authenticate.js` - `../middleware/rateLimit.js` diff --git a/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.md b/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.md new file mode 100644 index 0000000..5a053d7 --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.md @@ -0,0 +1,50 @@ +# `verae-zapier-middleware/routes/objectRoutes` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/routes/objectRoutes.js` +**Lines:** 91 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`objectRoutes`, `publicShareRoutes` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +_No top-level functions extracted._ + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | +| `asyncHandler` | `async (req, res` | + +## What it imports / requires + +- `express` +- `../errors.js` +- `../store/blobs.js` +- `../services/timestampService.js` + +## Call graph (identifiers invoked) + +`post`, `asyncHandler`, `async`, `from`, `createTimestamp`, `toString`, `slice`, `putBlob`, `status`, `json`, `get`, `readBlobPlaintext`, `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. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.rst b/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.rst new file mode 100644 index 0000000..f4f51f4 --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/routes/objectRoutes.rst @@ -0,0 +1,7 @@ +verae-zapier-middleware.routes.objectRoutes +=========================================== + +Generated API sheet for ``verae-zapier-middleware/routes/objectRoutes``. + +.. include:: objectRoutes.md + :literal: diff --git a/docs/sphinx/modules/verae-zapier-middleware/routes/receiptRoutes.md b/docs/sphinx/modules/verae-zapier-middleware/routes/receiptRoutes.md new file mode 100644 index 0000000..80ff9ae --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/routes/receiptRoutes.md @@ -0,0 +1,47 @@ +# `verae-zapier-middleware/routes/receiptRoutes` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/routes/receiptRoutes.js` +**Lines:** 46 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`receiptRoutes` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +_No top-level functions extracted._ + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `asyncHandler` | `async (req, res` | + +## What it imports / requires + +- `express` +- `../errors.js` +- `../services/timestampService.js` +- `../lib/receiptPdf.js` + +## Call graph (identifiers invoked) + +`receipt`, `get`, `asyncHandler`, `async`, `toLowerCase`, `getJobStatus`, `toISOString`, `buildReceiptPdf`, `setHeader`, `send`, `json` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-zapier-middleware/routes/receiptRoutes.rst b/docs/sphinx/modules/verae-zapier-middleware/routes/receiptRoutes.rst new file mode 100644 index 0000000..5986952 --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/routes/receiptRoutes.rst @@ -0,0 +1,7 @@ +verae-zapier-middleware.routes.receiptRoutes +============================================ + +Generated API sheet for ``verae-zapier-middleware/routes/receiptRoutes``. + +.. include:: receiptRoutes.md + :literal: diff --git a/docs/sphinx/modules/verae-zapier-middleware/routes/statusRoutes.md b/docs/sphinx/modules/verae-zapier-middleware/routes/statusRoutes.md index 6b5ec8a..9145436 100644 --- a/docs/sphinx/modules/verae-zapier-middleware/routes/statusRoutes.md +++ b/docs/sphinx/modules/verae-zapier-middleware/routes/statusRoutes.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-middleware` **Source:** `packages/verae-zapier-middleware/src/routes/statusRoutes.js` -**Lines:** 47 +**Lines:** 52 ## What this module is diff --git a/docs/sphinx/modules/verae-zapier-middleware/store/blobs.md b/docs/sphinx/modules/verae-zapier-middleware/store/blobs.md new file mode 100644 index 0000000..6378b66 --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/store/blobs.md @@ -0,0 +1,55 @@ +# `verae-zapier-middleware/store/blobs` + +**Package:** `verae-zapier-middleware` +**Source:** `packages/verae-zapier-middleware/src/store/blobs.js` +**Lines:** 137 + +## What this module is + +Implementation module in `verae-zapier-middleware`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`decryptBlob`, `putBlob`, `getBlobRecord`, `readBlobPlaintext`, `createShare`, `redeemShare` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `ensure` | `(none)` | — | `unknown` | see Call graph | +| `encrypt` | `plaintext, key` | plaintext: `Buffer`, key: `Buffer` | `unknown` | see Call graph | +| `decryptBlob` | `enc, key` | key: `Buffer` | `unknown` | see Call graph | +| `putBlob` | `{ tenantId, sha256, filename, bytes, publicMetadata = {}, privateMetadata = {} }` | params: `object`, params.tenantId: `string`, params.sha256: `string`, params.filename: `string`, params.bytes: `Buffer`, params.publicMetadata: `object`, params.privateMetadata: `object` | `unknown` | see Call graph | +| `getBlobRecord` | `id` | — | `unknown` | see Call graph | +| `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 | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `persist` | `(none)` | +| `persist` | `(none)` | + +## What it imports / requires + +- `node:crypto` +- `./db.js` + +## 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` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-zapier-middleware/store/blobs.rst b/docs/sphinx/modules/verae-zapier-middleware/store/blobs.rst new file mode 100644 index 0000000..1015a1c --- /dev/null +++ b/docs/sphinx/modules/verae-zapier-middleware/store/blobs.rst @@ -0,0 +1,7 @@ +verae-zapier-middleware.store.blobs +=================================== + +Generated API sheet for ``verae-zapier-middleware/store/blobs``. + +.. include:: blobs.md + :literal: diff --git a/docs/sphinx/modules/verae-zapier-middleware/store/db.md b/docs/sphinx/modules/verae-zapier-middleware/store/db.md index 46d0d86..869e8c5 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:** 115 +**Lines:** 117 ## What this module is diff --git a/package.json b/package.json index b9c391c..471a9a7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "test:activate": "npm --prefix packages/verae-activate test", "docs:modules": "python3 scripts/gen-module-docs.py", "docs:sphinx": "sphinx-build -b html docs/sphinx docs/sphinx/_build/html", - "docs:pdf": "python3 scripts/gen-module-pdfs.py" + "docs:pdf": "python3 scripts/gen-module-pdfs.py", + "test:offline": "bash scripts/test-offline.sh", + "nats:tunnel": "bash scripts/nats-tunnel.sh" }, "engines": { "node": ">=20.0.0" diff --git a/packages/verae-activate/creates/sha256.js b/packages/verae-activate/creates/sha256.js new file mode 100644 index 0000000..6c707b2 --- /dev/null +++ b/packages/verae-activate/creates/sha256.js @@ -0,0 +1,36 @@ +/** + * SHA256 of a string — local, no API. Lets you hash a file/text in a Zap. + * @module creates/sha256 + */ + +const { createHash } = require('node:crypto'); + +const perform = async (_z, bundle) => { + const text = String(bundle.inputData.text ?? ''); + const sha256 = createHash('sha256').update(text, 'utf8').digest('hex'); + return { textLength: text.length, sha256, mode: 'local' }; +}; + +module.exports = { + key: 'sha256_hash', + noun: 'Hash', + display: { + label: 'SHA256 Hash Text', + description: 'Computes SHA256 hex of text inside Zapier. No Verae call.', + }, + operation: { + cleanInputData: false, + inputFields: [{ key: 'text', label: 'Text', type: 'text', required: true }], + perform, + sample: { + textLength: 5, + sha256: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', + mode: 'local', + }, + outputFields: [ + { key: 'sha256', type: 'string' }, + { key: 'textLength', type: 'integer' }, + { key: 'mode', type: 'string' }, + ], + }, +}; diff --git a/packages/verae-activate/index.js b/packages/verae-activate/index.js index 97c8c57..a5c27c7 100644 --- a/packages/verae-activate/index.js +++ b/packages/verae-activate/index.js @@ -10,6 +10,7 @@ const authentication = require('./authentication'); const addNumbers = require('./creates/add_numbers'); const echo = require('./creates/echo'); +const sha256Hash = require('./creates/sha256'); module.exports = { version: require('./package.json').version, @@ -18,5 +19,6 @@ module.exports = { creates: { [addNumbers.key]: addNumbers, [echo.key]: echo, + [sha256Hash.key]: sha256Hash, }, }; diff --git a/packages/verae-activate/test/add_numbers.test.js b/packages/verae-activate/test/add_numbers.test.js index 56d22f1..e0ff825 100644 --- a/packages/verae-activate/test/add_numbers.test.js +++ b/packages/verae-activate/test/add_numbers.test.js @@ -14,6 +14,7 @@ describe('verae-activate app', () => { assert.equal(authentication.type, 'custom'); assert.ok(App.creates.add_numbers); assert.ok(App.creates.echo); + assert.ok(App.creates.sha256_hash); 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')); @@ -64,6 +65,13 @@ describe('verae-activate app', () => { assert.equal(result.sum, 5); }); + it('sha256 hashes hello', async () => { + const sha = require('../creates/sha256'); + const out = await sha.operation.perform({}, { inputData: { text: 'hello' }, authData: {} }); + assert.equal(out.sha256, '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'); + assert.equal(out.mode, 'local'); + }); + it('echo returns text length', async () => { const echo = require('../creates/echo'); const out = await echo.operation.perform({}, { inputData: { text: 'ab' }, authData: {} }); diff --git a/packages/verae-zapier-middleware/src/app.js b/packages/verae-zapier-middleware/src/app.js index 3d55a6e..aca14f8 100644 --- a/packages/verae-zapier-middleware/src/app.js +++ b/packages/verae-zapier-middleware/src/app.js @@ -26,7 +26,7 @@ export function createApp(options = {}) { const app = express(); app.disable('x-powered-by'); - app.use(express.json({ limit: '1mb' })); + app.use(express.json({ limit: '12mb' })); app.use(traceMiddleware); /** diff --git a/packages/verae-zapier-middleware/src/clients/veraeClient.js b/packages/verae-zapier-middleware/src/clients/veraeClient.js index 0869d4d..b27c644 100644 --- a/packages/verae-zapier-middleware/src/clients/veraeClient.js +++ b/packages/verae-zapier-middleware/src/clients/veraeClient.js @@ -121,6 +121,7 @@ async function mockGetStatus(jobId) { result: job.result, completedAt: job.completedAt, metadata: job.metadata, + privateMetadata: job.privateMetadata, error: job.error, sha256: job.sha256, }; diff --git a/packages/verae-zapier-middleware/src/lib/receiptPdf.js b/packages/verae-zapier-middleware/src/lib/receiptPdf.js new file mode 100644 index 0000000..5a64c7b --- /dev/null +++ b/packages/verae-zapier-middleware/src/lib/receiptPdf.js @@ -0,0 +1,64 @@ +/** + * Minimal PDF builder for retrieval receipts (no native deps). + * @module lib/receiptPdf + */ + +/** + * @param {string} s + * @returns {string} + */ +function pdfEscape(s) { + return String(s).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); +} + +/** + * @param {object} receipt + * @returns {Buffer} + */ +export function buildReceiptPdf(receipt) { + const lines = [ + 'Verae Time — certified retrieval receipt', + `Type: ${receipt.type}`, + `Job ID: ${receipt.jobId}`, + `SHA256: ${receipt.sha256 ?? ''}`, + `Original timestamp: ${receipt.timestamp ?? ''}`, + `Retrieved at: ${receipt.extraSeal?.retrievedAt ?? ''}`, + `Tenant: ${receipt.extraSeal?.tenantId ?? ''}`, + `Seal event: ${receipt.extraSeal?.event ?? ''}`, + `Certificate: ${String(receipt.certificate ?? '').slice(0, 80)}`, + ]; + + const commands = lines + .map((line, i) => { + const y = 720 - i * 18; + return `BT /F1 11 Tf 50 ${y} Td (${pdfEscape(line)}) Tj ET`; + }) + .join('\n'); + + const stream = `${commands}\n`; + const objects = [ + '1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj', + '2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj', + '3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj', + `4 0 obj << /Length ${Buffer.byteLength(stream)} >> stream\n${stream}endstream endobj`, + '5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj', + ]; + + let offset = '%PDF-1.4\n'.length; + const xref = [0]; + let body = '%PDF-1.4\n'; + for (const obj of objects) { + xref.push(offset); + const chunk = `${obj}\n`; + body += chunk; + offset += Buffer.byteLength(chunk); + } + const xrefStart = offset; + let xrefTable = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; + for (let i = 1; i < xref.length; i += 1) { + xrefTable += `${String(xref[i]).padStart(10, '0')} 00000 n \n`; + } + body += xrefTable; + body += `trailer << /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefStart}\n%%EOF\n`; + return Buffer.from(body, 'utf8'); +} diff --git a/packages/verae-zapier-middleware/src/routes/index.js b/packages/verae-zapier-middleware/src/routes/index.js index 9801e9b..abed398 100644 --- a/packages/verae-zapier-middleware/src/routes/index.js +++ b/packages/verae-zapier-middleware/src/routes/index.js @@ -12,6 +12,7 @@ import { webhookRoutes } from './webhookRoutes.js'; import { publicTenantRoutes, adminTenantRoutes } from './tenantRoutes.js'; import { hashRoutes } from './hashRoutes.js'; import { receiptRoutes } from './receiptRoutes.js'; +import { objectRoutes, publicShareRoutes } from './objectRoutes.js'; import { authenticate } from '../middleware/authenticate.js'; import { rateLimit } from '../middleware/rateLimit.js'; @@ -21,6 +22,7 @@ export const apiRoutes = Router(); apiRoutes.use('/v1/auth', authRoutes); apiRoutes.use('/v1', publicTenantRoutes); apiRoutes.use('/v1/admin', adminTenantRoutes); +apiRoutes.use('/v1/shares', publicShareRoutes); // Protected const protectedRoutes = Router(); @@ -33,5 +35,6 @@ protectedRoutes.use('/status', statusRoutes); protectedRoutes.use('/webhooks', webhookRoutes); protectedRoutes.use('/hashes', hashRoutes); protectedRoutes.use('/receipts', receiptRoutes); +protectedRoutes.use('/objects', objectRoutes); apiRoutes.use('/v1', protectedRoutes); diff --git a/packages/verae-zapier-middleware/src/routes/objectRoutes.js b/packages/verae-zapier-middleware/src/routes/objectRoutes.js new file mode 100644 index 0000000..4e84feb --- /dev/null +++ b/packages/verae-zapier-middleware/src/routes/objectRoutes.js @@ -0,0 +1,90 @@ +/** + * Encrypted LTS mock + share grants. + * @module routes/objectRoutes + */ + +import { Router } from 'express'; +import { asyncHandler, AppError } from '../errors.js'; +import { + putBlob, + readBlobPlaintext, + createShare, + redeemShare, +} from '../store/blobs.js'; +import { createTimestamp } from '../services/timestampService.js'; + +export const objectRoutes = Router(); + +objectRoutes.post( + '/', + asyncHandler(async (req, res) => { + const { filename, contentBase64, sha256, publicMetadata, privateMetadata, data } = req.body ?? {}; + if (!contentBase64 && !data) { + throw new AppError('contentBase64 or data is required', { status: 400, code: 'VALIDATION_ERROR' }); + } + const bytes = Buffer.from(String(contentBase64 ?? data), contentBase64 ? 'base64' : 'utf8'); + const stamp = await createTimestamp(req.auth, { + data: data ?? bytes.toString('utf8').slice(0, 1024), + sha256, + publicMetadata, + privateMetadata, + }); + const stored = putBlob({ + tenantId: req.auth.tenantId, + sha256: stamp.sha256 ?? sha256 ?? null, + filename, + bytes, + publicMetadata, + privateMetadata, + }); + res.status(201).json({ ...stored, jobId: stamp.jobId, existing: stamp.existing }); + }), +); + +objectRoutes.get( + '/:id', + asyncHandler(async (req, res) => { + try { + const rec = readBlobPlaintext(req.params.id, req.auth.tenantId); + if (!rec) throw new AppError('Object not found', { status: 404, code: 'NOT_FOUND' }); + res.json({ + id: rec.id, + filename: rec.filename, + size: rec.size, + sha256: rec.sha256, + publicMetadata: rec.publicMetadata, + privateMetadata: rec.privateMetadata, + contentBase64: rec.bytes.toString('base64'), + }); + } catch (err) { + if (err.code === 'FORBIDDEN') { + throw new AppError('Forbidden', { status: 403, code: 'FORBIDDEN' }); + } + throw err; + } + }), +); + +objectRoutes.post( + '/:id/share', + asyncHandler(async (req, res) => { + const share = createShare({ + blobId: req.params.id, + fromTenantId: req.auth.tenantId, + label: req.body?.label, + }); + if (!share) throw new AppError('Object not found', { status: 404, code: 'NOT_FOUND' }); + res.status(201).json(share); + }), +); + +export const publicShareRoutes = Router(); + +publicShareRoutes.get( + '/:token', + asyncHandler(async (req, res) => { + const payload = redeemShare(req.params.token); + if (!payload) throw new AppError('Share not found', { status: 404, code: 'NOT_FOUND' }); + res.json(payload); + }), +); diff --git a/packages/verae-zapier-middleware/src/routes/receiptRoutes.js b/packages/verae-zapier-middleware/src/routes/receiptRoutes.js index 5b9e666..2685ba1 100644 --- a/packages/verae-zapier-middleware/src/routes/receiptRoutes.js +++ b/packages/verae-zapier-middleware/src/routes/receiptRoutes.js @@ -6,6 +6,7 @@ import { Router } from 'express'; import { asyncHandler, AppError } from '../errors.js'; import { getJobStatus } from '../services/timestampService.js'; +import { buildReceiptPdf } from '../lib/receiptPdf.js'; export const receiptRoutes = Router(); @@ -33,10 +34,11 @@ receiptRoutes.get( }, }; if (format === 'pdf') { - throw new AppError('PDF receipts not implemented; use format=json', { - status: 501, - code: 'NOT_IMPLEMENTED', - }); + const pdf = buildReceiptPdf({ ...receipt, format: 'pdf' }); + res.setHeader('content-type', 'application/pdf'); + res.setHeader('content-disposition', `attachment; filename="verae-receipt-${jobId}.pdf"`); + res.send(pdf); + return; } res.json(receipt); }), diff --git a/packages/verae-zapier-middleware/src/routes/statusRoutes.js b/packages/verae-zapier-middleware/src/routes/statusRoutes.js index fca1659..84d3997 100644 --- a/packages/verae-zapier-middleware/src/routes/statusRoutes.js +++ b/packages/verae-zapier-middleware/src/routes/statusRoutes.js @@ -41,6 +41,11 @@ statusRoutes.get( throw new AppError('jobId is required', { status: 400, code: 'VALIDATION_ERROR' }); } const result = await getJobStatus(req.auth, jobId); + if (req.query.scope === 'public' && result.metadata) { + const { publicMetadata, ...restMeta } = result.metadata; + result.metadata = { ...restMeta, publicMetadata }; + delete result.privateMetadata; + } res.json(result); }), ); diff --git a/packages/verae-zapier-middleware/src/store/blobs.js b/packages/verae-zapier-middleware/src/store/blobs.js new file mode 100644 index 0000000..29cf799 --- /dev/null +++ b/packages/verae-zapier-middleware/src/store/blobs.js @@ -0,0 +1,136 @@ +/** + * Mock encrypted long-term object store + share grants. + * @module store/blobs + */ + +import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from 'node:crypto'; +import { getStore, persist } from './db.js'; + +const ALGO = 'aes-256-gcm'; + +function ensure() { + const store = getStore(); + store.blobs ??= {}; + store.shares ??= {}; + return store; +} + +/** + * @param {Buffer} plaintext + * @param {Buffer} key 32 bytes + */ +function encrypt(plaintext, key) { + const iv = randomBytes(12); + const cipher = createCipheriv(ALGO, key, iv); + const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); + const tag = cipher.getAuthTag(); + return { iv: iv.toString('base64'), tag: tag.toString('base64'), ciphertext: ciphertext.toString('base64') }; +} + +/** + * @param {{ iv: string, tag: string, ciphertext: string }} enc + * @param {Buffer} key + */ +export function decryptBlob(enc, key) { + const decipher = createDecipheriv(ALGO, key, Buffer.from(enc.iv, 'base64')); + decipher.setAuthTag(Buffer.from(enc.tag, 'base64')); + return Buffer.concat([ + decipher.update(Buffer.from(enc.ciphertext, 'base64')), + decipher.final(), + ]); +} + +/** + * @param {object} params + * @param {string} params.tenantId + * @param {string} [params.sha256] + * @param {string} [params.filename] + * @param {Buffer} params.bytes + * @param {object} [params.publicMetadata] + * @param {object} [params.privateMetadata] + */ +export function putBlob({ tenantId, sha256, filename, bytes, publicMetadata = {}, privateMetadata = {} }) { + const store = ensure(); + const id = randomUUID(); + const key = randomBytes(32); + const enc = encrypt(bytes, key); + store.blobs[id] = { + id, + tenantId, + sha256: sha256 ?? null, + filename: filename ?? 'object.bin', + size: bytes.length, + enc, + dek: key.toString('base64'), + publicMetadata, + privateMetadata, + createdAt: new Date().toISOString(), + }; + persist(); + return { id, sha256: store.blobs[id].sha256, size: bytes.length, filename: store.blobs[id].filename }; +} + +export function getBlobRecord(id) { + return ensure().blobs[id] ?? null; +} + +/** + * Decrypt for owner tenant only. + * @param {string} id + * @param {string} tenantId + */ +export function readBlobPlaintext(id, tenantId) { + const rec = getBlobRecord(id); + if (!rec) return null; + if (rec.tenantId !== tenantId) { + const err = new Error('Forbidden'); + err.code = 'FORBIDDEN'; + throw err; + } + return { + ...rec, + bytes: decryptBlob(rec.enc, Buffer.from(rec.dek, 'base64')), + }; +} + +/** + * @param {{ blobId: string, fromTenantId: string, label?: string }} params + */ +export function createShare({ blobId, fromTenantId, label }) { + const rec = getBlobRecord(blobId); + if (!rec || rec.tenantId !== fromTenantId) return null; + const token = `shr_${randomBytes(16).toString('hex')}`; + const store = ensure(); + store.shares[token] = { + token, + blobId, + fromTenantId, + label: label ?? rec.filename, + createdAt: new Date().toISOString(), + }; + persist(); + return { token, blobId, label: store.shares[token].label }; +} + +/** + * Redeem a share token: ciphertext + wrapped key for the recipient. + * @param {string} token + */ +export function redeemShare(token) { + const store = ensure(); + const share = store.shares[token]; + if (!share) return null; + const rec = store.blobs[share.blobId]; + if (!rec) return null; + return { + token, + blobId: rec.id, + filename: rec.filename, + size: rec.size, + sha256: rec.sha256, + publicMetadata: rec.publicMetadata ?? {}, + enc: rec.enc, + dek: rec.dek, + note: 'Mock share: dek is included so the recipient can decrypt. Production must wrap dek to recipient pubkey.', + }; +} diff --git a/packages/verae-zapier-middleware/src/store/db.js b/packages/verae-zapier-middleware/src/store/db.js index 2a1898b..a73f94d 100644 --- a/packages/verae-zapier-middleware/src/store/db.js +++ b/packages/verae-zapier-middleware/src/store/db.js @@ -33,6 +33,8 @@ export function emptyStore() { usage: {}, webhooks: [], jobWatchers: [], + blobs: {}, + shares: {}, }; } diff --git a/packages/verae-zapier-middleware/test/integration/objects.test.js b/packages/verae-zapier-middleware/test/integration/objects.test.js new file mode 100644 index 0000000..9613a5e --- /dev/null +++ b/packages/verae-zapier-middleware/test/integration/objects.test.js @@ -0,0 +1,124 @@ +import { describe, it, before, after } from 'node:test'; +import assert from 'node:assert/strict'; +import http from 'node:http'; +import { createApp } from '../../src/app.js'; +import { useTempStore, seedProTenant, seedFreeTenant } from '../helpers.js'; +import { clearMockJobs } from '../../src/clients/veraeClient.js'; +import { config } from '../../src/config.js'; + +describe('encrypted objects + share + pdf receipt', () => { + let ctx; + let server; + let port; + let ownerKey; + let otherKey; + + before(async () => { + config.mockVerae = true; + config.natsEnabled = false; + config.mockJobCompleteMs = 40; + clearMockJobs(); + ctx = useTempStore(); + ownerKey = seedProTenant().apiKey; + otherKey = seedFreeTenant({ id: 'tenant-other', veraeUsername: 'other', veraePassword: 'otherpass' }).apiKey; + const app = createApp(); + await new Promise((resolve) => { + server = app.listen(0, '127.0.0.1', () => { + port = server.address().port; + resolve(); + }); + }); + }); + + after(async () => { + await new Promise((r) => server.close(r)); + ctx.cleanup(); + }); + + it('stores encrypted blob, hides it from other tenants, shares via token', async () => { + const created = await call(port, 'POST', '/zapier/v1/objects', ownerKey, { + filename: 'secret.txt', + contentBase64: Buffer.from('top-secret').toString('base64'), + publicMetadata: { title: 'pub' }, + privateMetadata: { ssn: '000' }, + }); + assert.equal(created.status, 201); + const id = created.body.id; + + const mine = await call(port, 'GET', `/zapier/v1/objects/${id}`, ownerKey); + assert.equal(mine.status, 200); + assert.equal(Buffer.from(mine.body.contentBase64, 'base64').toString(), 'top-secret'); + assert.equal(mine.body.privateMetadata.ssn, '000'); + + const theirs = await call(port, 'GET', `/zapier/v1/objects/${id}`, otherKey); + assert.equal(theirs.status, 403); + + const share = await call(port, 'POST', `/zapier/v1/objects/${id}/share`, ownerKey, { label: 'for-other' }); + assert.equal(share.status, 201); + const redeemed = await call(port, 'GET', `/zapier/v1/shares/${share.body.token}`, otherKey); + assert.equal(redeemed.status, 200); + assert.equal(redeemed.body.filename, 'secret.txt'); + assert.ok(redeemed.body.dek); + assert.equal(redeemed.body.publicMetadata.title, 'pub'); + }); + + 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; + const rec = await raw(port, 'GET', `/zapier/v1/receipts/${jobId}?format=pdf`, ownerKey); + assert.equal(rec.status, 200); + assert.match(rec.body.toString('latin1').slice(0, 5), /%PDF-/); + }); +}); + +function call(port, method, path, apiKey, body) { + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: '127.0.0.1', + port, + path, + method, + headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' }, + }, + (res) => { + const chunks = []; + res.on('data', (c) => chunks.push(c)); + res.on('end', () => { + const buf = Buffer.concat(chunks); + let parsed = {}; + try { + parsed = buf.length ? JSON.parse(buf.toString()) : {}; + } catch { + parsed = { raw: buf.toString() }; + } + resolve({ status: res.statusCode, body: parsed }); + }); + }, + ); + req.on('error', reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); +} + +function raw(port, method, path, apiKey) { + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: '127.0.0.1', + port, + path, + method, + headers: { authorization: `Bearer ${apiKey}` }, + }, + (res) => { + const chunks = []; + res.on('data', (c) => chunks.push(c)); + res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks) })); + }, + ); + req.on('error', reject); + req.end(); + }); +} diff --git a/scripts/nats-tunnel.sh b/scripts/nats-tunnel.sh new file mode 100755 index 0000000..4242fbf --- /dev/null +++ b/scripts/nats-tunnel.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SSH tunnel NS1 JetStream (127.0.0.1:4222 on 70.88.205.138) to local 14222. +set -euo pipefail +HOST="${NATS_SSH_HOST:-marchon@70.88.205.138}" +LOCAL_PORT="${NATS_LOCAL_PORT:-14222}" +REMOTE="127.0.0.1:4222" + +if nc -z 127.0.0.1 "$LOCAL_PORT" 2>/dev/null; then + echo "already listening on 127.0.0.1:${LOCAL_PORT}" + exit 0 +fi + +ssh -fN -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \ + -L "${LOCAL_PORT}:${REMOTE}" "$HOST" +echo "NATS_URL=nats://127.0.0.1:${LOCAL_PORT}" +echo "export NATS_URL=nats://127.0.0.1:${LOCAL_PORT} NATS_ENABLED=true" diff --git a/scripts/test-offline.sh b/scripts/test-offline.sh new file mode 100755 index 0000000..058fb21 --- /dev/null +++ b/scripts/test-offline.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Tests that do not need Zapier login or live api.veraetime.net. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +export MOCK_VERAE=true +npm run gate:0 +npm run gate:1 +npm run gate:2 +npm run gate:3 +npm run gate:4 +npm run gate:5 +npm run gate:6 +npm run gate:10 +npm run gate:11 +npm --prefix packages/verae-zapier-middleware run test:hash +node --test packages/verae-zapier-middleware/test/integration/receipt.test.js +node --test packages/verae-zapier-middleware/test/integration/objects.test.js +npm run test:activate +npm run test:zappier +echo "OFFLINE SUITE PASSED"