@reticulum/dacar 1.1.1 → 1.1.2
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/package.json +1 -1
- package/src/cli/dacar.js +149 -12
- package/src/cli/store.js +36 -0
package/package.json
CHANGED
package/src/cli/dacar.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* dacar check <grantee> <relation> <object>
|
|
19
19
|
* dacar grants
|
|
20
20
|
* dacar revoke <grantee> <relation> <object> [--publish]
|
|
21
|
+
* dacar publish <file> [<file>...] | --all (work doc #8)
|
|
21
22
|
* dacar identity remember|forget|list ...
|
|
22
23
|
*
|
|
23
24
|
* Online flags: --node <hash>, --topic <topic>, --interface shared|auto|tcp,
|
|
@@ -28,6 +29,7 @@ import { parseArgs } from "node:util";
|
|
|
28
29
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
29
30
|
import { homedir } from "node:os";
|
|
30
31
|
import { join } from "node:path";
|
|
32
|
+
import { pathToFileURL } from "node:url";
|
|
31
33
|
import process from "node:process";
|
|
32
34
|
|
|
33
35
|
import { Identity, toHex } from "@reticulum/core";
|
|
@@ -219,16 +221,25 @@ async function _issue(args, action) {
|
|
|
219
221
|
|
|
220
222
|
// Record plaintext ledger.
|
|
221
223
|
const ledger = await store.loadLedger();
|
|
222
|
-
ledger.set(
|
|
224
|
+
ledger.set(tuple.key, { object: args.object, relation: args.relation, wildcard: args.object.endsWith("*") && args.object !== "*", firstSeen: Number(hlc >> 16n) });
|
|
223
225
|
await store.saveLedger(ledger);
|
|
224
226
|
|
|
225
|
-
out(payload
|
|
227
|
+
out(toHex(payload));
|
|
226
228
|
err(`✔ ${action === Action.GRANT ? "granted" : "revoked"} ${shortHash(grantee, args.fullHashes)} ${args.relation} on ${args.object}`);
|
|
227
229
|
err(` hlc : 0x${hlc.toString(16)}`);
|
|
228
230
|
err(` payload : hex on stdout (${payload.length} bytes)`);
|
|
229
231
|
|
|
230
232
|
if (args.publish) {
|
|
231
233
|
await publishDelta(args, store, identity, payload);
|
|
234
|
+
} else {
|
|
235
|
+
// Outbox (work doc #8): queue locally-issued deltas for `publish --all`.
|
|
236
|
+
// `--publish` sends immediately and never enqueues. (JS `grant` always
|
|
237
|
+
// applies locally — there is no `--no-apply` — so every non-publish grant
|
|
238
|
+
// is a candidate for later batch publish.)
|
|
239
|
+
const outbox = await store.loadOutbox();
|
|
240
|
+
outbox.push(payload);
|
|
241
|
+
await store.saveOutbox(outbox);
|
|
242
|
+
err(" (queued in outbox: `dacar publish --all` to flush)");
|
|
232
243
|
}
|
|
233
244
|
return 0;
|
|
234
245
|
}
|
|
@@ -336,6 +347,107 @@ async function cmdApply(args) {
|
|
|
336
347
|
return 1;
|
|
337
348
|
}
|
|
338
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Read a payload file (or stdin) and auto-detect hex (mirrors Python's
|
|
352
|
+
* `_read_payload_input`): an all-hex, even-length ASCII blob decodes to bytes.
|
|
353
|
+
* `--binary` forces raw bytes.
|
|
354
|
+
* @param {string} path
|
|
355
|
+
* @param {boolean} forceBinary
|
|
356
|
+
* @returns {Promise<Uint8Array>}
|
|
357
|
+
*/
|
|
358
|
+
async function readPayloadInput(path, forceBinary) {
|
|
359
|
+
const data = path === "-" ? new Uint8Array(await readStdin()) : await readFile(path);
|
|
360
|
+
return coercePayload(data, forceBinary);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Auto-detect a hex payload: if *all* bytes are ASCII hex characters (after
|
|
365
|
+
* trimming surrounding whitespace) and the length is even, decode to bytes;
|
|
366
|
+
* otherwise return the raw bytes unchanged.
|
|
367
|
+
* @param {Uint8Array | Buffer} data
|
|
368
|
+
* @param {boolean} forceBinary
|
|
369
|
+
* @returns {Uint8Array}
|
|
370
|
+
*/
|
|
371
|
+
function coercePayload(data, forceBinary) {
|
|
372
|
+
const bytes = new Uint8Array(data);
|
|
373
|
+
if (forceBinary || !bytes.length) return bytes;
|
|
374
|
+
let s;
|
|
375
|
+
try {
|
|
376
|
+
s = Buffer.from(bytes).toString("ascii");
|
|
377
|
+
} catch {
|
|
378
|
+
return bytes; // not ASCII -> raw bytes
|
|
379
|
+
}
|
|
380
|
+
const trimmed = s.trim();
|
|
381
|
+
if (!trimmed || trimmed.length % 2 !== 0) return bytes;
|
|
382
|
+
if (!/^[0-9a-fA-F]+$/.test(trimmed)) return bytes;
|
|
383
|
+
return hexToBytes(trimmed);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export { coercePayload };
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* `dacar publish` — push signed delta(s) to the rfed channel (§11.1, doc #8).
|
|
390
|
+
*
|
|
391
|
+
* Two modes:
|
|
392
|
+
* - `dacar publish <file> [<file>...]` — publish previously-signed delta
|
|
393
|
+
* payload(s) (exact bytes, no re-sign; multiple files → one batch).
|
|
394
|
+
* - `dacar publish --all` — pack + publish the outbox of locally-issued
|
|
395
|
+
* deltas, then clear it. A no-op (exit 0) on an empty outbox.
|
|
396
|
+
*
|
|
397
|
+
* Reuses the `grant --publish` machinery (`publishDelta`: boot RNS, announce,
|
|
398
|
+
* subscribe, publish).
|
|
399
|
+
*/
|
|
400
|
+
async function cmdPublish(args) {
|
|
401
|
+
const store = await openStore(args);
|
|
402
|
+
const identity = await store.loadIdentity();
|
|
403
|
+
if (!identity) throw new CliError("no signing identity (run `dacar init`)");
|
|
404
|
+
|
|
405
|
+
const useAll = !!args.all;
|
|
406
|
+
const files = args._positionals ?? [];
|
|
407
|
+
|
|
408
|
+
if (useAll && files.length) {
|
|
409
|
+
throw new CliError("publish: use either <file>... or --all, not both");
|
|
410
|
+
}
|
|
411
|
+
if (!useAll && !files.length) {
|
|
412
|
+
throw new CliError("publish: provide <file>... (one or more) or --all");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** @type {Uint8Array[]} */
|
|
416
|
+
let toPublish;
|
|
417
|
+
if (useAll) {
|
|
418
|
+
toPublish = await store.loadOutbox();
|
|
419
|
+
if (!toPublish.length) {
|
|
420
|
+
err("outbox empty (nothing to publish)");
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
toPublish = [];
|
|
425
|
+
for (const path of files) {
|
|
426
|
+
const data = await readPayloadInput(path, !!args.binary);
|
|
427
|
+
if (!data.length) throw new CliError(`empty payload: ${path}`);
|
|
428
|
+
toPublish.push(data);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Pack: a single delta as raw bytes, multiple as a batch payload.
|
|
433
|
+
const batch = toPublish.length === 1
|
|
434
|
+
? toPublish[0]
|
|
435
|
+
: DeltaReceiver.packPayloads(toPublish);
|
|
436
|
+
|
|
437
|
+
const label = useAll ? "outbox" : `${toPublish.length} file(s)`;
|
|
438
|
+
const kind = toPublish.length > 1 ? "a batch" : "a single delta";
|
|
439
|
+
err(` publishing ${toPublish.length} delta(s) (${label}) as ${kind}`);
|
|
440
|
+
|
|
441
|
+
// Reuse the grant --publish machinery (boot RNS, announce, subscribe, publish).
|
|
442
|
+
await publishDelta(args, store, identity, batch);
|
|
443
|
+
|
|
444
|
+
if (useAll) {
|
|
445
|
+
await store.saveOutbox([]);
|
|
446
|
+
err(" outbox cleared");
|
|
447
|
+
}
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
339
451
|
async function readStdin() {
|
|
340
452
|
const chunks = [];
|
|
341
453
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -447,7 +559,7 @@ async function cmdGrants(args) {
|
|
|
447
559
|
err(`${label} (${rows.length})`);
|
|
448
560
|
for (const { entry, active } of rows) {
|
|
449
561
|
const t = entry.tuple;
|
|
450
|
-
const row = ledger.get(
|
|
562
|
+
const row = ledger.get(t.key);
|
|
451
563
|
const rel = row?.relation || `[${shortHash(t.relationHash, args.fullHashes)}]`;
|
|
452
564
|
const obj = row?.object || "[hash]";
|
|
453
565
|
err(
|
|
@@ -486,6 +598,12 @@ const SUBCOMMANDS = {
|
|
|
486
598
|
opts: { node: "string", discover: "boolean", topic: "string", "rns-dir": "string", interface: "string" },
|
|
487
599
|
online: true,
|
|
488
600
|
},
|
|
601
|
+
publish: {
|
|
602
|
+
run: cmdPublish,
|
|
603
|
+
opts: { all: "boolean", binary: "boolean", node: "string", discover: "boolean", topic: "string", "rns-dir": "string", interface: "string" },
|
|
604
|
+
// variable file list (0..N) accessed via args._positionals
|
|
605
|
+
online: true,
|
|
606
|
+
},
|
|
489
607
|
apply: { run: cmdApply, opts: { binary: "boolean" }, positional: ["payload"], online: false },
|
|
490
608
|
check: { run: cmdCheck, opts: {}, positional: ["grantee", "relation", "object"], online: false },
|
|
491
609
|
grants: { run: cmdGrants, opts: { all: "boolean", revoked: "boolean" }, online: false },
|
|
@@ -512,11 +630,12 @@ function buildOptions(spec) {
|
|
|
512
630
|
// errors out, and threaded into bootRns to raise the Reticulum log
|
|
513
631
|
// threshold + log interface/announce diagnostics.
|
|
514
632
|
opts.verbose = { type: "boolean", short: "v" };
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
633
|
+
// --store / --identity / --full-hashes are global on every (sub)command
|
|
634
|
+
// (they were previously gated on `positional`, which silently dropped them
|
|
635
|
+
// for commands with no positionals — e.g. `sync`, `config show`, `grants`).
|
|
636
|
+
opts.store = { type: "string" };
|
|
637
|
+
opts.identity = { type: "string" };
|
|
638
|
+
opts["full-hashes"] = { type: "boolean" };
|
|
520
639
|
return opts;
|
|
521
640
|
}
|
|
522
641
|
|
|
@@ -568,7 +687,25 @@ async function main() {
|
|
|
568
687
|
}
|
|
569
688
|
}
|
|
570
689
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
690
|
+
// Only auto-run when invoked as the entry script (Node/Bun via `pathToFileURL`,
|
|
691
|
+
// Deno via `import.meta.main`), so the module can be imported in tests without
|
|
692
|
+
// triggering the CLI dispatch (mirrors how `./cli/store` + `./cli/session`
|
|
693
|
+
// are unit-tested).
|
|
694
|
+
const isMain = (() => {
|
|
695
|
+
try {
|
|
696
|
+
if (import.meta.main === true) return true; // Deno
|
|
697
|
+
} catch { /* not Deno */ }
|
|
698
|
+
try {
|
|
699
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
700
|
+
return true; // Node / Bun
|
|
701
|
+
}
|
|
702
|
+
} catch { /* pathToFileURL unavailable */ }
|
|
703
|
+
return false;
|
|
704
|
+
})();
|
|
705
|
+
|
|
706
|
+
if (isMain) {
|
|
707
|
+
main().then((code) => process.exit(code ?? 0)).catch((e) => {
|
|
708
|
+
err("fatal: " + (e?.stack || e));
|
|
709
|
+
process.exit(1);
|
|
710
|
+
});
|
|
711
|
+
}
|
package/src/cli/store.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* - `aliases` — msgpack `[{ hash, names[], note? }]`
|
|
17
17
|
* - `ledger` — msgpack `{ tupleHashHex: { object?, relation?, wildcard?, firstSeen } }`
|
|
18
18
|
* - `identities` — msgpack `{ hashHex: pubKeyBytes }` (durable issuer cache, doc #5)
|
|
19
|
+
* - `outbox` — msgpack `[payloadBytes, ...]` of locally-issued,
|
|
20
|
+
* not-yet-published signed Deltas (doc #8); flushed + cleared
|
|
21
|
+
* by `dacar publish --all`
|
|
19
22
|
*
|
|
20
23
|
* Secret material (the node's own identity private key) uses the adapter's
|
|
21
24
|
* dedicated `loadKey`/`saveKey` slot, matching `@reticulum/core`.
|
|
@@ -325,6 +328,39 @@ export class DacarStore {
|
|
|
325
328
|
if (own) keyring.registerSingle(own.identityHash, await own.getPublicKey());
|
|
326
329
|
return keyring;
|
|
327
330
|
}
|
|
331
|
+
|
|
332
|
+
// -- outbox (work doc #8) -----------------------------------------------
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Load the outbox of locally-issued, not-yet-published signed Delta
|
|
336
|
+
* payloads, in the order they were issued. Returns an empty array when no
|
|
337
|
+
* outbox record exists yet.
|
|
338
|
+
* @returns {Promise<Uint8Array[]>}
|
|
339
|
+
*/
|
|
340
|
+
async loadOutbox() {
|
|
341
|
+
const bytes = await this._adapter.get(NS, "outbox");
|
|
342
|
+
if (!bytes) return [];
|
|
343
|
+
let obj;
|
|
344
|
+
try {
|
|
345
|
+
obj = MsgPack.decode(bytes);
|
|
346
|
+
} catch {
|
|
347
|
+
return []; // corrupted -> treat as empty (do not crash the CLI)
|
|
348
|
+
}
|
|
349
|
+
if (!Array.isArray(obj)) return [];
|
|
350
|
+
return obj.filter((p) => p instanceof Uint8Array).map((p) => new Uint8Array(p));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Persist the outbox as a MessagePack array of signed Delta payloads.
|
|
355
|
+
* @param {Uint8Array[]} payloads
|
|
356
|
+
*/
|
|
357
|
+
async saveOutbox(payloads) {
|
|
358
|
+
await this._adapter.set(
|
|
359
|
+
NS,
|
|
360
|
+
"outbox",
|
|
361
|
+
MsgPack.encode(payloads.map((p) => new Uint8Array(p))),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
328
364
|
}
|
|
329
365
|
|
|
330
366
|
/**
|