Adjacent work: PDF receipts, encrypted blob+share mock, NATS tunnel, SHA256 Zapier action
Around remaining blockers (Zapier login, live Verae): generate PDF receipts without extra deps; AES-256-GCM object store with tenant isolation and share tokens; scripts/nats-tunnel.sh to NS1; SHA256 Hash Text local Zapier action; scripts/test-offline.sh for the no-login suite. Learned: other-tenant object GET is 403; share token is the mock unwrap path.
This commit is contained in:
parent
c8f9d01a04
commit
645a24909a
45 changed files with 1053 additions and 24 deletions
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue