@openlfcp/wire 0.1.0-rc.1

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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +27 -0
  3. package/dist/capability.d.ts +143 -0
  4. package/dist/capability.js +409 -0
  5. package/dist/cbor/decode.d.ts +23 -0
  6. package/dist/cbor/decode.js +163 -0
  7. package/dist/cbor/encode.d.ts +10 -0
  8. package/dist/cbor/encode.js +149 -0
  9. package/dist/cbor/index.d.ts +10 -0
  10. package/dist/cbor/index.js +10 -0
  11. package/dist/cbor/text.d.ts +11 -0
  12. package/dist/cbor/text.js +5 -0
  13. package/dist/cbor/value.d.ts +30 -0
  14. package/dist/cbor/value.js +23 -0
  15. package/dist/chain.d.ts +140 -0
  16. package/dist/chain.js +339 -0
  17. package/dist/control-sync.d.ts +69 -0
  18. package/dist/control-sync.js +44 -0
  19. package/dist/control.d.ts +173 -0
  20. package/dist/control.js +369 -0
  21. package/dist/cose.d.ts +81 -0
  22. package/dist/cose.js +133 -0
  23. package/dist/data-unit.d.ts +204 -0
  24. package/dist/data-unit.js +314 -0
  25. package/dist/endpoint.d.ts +51 -0
  26. package/dist/endpoint.js +116 -0
  27. package/dist/epoch.d.ts +109 -0
  28. package/dist/epoch.js +128 -0
  29. package/dist/fields.d.ts +18 -0
  30. package/dist/fields.js +78 -0
  31. package/dist/handshake.d.ts +175 -0
  32. package/dist/handshake.js +297 -0
  33. package/dist/have.d.ts +101 -0
  34. package/dist/have.js +268 -0
  35. package/dist/index.d.ts +21 -0
  36. package/dist/index.js +21 -0
  37. package/dist/invite.d.ts +80 -0
  38. package/dist/invite.js +247 -0
  39. package/dist/key-package.d.ts +92 -0
  40. package/dist/key-package.js +132 -0
  41. package/dist/message.d.ts +367 -0
  42. package/dist/message.js +690 -0
  43. package/dist/objects.d.ts +154 -0
  44. package/dist/objects.js +156 -0
  45. package/dist/principal.d.ts +43 -0
  46. package/dist/principal.js +81 -0
  47. package/dist/session-state.d.ts +47 -0
  48. package/dist/session-state.js +49 -0
  49. package/dist/snapshot.d.ts +86 -0
  50. package/dist/snapshot.js +189 -0
  51. package/dist/transition.d.ts +97 -0
  52. package/dist/transition.js +130 -0
  53. package/package.json +53 -0
