Add Zapier interface simulator, tree-node Merkle lookups, and user docs
Some checks are pending
offline / test (push) Waiting to run

In-process trace console validates hops, faults, recoveries, and
suggested changes before zapier-platform push. User guide covers
signup through central-chain and bulk-summary tree-node hash lookup.
This commit is contained in:
George Lambert 2026-09-11 12:58:03 -04:00
commit f3dc0e6eee
56 changed files with 2341 additions and 8 deletions

View file

@ -54,6 +54,7 @@ If later attach jobs exist for that hash, `receipts` is an array: seal first, th
| `verae.archive.reply.<correlationId>` | archive on bloom hit | aggregator |
Query payload: `{ correlationId, sha256, tenantId, kinds[] }`.
`kinds` may include `tree` for Merkle leaf proofs (hashes sealed only as a bulk summary root).
Reply payload: `{ archiveId, sha256, records[] }`.
Bloom miss → no reply. Aggregator timeout → complete with whatever arrived.

View file

@ -32,6 +32,9 @@ Zapier cloud **never** connects to NATS. Only middleware workers and archives do
| request-splitter | `verae-request-splitter` | In middleware | HTTP body / multipart | chain hash; `verae.archive.put` |
| archive-aggregator | `verae-archive-aggregator` | Worker / in wait | `includeAttached` on wait | `verae.archive.query`; reads `verae.archive.reply.<id>` |
| archive-worm | `verae-archive-worm` | N copies | `verae.archive.query`, `verae.archive.put` | `verae.archive.reply.<id>` if bloom hits |
| tree-node | `verae-tree-node` | N copies (WORM role) | `verae.archive.query`, `verae.archive.put` kind `tree` | `verae.archive.reply.<id>` if bloom hits |
| zapier-simulator | `verae-zapier-simulator` | Local HTTP :3847 | operator browser | in-process replay of all addresses |
| zapier-user-docs | `zapier-user-docs` | Static | — | catalog `/user-docs/` |
| verae-chain-client | `verae-chain-client` | Library | — | HTTPS `api.veraetime.net` or MOCK |
| docs-master | `zapier-docs-master` | Static | — | published on zapier.georgelambert.org |

View file

@ -0,0 +1,44 @@
# Tree nodes: bulk Merkle summaries vs itemized chain seals
The main Verae chain stores **hash + time + block + certificate**. A Zap that timestamps one document writes that document’s SHA-256 as its own seal.
A **batch** of many documents is different: middleware builds a Merkle tree of the item hashes, seals **only the Merkle root** on the chain, and writes each **leaf proof** to external tree-node NATS archives. A later lookup of a member hash will **miss** on the central chain unless the Zap also queries tree nodes.
```text
Zapier HTTPS zappier-edge HTTPS middleware
|
Merkle root | verae.archive.put kind=tree
(chain seal) | (leaf proof, sharded)
v
tree-node-east/west/central
bloom miss = silence
^
hash lookup includeTree | verae.archive.query
| kinds=["tree"]
aggregator merges proof + root seal
```
## When to use which Zapier search
| Search | Hits |
|--------|------|
| Find Timestamp by SHA256 | Hash was registered as its own chain seal |
| Find Hash (tree nodes + central chain) | Chain first, then `includeTree=true` broadcast |
## Receipt shape for a bulk leaf
```json
{
"sha256": "<leaf>",
"exists": true,
"itemizedOnMainChain": false,
"merkleRoot": "<root sealed on chain>",
"proofOk": true,
"receipts": [
{ "kind": "seal", "of": "merkleRoot", "sha256": "<root>", "certificate": "…" },
{ "kind": "tree-leaf", "archiveId": "tree-node-east", "proof": […], "leafIndex": 1 }
]
}
```
Validate this path in the simulator (`packages/verae-zapier-simulator`) before pushing the Zapier app.

View file

@ -27,3 +27,18 @@ After seal is known, aggregator publishes `verae.archive.query`. Each WORM: bloo
## 5. Multipart attachments
Splitter emits one `archive.put` per file (`kind: file`, `contentSha256`). Chain never stores bytes.
## 6. Batch Merkle (bulk summary)
1. Zapier → `POST /v1/timestamp/batch` (one item per line)
2. Middleware hashes each item (leaves), builds Merkle tree
3. Chain seals **only the root**
4. Each leaf proof → `verae.archive.put` `kind: tree` on a sharded tree node
5. Central `GET /hashes/{leaf}` → miss (`itemizedOnMainChain: false`)
6. `GET /hashes/{leaf}?includeTree=true` → aggregator broadcasts `verae.archive.query` `kinds=["tree"]`
7. Tree node bloom hit → `archive.reply.<id>` with proof; others silent
8. Response: root seal + `tree-leaf` receipt
## 7. Simulator
`packages/verae-zapier-simulator` replays flows 1–6 in-process with a trace console, fault injection, and modification suggestions. It does not connect to live NATS.

