Add Zapier interface simulator, tree-node Merkle lookups, and user docs
Some checks are pending
offline / test (push) Waiting to run
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:
parent
3daa88866d
commit
f3dc0e6eee
56 changed files with 2341 additions and 8 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
44
docs/02-architecture/tree-nodes.md
Normal file
44
docs/02-architecture/tree-nodes.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
7
packages/docs-master/modules/verae-tree-node/NATS.md
Normal file
7
packages/docs-master/modules/verae-tree-node/NATS.md
Normal 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) |
|
||||
9
packages/docs-master/modules/verae-tree-node/SUMMARY.md
Normal file
9
packages/docs-master/modules/verae-tree-node/SUMMARY.md
Normal 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`.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
3
packages/docs-master/modules/zapier-user-docs/NATS.md
Normal file
3
packages/docs-master/modules/zapier-user-docs/NATS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# NATS — zapier-user-docs
|
||||
|
||||
Static docs. Users never configure NATS in Zapier.
|
||||
3
packages/docs-master/modules/zapier-user-docs/SUMMARY.md
Normal file
3
packages/docs-master/modules/zapier-user-docs/SUMMARY.md
Normal 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.
|
||||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
13
packages/verae-tree-node/NATS.md
Normal file
13
packages/verae-tree-node/NATS.md
Normal 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.
|
||||
7
packages/verae-tree-node/README.md
Normal file
7
packages/verae-tree-node/README.md
Normal 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`
|
||||
11
packages/verae-tree-node/SUMMARY.md
Normal file
11
packages/verae-tree-node/SUMMARY.md
Normal 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.
|
||||
8
packages/verae-tree-node/package.json
Normal file
8
packages/verae-tree-node/package.json
Normal 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" }
|
||||
}
|
||||
96
packages/verae-tree-node/src/merkle.js
Normal file
96
packages/verae-tree-node/src/merkle.js
Normal 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
Loading…
Reference in a new issue