From c8f9d01a041236e60ae637b09afd9e2f4ec7a652 Mon Sep 17 00:00:00 2001 From: George Lambert Date: Wed, 9 Sep 2026 02:54:46 -0400 Subject: [PATCH] Milestone 4: mock retrieval receipt JSON (extra seal) GET /zapier/v1/receipts/{jobId}?format=json returns a retrieval receipt with extraSeal.event=document.retrieved. PDF format is 501 until legal copy exists. OPEN.md lists work that needs Zapier login or live Verae. --- docs/01-product/features-a-m.md | 2 +- docs/OPEN.md | 11 +++ .../src/routes/index.js | 2 + .../src/routes/receiptRoutes.js | 43 +++++++++++ .../test/integration/receipt.test.js | 72 +++++++++++++++++++ 5 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 docs/OPEN.md create mode 100644 packages/verae-zapier-middleware/src/routes/receiptRoutes.js create mode 100644 packages/verae-zapier-middleware/test/integration/receipt.test.js diff --git a/docs/01-product/features-a-m.md b/docs/01-product/features-a-m.md index 05e9603..c89a982 100644 --- a/docs/01-product/features-a-m.md +++ b/docs/01-product/features-a-m.md @@ -12,7 +12,7 @@ Source: [workspace-brief.md](../00-sources/workspace-brief.md). Owners after com | 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 | | 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 | verae mock | Missing. Not a payment invoice | No | +| i | Certified retrieval receipt (PDF/JSON) extra seal | `GET /zapier/v1/receipts/{jobId}?format=json` | **JSON mock done**; PDF 501 | 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 new file mode 100644 index 0000000..4163234 --- /dev/null +++ b/docs/OPEN.md @@ -0,0 +1,11 @@ +# Still open (needs you or live Verae) + +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`). + +Everything else for morning Add Numbers is ready. diff --git a/packages/verae-zapier-middleware/src/routes/index.js b/packages/verae-zapier-middleware/src/routes/index.js index 0b38cc3..9801e9b 100644 --- a/packages/verae-zapier-middleware/src/routes/index.js +++ b/packages/verae-zapier-middleware/src/routes/index.js @@ -11,6 +11,7 @@ import { statusRoutes } from './statusRoutes.js'; import { webhookRoutes } from './webhookRoutes.js'; import { publicTenantRoutes, adminTenantRoutes } from './tenantRoutes.js'; import { hashRoutes } from './hashRoutes.js'; +import { receiptRoutes } from './receiptRoutes.js'; import { authenticate } from '../middleware/authenticate.js'; import { rateLimit } from '../middleware/rateLimit.js'; @@ -31,5 +32,6 @@ protectedRoutes.use('/verify', verifyRoutes); protectedRoutes.use('/status', statusRoutes); protectedRoutes.use('/webhooks', webhookRoutes); protectedRoutes.use('/hashes', hashRoutes); +protectedRoutes.use('/receipts', receiptRoutes); apiRoutes.use('/v1', protectedRoutes); diff --git a/packages/verae-zapier-middleware/src/routes/receiptRoutes.js b/packages/verae-zapier-middleware/src/routes/receiptRoutes.js new file mode 100644 index 0000000..5b9e666 --- /dev/null +++ b/packages/verae-zapier-middleware/src/routes/receiptRoutes.js @@ -0,0 +1,43 @@ +/** + * Mock certified retrieval receipt (JSON). Not a payment invoice. + * @module routes/receiptRoutes + */ + +import { Router } from 'express'; +import { asyncHandler, AppError } from '../errors.js'; +import { getJobStatus } from '../services/timestampService.js'; + +export const receiptRoutes = Router(); + +receiptRoutes.get( + '/:jobId', + asyncHandler(async (req, res) => { + const { jobId } = req.params; + const format = String(req.query.format || 'json').toLowerCase(); + const status = await getJobStatus(req.auth, jobId); + if (status.status !== 'completed') { + throw new AppError('Receipt only for completed jobs', { status: 409, code: 'NOT_COMPLETE' }); + } + const retrievedAt = new Date().toISOString(); + const receipt = { + type: 'verae.retrieval-receipt', + format: 'json', + jobId, + sha256: status.sha256 ?? status.metadata?.sha256, + timestamp: status.completedAt ?? status.metadata?.timestamp, + certificate: status.result, + extraSeal: { + event: 'document.retrieved', + retrievedAt, + tenantId: req.auth.tenantId, + }, + }; + if (format === 'pdf') { + throw new AppError('PDF receipts not implemented; use format=json', { + status: 501, + code: 'NOT_IMPLEMENTED', + }); + } + res.json(receipt); + }), +); diff --git a/packages/verae-zapier-middleware/test/integration/receipt.test.js b/packages/verae-zapier-middleware/test/integration/receipt.test.js new file mode 100644 index 0000000..d78b407 --- /dev/null +++ b/packages/verae-zapier-middleware/test/integration/receipt.test.js @@ -0,0 +1,72 @@ +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 } from '../helpers.js'; +import { clearMockJobs } from '../../src/clients/veraeClient.js'; +import { config } from '../../src/config.js'; + +describe('retrieval receipt JSON', () => { + let ctx; + let server; + let port; + let apiKey; + + before(async () => { + config.mockVerae = true; + config.natsEnabled = false; + config.mockJobCompleteMs = 50; + clearMockJobs(); + ctx = useTempStore(); + apiKey = seedProTenant().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('issues a JSON extra-seal receipt for a completed job', async () => { + const created = await call(port, 'POST', '/zapier/v1/timestamp/wait', apiKey, { data: 'receipt-me' }); + assert.equal(created.body.status, 'completed'); + const jobId = created.body.id; + const rec = await call(port, 'GET', `/zapier/v1/receipts/${jobId}?format=json`, apiKey); + assert.equal(rec.status, 200); + assert.equal(rec.body.type, 'verae.retrieval-receipt'); + assert.equal(rec.body.extraSeal.event, 'document.retrieved'); + assert.equal(rec.body.jobId, jobId); + }); +}); + +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) => { + let data = ''; + res.on('data', (c) => { + data += c; + }); + res.on('end', () => { + resolve({ status: res.statusCode, body: data ? JSON.parse(data) : {} }); + }); + }, + ); + req.on('error', reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); +}