@uuaid/pillar 0.2.0-alpha.12 → 0.2.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INTERACTION-CONVENTION.md +216 -0
- package/README.md +275 -11
- package/bin/pillar-blob.mjs +112 -0
- package/bin/pillar-directory.mjs +231 -0
- package/bin/pillar-seed.mjs +112 -13
- package/bin/pillar.mjs +6 -2
- package/package.json +20 -12
- package/src/blob/blob.mjs +426 -0
- package/src/blob/broker.mjs +129 -0
- package/src/blob/http.mjs +196 -0
- package/src/blob/store.mjs +55 -0
- package/src/identity/keychain.mjs +27 -10
- package/src/identity/tier.mjs +24 -3
- package/src/index.mjs +25 -2
- package/src/net/carrier-client.mjs +55 -9
- package/src/net/carrier-server.mjs +169 -11
- package/src/net/delegation.mjs +152 -0
- package/src/net/envelope.mjs +81 -0
- package/src/net/seed-registry.mjs +467 -0
- package/src/net/seed-tier.mjs +185 -0
- package/src/pillar.mjs +96 -3
package/src/pillar.mjs
CHANGED
|
@@ -19,10 +19,14 @@ import { CID } from "multiformats/cid";
|
|
|
19
19
|
import * as raw from "multiformats/codecs/raw";
|
|
20
20
|
import { sha256 } from "multiformats/hashes/sha2";
|
|
21
21
|
import { createTransport, PILLAR_PROTOCOL, uuaidToDhtKey, peerIdFromEd25519Hex } from "./net/transport.mjs";
|
|
22
|
-
import { seal, open, decrypt, envelopeSha, ENVELOPE_VERSION } from "./net/envelope.mjs";
|
|
22
|
+
import { seal, open, decrypt, envelopeSha, mkEnvelopeId, ENVELOPE_VERSION } from "./net/envelope.mjs";
|
|
23
23
|
import { Mailbox } from "./mailbox/mailbox.mjs";
|
|
24
24
|
import { CarrierClient } from "./net/carrier-client.mjs";
|
|
25
25
|
import { Observability } from "./observability.mjs";
|
|
26
|
+
import {
|
|
27
|
+
BLOB_KIND, MAX_INLINE_BODY_BYTES, BlobError, BlobSizeError, envelopeBodyBytes, preflightBodyBytes,
|
|
28
|
+
prepareBlobSend, fetchAndDecryptBlob, prepareBrokerBlobSend, fetchBrokerBlob,
|
|
29
|
+
} from "./blob/blob.mjs";
|
|
26
30
|
|
|
27
31
|
const VALUE_STORE_PREFIX = "/uuaid/1.0";
|
|
28
32
|
|
|
@@ -41,6 +45,10 @@ export class Pillar {
|
|
|
41
45
|
trust = null, // { policy, allow, block } from loadTrustPolicy(); null = open
|
|
42
46
|
tierGrant = null, // signed grant object (tier.mjs) sent with carrier delivers
|
|
43
47
|
metrics = null, // Observability instance; a default is created internally
|
|
48
|
+
blobStore = null, // BlobStore (src/blob/store.mjs) for the blob transport
|
|
49
|
+
blobBroker = null, // challenge broker for blob reserve/finalize/redeem
|
|
50
|
+
blobClient = null, // PillarBlobHttpClient for the production S3 broker path
|
|
51
|
+
senderKeyCheck = null, // registry-active gate: (uuaid, pubkeyHex) => status
|
|
44
52
|
}) {
|
|
45
53
|
if (!keychain?._identity) throw new Error("Pillar: keychain must be loaded before construction");
|
|
46
54
|
this.keychain = keychain;
|
|
@@ -58,6 +66,12 @@ export class Pillar {
|
|
|
58
66
|
this.node = null;
|
|
59
67
|
this.trust = trust;
|
|
60
68
|
this.metrics = metrics ?? new Observability();
|
|
69
|
+
this.blobStore = blobStore;
|
|
70
|
+
this.blobBroker = blobBroker;
|
|
71
|
+
this.blobClient = blobClient;
|
|
72
|
+
this.blobTier = tierGrant?.tier ?? "community";
|
|
73
|
+
this.blobMaxBodyBytes = Number.isInteger(tierGrant?.maxBodyBytes) ? tierGrant.maxBodyBytes : null;
|
|
74
|
+
this.senderKeyCheck = senderKeyCheck;
|
|
61
75
|
this._envelopeHandlers = new Set();
|
|
62
76
|
this._openStreams = new Map(); // peerIdString -> pushable
|
|
63
77
|
}
|
|
@@ -240,7 +254,7 @@ export class Pillar {
|
|
|
240
254
|
* fleet deployment we bypass DHT via the address book which is populated
|
|
241
255
|
* once at fleet-up time.
|
|
242
256
|
*/
|
|
243
|
-
async send({ recipient, recipientPublicKey, kind = "message", payload = {}, resolveTimeoutMs = 5000 }) {
|
|
257
|
+
async send({ recipient, recipientPublicKey, kind = "message", payload = {}, envelopeId = null, resolveTimeoutMs = 5000 }) {
|
|
244
258
|
if (!this.node && !this.carrierClient) {
|
|
245
259
|
this.metrics.inc("send.error.not-started");
|
|
246
260
|
throw new Error("pillar not started");
|
|
@@ -265,7 +279,12 @@ export class Pillar {
|
|
|
265
279
|
throw new Error(`send: no public key known for ${recipient} — encryption is mandatory; register the peer (address book / fleet.json) or pass recipientPublicKey`);
|
|
266
280
|
}
|
|
267
281
|
|
|
268
|
-
const envelope = seal(this.keychain, { recipient, recipientPublicKey: pubkey, kind, payload });
|
|
282
|
+
const envelope = seal(this.keychain, { recipient, recipientPublicKey: pubkey, kind, payload, id: envelopeId ?? undefined });
|
|
283
|
+
const hardCap = preflightBodyBytes(envelopeBodyBytes(envelope), { maxBodyBytes: MAX_INLINE_BODY_BYTES });
|
|
284
|
+
if (!hardCap.ok) {
|
|
285
|
+
this.metrics.inc("send.error.direct-cap");
|
|
286
|
+
throw new BlobSizeError(`envelope ${hardCap.bytes}B exceeds the ${MAX_INLINE_BODY_BYTES}B hard cap`, { size: hardCap.bytes, max: MAX_INLINE_BODY_BYTES });
|
|
287
|
+
}
|
|
269
288
|
const persisted = this.mailbox.appendOutbound(envelope);
|
|
270
289
|
|
|
271
290
|
// 1. Address book path: direct libp2p dial (lowest latency, no third party).
|
|
@@ -343,6 +362,66 @@ export class Pillar {
|
|
|
343
362
|
return { group, results };
|
|
344
363
|
}
|
|
345
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Blob send — the oversized-payload path. Explicit flow (blob.mjs header):
|
|
367
|
+
* broker reserve -> local AES-256-GCM encrypt (fresh DEK + nonce, AAD bound
|
|
368
|
+
* to this envelope) -> store upload -> broker finalize -> seal a kind
|
|
369
|
+
* "blob-ref" envelope carrying the signed ref (DEK inside the sealed
|
|
370
|
+
* payload, never in metadata) -> deliver.
|
|
371
|
+
*
|
|
372
|
+
* Preflight is exact: the placeholder-free body is measured against the
|
|
373
|
+
* tier inline limit and the 2 MiB direct hard cap BEFORE any store or
|
|
374
|
+
* broker call. The envelope id is generated first so the GCM AAD can bind
|
|
375
|
+
* the ciphertext to it.
|
|
376
|
+
*/
|
|
377
|
+
async sendBlob({ recipient, recipientPublicKey, kind = BLOB_KIND, data, ttlMs, resolveTimeoutMs = 5000 }) {
|
|
378
|
+
if (!this.blobClient && !this.blobStore) throw new BlobError("no-store", "sendBlob: configure blobClient or blobStore on the Pillar");
|
|
379
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
380
|
+
// Reserve an envelope id so the blob ciphertext is AAD-bound to it.
|
|
381
|
+
const envelopeId = mkEnvelopeId();
|
|
382
|
+
const placeholder = { blobRef: { v: 1, envelopeId } };
|
|
383
|
+
// Seal a disposable probe before any broker/S3 side effect. Besides sizing,
|
|
384
|
+
// this enforces recipient UUAID ↔ public-key binding up front.
|
|
385
|
+
const probe = seal(this.keychain, { recipient, recipientPublicKey, kind, payload: placeholder, id: envelopeId });
|
|
386
|
+
const pf = preflightBodyBytes(envelopeBodyBytes(probe), { tier: this.blobTier, maxBodyBytes: this.blobMaxBodyBytes });
|
|
387
|
+
if (!pf.ok) {
|
|
388
|
+
this.metrics.inc(`sendBlob.preflight.${pf.source}`);
|
|
389
|
+
throw new BlobError("blob-too-large", `sendBlob: placeholder body ${pf.bytes}B exceeds ${pf.source} limit ${pf.limit}B`);
|
|
390
|
+
}
|
|
391
|
+
const prepared = this.blobClient
|
|
392
|
+
? await prepareBrokerBlobSend(this.keychain, { data: buf, envelopeId, recipient, ttlMs, client: this.blobClient })
|
|
393
|
+
: await prepareBlobSend(this.keychain, {
|
|
394
|
+
data: buf, envelopeId, recipient, ttlMs,
|
|
395
|
+
store: this.blobStore, broker: this.blobBroker,
|
|
396
|
+
});
|
|
397
|
+
const r = await this.send({
|
|
398
|
+
recipient, recipientPublicKey, kind,
|
|
399
|
+
payload: { blobRef: prepared.ref }, envelopeId, resolveTimeoutMs,
|
|
400
|
+
});
|
|
401
|
+
this.metrics.inc("sendBlob.ok");
|
|
402
|
+
return { ...r, blob: { object: prepared.object, hash: prepared.hash, size: prepared.size, metadata: prepared.metadata } };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Redeem a blob-ref from an inbound payload — fetch + hash-check +
|
|
407
|
+
* single-use broker redemption + AES-256-GCM decrypt with the ref's DEK.
|
|
408
|
+
* The registry-active sender key gate runs BEFORE the broker redemption
|
|
409
|
+
* burns. Returns { data, object, hash, size }.
|
|
410
|
+
*/
|
|
411
|
+
async receiveBlob(payload, { envelopeId = null, now = Date.now() } = {}) {
|
|
412
|
+
const ref = payload?.blobRef;
|
|
413
|
+
if (!ref) throw new BlobError("blob-ref-invalid", "receiveBlob: payload carries no blobRef");
|
|
414
|
+
if (!this.blobClient && !this.blobStore) throw new BlobError("no-store", "receiveBlob: configure blobClient or blobStore on the Pillar");
|
|
415
|
+
const r = this.blobClient
|
|
416
|
+
? await fetchBrokerBlob({ ref, envelopeId, recipient: this.uuaid, now, client: this.blobClient })
|
|
417
|
+
: await fetchAndDecryptBlob({
|
|
418
|
+
ref, store: this.blobStore, envelopeId, now,
|
|
419
|
+
broker: this.blobBroker, senderKeyCheck: this.senderKeyCheck,
|
|
420
|
+
});
|
|
421
|
+
this.metrics.inc("receiveBlob.ok");
|
|
422
|
+
return r;
|
|
423
|
+
}
|
|
424
|
+
|
|
346
425
|
/**
|
|
347
426
|
* Start pulling our inbox from every configured carrier. Received envelopes
|
|
348
427
|
* flow through the same verification + mailbox + handler pipeline as
|
|
@@ -400,6 +479,10 @@ export class Pillar {
|
|
|
400
479
|
}
|
|
401
480
|
|
|
402
481
|
async _deliverToPeer(envelope, peerIdStr, multiaddrStr) {
|
|
482
|
+
const bytes = envelopeBodyBytes(envelope);
|
|
483
|
+
if (bytes > MAX_INLINE_BODY_BYTES) {
|
|
484
|
+
throw new BlobSizeError(`direct envelope ${bytes}B exceeds the ${MAX_INLINE_BODY_BYTES}B hard cap`, { size: bytes, max: MAX_INLINE_BODY_BYTES });
|
|
485
|
+
}
|
|
403
486
|
const { peerIdFromString } = await import("@libp2p/peer-id");
|
|
404
487
|
const { multiaddr } = await import("@multiformats/multiaddr");
|
|
405
488
|
const peerId = peerIdFromString(peerIdStr);
|
|
@@ -444,6 +527,10 @@ export class Pillar {
|
|
|
444
527
|
const pubkey = recipientPublicKey ?? this.addressBook?.get(recipient)?.publicKey;
|
|
445
528
|
if (!pubkey) throw new Error(`sendDirect: no public key for ${recipient} — encryption is mandatory`);
|
|
446
529
|
const envelope = seal(this.keychain, { recipient, recipientPublicKey: pubkey, kind, payload });
|
|
530
|
+
const bytes = envelopeBodyBytes(envelope);
|
|
531
|
+
if (bytes > MAX_INLINE_BODY_BYTES) {
|
|
532
|
+
throw new BlobSizeError(`direct envelope ${bytes}B exceeds the ${MAX_INLINE_BODY_BYTES}B hard cap`, { size: bytes, max: MAX_INLINE_BODY_BYTES });
|
|
533
|
+
}
|
|
447
534
|
this.mailbox.appendOutbound(envelope);
|
|
448
535
|
const { peerIdFromString } = await import("@libp2p/peer-id");
|
|
449
536
|
const { multiaddr } = await import("@multiformats/multiaddr");
|
|
@@ -465,8 +552,14 @@ export class Pillar {
|
|
|
465
552
|
async _handleStream(stream, connection) {
|
|
466
553
|
try {
|
|
467
554
|
let buf = "";
|
|
555
|
+
let receivedBytes = 0;
|
|
468
556
|
for await (const chunk of stream) {
|
|
469
557
|
const bytes = chunk?.subarray ? chunk.subarray() : chunk;
|
|
558
|
+
receivedBytes += bytes.byteLength ?? bytes.length;
|
|
559
|
+
if (receivedBytes > MAX_INLINE_BODY_BYTES + 1) {
|
|
560
|
+
this.metrics.inc("received.rejected.direct-cap");
|
|
561
|
+
throw new BlobSizeError(`incoming direct envelope exceeds the ${MAX_INLINE_BODY_BYTES}B hard cap`, { size: receivedBytes, max: MAX_INLINE_BODY_BYTES });
|
|
562
|
+
}
|
|
470
563
|
buf += u8ToString(bytes);
|
|
471
564
|
const newlineIdx = buf.indexOf("\n");
|
|
472
565
|
if (newlineIdx < 0) continue;
|