View file

@ -17,6 +17,9 @@ Summaries, NATS contracts, and message flows for every Verae Time × Zapier modu
| verae-request-splitter | https://git.georgelambert.org/marchon/verae-request-splitter |
| verae-archive-worm | https://git.georgelambert.org/marchon/verae-archive-worm |
| verae-archive-aggregator | https://git.georgelambert.org/marchon/verae-archive-aggregator |
| verae-tree-node | https://git.georgelambert.org/marchon/verae-tree-node |
| verae-zapier-simulator | https://git.georgelambert.org/marchon/verae-zapier-simulator |
| zapier-user-docs | https://git.georgelambert.org/marchon/zapier-user-docs |
| **zapier-docs-master** (this repo) | https://git.georgelambert.org/marchon/zapier-docs-master |
Clone (SSH port 2223):
@ -34,6 +37,9 @@ Clone (SSH port 2223):
| request-splitter | Hash vs files | — | archive.put |
| archive-worm | Bloom WORM node | archive.query, archive.put | archive.reply.* (hit only) |
| archive-aggregator | Merge archive replies | archive.reply.* | archive.query |
| tree-node | Merkle leaf proofs | archive.query, archive.put | archive.reply.* (hit only) |
| zapier-simulator | Trace console (in-process) | — | — |
| zapier-user-docs | Signup → lookup guide | — | — |
## Documents in this repo

View file

@ -54,6 +54,7 @@ If later attach jobs exist for that hash, `receipts` is an array: seal first, th
| `verae.archive.reply.<correlationId>` | archive on bloom hit | aggregator |
Query payload: `{ correlationId, sha256, tenantId, kinds[] }`.
`kinds` may include `tree` for Merkle leaf proofs (hashes sealed only as a bulk summary root).
Reply payload: `{ archiveId, sha256, records[] }`.
Bloom miss → no reply. Aggregator timeout → complete with whatever arrived.

View file

@ -32,6 +32,9 @@ Zapier cloud **never** connects to NATS. Only middleware workers and archives do
| request-splitter | `verae-request-splitter` | In middleware | HTTP body / multipart | chain hash; `verae.archive.put` |
| archive-aggregator | `verae-archive-aggregator` | Worker / in wait | `includeAttached` on wait | `verae.archive.query`; reads `verae.archive.reply.<id>` |
| archive-worm | `verae-archive-worm` | N copies | `verae.archive.query`, `verae.archive.put` | `verae.archive.reply.<id>` if bloom hits |
| tree-node | `verae-tree-node` | N copies (WORM role) | `verae.archive.query`, `verae.archive.put` kind `tree` | `verae.archive.reply.<id>` if bloom hits |
| zapier-simulator | `verae-zapier-simulator` | Local HTTP :3847 | operator browser | in-process replay of all addresses |
| zapier-user-docs | `zapier-user-docs` | Static | — | catalog `/user-docs/` |
| verae-chain-client | `verae-chain-client` | Library | — | HTTPS `api.veraetime.net` or MOCK |
| docs-master | `zapier-docs-master` | Static | — | published on zapier.georgelambert.org |

View file

@ -0,0 +1,7 @@
# NATS — verae-tree-node
| Direction | Address | From / to | Body |
|-----------|---------|-----------|------|
| IN | `verae.archive.put` | merkle builder | `{ sha256, kind: "tree", record: { merkleRoot, proof, leafIndex, chainSealJobId } }` |
| IN | `verae.archive.query` | aggregator (broadcast) | `{ correlationId, sha256, kinds: ["tree", …] }` |
| OUT | `verae.archive.reply.<correlationId>` | aggregator | only if bloom.mightHave(sha256) |

View file

