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.
This commit is contained in:
George Lambert 2026-09-09 02:54:46 -04:00
parent 51ae79b75f
commit c8f9d01a04
5 changed files with 129 additions and 1 deletions

View file

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

View file

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

View file

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