@rubric-protocol/attest-decision 1.0.0

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/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @rubric-protocol/attest-decision — core SDK (DAR/0.1).
3
+ *
4
+ * Public surface: fire-and-forget attestation (`Attestor`), the DAR builder,
5
+ * JCS canonicalization, SHA3-256 hashing, ULID minting, the durable spool, and
6
+ * transports. See spec/dar-0.1.md and tasks/P1.md.
7
+ */
8
+ export { DAR_VERSION, HASH_ALGORITHM, HASH_PREFIX, TIERED_ATTEST_PATH, API_KEY_ENV, } from "./constants.js";
9
+ export { canonicalize, canonicalizeToBytes, JcsError } from "./jcs.js";
10
+ export { sha3_256, sha3_256Hex, hashJson } from "./hash.js";
11
+ export { ulid, monotonicUlidFactory } from "./ulid.js";
12
+ export { DarBuilder, leafHash, canonicalDar, decisionHashOf, toPayload, } from "./dar.js";
13
+ export { Spool, DEFAULT_MAX_BYTES } from "./spool.js";
14
+ export { HttpTransport, } from "./transport.js";
15
+ export { Attestor } from "./attestor.js";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,WAAW,EACX,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,WAAW,GAOZ,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EACL,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,SAAS,GAIV,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAuC,MAAM,YAAY,CAAC;AAC3F,OAAO,EACL,aAAa,GAGd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAwB,MAAM,eAAe,CAAC"}
package/dist/jcs.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * JCS canonicalization — RFC 8785 (spec/dar-0.1.md §3).
3
+ *
4
+ * We lean on the platform: ECMAScript's Number-to-String and `JSON.stringify`
5
+ * string escaping are byte-for-byte what RFC 8785 mandates (§3.2.2.2/§3.2.2.3),
6
+ * and the default string sort compares by UTF-16 code unit — exactly JCS's key
7
+ * ordering rule. So canonicalization reduces to: recurse, sort object keys, and
8
+ * defer scalar serialization to `JSON.stringify`.
9
+ */
10
+ declare class JcsError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ /** Canonicalize a JSON value to its RFC 8785 (JCS) string form. */
14
+ export declare function canonicalize(value: unknown): string;
15
+ /** Canonicalize a JSON value to its RFC 8785 (JCS) UTF-8 bytes. */
16
+ export declare function canonicalizeToBytes(value: unknown): Buffer;
17
+ export { JcsError };
18
+ //# sourceMappingURL=jcs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jcs.d.ts","sourceRoot":"","sources":["../src/jcs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAM,QAAS,SAAQ,KAAK;gBACd,OAAO,EAAE,MAAM;CAI5B;AA0FD,mEAAmE;AACnE,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE1D;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC"}
package/dist/jcs.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * JCS canonicalization — RFC 8785 (spec/dar-0.1.md §3).
3
+ *
4
+ * We lean on the platform: ECMAScript's Number-to-String and `JSON.stringify`
5
+ * string escaping are byte-for-byte what RFC 8785 mandates (§3.2.2.2/§3.2.2.3),
6
+ * and the default string sort compares by UTF-16 code unit — exactly JCS's key
7
+ * ordering rule. So canonicalization reduces to: recurse, sort object keys, and
8
+ * defer scalar serialization to `JSON.stringify`.
9
+ */
10
+ class JcsError extends Error {
11
+ constructor(message) {
12
+ super(`JCS: ${message}`);
13
+ this.name = "JcsError";
14
+ }
15
+ }
16
+ function serialize(v) {
17
+ if (v === null)
18
+ return "null";
19
+ const t = typeof v;
20
+ if (t === "number") {
21
+ const n = v;
22
+ if (!Number.isFinite(n)) {
23
+ throw new JcsError("non-finite number (NaN/Infinity) is not representable");
24
+ }
25
+ // Spec §3.3: integers beyond the safe range MUST be carried as strings — a
26
+ // JS number cannot represent them exactly, so we reject rather than hash a
27
+ // silently-rounded value.
28
+ if (Number.isInteger(n) && !Number.isSafeInteger(n)) {
29
+ throw new JcsError(`integer ${n} exceeds Number.MAX_SAFE_INTEGER; carry it as a string (spec §3.3)`);
30
+ }
31
+ // ES Number::toString === RFC 8785 §3.2.2.3. JSON.stringify(-0) === "0".
32
+ return JSON.stringify(n);
33
+ }
34
+ if (t === "boolean")
35
+ return v ? "true" : "false";
36
+ // RFC 8785 §3.2.2.2 escaping is exactly JSON.stringify's minimal escaping.
37
+ if (t === "string")
38
+ return escapeString(v);
39
+ if (t === "bigint") {
40
+ throw new JcsError("bigint is not JSON; carry large integers as strings");
41
+ }
42
+ if (Array.isArray(v)) {
43
+ return "[" + v.map(serializeElement).join(",") + "]";
44
+ }
45
+ if (t === "object") {
46
+ const proto = Object.getPrototypeOf(v);
47
+ if (proto !== Object.prototype && proto !== null) {
48
+ // Date, Map, Set, class instances, etc. are not part of the JSON model.
49
+ throw new JcsError("non-plain object is not JSON-canonicalizable");
50
+ }
51
+ const obj = v;
52
+ // Default sort compares by UTF-16 code unit — the RFC 8785 ordering.
53
+ const keys = Object.keys(obj).sort();
54
+ return ("{" +
55
+ keys
56
+ .map((k) => {
57
+ const val = obj[k];
58
+ rejectNonJson(val, `property '${k}'`);
59
+ return escapeString(k) + ":" + serialize(val);
60
+ })
61
+ .join(",") +
62
+ "}");
63
+ }
64
+ // undefined, function, symbol at the top level.
65
+ throw new JcsError(`unsupported value of type '${t}'`);
66
+ }
67
+ /**
68
+ * Spec §3.3: `undefined`, functions, and symbols MUST NOT appear in a payload —
69
+ * we reject rather than silently stripping them, so a malformed producer input
70
+ * cannot canonicalize to bytes that differ from the producer's intent.
71
+ */
72
+ function rejectNonJson(value, where) {
73
+ const t = typeof value;
74
+ if (value === undefined || t === "function" || t === "symbol") {
75
+ throw new JcsError(`${where} has non-JSON value of type '${value === undefined ? "undefined" : t}'`);
76
+ }
77
+ }
78
+ function serializeElement(el) {
79
+ rejectNonJson(el, "array element");
80
+ return serialize(el);
81
+ }
82
+ // Unpaired UTF-16 surrogate: a high surrogate not followed by a low, or a low
83
+ // not preceded by a high — i.e. invalid Unicode that RFC 8785 does not admit.
84
+ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
85
+ /** JSON.stringify a string after rejecting invalid Unicode (unpaired surrogates). */
86
+ function escapeString(s) {
87
+ if (LONE_SURROGATE.test(s)) {
88
+ throw new JcsError("string contains an unpaired surrogate (invalid Unicode)");
89
+ }
90
+ return JSON.stringify(s);
91
+ }
92
+ /** Canonicalize a JSON value to its RFC 8785 (JCS) string form. */
93
+ export function canonicalize(value) {
94
+ return serialize(value);
95
+ }
96
+ /** Canonicalize a JSON value to its RFC 8785 (JCS) UTF-8 bytes. */
97
+ export function canonicalizeToBytes(value) {
98
+ return Buffer.from(serialize(value), "utf8");
99
+ }
100
+ export { JcsError };
101
+ //# sourceMappingURL=jcs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jcs.js","sourceRoot":"","sources":["../src/jcs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,QAAS,SAAQ,KAAK;IAC1B,YAAY,OAAe;QACzB,KAAK,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAED,SAAS,SAAS,CAAC,CAAU;IAC3B,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAE9B,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC;IAEnB,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,MAAM,CAAC,GAAG,CAAW,CAAC;QACtB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,QAAQ,CAAC,uDAAuD,CAAC,CAAC;QAC9E,CAAC;QACD,2EAA2E;QAC3E,2EAA2E;QAC3E,0BAA0B;QAC1B,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,QAAQ,CAAC,WAAW,CAAC,oEAAoE,CAAC,CAAC;QACvG,CAAC;QACD,yEAAyE;QACzE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IAED,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAEjD,2EAA2E;IAC3E,IAAI,CAAC,KAAK,QAAQ;QAAE,OAAO,YAAY,CAAC,CAAW,CAAC,CAAC;IAErD,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,MAAM,IAAI,QAAQ,CAAC,qDAAqD,CAAC,CAAC;IAC5E,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACrB,OAAO,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACvD,CAAC;IAED,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;QACvC,IAAI,KAAK,KAAK,MAAM,CAAC,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACjD,wEAAwE;YACxE,MAAM,IAAI,QAAQ,CAAC,8CAA8C,CAAC,CAAC;QACrE,CAAC;QACD,MAAM,GAAG,GAAG,CAA4B,CAAC;QACzC,qEAAqE;QACrE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACrC,OAAO,CACL,GAAG;YACH,IAAI;iBACD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;gBACT,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;gBACnB,aAAa,CAAC,GAAG,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;gBACtC,OAAO,YAAY,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;YAChD,CAAC,CAAC;iBACD,IAAI,CAAC,GAAG,CAAC;YACZ,GAAG,CACJ,CAAC;IACJ,CAAC;IAED,gDAAgD;IAChD,MAAM,IAAI,QAAQ,CAAC,8BAA8B,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,KAAc,EAAE,KAAa;IAClD,MAAM,CAAC,GAAG,OAAO,KAAK,CAAC;IACvB,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC9D,MAAM,IAAI,QAAQ,CAAC,GAAG,KAAK,gCAAgC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACvG,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,EAAW;IACnC,aAAa,CAAC,EAAE,EAAE,eAAe,CAAC,CAAC;IACnC,OAAO,SAAS,CAAC,EAAE,CAAC,CAAC;AACvB,CAAC;AAED,8EAA8E;AAC9E,8EAA8E;AAC9E,MAAM,cAAc,GAAG,wEAAwE,CAAC;AAEhG,qFAAqF;AACrF,SAAS,YAAY,CAAC,CAAS;IAC7B,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,QAAQ,CAAC,yDAAyD,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;AAC/C,CAAC;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { DarCore, PayloadRecord } from "./constants.js";
2
+ declare const DEFAULT_MAX_BYTES: number;
3
+ export interface SpoolRecord {
4
+ seq: number;
5
+ dar: DarCore;
6
+ /** Raw content, present only in `payload` mode (spec §4). */
7
+ payload?: PayloadRecord;
8
+ }
9
+ export interface SpoolOptions {
10
+ maxBytes?: number;
11
+ /** Called when the drop-oldest cap policy discards records (data loss). */
12
+ onDrop?: (count: number) => void;
13
+ }
14
+ export declare class Spool {
15
+ private readonly path;
16
+ private readonly dir;
17
+ private readonly ackPath;
18
+ private readonly maxBytes;
19
+ private readonly onDrop?;
20
+ private fd;
21
+ private seq;
22
+ private ackedThrough;
23
+ private bytes;
24
+ private droppedForCap;
25
+ private compactionPending;
26
+ private pendingRecords;
27
+ constructor(path: string, options?: SpoolOptions);
28
+ /** Records written but not yet acknowledged, in FIFO order. */
29
+ pending(): SpoolRecord[];
30
+ /** Count of records dropped by the drop-oldest cap policy so far. */
31
+ droppedCount(): number;
32
+ currentSeq(): number;
33
+ /**
34
+ * Append a record durably (single `writeSync`, no fsync). Returns its seq.
35
+ * Never compacts inline — that would put a multi-MB `writeFileSync`+`fsync` on
36
+ * the caller's `attest()` path. Crossing the cap only flags compaction, which
37
+ * `compactIfNeeded()` performs off the caller path (see the Attestor flush).
38
+ */
39
+ append(dar: DarCore, payload?: PayloadRecord): number;
40
+ /** Whether the file has grown past the cap and awaits compaction. */
41
+ needsCompaction(): boolean;
42
+ /** Reclaim acked space and enforce the cap (drop-oldest). Off the caller path. */
43
+ compactIfNeeded(): void;
44
+ /**
45
+ * Acknowledge every record with seq <= `throughSeq` (they were delivered).
46
+ * Advances the durable watermark; truncates the file once fully drained.
47
+ */
48
+ ack(throughSeq: number): void;
49
+ /** Flush OS buffers to disk. Called off the caller path, before network I/O. */
50
+ fsync(): void;
51
+ close(): void;
52
+ private recover;
53
+ private compact;
54
+ private readAck;
55
+ private writeAck;
56
+ }
57
+ export { DEFAULT_MAX_BYTES };
58
+ //# sourceMappingURL=spool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spool.d.ts","sourceRoot":"","sources":["../src/spool.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE7D,QAAA,MAAM,iBAAiB,QAAmB,CAAC;AAE3C,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,OAAO,CAAC;IACb,6DAA6D;IAC7D,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AA+BD,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAA0B;IAElD,OAAO,CAAC,EAAE,CAAS;IACnB,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,cAAc,CAAqB;gBAE/B,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB;IAgBpD,+DAA+D;IAC/D,OAAO,IAAI,WAAW,EAAE;IAIxB,qEAAqE;IACrE,YAAY,IAAI,MAAM;IAItB,UAAU,IAAI,MAAM;IAIpB;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM;IAYrD,qEAAqE;IACrE,eAAe,IAAI,OAAO;IAI1B,kFAAkF;IAClF,eAAe,IAAI,IAAI;IAMvB;;;OAGG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAY7B,gFAAgF;IAChF,KAAK,IAAI,IAAI;IAIb,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,OAAO;IAwBf,OAAO,CAAC,OAAO;IA0Bf,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,QAAQ;CAKjB;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
package/dist/spool.js ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Durable append-only spool (tasks/P1.md).
3
+ *
4
+ * Durability model: `attest()` must add <1 ms to the caller, so we CANNOT fsync
5
+ * on the append path. Instead each record is appended with a single `writeSync`.
6
+ * A `writeSync` hands the bytes to the kernel, so they survive process death
7
+ * (`kill -9`) — only a machine/OS crash could lose them, and that is what the
8
+ * background `fsync()` (called off the caller path, before each network flush)
9
+ * defends against. On restart, `pending()` replays everything not yet acked, so
10
+ * zero spooled records are lost.
11
+ *
12
+ * Format: one JSON line per record, `{"seq":N,"dar":{...}}\n`. Acknowledgement
13
+ * is a monotonic high-water mark `ackedThrough` persisted in a `<path>.ack`
14
+ * sidecar; records are FIFO so a single watermark suffices. When the file
15
+ * exceeds `maxBytes` it is compacted (acked records dropped; then oldest pending
16
+ * dropped — "drop-oldest" — until under cap).
17
+ */
18
+ import { closeSync, fstatSync, fsyncSync, ftruncateSync, mkdirSync, openSync, readFileSync, renameSync, writeSync, } from "node:fs";
19
+ import { dirname } from "node:path";
20
+ const DEFAULT_MAX_BYTES = 50 * 1024 * 1024; // 50 MB (tasks/P1.md)
21
+ /** Write an entire buffer, looping over short writes (write(2) may be partial). */
22
+ function writeFully(fd, buf) {
23
+ let offset = 0;
24
+ while (offset < buf.length) {
25
+ offset += writeSync(fd, buf, offset, buf.length - offset);
26
+ }
27
+ }
28
+ /** Durably write a small file (write + fsync). */
29
+ function writeFileDurable(path, contents) {
30
+ const fd = openSync(path, "w");
31
+ try {
32
+ writeFully(fd, Buffer.from(contents, "utf8"));
33
+ fsyncSync(fd);
34
+ }
35
+ finally {
36
+ closeSync(fd);
37
+ }
38
+ }
39
+ /** fsync a directory so a rename/create within it is durable. */
40
+ function fsyncDir(path) {
41
+ const fd = openSync(path, "r");
42
+ try {
43
+ fsyncSync(fd);
44
+ }
45
+ finally {
46
+ closeSync(fd);
47
+ }
48
+ }
49
+ export class Spool {
50
+ path;
51
+ dir;
52
+ ackPath;
53
+ maxBytes;
54
+ onDrop;
55
+ fd;
56
+ seq = 0;
57
+ ackedThrough = 0;
58
+ bytes = 0;
59
+ droppedForCap = 0;
60
+ compactionPending = false;
61
+ pendingRecords = [];
62
+ constructor(path, options = {}) {
63
+ this.path = path;
64
+ this.dir = dirname(path);
65
+ this.ackPath = `${path}.ack`;
66
+ this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
67
+ this.onDrop = options.onDrop;
68
+ // Create the spool directory if missing so a documented path like
69
+ // /var/lib/rubric/attest.spool works without a separate mkdir.
70
+ mkdirSync(this.dir, { recursive: true });
71
+ this.ackedThrough = this.readAck();
72
+ this.recover();
73
+ // Open the append fd after recovery has read the current contents.
74
+ this.fd = openSync(this.path, "a");
75
+ this.bytes = fstatSync(this.fd).size;
76
+ }
77
+ /** Records written but not yet acknowledged, in FIFO order. */
78
+ pending() {
79
+ return this.pendingRecords.slice();
80
+ }
81
+ /** Count of records dropped by the drop-oldest cap policy so far. */
82
+ droppedCount() {
83
+ return this.droppedForCap;
84
+ }
85
+ currentSeq() {
86
+ return this.seq;
87
+ }
88
+ /**
89
+ * Append a record durably (single `writeSync`, no fsync). Returns its seq.
90
+ * Never compacts inline — that would put a multi-MB `writeFileSync`+`fsync` on
91
+ * the caller's `attest()` path. Crossing the cap only flags compaction, which
92
+ * `compactIfNeeded()` performs off the caller path (see the Attestor flush).
93
+ */
94
+ append(dar, payload) {
95
+ const seq = ++this.seq;
96
+ const record = payload ? { seq, dar, payload } : { seq, dar };
97
+ const line = JSON.stringify(record) + "\n";
98
+ const buf = Buffer.from(line, "utf8");
99
+ writeFully(this.fd, buf); // loop over short writes so a line is never torn
100
+ this.bytes += buf.byteLength;
101
+ this.pendingRecords.push(record);
102
+ if (this.bytes > this.maxBytes)
103
+ this.compactionPending = true;
104
+ return seq;
105
+ }
106
+ /** Whether the file has grown past the cap and awaits compaction. */
107
+ needsCompaction() {
108
+ return this.compactionPending;
109
+ }
110
+ /** Reclaim acked space and enforce the cap (drop-oldest). Off the caller path. */
111
+ compactIfNeeded() {
112
+ if (!this.compactionPending)
113
+ return;
114
+ this.compact();
115
+ this.compactionPending = false;
116
+ }
117
+ /**
118
+ * Acknowledge every record with seq <= `throughSeq` (they were delivered).
119
+ * Advances the durable watermark; truncates the file once fully drained.
120
+ */
121
+ ack(throughSeq) {
122
+ if (throughSeq <= this.ackedThrough)
123
+ return;
124
+ this.ackedThrough = throughSeq;
125
+ this.pendingRecords = this.pendingRecords.filter((r) => r.seq > throughSeq);
126
+ this.writeAck();
127
+ if (this.pendingRecords.length === 0) {
128
+ // Steady state: everything delivered. Reclaim the file entirely.
129
+ ftruncateSync(this.fd, 0);
130
+ this.bytes = 0;
131
+ }
132
+ }
133
+ /** Flush OS buffers to disk. Called off the caller path, before network I/O. */
134
+ fsync() {
135
+ fsyncSync(this.fd);
136
+ }
137
+ close() {
138
+ closeSync(this.fd);
139
+ }
140
+ // --- internals ---
141
+ recover() {
142
+ let raw;
143
+ try {
144
+ raw = readFileSync(this.path, "utf8");
145
+ }
146
+ catch {
147
+ return; // no spool file yet
148
+ }
149
+ let maxSeq = 0;
150
+ for (const line of raw.split("\n")) {
151
+ if (line.length === 0)
152
+ continue;
153
+ let record;
154
+ try {
155
+ record = JSON.parse(line);
156
+ }
157
+ catch {
158
+ continue; // tolerate a torn trailing line from a crash mid-write
159
+ }
160
+ if (typeof record.seq !== "number")
161
+ continue;
162
+ if (record.seq > maxSeq)
163
+ maxSeq = record.seq;
164
+ if (record.seq > this.ackedThrough)
165
+ this.pendingRecords.push(record);
166
+ }
167
+ this.pendingRecords.sort((a, b) => a.seq - b.seq);
168
+ this.seq = maxSeq;
169
+ }
170
+ compact() {
171
+ // Drop oldest pending until the surviving set fits under the cap.
172
+ const kept = this.pendingRecords.slice();
173
+ let size = kept.reduce((n, r) => n + Buffer.byteLength(JSON.stringify(r) + "\n"), 0);
174
+ let droppedNow = 0;
175
+ while (size > this.maxBytes && kept.length > 0) {
176
+ const dropped = kept.shift();
177
+ size -= Buffer.byteLength(JSON.stringify(dropped) + "\n");
178
+ this.droppedForCap++;
179
+ droppedNow++;
180
+ }
181
+ const tmp = `${this.path}.compact`;
182
+ const body = kept.map((r) => JSON.stringify(r) + "\n").join("");
183
+ writeFileDurable(tmp, body);
184
+ closeSync(this.fd);
185
+ renameSync(tmp, this.path);
186
+ fsyncDir(this.dir); // make the rename durable across a power loss
187
+ this.fd = openSync(this.path, "a");
188
+ this.bytes = fstatSync(this.fd).size;
189
+ this.pendingRecords = kept;
190
+ // Surface the drop-oldest data loss rather than discarding silently.
191
+ if (droppedNow > 0)
192
+ this.onDrop?.(droppedNow);
193
+ }
194
+ readAck() {
195
+ try {
196
+ const n = Number.parseInt(readFileSync(this.ackPath, "utf8").trim(), 10);
197
+ return Number.isFinite(n) && n >= 0 ? n : 0;
198
+ }
199
+ catch {
200
+ return 0;
201
+ }
202
+ }
203
+ writeAck() {
204
+ // fsync the watermark so a crash cannot resurrect already-acked records
205
+ // (bounding duplicate re-delivery on recovery).
206
+ writeFileDurable(this.ackPath, String(this.ackedThrough));
207
+ }
208
+ }
209
+ export { DEFAULT_MAX_BYTES };
210
+ //# sourceMappingURL=spool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spool.js","sourceRoot":"","sources":["../src/spool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Batch transport. One POST per flush to /v1/tiered-attest (tasks/P1.md).
3
+ * The SDK reads exactly one credential env var: RUBRIC_API_KEY (CLAUDE.md).
4
+ */
5
+ import { type DarCore, type PayloadRecord } from "./constants.js";
6
+ export interface Transport {
7
+ /**
8
+ * Deliver one batch. `records` are hashes-only DAR cores; `payloads` (raw
9
+ * content) is present only in `payload` mode and rides in the envelope, never
10
+ * in a core. Must reject on failure so the spool retains the batch.
11
+ */
12
+ send(records: DarCore[], payloads?: PayloadRecord[]): Promise<void>;
13
+ }
14
+ export interface HttpTransportOptions {
15
+ baseUrl: string;
16
+ /** Defaults to reading process.env.RUBRIC_API_KEY at send time. */
17
+ apiKey?: () => string | undefined;
18
+ fetchImpl?: typeof fetch;
19
+ /** Abort a POST after this many ms so a hung endpoint can't stall flushing. Default 30000. */
20
+ timeoutMs?: number;
21
+ /** Permit a non-HTTPS baseUrl (localhost is always allowed). Default false. */
22
+ allowInsecure?: boolean;
23
+ }
24
+ /** Default transport: one JSON POST of the batch to `${baseUrl}/v1/tiered-attest`. */
25
+ export declare class HttpTransport implements Transport {
26
+ private readonly baseUrl;
27
+ private readonly apiKey;
28
+ private readonly fetchImpl;
29
+ private readonly timeoutMs;
30
+ constructor(options: HttpTransportOptions);
31
+ send(records: DarCore[], payloads?: PayloadRecord[]): Promise<void>;
32
+ }
33
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAmC,KAAK,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEnG,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,QAAQ,CAAC,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAMD,sFAAsF;AACtF,qBAAa,aAAc,YAAW,SAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,OAAO,EAAE,oBAAoB;IAcnC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,QAAQ,CAAC,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAwB1E"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Batch transport. One POST per flush to /v1/tiered-attest (tasks/P1.md).
3
+ * The SDK reads exactly one credential env var: RUBRIC_API_KEY (CLAUDE.md).
4
+ */
5
+ import { API_KEY_ENV, TIERED_ATTEST_PATH } from "./constants.js";
6
+ function isLocalhost(u) {
7
+ return u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1";
8
+ }
9
+ /** Default transport: one JSON POST of the batch to `${baseUrl}/v1/tiered-attest`. */
10
+ export class HttpTransport {
11
+ baseUrl;
12
+ apiKey;
13
+ fetchImpl;
14
+ timeoutMs;
15
+ constructor(options) {
16
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
17
+ // Refuse to send the bearer credential over cleartext unless explicitly allowed.
18
+ const parsed = new URL(this.baseUrl);
19
+ if (parsed.protocol !== "https:" && !isLocalhost(parsed) && !options.allowInsecure) {
20
+ throw new Error(`HttpTransport: refusing non-HTTPS baseUrl '${this.baseUrl}' (set allowInsecure to override)`);
21
+ }
22
+ this.apiKey = options.apiKey ?? (() => process.env[API_KEY_ENV]);
23
+ this.fetchImpl = options.fetchImpl ?? fetch;
24
+ this.timeoutMs = options.timeoutMs ?? 30_000;
25
+ }
26
+ async send(records, payloads) {
27
+ const key = this.apiKey();
28
+ if (!key)
29
+ throw new Error(`${API_KEY_ENV} is not set`);
30
+ const body = payloads && payloads.length > 0 ? { records, payloads } : { records };
31
+ const controller = new AbortController();
32
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
33
+ try {
34
+ const res = await this.fetchImpl(`${this.baseUrl}${TIERED_ATTEST_PATH}`, {
35
+ method: "POST",
36
+ headers: {
37
+ "content-type": "application/json",
38
+ authorization: `Bearer ${key}`,
39
+ },
40
+ body: JSON.stringify(body),
41
+ signal: controller.signal,
42
+ });
43
+ if (!res.ok) {
44
+ throw new Error(`tiered-attest POST failed: ${res.status} ${res.statusText}`);
45
+ }
46
+ }
47
+ finally {
48
+ clearTimeout(timer);
49
+ }
50
+ }
51
+ }
52
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAoC,MAAM,gBAAgB,CAAC;AAsBnG,SAAS,WAAW,CAAC,CAAM;IACzB,OAAO,CAAC,CAAC,QAAQ,KAAK,WAAW,IAAI,CAAC,CAAC,QAAQ,KAAK,WAAW,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC;AAC1F,CAAC;AAED,sFAAsF;AACtF,MAAM,OAAO,aAAa;IACP,OAAO,CAAS;IAChB,MAAM,CAA2B;IACjC,SAAS,CAAe;IACxB,SAAS,CAAS;IAEnC,YAAY,OAA6B;QACvC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACnD,iFAAiF;QACjF,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;YACnF,MAAM,IAAI,KAAK,CACb,8CAA8C,IAAI,CAAC,OAAO,mCAAmC,CAC9F,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;QACjE,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;QAC5C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAkB,EAAE,QAA0B;QACvD,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,WAAW,aAAa,CAAC,CAAC;QAEvD,MAAM,IAAI,GAAG,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;QACnF,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACnE,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,kBAAkB,EAAE,EAAE;gBACvE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,aAAa,EAAE,UAAU,GAAG,EAAE;iBAC/B;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAC1B,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;YAChF,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;CACF"}
package/dist/ulid.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Create a monotonic ULID generator. `now` returns epoch milliseconds
3
+ * (injectable for deterministic tests). If the clock does not advance (or goes
4
+ * backwards) the randomness is incremented to keep ids strictly increasing.
5
+ */
6
+ export declare function monotonicUlidFactory(now?: () => number): () => string;
7
+ /** Default process-wide monotonic ULID generator. */
8
+ export declare const ulid: () => string;
9
+ //# sourceMappingURL=ulid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ulid.d.ts","sourceRoot":"","sources":["../src/ulid.ts"],"names":[],"mappings":"AAoDA;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,MAAM,MAAiB,GAAG,MAAM,MAAM,CAc/E;AAED,qDAAqD;AACrD,eAAO,MAAM,IAAI,QAjByD,MAiBhC,CAAC"}
package/dist/ulid.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ULID (https://github.com/ulid/spec): 48-bit millisecond timestamp + 80 bits
3
+ * of randomness, Crockford base32, 26 chars. Used for `decisionId` (spec §2).
4
+ *
5
+ * A monotonic factory guarantees strictly increasing ids even within the same
6
+ * millisecond (randomness is incremented), so per-agent `prev` chains built from
7
+ * mint order are well-defined.
8
+ */
9
+ import { randomFillSync } from "node:crypto";
10
+ const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford base32 (no I,L,O,U)
11
+ const TIME_LEN = 10;
12
+ const RAND_LEN = 16;
13
+ function encodeTime(ms) {
14
+ if (!Number.isInteger(ms) || ms < 0 || ms > 0xffffffffffff) {
15
+ throw new Error(`ulid: time ${ms} out of 48-bit range`);
16
+ }
17
+ let out = "";
18
+ let t = ms;
19
+ for (let i = 0; i < TIME_LEN; i++) {
20
+ out = ENCODING[t % 32] + out;
21
+ t = Math.floor(t / 32);
22
+ }
23
+ return out;
24
+ }
25
+ function randomDigits() {
26
+ // Each byte's low 5 bits are uniform over 0..31 (256 is a multiple of 32).
27
+ const bytes = randomFillSync(new Uint8Array(RAND_LEN));
28
+ return Array.from(bytes, (b) => b & 31);
29
+ }
30
+ function incrementDigits(digits) {
31
+ const next = digits.slice();
32
+ for (let i = RAND_LEN - 1; i >= 0; i--) {
33
+ if (next[i] < 31) {
34
+ next[i]++;
35
+ return next;
36
+ }
37
+ next[i] = 0;
38
+ }
39
+ // Overflow within one millisecond (2^80 ids) — astronomically unlikely.
40
+ return randomDigits();
41
+ }
42
+ function encodeDigits(digits) {
43
+ let out = "";
44
+ for (const d of digits)
45
+ out += ENCODING[d];
46
+ return out;
47
+ }
48
+ /**
49
+ * Create a monotonic ULID generator. `now` returns epoch milliseconds
50
+ * (injectable for deterministic tests). If the clock does not advance (or goes
51
+ * backwards) the randomness is incremented to keep ids strictly increasing.
52
+ */
53
+ export function monotonicUlidFactory(now = Date.now) {
54
+ let lastTime = -1;
55
+ let lastRand = [];
56
+ return function ulid() {
57
+ let t = Math.trunc(now());
58
+ if (t <= lastTime) {
59
+ t = lastTime;
60
+ lastRand = incrementDigits(lastRand);
61
+ }
62
+ else {
63
+ lastTime = t;
64
+ lastRand = randomDigits();
65
+ }
66
+ return encodeTime(t) + encodeDigits(lastRand);
67
+ };
68
+ }
69
+ /** Default process-wide monotonic ULID generator. */
70
+ export const ulid = monotonicUlidFactory();
71
+ //# sourceMappingURL=ulid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ulid.js","sourceRoot":"","sources":["../src/ulid.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,QAAQ,GAAG,kCAAkC,CAAC,CAAC,gCAAgC;AACrF,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,QAAQ,GAAG,EAAE,CAAC;AAEpB,SAAS,UAAU,CAAC,EAAU;IAC5B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,cAAc,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,cAAc,EAAE,sBAAsB,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,CAAC,GAAG,EAAE,CAAC;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,GAAG,GAAG,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC;QAC7B,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,YAAY;IACnB,2EAA2E;IAC3E,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IACvD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,eAAe,CAAC,MAAgB;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,QAAQ,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,CAAC,CAAE,GAAG,EAAE,EAAE,CAAC;YAClB,IAAI,CAAC,CAAC,CAAE,EAAE,CAAC;YACX,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACd,CAAC;IACD,wEAAwE;IACxE,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED,SAAS,YAAY,CAAC,MAAgB;IACpC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,GAAG,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3C,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAoB,IAAI,CAAC,GAAG;IAC/D,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC;IAClB,IAAI,QAAQ,GAAa,EAAE,CAAC;IAC5B,OAAO,SAAS,IAAI;QAClB,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;YAClB,CAAC,GAAG,QAAQ,CAAC;YACb,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QACvC,CAAC;aAAM,CAAC;YACN,QAAQ,GAAG,CAAC,CAAC;YACb,QAAQ,GAAG,YAAY,EAAE,CAAC;QAC5B,CAAC;QACD,OAAO,UAAU,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,MAAM,CAAC,MAAM,IAAI,GAAG,oBAAoB,EAAE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@rubric-protocol/attest-decision",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Rubric core SDK: DAR/0.1 builder, JCS+SHA3-256, fire-and-forget attest()",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/0xsims/rubric-attest.git",
10
+ "directory": "packages/attest-decision"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "development": "./src/index.ts",
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": ["dist", "src"],
22
+ "publishConfig": { "access": "public" },
23
+ "scripts": {
24
+ "prepublishOnly": "npm run build",
25
+ "build": "tsc -p tsconfig.build.json",
26
+ "test": "vitest run"
27
+ }
28
+ }