@ -0,0 +1,9 @@
# verae-tree-node
**Job:** Hold inclusion proofs for SHA-256 leaves that were registered only as part of a bulk Merkle summary. The main Verae chain itemizes the **root**, not each leaf.
**Expects messages from:** middleware batch path (`verae.archive.put` kind `tree`); aggregator (`verae.archive.query`).
**Sends messages to:** `verae.archive.reply.<correlationId>` on bloom hit.
**Does not talk to Zapier.** Same bloom-silence rule as `verae-archive-worm`.

View file

@ -0,0 +1,3 @@
# NATS — verae-zapier-simulator
In-process replay of `jobs.watch`, `jobs.events`, `archive.put`, `archive.query`, `archive.reply.*`. No live subscribe.

View file

@ -0,0 +1,7 @@
# verae-zapier-simulator
**Job:** Zapier-like UI + hop-by-hop trace console to validate messaging, assumptions, delays, failures, recoveries, and suggested modifications before `zapier-platform push`.
**Expects:** HTTP `:3847` or `Simulator.run` in tests.
**Sends:** nothing to live NATS / Zapier / Verae.

View file

@ -0,0 +1,3 @@
# NATS — zapier-user-docs
Static docs. Users never configure NATS in Zapier.

View file

@ -0,0 +1,3 @@
# zapier-user-docs
**Job:** Customer guide from signup through register, central-chain lookup, and tree-node lookup of bulk-summary leaves.

View file