@@ -0,0 +1,149 @@
1
+ import { LfcpError } from "@openlfcp/core";
2
+ import { strictUtf8Decoder, utf8Encoder } from "./text.js";
3
+ import { isCborKey, isCborMap, MAX_DEPTH, UINT64_MAX } from "./value.js";
4
+ /** Byte order of encoded map keys (§5.2 rule 4): shorter encodings first, then bytewise. */
5
+ export function compareEncodedKeys(a, b) {
6
+ if (a.length !== b.length)
7
+ return a.length - b.length;
8
+ for (let i = 0; i < a.length; i += 1) {
9
+ const d = a[i] - b[i];
10
+ if (d !== 0)
11
+ return d;
12
+ }
13
+ return 0;
14
+ }
15
+ class Writer {
16
+ buf = new Uint8Array(256);
17
+ len = 0;
18
+ reserve(n) {
19
+ if (this.len + n <= this.buf.length)
20
+ return;
21
+ let size = this.buf.length * 2;
22
+ while (size < this.len + n)
23
+ size *= 2;
24
+ const next = new Uint8Array(size);
25
+ next.set(this.buf.subarray(0, this.len));
26
+ this.buf = next;
27
+ }
28
+ byte(b) {
29
+ this.reserve(1);
30
+ this.buf[this.len++] = b;
31
+ }
32
+ bytes(b) {
33
+ this.reserve(b.length);
34
+ this.buf.set(b, this.len);
35
+ this.len += b.length;
36
+ }
37
+ /** Major type and argument in the shortest form (§5.2 rule 1). */
38
+ head(major, n) {
39
+ const m = major << 5;
40
+ const v = BigInt(n);
41
+ if (v < 24n) {
42
+ this.byte(m | Number(v));
43
+ }
44
+ else if (v <= 0xffn) {
45
+ this.byte(m | 24);
46
+ this.byte(Number(v));
47
+ }
48
+ else if (v <= 0xffffn) {
49
+ this.byte(m | 25);
50
+ this.bigEndian(v, 2);
51
+ }
52
+ else if (v <= 0xffffffffn) {
53
+ this.byte(m | 26);
54
+ this.bigEndian(v, 4);
55
+ }
56
+ else {
57
+ this.byte(m | 27);
58
+ this.bigEndian(v, 8);
59
+ }
60
+ }
61
+ bigEndian(v, size) {
62
+ for (let i = size - 1; i >= 0; i -= 1)
63
+ this.byte(Number((v >> BigInt(8 * i)) & 0xffn));
64
+ }
65
+ result() {
66
+ return this.buf.slice(0, this.len);
67
+ }
68
+ }
69
+ function writeInteger(w, value) {
70
+ if (typeof value === "number" && !Number.isSafeInteger(value)) {
71
+ throw new LfcpError(Number.isInteger(value) ? "CBOR_OUT_OF_RANGE" : "CBOR_UNSUPPORTED_TYPE", Number.isInteger(value)
72
+ ? "integers beyond the safe range must be bigint"
73
+ : "floating-point numbers are not used by LFCP");
74
+ }
75
+ const v = BigInt(value);
76
+ if (v > UINT64_MAX || v < -(UINT64_MAX + 1n))
77
+ throw new LfcpError("CBOR_OUT_OF_RANGE", "integer outside -2^64 .. 2^64-1");
78
+ if (v >= 0n)
79
+ w.head(0, v);
80
+ else
81
+ w.head(1, -1n - v);
82
+ }
83
+ function writeValue(w, value, depth) {
84
+ if (depth > MAX_DEPTH)
85
+ throw new LfcpError("CBOR_TOO_DEEP", `nesting deeper than ${MAX_DEPTH}`);
86
+ if (value === null) {
87
+ w.byte(0xf6);
88
+ return;
89
+ }
90
+ if (typeof value === "boolean") {
91
+ w.byte(value ? 0xf5 : 0xf4);
92
+ return;
93
+ }
94
+ if (typeof value === "number" || typeof value === "bigint") {
95
+ writeInteger(w, value);
96
+ return;
97
+ }
98
+ if (value instanceof Uint8Array) {
99
+ w.head(2, value.length);
100
+ w.bytes(value);
101
+ return;
102
+ }
103
+ if (typeof value === "string") {
104
+ const raw = utf8Encoder.encode(value);
105
+ // TextEncoder replaces lone surrogates; refuse instead of changing the text.
106
+ if (strictUtf8Decoder.decode(raw) !== value)
107
+ throw new LfcpError("CBOR_INVALID_UTF8", "string contains lone surrogates");
108
+ w.head(3, raw.length);
109
+ w.bytes(raw);
110
+ return;
111
+ }
112
+ if (Array.isArray(value)) {
113
+ w.head(4, value.length);
114
+ for (const item of value)
115
+ writeValue(w, item, depth + 1);
116
+ return;
117
+ }
118
+ if (isCborMap(value)) {
119
+ const encoded = value.entries.map(([k, v]) => {
120
+ if (!isCborKey(k))
121
+ throw new LfcpError("CBOR_UNSUPPORTED_TYPE", "map keys must be integers, text or byte strings");
122
+ return [encode(k), v];
123
+ });
124
+ encoded.sort((a, b) => compareEncodedKeys(a[0], b[0]));
125
+ for (let i = 1; i < encoded.length; i += 1) {
126
+ if (compareEncodedKeys(encoded[i - 1][0], encoded[i][0]) === 0) {
127
+ throw new LfcpError("CBOR_DUPLICATE_KEY", "map contains a duplicate key");
128
+ }
129
+ }
130
+ w.head(5, encoded.length);
131
+ for (const [k, v] of encoded) {
132
+ w.bytes(k);
133
+ writeValue(w, v, depth + 1);
134
+ }
135
+ return;
136
+ }
137
+ throw new LfcpError("CBOR_UNSUPPORTED_TYPE", `cannot encode ${typeof value} as LFCP CBOR`);
138
+ }
139
+ /**
140
+ * Deterministic CBOR encoding (LFCP-WIRE-01 §5.2): shortest heads,
141
+ * definite lengths, map keys sorted by encoded length then bytewise,
142
+ * duplicate keys rejected, array order kept, no tags.
143
+ */
144
+ export function encode(value) {
145
+ const w = new Writer();
146
+ writeValue(w, value, 0);
147
+ return w.result();
148
+ }
149
+ //# sourceMappingURL=encode.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Low-level deterministic CBOR for LFCP-WIRE-01 §5.2.
3
+ *
4
+ * This is a building block for COSE, Wire structures and vectors; it is not
5
+ * the LFCP API. Import it from `@openlfcp/wire/cbor`.
6
+ */
7
+ export { decodeDeterministic, decodeStrict, isDeterministic } from "./decode.js";
8
+ export { compareEncodedKeys, encode } from "./encode.js";
9
+ export { type CborKey, type CborMap, type CborValue, cborMap, isCborMap, MAX_DEPTH, } from "./value.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Low-level deterministic CBOR for LFCP-WIRE-01 §5.2.
3
+ *
4
+ * This is a building block for COSE, Wire structures and vectors; it is not
5
+ * the LFCP API. Import it from `@openlfcp/wire/cbor`.
6
+ */
7
+ export { decodeDeterministic, decodeStrict, isDeterministic } from "./decode.js";
8
+ export { compareEncodedKeys, encode } from "./encode.js";
9
+ export { cborMap, isCborMap, MAX_DEPTH, } from "./value.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ interface Utf8Encoder {
2
+ encode(text: string): Uint8Array;
3
+ }
4
+ interface Utf8Decoder {
5
+ decode(bytes: Uint8Array): string;
6
+ }
7
+ export declare const utf8Encoder: Utf8Encoder;
8
+ /** Throws on malformed UTF-8 instead of inserting U+FFFD. */
9
+ export declare const strictUtf8Decoder: Utf8Decoder;
10
+ export {};
11
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1,5 @@
1
+ const g = globalThis;
2
+ export const utf8Encoder = new g.TextEncoder();
3
+ /** Throws on malformed UTF-8 instead of inserting U+FFFD. */
4
+ export const strictUtf8Decoder = new g.TextDecoder("utf-8", { fatal: true });
5
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The CBOR data model LFCP uses (LFCP-WIRE-01 §5). Everything else is
3
+ * unrepresentable here and rejected by the decoder:
4
+ *
5
+ * - integers (major types 0 and 1, §5.1): `number` when it is a safe
6
+ * integer, `bigint` otherwise, in -2^64 .. 2^64-1;
7
+ * - byte strings: `Uint8Array`; text strings: `string` (well-formed UTF-8);
8
+ * - arrays: `CborValue[]`, order preserved (§5.2 rule 5);
9
+ * - maps: `CborMap`, an explicit entry list, never a JS object (§5.2 rule 4);
10
+ * - `true`, `false`, `null`.
11
+ *
12
+ * No tags (§5.2 rule 6: LFCP requires none in deterministic structures), no
13
+ * floating-point numbers, no `undefined`, no other simple values.
14
+ */
15
+ export type CborValue = CborKey | boolean | null | readonly CborValue[] | CborMap;
16
+ /** Map keys: integers, text strings and byte strings. */
17
+ export type CborKey = number | bigint | string | Uint8Array;
18
+ /** A CBOR map. Entry order is irrelevant on input; encoding sorts the keys. */
19
+ export interface CborMap {
20
+ readonly kind: "cbor-map";
21
+ readonly entries: readonly (readonly [CborKey, CborValue])[];
22
+ }
23
+ export declare function isCborMap(value: unknown): value is CborMap;
24
+ export declare function isCborKey(value: unknown): value is CborKey;
25
+ /** Builds a map from `[key, value]` pairs. Duplicate keys are rejected when encoding. */
26
+ export declare function cborMap(entries: Iterable<readonly [CborKey, CborValue]>): CborMap;
27
+ /** Largest nesting depth accepted when encoding or decoding; an SDK safety limit, not a protocol rule. */
28
+ export declare const MAX_DEPTH = 128;
29
+ export declare const UINT64_MAX: bigint;
30
+ //# sourceMappingURL=value.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { LfcpError } from "@openlfcp/core";
2
+ export function isCborMap(value) {
3
+ return (typeof value === "object" && value !== null && value.kind === "cbor-map");
4
+ }
5
+ export function isCborKey(value) {
6
+ return (typeof value === "number" ||
7
+ typeof value === "bigint" ||
8
+ typeof value === "string" ||
9
+ value instanceof Uint8Array);
10
+ }
11
+ /** Builds a map from `[key, value]` pairs. Duplicate keys are rejected when encoding. */
12
+ export function cborMap(entries) {
13
+ const list = [...entries].map(([k, v]) => {
14
+ if (!isCborKey(k))
15
+ throw new LfcpError("CBOR_UNSUPPORTED_TYPE", "map keys must be integers, text or byte strings");
16
+ return [k, v];
17
+ });
18
+ return Object.freeze({ kind: "cbor-map", entries: Object.freeze(list) });
19
+ }
20
+ /** Largest nesting depth accepted when encoding or decoding; an SDK safety limit, not a protocol rule. */
21
+ export const MAX_DEPTH = 128;
22
+ export const UINT64_MAX = (1n << 64n) - 1n;
23
+ //# sourceMappingURL=value.js.map
@@ -0,0 +1,140 @@
1
+ import { type ControlRecordId, type DataEpoch, type Hash32, LfcpError, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ import { type Authorization, type Grant } from "./capability.js";
3
+ import { type ControlRecord } from "./control.js";
4
+ import type { Endpoint } from "./endpoint.js";
5
+ import type { ActorHave } from "./have.js";
6
+ import type { PrincipalDescriptor } from "./principal.js";
7
+ /**
8
+ * Control Chain validation (LFCP-WIRE-01 §13, §13.1, §13.2, §15).
9
+ *
10
+ * A pure function over exact signed Control Records: no I/O, clock or
11
+ * randomness. The records may arrive in any order; the chain is ordered by
12
+ * its own links (prev_control_id from Genesis), so every permutation of a
13
+ * set gives the same result. Diagnostics are sorted by sequence and then
14
+ * record ID only to make them reproducible; nothing ever chooses a branch.
15
+ *
16
+ * A fork (two validly signed records naming the same previous record) is a
17
+ * conflict, never resolved by time, ID order, arrival order or server
18
+ * preference (§13.2). A chain containing a Coordinator Recovery (7) or
19
+ * Resource Tombstone (8) record is refused with PROTOCOL_UNSUPPORTED
20
+ * (MVP-0.1-PROTOCOL-SCOPE §4, DV1). Extension records (32 and above) need
21
+ * owner authority (§14) and stay in the chain, since the links run through
22
+ * them, but are not applied to the derived state; they are listed in
23
+ * `unappliedRecords`.
24
+ */
25
+ /** The initial and current route (§15, §20). */
26
+ export interface ControlRoute {
27
+ readonly endpoints: readonly Endpoint[];
28
+ readonly coordinatorUrl: string;
29
+ }
30
+ /** A Data Epoch and its DEK commitment (§11, §15, §19). */
31
+ export interface ControlEpoch {
32
+ readonly epoch: DataEpoch;
33
+ readonly dekCommitment: Hash32;
34
+ }
35
+ /**
36
+ * One Data Epoch in the chain's history (§11, §15, §19): opened by Genesis
37
+ * (epoch 0) or a Key Epoch record, and closed by the next Key Epoch record,
38
+ * which records its final frontier (§19.1). Kept for every epoch, so units
39
+ * of any older epoch stay evaluable.
40
+ */
41
+ export interface EpochHistory extends ControlEpoch {
42
+ readonly openedBy: ControlRecordId;
43
+ /** The Key Epoch record that closed this epoch, or null while it is current. */
44
+ readonly closedBy: ControlRecordId | null;
45
+ /** The accepted final frontier of this epoch (canonical, G-CP1), or null while it is current. */
46
+ readonly finalFrontier: readonly ActorHave[] | null;
47
+ }
48
+ /**
49
+ * The validated state of one Resource's Control Chain. This is the single
50
+ * Control state type: LFCP-021 (capabilities) and LFCP-023 (epochs and
51
+ * cutoff) extend it here and in `applyRecord`, never with a parallel type.
52
+ *
53
+ * LFCP-020 fills the chain position and what Genesis establishes;
54
+ * LFCP-021 adds the grants and applies route updates; LFCP-023 applies
55
+ * Key Epoch rotation (`epoch`, `epochs`).
56
+ */
57
+ export interface ControlState {
58
+ readonly resourceId: ResourceId;
59
+ readonly genesisId: ControlRecordId;
60
+ /** The current Control Head: the record ID of the last validated record. */
61
+ readonly head: ControlRecordId;
62
+ readonly seq: bigint;
63
+ readonly dataProfile: string;
64
+ /** The owner: the Genesis owner, or the new owner after a verified transfer (§15, §23.3). */
65
+ readonly owner: PrincipalDescriptor;
66
+ readonly route: ControlRoute;
67
+ /** §20 route version of `route`; Genesis implies route version 0 (§15). */
68
+ readonly routeVersion: bigint;
69
+ /** The current Data Epoch. */
70
+ readonly epoch: ControlEpoch;
71
+ /** Every Data Epoch so far, by epoch number (decimal string) (§19, LFCP-023). */
72
+ readonly epochs: ReadonlyMap<string, EpochHistory>;
73
+ /** Grants created through this head (§17.2, §18.1), by grant ID hex; see capability.ts. */
74
+ readonly grants: ReadonlyMap<string, Grant>;
75
+ /**
76
+ * Principal Descriptors carried by applied records (Genesis owner, grant
77
+ * subjects, claimants), by Principal ID hex. Descriptors are
78
+ * self-certifying (§7), so knowing one grants nothing; it only lets later
79
+ * records by that Principal be verified.
80
+ */
81
+ readonly principals: ReadonlyMap<string, PrincipalDescriptor>;
82
+ }
83
+ /** Why a chain is invalid. */
84
+ export type ChainProblem = "MALFORMED" | "UNSUPPORTED_TYPE" | "NO_GENESIS" | "GENESIS_SIGNER" | "RESOURCE" | "SEQUENCE" | "PREVIOUS" | "SIGNATURE" | "UNRESOLVED_ISSUER" | "EPOCH" | "UNAUTHORIZED" | "DEFERRED_TYPE";
85
+ export type ChainResult = {
86
+ readonly kind: "linear";
87
+ readonly state: ControlState;
88
+ /** The validated records in chain order (from the start, Genesis included when no start was given). */
89
+ readonly records: readonly ControlRecord[];
90
+ /** Extension records kept in the chain but not applied (mvpSupported = false), in chain order. */
91
+ readonly unappliedRecords: readonly ControlRecordId[];
92
+ /**
93
+ * The validated state at any head of this chain (from the start, or
94
+ * Genesis), for evaluating authority at an older Control Head. States
95
+ * are kept by immutable head ID; undefined for a head not on the chain.
96
+ */
97
+ readonly stateAt: (head: Uint8Array) => ControlState | undefined;
98
+ } | {
99
+ readonly kind: "conflict";
100
+ readonly wireCode: "CONTROL_CONFLICT";
101
+ /** The record both branches name as previous; null when two Genesis records compete. */
102
+ readonly commonHead: ControlRecordId | null;
103
+ /** The contested sequence. */
104
+ readonly seq: bigint;
105
+ /** The competing record IDs, sorted by bytes for reproducible output (not a ranking). */
106
+ readonly competing: readonly ControlRecordId[];
107
+ /** The validated common prefix up to commonHead (null for a Genesis conflict); no branch is applied. */
108
+ readonly prefixState: ControlState | null;
109
+ } | {
110
+ readonly kind: "invalid";
111
+ readonly problem: ChainProblem;
112
+ readonly wireCode: string;
113
+ /** Position in the input array of the offending record (null when no record is at fault). */
114
+ readonly index: number | null;
115
+ readonly recordId: ControlRecordId | null;
116
+ readonly error: LfcpError;
117
+ };
118
+ export interface ChainOptions {
119
+ /** Continue after an already validated head instead of starting from Genesis. */
120
+ readonly start?: ControlState;
121
+ /**
122
+ * Resolves an issuer that no earlier record in the chain describes.
123
+ * The returned descriptor is checked against the ID like any other.
124
+ */
125
+ readonly resolvePrincipal?: (id: PrincipalId) => PrincipalDescriptor | undefined;
126
+ /**
127
+ * Authorization, called for every non-Genesis record with the state
128
+ * before it (the previous head). Defaults to authorizeControlRecord, the
129
+ * LFCP-021 capability engine; a caller may substitute a policy (tests).
130
+ * A refusal makes the chain invalid (UNAUTHORIZED).
131
+ */
132
+ readonly authorize?: (record: ControlRecord, state: ControlState) => boolean | Authorization;
133
+ }
134
+ /**
135
+ * Validates a set of exact signed Control Records of one Resource, from
136
+ * Genesis or after `options.start`. Input objects that are already decoded
137
+ * are re-decoded from their exact bytes, never trusted as decoded.
138
+ */
139
+ export declare function validateControlChain(inputs: readonly (Uint8Array | ControlRecord)[], options?: ChainOptions): ChainResult;
140
+ //# sourceMappingURL=chain.d.ts.map