commit 9bafd34b2e4be4a052b4f1ce2ec66f10781ed011 Author: George Lambert Date: Fri Sep 11 17:15:45 2026 -0400 Initial import of verae-request-splitter from zapier monorepo diff --git a/NATS.md b/NATS.md new file mode 100644 index 0000000..0e12c1b --- /dev/null +++ b/NATS.md @@ -0,0 +1,8 @@ +# NATS — verae-request-splitter + +| Direction | Address | To | Body | +|-----------|---------|-----|------| +| OUT | `verae.archive.put` | each WORM | one message per publicMeta / privateMeta / file | +| OUT | (in-process) chain | middleware | `{ sha256, hashAlg, data? }` | + +No inbound NATS in the in-process embedding. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3016526 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# verae-request-splitter + +Splits a Zapier/middleware body into (1) the **chain hash** and (2) off-chain **archive puts**. File bytes never go on `api.veraetime.net`. + +**Forgejo:** https://git.georgelambert.org/marchon/verae-request-splitter +**Catalog:** https://zapier.georgelambert.org/packages/verae-request-splitter/README.pdf +**NATS:** https://zapier.georgelambert.org/docs-master/modules/verae-request-splitter/NATS.pdf + +## Run + +```bash +cd packages/verae-request-splitter +npm test +``` + +Today middleware calls this **in-process**. A later split-out can subscribe `verae.splitter.in`. + +## Outputs + +| Path | Content | +|------|---------| +| Chain | `{ sha256, hashAlg }` only | +| `verae.archive.put` | each `publicMeta` / `privateMeta` / `file` | +| Batch | Merkle **root** on chain; leaves → tree-node puts (`kind=tree`) | + +## Depends on + +- middleware-http (caller) +- archive-worm / tree-node (puts) +- chain client (hash seal) + +Does not talk to Zapier or the job poller. diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000..45c898f --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,13 @@ +# verae-request-splitter + +**Job:** Peel a request into (1) chain hash and (2) off-chain archive puts. + +**Expects messages from:** middleware-http (in-process call today; later NATS `verae.splitter.in` if split out). + +**Sends messages to:** +- Chain path: `{ sha256, hashAlg, data? }` +- `verae.archive.put` for each `publicMeta` / `privateMeta` / `file` + +**Does not talk to Zapier or NATS job poller.** + +**Test:** `npm test` — hash-only has zero puts; two files + public meta → 3 puts. diff --git a/package.json b/package.json new file mode 100644 index 0000000..f058e9b --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "verae-request-splitter", + "version": "0.1.0", + "type": "module", + "description": "Split hash/sign vs attachments for chain vs WORM archives", + "scripts": { + "test": "node --test test/*.test.js" + }, + "engines": { "node": ">=20" } +} diff --git a/src/splitRequest.js b/src/splitRequest.js new file mode 100644 index 0000000..7250d05 --- /dev/null +++ b/src/splitRequest.js @@ -0,0 +1,71 @@ +/** + * Split a Zapier/middleware request into chain vs archive parts. + * @module splitRequest + */ + +import { createHash } from 'node:crypto'; + +/** + * @param {string} data + * @returns {string} + */ +export function sha256Hex(data) { + return createHash('sha256').update(String(data), 'utf8').digest('hex'); +} + +/** + * @param {object} body + * @returns {{ + * chain: { sha256: string, hashAlg: string, data?: string }, + * archivePuts: Array<{ kind: string, sha256: string, record: object }>, + * includeAttached: boolean + * }} + */ +export function splitRequest(body = {}) { + const sha256 = (body.sha256 || (body.data != null ? sha256Hex(body.data) : '')).toLowerCase(); + if (!sha256) { + throw new Error('data or sha256 is required'); + } + const includeAttached = Boolean(body.includeAttached); + const archivePuts = []; + if (body.publicMetadata && Object.keys(body.publicMetadata).length) { + archivePuts.push({ + kind: 'publicMeta', + sha256, + record: { publicMetadata: body.publicMetadata }, + }); + } + if (body.privateMetadata && Object.keys(body.privateMetadata).length) { + archivePuts.push({ + kind: 'privateMeta', + sha256, + record: { privateMetadata: body.privateMetadata }, + }); + } + const files = Array.isArray(body.files) ? body.files : []; + for (const f of files) { + const bytes = f.contentBase64 + ? Buffer.from(String(f.contentBase64), 'base64') + : Buffer.from(String(f.data ?? ''), 'utf8'); + const fileHash = sha256Hex(bytes); + archivePuts.push({ + kind: 'file', + sha256, + record: { + path: f.path || f.filename || 'file.bin', + size: bytes.length, + contentSha256: fileHash, + contentBase64: bytes.toString('base64'), + }, + }); + } + return { + chain: { + sha256, + hashAlg: body.hashAlg || 'SHA256', + data: body.data, + }, + archivePuts, + includeAttached, + }; +} diff --git a/test/splitRequest.test.js b/test/splitRequest.test.js new file mode 100644 index 0000000..553904e --- /dev/null +++ b/test/splitRequest.test.js @@ -0,0 +1,32 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { splitRequest, sha256Hex } from '../src/splitRequest.js'; + +describe('splitRequest', () => { + it('hash-only has no archive puts', () => { + const out = splitRequest({ data: 'hello' }); + assert.equal(out.chain.sha256, sha256Hex('hello')); + assert.equal(out.archivePuts.length, 0); + assert.equal(out.includeAttached, false); + }); + + it('splits public metadata and two files onto archive puts', () => { + const out = splitRequest({ + data: 'hello', + publicMetadata: { title: 't' }, + files: [ + { path: 'a.txt', data: 'A' }, + { path: 'b.txt', data: 'B' }, + ], + includeAttached: true, + }); + assert.equal(out.archivePuts.length, 3); + assert.equal(out.archivePuts.filter((p) => p.kind === 'file').length, 2); + assert.equal(out.includeAttached, true); + assert.equal(out.chain.sha256, sha256Hex('hello')); + }); + + it('throws without data or sha256', () => { + assert.throws(() => splitRequest({}), /data or sha256/); + }); +});