@ -41,13 +41,26 @@ export function aggregateAttached({ sha256, tenantId, kinds = ['publicMeta', 'pr
* @param {object[]} archiveRecords
*/
export function mergeReceipts(chainReceipts, archiveRecords) {
const extra = archiveRecords
.filter((r) => r.kind === 'publicMeta' || r.kind === 'privateMeta' || r.kind === 'file')
.map((r) => ({
const extra = archiveRecords.map((r) => {
if (r.kind === 'tree') {
return {
kind: 'tree-leaf',
archiveId: r.archiveId,
attachedAt: r.storedAt,
itemizedOnMainChain: false,
merkleRoot: r.record?.merkleRoot,
proof: r.record?.proof,
leafIndex: r.record?.leafIndex,
chainSealJobId: r.record?.chainSealJobId,
...r.record,
};
}
return {
kind: r.kind === 'file' ? 'file-attach' : 'metadata-attach',
archiveId: r.archiveId,
attachedAt: r.storedAt,
...r.record,
}));
};
});
return [...chainReceipts, ...extra];
}

View file

@ -25,4 +25,20 @@ describe('aggregateAttached', () => {
assert.equal(merged[0].kind, 'seal');
assert.equal(merged[1].kind, 'metadata-attach');
});
it('merges tree-leaf receipts from bulk summaries', () => {
const seal = [{ kind: 'seal', of: 'merkleRoot', sha256: 'aa'.repeat(32) }];
const recs = [
{
kind: 'tree',
archiveId: 'tree-node-east',
storedAt: '2026-01-02T00:00:00Z',
record: { merkleRoot: 'aa'.repeat(32), leafIndex: 1, proof: [] },
},
];
const merged = mergeReceipts(seal, recs);
assert.equal(merged[1].kind, 'tree-leaf');
assert.equal(merged[1].itemizedOnMainChain, false);
assert.equal(merged[1].archiveId, 'tree-node-east');
});
});

View file

@ -0,0 +1,13 @@
# NATS — verae-tree-node
Tree nodes are WORM archives that store **Merkle leaf proofs**. They listen on the same archive subjects so a leaf that was never itemized on the main chain can still be found.
Zapier never connects here. Middleware aggregator broadcasts; a tree node that does not hold the leaf stays silent.
| Direction | Address | From / to | Body |
|-----------|---------|-----------|------|
| IN | `verae.archive.put` | batch splitter / merkle builder | `{ sha256, tenantId, kind: "tree", record: { merkleRoot, proof, leafIndex, chainSealJobId }, traceId }` |
| IN | `verae.archive.query` | aggregator (broadcast, **no queue group**) | `{ correlationId, sha256, tenantId, kinds: ["tree", …], traceId }` |
| OUT | `verae.archive.reply.<correlationId>` | aggregator | `{ archiveId, sha256, records[], traceId }` **only if bloom.mightHave(sha256)** |
`record.merkleRoot` is the hash sealed on the main Verae chain. `proof` lets a verifier recompute the root from this leaf.

View file

@ -0,0 +1,7 @@
# verae-tree-node
External tree-node role: Bloom-filtered WORM store for Merkle **leaf** receipts.
When Zapier submits a **batch**, middleware builds a Merkle tree of item SHA-256s, seals **only the root** on `api.veraetime.net`, and puts each leaf proof on a sharded tree node. A later lookup of a leaf that is not on the main chain fans out `verae.archive.query` with `kinds: ["tree"]`. Nodes that do not hold the leaf send nothing.
Clone: `ssh://git@git.georgelambert.org:2223/marchon/verae-tree-node.git`

View file

@ -0,0 +1,11 @@
# verae-tree-node
**Job:** Hold inclusion proofs for SHA-256 leaves that were registered only as part of a bulk Merkle summary. The main Verae chain itemizes the **root**, not each leaf.
**Expects messages from:** middleware batch path (`verae.archive.put` kind `tree`); aggregator (`verae.archive.query`).
**Sends messages to:** `verae.archive.reply.<correlationId>` on bloom hit.
**Does not talk to Zapier.** Same bloom-silence rule as `verae-archive-worm`.
**Test:** `npm test` — three leaves, root sealed, each leaf verifies; missing leaf has no proof.

View file

@ -0,0 +1,8 @@
{
"name": "verae-tree-node",
"version": "0.1.0",
"type": "module",
"description": "Merkle leaf proofs for hashes only sealed as a bulk summary root on the main Verae chain",
"scripts": { "test": "node --test test/*.test.js" },
"engines": { "node": ">=20" }
}

View file

@ -0,0 +1,96 @@
/**
* Merkle tree over SHA-256 leaf hex digests.
* Only the root is sealed on the main Verae chain; leaves live on tree nodes.
* @module merkle
*/
import { createHash } from 'node:crypto';
/**
* @param {string|Buffer|Uint8Array} data
* @returns {string} lowercase hex
*/
export function sha256Hex(data) {
const buf = Buffer.isBuffer(data) ? data : Buffer.from(String(data), 'utf8');
return createHash('sha256').update(buf).digest('hex');
}
/**
* Parent of two 32-byte hex children (left || right).
* Odd last child is duplicated (Bitcoin-style).
* @param {string} left
* @param {string} right
*/
export function parentHash(left, right) {
return createHash('sha256')
.update(Buffer.from(left, 'hex'))
.update(Buffer.from(right, 'hex'))
.digest('hex');
}
/**
* @param {string[]} leaves hex SHA-256 digests
* @returns {{ root: string, layers: string[][], leaves: string[] }}
*/
export function buildMerkle(leaves) {
if (!leaves.length) {
throw new Error('at least one leaf is required');
}
const layer0 = leaves.map((h) => String(h).toLowerCase());
const layers = [layer0];
let layer = layer0;
while (layer.length > 1) {
const next = [];
for (let i = 0; i < layer.length; i += 2) {
const left = layer[i];
const right = layer[i + 1] ?? layer[i];
next.push(parentHash(left, right));
}
layer = next;
layers.push(layer);
}
return { root: layer[0], layers, leaves: layer0 };
}
/**
* Inclusion proof for leaf index.
* @param {string[][]} layers
* @param {number} index
* @returns {Array<{ sibling: string, position: 'left'|'right' }>}
*/
export function merkleProof(layers, index) {
const proof = [];
let i = index;
for (let level = 0; level < layers.length - 1; level += 1) {
const layer = layers[level];
const isRight = i % 2 === 1;
const sib = isRight ? i - 1 : i + 1;
const sibling = layer[sib] ?? layer[i];
proof.push({ sibling, position: isRight ? 'left' : 'right' });
i = Math.floor(i / 2);
}
return proof;
}
/**
* @param {string} leaf
* @param {Array<{ sibling: string, position: 'left'|'right' }>} proof
* @param {string} root
*/
export function verifyProof(leaf, proof, root) {
let h = String(leaf).toLowerCase();
for (const step of proof) {
h = step.position === 'left' ? parentHash(step.sibling, h) : parentHash(h, step.sibling);
}
return h === String(root).toLowerCase();
}
/**
* Shard a leaf onto one of N tree nodes (others stay silent on bloom miss).
* @param {string} sha256
* @param {number} n
*/
export function shardIndex(sha256, n) {
const b = Buffer.from(String(sha256).slice(0, 8), 'hex');
return n <= 0 ? 0 : b.readUInt32BE(0) % n;
}

Some files were not shown because too many files have changed in this diff Show more