sync: pick up fail-closed Verae cert fix already deployed live (from fail-closed-verae-certs @1107272: src/app.ts, src/upstream.ts, tests)

This commit is contained in:
George Lambert 2026-10-05 14:01:45 -04:00
commit e64a9322ac
7 changed files with 225 additions and 5 deletions

View file

@ -73,6 +73,45 @@ function parseResultFormats(req: Request): Set<string> {
return set;
}
/** Production fail-closed: never invent mock-cert-* when an upstream is configured. */
function upstreamConfigured(): boolean {
return Boolean((process.env.ZAPPIER_UPSTREAM || '').trim());
}
/** Dev-only local mock certificates. Allowed only when upstream is unset. */
function allowMockCerts(): boolean {
return process.env.ALLOW_MOCK_CERTS === 'true' && !upstreamConfigured();
}
function rejectUpstreamTimestamp(
res: Response,
upstream: { status: number; body: unknown } | null,
): void {
if (upstreamConfigured()) {
const status =
upstream && typeof upstream.status === 'number' && upstream.status >= 400
? upstream.status
: 502;
const body: Record<string, unknown> = {
upstreamStatus: upstream?.status ?? null,
};
if (upstream?.body && typeof upstream.body === 'object' && !Array.isArray(upstream.body)) {
Object.assign(body, upstream.body as Record<string, unknown>);
} else if (typeof upstream?.body === 'string' && upstream.body) {
body.detail = upstream.body;
}
// Edge error wins over any upstream "error" field.
body.error = 'upstream_timestamp_failed';
res.status(status).json(body);
return;
}
res.status(503).json({
error: 'timestamp_upstream_unconfigured',
hint: 'Set ZAPPIER_UPSTREAM for real Verae stamps, or ALLOW_MOCK_CERTS=true for local mock certificates.',
});
}
function stampToText(row: {
documentName?: string;
sha256: string;
@ -430,7 +469,13 @@ export function buildApp(deps: AppDeps = {}): {
return;
}
// Local mock when upstream unset/error
// Fail closed when upstream is configured (or mock not explicitly allowed).
if (!allowMockCerts()) {
rejectUpstreamTimestamp(res, upstream);
return;
}
// Dev-only local mock when ZAPPIER_UPSTREAM unset and ALLOW_MOCK_CERTS=true
const existing = hashIndex.get(sha256);
if (existing) {
const mockBody = {
@ -685,7 +730,13 @@ export function buildApp(deps: AppDeps = {}): {
return;
}
// Local mock when upstream unset or error — Verae BatchTimestampResponse + edge extras
// Fail closed when upstream is configured (or mock not explicitly allowed).
if (!allowMockCerts()) {
rejectUpstreamTimestamp(res, upstream);
return;
}
// Dev-only local mock when ZAPPIER_UPSTREAM unset and ALLOW_MOCK_CERTS=true
const jobIds: string[] = [];
const results: {
sha256: string;

View file

@ -65,7 +65,7 @@ export async function fetchVerae(
/**
* After zappier meters the call, forward Verae operations to middleware / API.
* When ZAPPIER_UPSTREAM is unset, callers keep the local mock.
* When ZAPPIER_UPSTREAM is unset, callers may use ALLOW_MOCK_CERTS=true for local mock.
*/
export async function proxyVerae(
req: Request,

View file

@ -5,6 +5,19 @@ import { InMemoryCustomerRepo } from '../src/auth';
import { InMemorySessionRepo } from '../src/accounts';
describe('xVerae- long-lived API tokens', () => {
const prevAllowMock = process.env.ALLOW_MOCK_CERTS;
const prevUpstream = process.env.ZAPPIER_UPSTREAM;
beforeAll(() => {
delete process.env.ZAPPIER_UPSTREAM;
process.env.ALLOW_MOCK_CERTS = 'true';
});
afterAll(() => {
if (prevAllowMock === undefined) delete process.env.ALLOW_MOCK_CERTS;
else process.env.ALLOW_MOCK_CERTS = prevAllowMock;
if (prevUpstream === undefined) delete process.env.ZAPPIER_UPSTREAM;
else process.env.ZAPPIER_UPSTREAM = prevUpstream;
});
it('portal creates token once; stamp auth accepts xVerae- and Bearer', async () => {
const customers = new InMemoryCustomerRepo(
DEFAULT_CUSTOMERS.map((c) =>

View file

@ -4,6 +4,19 @@ import { buildApp } from '../src/app';
const KEY = 'key-ada'; // seeded free-tier customer
describe('Zappier API', () => {
const prevAllowMock = process.env.ALLOW_MOCK_CERTS;
const prevUpstream = process.env.ZAPPIER_UPSTREAM;
beforeAll(() => {
delete process.env.ZAPPIER_UPSTREAM;
process.env.ALLOW_MOCK_CERTS = 'true';
});
afterAll(() => {
if (prevAllowMock === undefined) delete process.env.ALLOW_MOCK_CERTS;
else process.env.ALLOW_MOCK_CERTS = prevAllowMock;
if (prevUpstream === undefined) delete process.env.ZAPPIER_UPSTREAM;
else process.env.ZAPPIER_UPSTREAM = prevUpstream;
});
it('rejects calls without an API key', async () => {
const { app } = buildApp();
const res = await request(app).get('/v1/status');
@ -24,7 +37,8 @@ describe('Zappier API', () => {
.post('/v1/timestamp')
.set('x-api-key', KEY)
.send({ data: 'abc' });
expect(a.status).toBe(202);
expect(a.status).toBe(200);
expect(String(a.body.receipt_id || '')).toMatch(/^mock-cert-/);
const b = await request(app)
.post('/v1/timestamp')
.set('x-api-key', KEY)

View file

@ -9,6 +9,19 @@ import {
import { emptyChannelRates } from '../src/billing-channels';
describe('billing headers', () => {
const prevAllowMock = process.env.ALLOW_MOCK_CERTS;
const prevUpstream = process.env.ZAPPIER_UPSTREAM;
beforeAll(() => {
delete process.env.ZAPPIER_UPSTREAM;
process.env.ALLOW_MOCK_CERTS = 'true';
});
afterAll(() => {
if (prevAllowMock === undefined) delete process.env.ALLOW_MOCK_CERTS;
else process.env.ALLOW_MOCK_CERTS = prevAllowMock;
if (prevUpstream === undefined) delete process.env.ZAPPIER_UPSTREAM;
else process.env.ZAPPIER_UPSTREAM = prevUpstream;
});
it('returns spend and balance on metered calls', async () => {
const table = emptyChannelRates();
table.rates.api.individual.timestamp = 10;
@ -23,7 +36,7 @@ describe('billing headers', () => {
.set('x-api-key', 'key-ada')
.set('x-billing-channel', 'api')
.send({ sha256: '1'.repeat(64) });
expect(res.status).toBe(202);
expect(res.status).toBe(200);
expect(res.headers[BILLING_HEADER_SPEND.toLowerCase()]).toBe('10');
expect(res.headers[BILLING_HEADER_BALANCE.toLowerCase()]).toBe('500');
expect(res.headers[BILLING_HEADER_CHANNEL.toLowerCase()]).toBe('api');

View file

@ -0,0 +1,116 @@
import request from 'supertest';
import { buildApp } from '../src/app';
const KEY = 'key-ada';
describe('fail-closed Verae timestamp certificates', () => {
const prevAllow = process.env.ALLOW_MOCK_CERTS;
const prevUpstream = process.env.ZAPPIER_UPSTREAM;
afterEach(() => {
if (prevAllow === undefined) delete process.env.ALLOW_MOCK_CERTS;
else process.env.ALLOW_MOCK_CERTS = prevAllow;
if (prevUpstream === undefined) delete process.env.ZAPPIER_UPSTREAM;
else process.env.ZAPPIER_UPSTREAM = prevUpstream;
});
it('returns upstream status (not mock-cert) when ZAPPIER_UPSTREAM is set and upstream is non-2xx', async () => {
process.env.ZAPPIER_UPSTREAM = 'http://upstream.test';
delete process.env.ALLOW_MOCK_CERTS;
const orig = globalThis.fetch;
globalThis.fetch = jest.fn(async () => {
return new Response(JSON.stringify({ error: 'verae_unavailable' }), {
status: 503,
headers: { 'content-type': 'application/json' },
});
}) as typeof fetch;
try {
const { app } = buildApp();
const res = await request(app)
.post('/v1/timestamp')
.set('x-api-key', KEY)
.send({ sha256: 'e'.repeat(64) });
expect(res.status).toBe(503);
expect(res.body.error).toBe('upstream_timestamp_failed');
expect(res.body.upstreamStatus).toBe(503);
expect(String(res.body.receipt_id || '')).not.toMatch(/^mock-cert-/);
expect(String(res.body.certificate || '')).not.toMatch(/^mock-cert-/);
} finally {
globalThis.fetch = orig;
}
});
it('returns 502 when upstream is configured but fetch yields no usable response body path', async () => {
process.env.ZAPPIER_UPSTREAM = 'http://upstream.test';
delete process.env.ALLOW_MOCK_CERTS;
const orig = globalThis.fetch;
// 404 on both preferred and legacy paths → still configured, fail closed (not mock)
globalThis.fetch = jest.fn(async () => {
return new Response(JSON.stringify({ error: 'not_found' }), {
status: 404,
headers: { 'content-type': 'application/json' },
});
}) as typeof fetch;
try {
const { app } = buildApp();
const res = await request(app)
.post('/v1/timestamp')
.set('x-api-key', KEY)
.send({ data: 'fail-closed-404' });
expect(res.status).toBe(404);
expect(res.body.error).toBe('upstream_timestamp_failed');
expect(String(JSON.stringify(res.body))).not.toMatch(/mock-cert-/);
} finally {
globalThis.fetch = orig;
}
});
it('batch timestamps fail closed when upstream returns 500', async () => {
process.env.ZAPPIER_UPSTREAM = 'http://upstream.test';
delete process.env.ALLOW_MOCK_CERTS;
const orig = globalThis.fetch;
globalThis.fetch = jest.fn(async () => {
return new Response(JSON.stringify({ error: 'boom' }), {
status: 500,
headers: { 'content-type': 'application/json' },
});
}) as typeof fetch;
try {
const { app } = buildApp();
const res = await request(app)
.post('/v1/timestamps')
.set('x-api-key', KEY)
.send({ sha256s: ['f'.repeat(64)] });
expect(res.status).toBe(500);
expect(res.body.error).toBe('upstream_timestamp_failed');
expect(String(JSON.stringify(res.body))).not.toMatch(/mock-cert-/);
} finally {
globalThis.fetch = orig;
}
});
it('without upstream, mock certs require ALLOW_MOCK_CERTS=true', async () => {
delete process.env.ZAPPIER_UPSTREAM;
delete process.env.ALLOW_MOCK_CERTS;
const { app } = buildApp();
const res = await request(app)
.post('/v1/timestamp')
.set('x-api-key', KEY)
.send({ data: 'no-mock-without-flag' });
expect(res.status).toBe(503);
expect(res.body.error).toBe('timestamp_upstream_unconfigured');
expect(String(JSON.stringify(res.body))).not.toMatch(/mock-cert-/);
});
it('ALLOW_MOCK_CERTS=true with upstream unset still emits mock-cert for local dev', async () => {
delete process.env.ZAPPIER_UPSTREAM;
process.env.ALLOW_MOCK_CERTS = 'true';
const { app } = buildApp();
const res = await request(app)
.post('/v1/timestamp')
.set('x-api-key', KEY)
.send({ data: 'dev-mock-ok' });
expect(res.status).toBe(200);
expect(String(res.body.receipt_id)).toMatch(/^mock-cert-/);
});
});

View file

@ -3,6 +3,19 @@ import { buildApp, DEFAULT_CUSTOMERS } from '../src/app';
import { emptyChannelRates } from '../src/billing-channels';
describe('POST /v1/timestamps batch', () => {
const prevAllowMock = process.env.ALLOW_MOCK_CERTS;
const prevUpstream = process.env.ZAPPIER_UPSTREAM;
beforeAll(() => {
delete process.env.ZAPPIER_UPSTREAM;
process.env.ALLOW_MOCK_CERTS = 'true';
});
afterAll(() => {
if (prevAllowMock === undefined) delete process.env.ALLOW_MOCK_CERTS;
else process.env.ALLOW_MOCK_CERTS = prevAllowMock;
if (prevUpstream === undefined) delete process.env.ZAPPIER_UPSTREAM;
else process.env.ZAPPIER_UPSTREAM = prevUpstream;
});
it('registers many hashes in one call and skips existing for free', async () => {
const table = emptyChannelRates();
table.rates.zapier.individual.timestamp = 10;