@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,154 @@
1
+ import { type ActorSequence, type ControlRecordId, type DataEpoch, type DataUnitId, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ import { type CborValue } from "./cbor/index.js";
3
+ import { type SignedObject } from "./cose.js";
4
+ import { type ActorHave } from "./have.js";
5
+ /**
6
+ * Typed payloads of the persistent LFCP signed objects (LFCP-WIRE-01 §13,
7
+ * §25, §26, §29), on top of the canonical COSE_Sign1 layer in cose.ts.
8
+ *
9
+ * `parseX(bytes)` returns `{ signed, payload }`: `signed` is the
10
+ * `parseSignedObject` result and `payload` is decoded from
11
+ * `signed.payloadBytes`. The decoders check structure only: the field set,
12
+ * field types and byte lengths, and the structural rules named on each
13
+ * payload. Signer authority, chain linkage, epochs, cutoff frontiers, AEAD
14
+ * and HPKE belong to LFCP-020 to LFCP-025.
15
+ *
16
+ * Exact bytes: hashing and signature verification always use the received
17
+ * bytes. The object ID is SHA-256 of `signed.bytes` and the signature is
18
+ * checked over `signed.protectedBytes` and `signed.payloadBytes`, exactly as
19
+ * received. The N7 rule (§5.2, §10.3: decode, re-encode and compare) is only a
20
+ * rejection test. Its re-encoding is compared and then thrown away; it never
21
+ * becomes an object's bytes, and no ID or signature input is ever rebuilt
22
+ * from a parsed payload.
23
+ *
24
+ * Errors and their wire mapping (ADR 0001):
25
+ * - COSE_MALFORMED, CBOR_* and INVALID_STRUCTURE → MALFORMED_MESSAGE;
26
+ * - UNSUPPORTED_VALUE (reserved core Control Record type) →
27
+ * INVALID_CONTROL_CHAIN (§14);
28
+ * - INVALID_CONTROL_CHAIN (a Genesis not at control_seq 0 with a null
29
+ * link, §13.1) → INVALID_CONTROL_CHAIN;
30
+ * - a failed `verifySignedObject` against the expected signer → INVALID_SIGNATURE.
31
+ */
32
+ /** A parsed signed object: the exact received object and its typed payload. */
33
+ export interface Parsed<P> {
34
+ readonly signed: SignedObject;
35
+ readonly payload: P;
36
+ }
37
+ /** §14 Control Record types defined by LFCP-WIRE-01. 9–31 are reserved; 32 and up are extensions. */
38
+ export declare const CONTROL_TYPE: Readonly<{
39
+ GENESIS: 0n;
40
+ CAPABILITY_GRANT: 1n;
41
+ CAPABILITY_REVOKE: 2n;
42
+ CAPABILITY_CLAIM: 3n;
43
+ KEY_EPOCH: 4n;
44
+ ROUTE_UPDATE: 5n;
45
+ OWNER_TRANSFER_COMMIT: 6n;
46
+ COORDINATOR_RECOVERY: 7n;
47
+ RESOURCE_TOMBSTONE: 8n;
48
+ }>;
49
+ /**
50
+ * `control-record-payload` (§13). The body is the decoded `any` value. It is
51
+ * deterministic CBOR (checked with the payload) but not typed here; typed
52
+ * bodies are LFCP-019.
53
+ */
54
+ export interface ControlRecordPayload {
55
+ readonly kind: "control-record";
56
+ readonly resourceId: ResourceId;
57
+ readonly controlSeq: bigint;
58
+ readonly prevControlId: ControlRecordId | null;
59
+ readonly controlType: bigint;
60
+ /** True for extension types (32 and up), which §14 lets a receiver keep without interpreting. */
61
+ readonly extension: boolean;
62
+ /**
63
+ * The payload's issuer field. Whether the issuer may issue this record
64
+ * (and who must have signed it) is decided by LFCP-019 to LFCP-021.
65
+ */
66
+ readonly issuer: PrincipalId;
67
+ readonly body: CborValue;
68
+ }
69
+ /** `data-unit-payload` (§26). */
70
+ export interface DataUnitPayload {
71
+ readonly kind: "data-unit";
72
+ readonly resourceId: ResourceId;
73
+ readonly dataEpoch: DataEpoch;
74
+ readonly actor: PrincipalId;
75
+ readonly actorSeq: ActorSequence;
76
+ readonly prevDataUnitId: DataUnitId | null;
77
+ readonly controlHead: ControlRecordId;
78
+ readonly ciphertext: Uint8Array;
79
+ }
80
+ /** `key-package-payload` (§25). */
81
+ export interface KeyPackagePayload {
82
+ readonly kind: "key-package";
83
+ readonly resourceId: ResourceId;
84
+ readonly dataEpoch: DataEpoch;
85
+ readonly recipient: PrincipalId;
86
+ readonly controlHead: ControlRecordId;
87
+ readonly sender: PrincipalId;
88
+ readonly hpkeEnc: Uint8Array;
89
+ readonly hpkeCiphertext: Uint8Array;
90
+ }
91
+ /** `snapshot-payload` (§29). */
92
+ export interface SnapshotPayload {
93
+ readonly kind: "snapshot";
94
+ readonly resourceId: ResourceId;
95
+ readonly dataEpoch: DataEpoch;
96
+ readonly publisher: PrincipalId;
97
+ readonly snapshotSeq: bigint;
98
+ readonly controlHead: ControlRecordId;
99
+ readonly frontier: readonly ActorHave[];
100
+ readonly ciphertext: Uint8Array;
101
+ }
102
+ /**
103
+ * Structural rules: the §13 field set, a §14 type that is not reserved
104
+ * (UNSUPPORTED_VALUE otherwise), and a Genesis record (type 0) at
105
+ * control_seq 0 with a null link (§13.1: INVALID_CONTROL_CHAIN otherwise).
106
+ * Sequence continuity and prev_control_id linkage are chain validation
107
+ * (LFCP-020).
108
+ */
109
+ export declare function controlRecordPayloadFromCbor(value: CborValue): ControlRecordPayload;
110
+ /**
111
+ * Structural rules: the §26 field set and actor sequence >= 1 (§8, N4).
112
+ * A non-null previous unit at sequence 1 is not rejected here: §26.2 asks
113
+ * for it to be reported to the sync engine, and the actor_seq1_prev_not_null_D1
114
+ * vector's disposition is "report", not reject (LFCP-025, LFCP-028).
115
+ */
116
+ export declare function dataUnitPayloadFromCbor(value: CborValue): DataUnitPayload;
117
+ /**
118
+ * Structural rules: the §25 field set, with `5 => bstr .size 32` (HPKE
119
+ * enc) and `6 => bstr .size 48` (the 32-byte DEK and the 16-byte tag).
120
+ * HPKE and authority checks are key-package.ts.
121
+ */
122
+ export declare function keyPackagePayloadFromCbor(value: CborValue): KeyPackagePayload;
123
+ /**
124
+ * Structural rules: the §29 field set, a Snapshot Sequence of at least 1
125
+ * (§29: "Snapshot Sequences begin at 1"; §29 names no code, so this is
126
+ * INVALID_STRUCTURE like actor sequence 0) and a canonical frontier
127
+ * (§28.1, §28.2, N6).
128
+ */
129
+ export declare function snapshotPayloadFromCbor(value: CborValue): SnapshotPayload;
130
+ /** Decodes standalone payload bytes; they must be deterministic CBOR (N7). */
131
+ export declare const decodeControlRecordPayload: (payloadBytes: Uint8Array) => ControlRecordPayload;
132
+ export declare const decodeDataUnitPayload: (payloadBytes: Uint8Array) => DataUnitPayload;
133
+ export declare const decodeKeyPackagePayload: (payloadBytes: Uint8Array) => KeyPackagePayload;
134
+ export declare const decodeSnapshotPayload: (payloadBytes: Uint8Array) => SnapshotPayload;
135
+ /** Parses a received Control Record (§13) without verifying its signature. */
136
+ export declare const parseControlRecord: (bytes: Uint8Array) => Parsed<ControlRecordPayload>;
137
+ /** Parses a received Data Unit (§26) without verifying its signature. */
138
+ export declare const parseDataUnit: (bytes: Uint8Array) => Parsed<DataUnitPayload>;
139
+ /** Parses a received Key Package (§25) without verifying its signature. */
140
+ export declare const parseKeyPackage: (bytes: Uint8Array) => Parsed<KeyPackagePayload>;
141
+ /** Parses a received Snapshot (§29) without verifying its signature. */
142
+ export declare const parseSnapshot: (bytes: Uint8Array) => Parsed<SnapshotPayload>;
143
+ /**
144
+ * The Principal that must have signed a data-plane object, read from its
145
+ * payload: the actor of a Data Unit (§26), the sender of a Key Package
146
+ * (§25) and the publisher of a Snapshot (§29). The caller resolves this ID to
147
+ * a Principal Descriptor and passes it to `verifySignedObject`; a `kid` that
148
+ * names anyone else fails as KID_MISMATCH (INVALID_SIGNATURE, G1/N2).
149
+ *
150
+ * Control Records have no hook: who must sign one depends on its type and on
151
+ * the chain (LFCP-019 to LFCP-021). Their payload's `issuer` is exposed as is.
152
+ */
153
+ export declare function expectedSignerOf(payload: DataUnitPayload | KeyPackagePayload | SnapshotPayload): PrincipalId;
154
+ //# sourceMappingURL=objects.d.ts.map
@@ -0,0 +1,156 @@
1
+ import { actorSequence, controlRecordId, dataEpoch, dataUnitId, LfcpError, principalId, resourceId, } from "@openlfcp/core";
2
+ import { decodeDeterministic, decodeStrict } from "./cbor/index.js";
3
+ import { parseSignedObject } from "./cose.js";
4
+ import { Fields, invalid } from "./fields.js";
5
+ import { canonicalFrontierFromCbor } from "./have.js";
6
+ /** §14 Control Record types defined by LFCP-WIRE-01. 9–31 are reserved; 32 and up are extensions. */
7
+ export const CONTROL_TYPE = Object.freeze({
8
+ GENESIS: 0n,
9
+ CAPABILITY_GRANT: 1n,
10
+ CAPABILITY_REVOKE: 2n,
11
+ CAPABILITY_CLAIM: 3n,
12
+ KEY_EPOCH: 4n,
13
+ ROUTE_UPDATE: 5n,
14
+ OWNER_TRANSFER_COMMIT: 6n,
15
+ COORDINATOR_RECOVERY: 7n,
16
+ RESOURCE_TOMBSTONE: 8n,
17
+ });
18
+ const FIRST_RESERVED_CONTROL_TYPE = 9n;
19
+ const FIRST_EXTENSION_CONTROL_TYPE = 32n;
20
+ const ALL_FIELDS = [0, 1, 2, 3, 4, 5, 6];
21
+ /**
22
+ * Structural rules: the §13 field set, a §14 type that is not reserved
23
+ * (UNSUPPORTED_VALUE otherwise), and a Genesis record (type 0) at
24
+ * control_seq 0 with a null link (§13.1: INVALID_CONTROL_CHAIN otherwise).
25
+ * Sequence continuity and prev_control_id linkage are chain validation
26
+ * (LFCP-020).
27
+ */
28
+ export function controlRecordPayloadFromCbor(value) {
29
+ const f = new Fields(value, "control-record-payload", ALL_FIELDS.slice(0, 6));
30
+ const rid = resourceId(f.bytes(0, 32));
31
+ const controlSeq = f.uint(1);
32
+ const prev = f.bytesOrNull(2, 32);
33
+ const controlType = f.uint(3);
34
+ const issuer = principalId(f.bytes(4, 32));
35
+ if (controlType >= FIRST_RESERVED_CONTROL_TYPE && controlType < FIRST_EXTENSION_CONTROL_TYPE) {
36
+ throw new LfcpError("UNSUPPORTED_VALUE", `Control Record type ${controlType} is reserved for LFCP core (§14)`);
37
+ }
38
+ if (controlType === CONTROL_TYPE.GENESIS && (controlSeq !== 0n || prev !== null))
39
+ throw new LfcpError("INVALID_CONTROL_CHAIN", "a Genesis record must have control_seq 0 and a null prev_control_id (§13.1)");
40
+ return Object.freeze({
41
+ kind: "control-record",
42
+ resourceId: rid,
43
+ controlSeq,
44
+ prevControlId: prev === null ? null : controlRecordId(prev),
45
+ controlType,
46
+ extension: controlType >= FIRST_EXTENSION_CONTROL_TYPE,
47
+ issuer,
48
+ body: f.any(5),
49
+ });
50
+ }
51
+ /**
52
+ * Structural rules: the §26 field set and actor sequence >= 1 (§8, N4).
53
+ * A non-null previous unit at sequence 1 is not rejected here: §26.2 asks
54
+ * for it to be reported to the sync engine, and the actor_seq1_prev_not_null_D1
55
+ * vector's disposition is "report", not reject (LFCP-025, LFCP-028).
56
+ */
57
+ export function dataUnitPayloadFromCbor(value) {
58
+ const f = new Fields(value, "data-unit-payload", ALL_FIELDS);
59
+ const prev = f.bytesOrNull(4, 32);
60
+ const actorSeq = f.uint(3);
61
+ if (actorSeq === 0n)
62
+ f.fail(3, "(actor sequence) must be at least 1 (§8)");
63
+ return Object.freeze({
64
+ kind: "data-unit",
65
+ resourceId: resourceId(f.bytes(0, 32)),
66
+ dataEpoch: dataEpoch(f.uint(1)),
67
+ actor: principalId(f.bytes(2, 32)),
68
+ actorSeq: actorSequence(actorSeq),
69
+ prevDataUnitId: prev === null ? null : dataUnitId(prev),
70
+ controlHead: controlRecordId(f.bytes(5, 32)),
71
+ ciphertext: f.bytes(6),
72
+ });
73
+ }
74
+ /**
75
+ * Structural rules: the §25 field set, with `5 => bstr .size 32` (HPKE
76
+ * enc) and `6 => bstr .size 48` (the 32-byte DEK and the 16-byte tag).
77
+ * HPKE and authority checks are key-package.ts.
78
+ */
79
+ export function keyPackagePayloadFromCbor(value) {
80
+ const f = new Fields(value, "key-package-payload", ALL_FIELDS);
81
+ return Object.freeze({
82
+ kind: "key-package",
83
+ resourceId: resourceId(f.bytes(0, 32)),
84
+ dataEpoch: dataEpoch(f.uint(1)),
85
+ recipient: principalId(f.bytes(2, 32)),
86
+ controlHead: controlRecordId(f.bytes(3, 32)),
87
+ sender: principalId(f.bytes(4, 32)),
88
+ hpkeEnc: f.bytes(5, 32),
89
+ hpkeCiphertext: f.bytes(6, 48),
90
+ });
91
+ }
92
+ /**
93
+ * Structural rules: the §29 field set, a Snapshot Sequence of at least 1
94
+ * (§29: "Snapshot Sequences begin at 1"; §29 names no code, so this is
95
+ * INVALID_STRUCTURE like actor sequence 0) and a canonical frontier
96
+ * (§28.1, §28.2, N6).
97
+ */
98
+ export function snapshotPayloadFromCbor(value) {
99
+ const f = new Fields(value, "snapshot-payload", ALL_FIELDS);
100
+ const snapshotSeq = f.uint(3);
101
+ if (snapshotSeq === 0n)
102
+ f.fail(3, "(Snapshot Sequence) must be at least 1 (§29)");
103
+ return Object.freeze({
104
+ kind: "snapshot",
105
+ resourceId: resourceId(f.bytes(0, 32)),
106
+ dataEpoch: dataEpoch(f.uint(1)),
107
+ publisher: principalId(f.bytes(2, 32)),
108
+ snapshotSeq,
109
+ controlHead: controlRecordId(f.bytes(4, 32)),
110
+ frontier: canonicalFrontierFromCbor(f.any(5)),
111
+ ciphertext: f.bytes(6),
112
+ });
113
+ }
114
+ function decodePayload(payloadBytes, from) {
115
+ if (!(payloadBytes instanceof Uint8Array))
116
+ invalid("payload", "not a byte string");
117
+ return from(decodeDeterministic(payloadBytes));
118
+ }
119
+ function parseWith(bytes, from) {
120
+ const signed = parseSignedObject(bytes); // also applies N7 to the payload
121
+ return Object.freeze({ signed, payload: from(decodeStrict(signed.payloadBytes)) });
122
+ }
123
+ /** Decodes standalone payload bytes; they must be deterministic CBOR (N7). */
124
+ export const decodeControlRecordPayload = (payloadBytes) => decodePayload(payloadBytes, controlRecordPayloadFromCbor);
125
+ export const decodeDataUnitPayload = (payloadBytes) => decodePayload(payloadBytes, dataUnitPayloadFromCbor);
126
+ export const decodeKeyPackagePayload = (payloadBytes) => decodePayload(payloadBytes, keyPackagePayloadFromCbor);
127
+ export const decodeSnapshotPayload = (payloadBytes) => decodePayload(payloadBytes, snapshotPayloadFromCbor);
128
+ /** Parses a received Control Record (§13) without verifying its signature. */
129
+ export const parseControlRecord = (bytes) => parseWith(bytes, controlRecordPayloadFromCbor);
130
+ /** Parses a received Data Unit (§26) without verifying its signature. */
131
+ export const parseDataUnit = (bytes) => parseWith(bytes, dataUnitPayloadFromCbor);
132
+ /** Parses a received Key Package (§25) without verifying its signature. */
133
+ export const parseKeyPackage = (bytes) => parseWith(bytes, keyPackagePayloadFromCbor);
134
+ /** Parses a received Snapshot (§29) without verifying its signature. */
135
+ export const parseSnapshot = (bytes) => parseWith(bytes, snapshotPayloadFromCbor);
136
+ /**
137
+ * The Principal that must have signed a data-plane object, read from its
138
+ * payload: the actor of a Data Unit (§26), the sender of a Key Package
139
+ * (§25) and the publisher of a Snapshot (§29). The caller resolves this ID to
140
+ * a Principal Descriptor and passes it to `verifySignedObject`; a `kid` that
141
+ * names anyone else fails as KID_MISMATCH (INVALID_SIGNATURE, G1/N2).
142
+ *
143
+ * Control Records have no hook: who must sign one depends on its type and on
144
+ * the chain (LFCP-019 to LFCP-021). Their payload's `issuer` is exposed as is.
145
+ */
146
+ export function expectedSignerOf(payload) {
147
+ switch (payload.kind) {
148
+ case "data-unit":
149
+ return payload.actor;
150
+ case "key-package":
151
+ return payload.sender;
152
+ case "snapshot":
153
+ return payload.publisher;
154
+ }
155
+ }
156
+ //# sourceMappingURL=objects.js.map
@@ -0,0 +1,43 @@
1
+ import { type PrincipalId } from "@openlfcp/core";
2
+ import { type CborMap, type CborValue } from "./cbor/index.js";
3
+ /**
4
+ * A Principal Descriptor (LFCP-WIRE-01 §7): the public identity of a
5
+ * Principal. It holds public keys only; private keys stay in
6
+ * @openlfcp/crypto key pairs.
7
+ *
8
+ * CBOR: { 0 => principal-id, 1 => ed25519-public-key, 2 => x25519-public-key },
9
+ * every value a 32-byte byte string.
10
+ */
11
+ export interface PrincipalDescriptor {
12
+ readonly principalId: PrincipalId;
13
+ readonly ed25519PublicKey: Uint8Array;
14
+ readonly x25519PublicKey: Uint8Array;
15
+ }
16
+ /** principal_id = SHA-256(ASCII("LFCP-PRINCIPAL-v1") || ed25519_public_key || x25519_public_key) (§7). */
17
+ export declare function derivePrincipalId(ed25519PublicKey: Uint8Array, x25519PublicKey: Uint8Array): PrincipalId;
18
+ /** Builds the descriptor for two public keys, deriving the Principal ID. */
19
+ export declare function principalDescriptor(ed25519PublicKey: Uint8Array, x25519PublicKey: Uint8Array): PrincipalDescriptor;
20
+ /** The descriptor for a Principal's key pairs. Only their public keys are read. */
21
+ export declare function principalDescriptorFromKeys(signing: {
22
+ readonly publicKey: Uint8Array;
23
+ }, agreement: {
24
+ readonly publicKey: Uint8Array;
25
+ }): PrincipalDescriptor;
26
+ /** The descriptor as a CBOR map, for embedding in larger Wire structures. */
27
+ export declare function principalDescriptorToCbor(descriptor: PrincipalDescriptor): CborMap;
28
+ /** Deterministic CBOR bytes of the descriptor (§5.2). */
29
+ export declare function encodePrincipalDescriptor(descriptor: PrincipalDescriptor): Uint8Array;
30
+ /**
31
+ * Validates a decoded descriptor: exactly the keys 0, 1 and 2 (the §7 map
32
+ * is closed), each a 32-byte byte string, and a Principal ID equal to the
33
+ * §7 hash of the two public keys ("A verifier MUST recompute the ID
34
+ * whenever a descriptor is received"), and an Ed25519 key that is a
35
+ * canonical point encoding not of small order (§7, §10.5.1).
36
+ */
37
+ export declare function principalDescriptorFromCbor(value: CborValue): PrincipalDescriptor;
38
+ /**
39
+ * Decodes received descriptor bytes. They must be deterministic CBOR
40
+ * (§5.2; CBOR_* errors otherwise) and a valid descriptor.
41
+ */
42
+ export declare function decodePrincipalDescriptor(bytes: Uint8Array): PrincipalDescriptor;
43
+ //# sourceMappingURL=principal.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { bytesEqual, LfcpError, principalId } from "@openlfcp/core";
2
+ import { isValidEd25519PublicKey, sha256 } from "@openlfcp/crypto";
3
+ import { cborMap, decodeStrict, encode, isCborMap, } from "./cbor/index.js";
4
+ const KEY_LENGTH = 32;
5
+ const DOMAIN = Uint8Array.from("LFCP-PRINCIPAL-v1", (c) => c.charCodeAt(0));
6
+ function invalid(why) {
7
+ throw new LfcpError("INVALID_PRINCIPAL_DESCRIPTOR", `invalid Principal Descriptor: ${why}`);
8
+ }
9
+ function publicKey(kind, bytes) {
10
+ if (!(bytes instanceof Uint8Array) || bytes.length !== KEY_LENGTH)
11
+ invalid(`${kind} must be a ${KEY_LENGTH}-byte byte string`);
12
+ return Uint8Array.from(bytes);
13
+ }
14
+ /** principal_id = SHA-256(ASCII("LFCP-PRINCIPAL-v1") || ed25519_public_key || x25519_public_key) (§7). */
15
+ export function derivePrincipalId(ed25519PublicKey, x25519PublicKey) {
16
+ const ed = publicKey("the Ed25519 public key", ed25519PublicKey);
17
+ const x = publicKey("the X25519 public key", x25519PublicKey);
18
+ const input = new Uint8Array(DOMAIN.length + 2 * KEY_LENGTH);
19
+ input.set(DOMAIN, 0);
20
+ input.set(ed, DOMAIN.length);
21
+ input.set(x, DOMAIN.length + KEY_LENGTH);
22
+ return principalId(sha256(input));
23
+ }
24
+ /** Builds the descriptor for two public keys, deriving the Principal ID. */
25
+ export function principalDescriptor(ed25519PublicKey, x25519PublicKey) {
26
+ return Object.freeze({
27
+ principalId: derivePrincipalId(ed25519PublicKey, x25519PublicKey),
28
+ ed25519PublicKey: Uint8Array.from(ed25519PublicKey),
29
+ x25519PublicKey: Uint8Array.from(x25519PublicKey),
30
+ });
31
+ }
32
+ /** The descriptor for a Principal's key pairs. Only their public keys are read. */
33
+ export function principalDescriptorFromKeys(signing, agreement) {
34
+ return principalDescriptor(signing.publicKey, agreement.publicKey);
35
+ }
36
+ /** The descriptor as a CBOR map, for embedding in larger Wire structures. */
37
+ export function principalDescriptorToCbor(descriptor) {
38
+ return cborMap([
39
+ [0, descriptor.principalId],
40
+ [1, descriptor.ed25519PublicKey],
41
+ [2, descriptor.x25519PublicKey],
42
+ ]);
43
+ }
44
+ /** Deterministic CBOR bytes of the descriptor (§5.2). */
45
+ export function encodePrincipalDescriptor(descriptor) {
46
+ return encode(principalDescriptorToCbor(descriptor));
47
+ }
48
+ /**
49
+ * Validates a decoded descriptor: exactly the keys 0, 1 and 2 (the §7 map
50
+ * is closed), each a 32-byte byte string, and a Principal ID equal to the
51
+ * §7 hash of the two public keys ("A verifier MUST recompute the ID
52
+ * whenever a descriptor is received"), and an Ed25519 key that is a
53
+ * canonical point encoding not of small order (§7, §10.5.1).
54
+ */
55
+ export function principalDescriptorFromCbor(value) {
56
+ if (!isCborMap(value))
57
+ invalid("not a map");
58
+ const fields = new Map(value.entries);
59
+ if (value.entries.length !== 3 || ![0, 1, 2].every((k) => fields.has(k)))
60
+ invalid("fields must be exactly 0, 1 and 2");
61
+ const id = publicKey("the Principal ID", fields.get(0));
62
+ const descriptor = principalDescriptor(publicKey("the Ed25519 public key", fields.get(1)), publicKey("the X25519 public key", fields.get(2)));
63
+ if (!bytesEqual(id, descriptor.principalId)) {
64
+ throw new LfcpError("PRINCIPAL_ID_MISMATCH", "the Principal ID does not match the descriptor's public keys");
65
+ }
66
+ if (!isValidEd25519PublicKey(descriptor.ed25519PublicKey))
67
+ invalid("the Ed25519 public key is not a canonical point encoding of large order");
68
+ return descriptor;
69
+ }
70
+ /**
71
+ * Decodes received descriptor bytes. They must be deterministic CBOR
72
+ * (§5.2; CBOR_* errors otherwise) and a valid descriptor.
73
+ */
74
+ export function decodePrincipalDescriptor(bytes) {
75
+ const value = decodeStrict(bytes);
76
+ if (!bytesEqual(encode(value), bytes)) {
77
+ throw new LfcpError("CBOR_NON_CANONICAL", "descriptor bytes are not the deterministic encoding");
78
+ }
79
+ return principalDescriptorFromCbor(value);
80
+ }
81
+ //# sourceMappingURL=principal.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The LFCP connection state machines (LFCP-WIRE-01 §63, §64) as pure
3
+ * transition functions: exactly the drawn edges, plus a connection-loss
4
+ * edge from every live state (G-SM1) and the CONNECTING failure edge
5
+ * (G-SM3). An event that has no edge from the current state returns
6
+ * undefined: the caller treats it as a protocol violation. No I/O.
7
+ *
8
+ * The per-resource sync machine (§65) belongs to the client sync session
9
+ * (LFCP-039a).
10
+ */
11
+ /** §63 client connection states. */
12
+ export type ClientConnectionState = "DISCONNECTED" | "CONNECTING" | "NEGOTIATING" | "AUTHENTICATING" | "READY";
13
+ export type ClientConnectionEvent =
14
+ /** open WebSocket */
15
+ "OPEN"
16
+ /** WebSocket + lfcp-1 accepted */
17
+ | "CONNECTED"
18
+ /** connection failed or lfcp-1 not accepted (G-SM3) */
19
+ | "CONNECT_FAILED"
20
+ /** HELLO sent, CHALLENGE received */
21
+ | "CHALLENGED"
22
+ /** AUTH sent, READY received */
23
+ | "READY_RECEIVED"
24
+ /** open/close resources while READY */
25
+ | "RESOURCE"
26
+ /** a fatal error during negotiation */
27
+ | "FATAL_ERROR" | "AUTH_FAILURE"
28
+ /** the socket closed or the connection was lost (G-SM1) */
29
+ | "CONNECTION_LOST";
30
+ /** The §63 client transition, or undefined when the event has no edge from `state`. */
31
+ export declare function clientConnectionTransition(state: ClientConnectionState, event: ClientConnectionEvent): ClientConnectionState | undefined;
32
+ /** §64 server session states. */
33
+ export type ServerSessionState = "ACCEPTED" | "WAIT_HELLO" | "WAIT_AUTH" | "READY" | "CLOSED";
34
+ export type ServerSessionEvent =
35
+ /** the session starts waiting for HELLO */
36
+ "START"
37
+ /** valid HELLO, CHALLENGE sent */
38
+ | "VALID_HELLO"
39
+ /** valid AUTH, READY sent */
40
+ | "VALID_AUTH"
41
+ /** an LFCP message on a READY session */
42
+ | "MESSAGE" | "PROTOCOL_VIOLATION" | "AUTH_FAILURE" | "FATAL_ERROR"
43
+ /** the socket closed (G-SM1) */
44
+ | "SOCKET_CLOSED";
45
+ /** The §64 server transition, or undefined when the event has no edge from `state`. */
46
+ export declare function serverSessionTransition(state: ServerSessionState, event: ServerSessionEvent): ServerSessionState | undefined;
47
+ //# sourceMappingURL=session-state.d.ts.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The LFCP connection state machines (LFCP-WIRE-01 §63, §64) as pure
3
+ * transition functions: exactly the drawn edges, plus a connection-loss
4
+ * edge from every live state (G-SM1) and the CONNECTING failure edge
5
+ * (G-SM3). An event that has no edge from the current state returns
6
+ * undefined: the caller treats it as a protocol violation. No I/O.
7
+ *
8
+ * The per-resource sync machine (§65) belongs to the client sync session
9
+ * (LFCP-039a).
10
+ */
11
+ const CLIENT = {
12
+ DISCONNECTED: { OPEN: "CONNECTING" },
13
+ CONNECTING: {
14
+ CONNECTED: "NEGOTIATING",
15
+ CONNECT_FAILED: "DISCONNECTED",
16
+ CONNECTION_LOST: "DISCONNECTED",
17
+ },
18
+ NEGOTIATING: {
19
+ CHALLENGED: "AUTHENTICATING",
20
+ FATAL_ERROR: "DISCONNECTED",
21
+ CONNECTION_LOST: "DISCONNECTED",
22
+ },
23
+ AUTHENTICATING: {
24
+ READY_RECEIVED: "READY",
25
+ AUTH_FAILURE: "DISCONNECTED",
26
+ CONNECTION_LOST: "DISCONNECTED",
27
+ },
28
+ READY: { RESOURCE: "READY", CONNECTION_LOST: "DISCONNECTED" },
29
+ };
30
+ /** The §63 client transition, or undefined when the event has no edge from `state`. */
31
+ export function clientConnectionTransition(state, event) {
32
+ return CLIENT[state][event];
33
+ }
34
+ const SERVER = {
35
+ ACCEPTED: { START: "WAIT_HELLO", SOCKET_CLOSED: "CLOSED" },
36
+ WAIT_HELLO: {
37
+ VALID_HELLO: "WAIT_AUTH",
38
+ PROTOCOL_VIOLATION: "CLOSED",
39
+ SOCKET_CLOSED: "CLOSED",
40
+ },
41
+ WAIT_AUTH: { VALID_AUTH: "READY", AUTH_FAILURE: "CLOSED", SOCKET_CLOSED: "CLOSED" },
42
+ READY: { MESSAGE: "READY", FATAL_ERROR: "CLOSED", SOCKET_CLOSED: "CLOSED" },
43
+ CLOSED: {},
44
+ };
45
+ /** The §64 server transition, or undefined when the event has no edge from `state`. */
46
+ export function serverSessionTransition(state, event) {
47
+ return SERVER[state][event];
48
+ }
49
+ //# sourceMappingURL=session-state.js.map
@@ -0,0 +1,86 @@
1
+ import { type ControlRecordId, type DataEpoch, type Hash32, type PrincipalId } from "@openlfcp/core";
2
+ import { type ResourceDEK } from "@openlfcp/crypto";
3
+ import { type Signer } from "./cose.js";
4
+ import type { DataProfileCodec } from "./data-unit.js";
5
+ import type { ControlView } from "./epoch.js";
6
+ import { type ActorHave } from "./have.js";
7
+ import { type Parsed, type SnapshotPayload } from "./objects.js";
8
+ import type { PrincipalDescriptor } from "./principal.js";
9
+ /** The fields of a Snapshot that its AAD binds (§29.1.3): payload fields 0-5. */
10
+ export type SnapshotAadFields = Pick<SnapshotPayload, "resourceId" | "dataEpoch" | "publisher" | "snapshotSeq" | "controlHead" | "frontier">;
11
+ /**
12
+ * §29.1.3: deterministic CBOR of ["LFCP-SNAPSHOT-v1", resource_id,
13
+ * data_epoch, publisher, snapshot_sequence, control_head,
14
+ * canonical_frontier]. The frontier must already be canonical: it is
15
+ * encoded as given and refused otherwise, never re-ordered.
16
+ */
17
+ export declare function snapshotAad(fields: SnapshotAadFields): Uint8Array;
18
+ /** The deterministic §29 payload, keys 0-6; checked by the receiver rules before it is returned. */
19
+ export declare function encodeSnapshotPayload(fields: SnapshotAadFields & {
20
+ readonly ciphertext: Uint8Array;
21
+ }): Uint8Array;
22
+ export interface SealedSnapshot {
23
+ readonly bytes: Uint8Array;
24
+ /** §29: SHA-256 of the exact COSE_Sign1 bytes. */
25
+ readonly snapshotId: Hash32;
26
+ }
27
+ /**
28
+ * Encrypts `plaintext` and signs the Snapshot as `publisher` (the publisher
29
+ * field is the signer's Principal).
30
+ *
31
+ * BUILDING BLOCK: the sequence must come from a SnapshotSequenceReservation
32
+ * and never be reused for one (resource, epoch, publisher) (§29.1.2). Use
33
+ * createSnapshot (@openlfcp/client), which enforces that.
34
+ */
35
+ export declare function sealSnapshot(fields: Omit<SnapshotAadFields, "publisher">, plaintext: Uint8Array, dek: ResourceDEK, publisher: Signer): SealedSnapshot;
36
+ export type SnapshotRejectReason = "MALFORMED" | "OTHER_RESOURCE" | "UNKNOWN_PUBLISHER" | "SIGNATURE" | "UNKNOWN_CONTROL_HEAD" | "UNAUTHORIZED" | "UNKNOWN_EPOCH" | "BEYOND_CUTOFF";
37
+ export interface SnapshotRejected {
38
+ readonly kind: "rejected";
39
+ readonly reason: SnapshotRejectReason;
40
+ readonly wireCode: "MALFORMED_MESSAGE" | "MISSING_DEPENDENCY" | "INVALID_SIGNATURE" | "AUTHORIZATION_FAILED" | "STALE_DATA_EPOCH";
41
+ readonly message: string;
42
+ }
43
+ export type SnapshotCheck = {
44
+ readonly kind: "valid";
45
+ readonly parsed: Parsed<SnapshotPayload>;
46
+ readonly snapshotId: Hash32;
47
+ } | SnapshotRejected;
48
+ export interface SnapshotCheckOptions {
49
+ /** Resolves a publisher that no record of the chain describes. */
50
+ readonly resolvePrincipal?: (id: PrincipalId) => PrincipalDescriptor | undefined;
51
+ }
52
+ /** Steps 1-8: everything that needs no DEK, for servers and clients. */
53
+ export declare function checkSnapshot(view: ControlView, bytes: Uint8Array, options?: SnapshotCheckOptions): SnapshotCheck;
54
+ /**
55
+ * §29 (G-EP4): once a Key Epoch closes the Snapshot's epoch, the Snapshot
56
+ * may cover only units within that epoch's final frontier; units beyond it
57
+ * are quarantined (§19.1), so a Snapshot that includes them would merge
58
+ * stale work. Evaluated against the latest known state, as §19.1 does for
59
+ * Data Units. Returns why, or undefined when within.
60
+ */
61
+ export declare function beyondCutoff(view: ControlView, epoch: DataEpoch, frontier: readonly ActorHave[]): string | undefined;
62
+ export type ReceivedSnapshot<T> = {
63
+ readonly kind: "accepted";
64
+ readonly snapshotId: Hash32;
65
+ readonly publisher: PrincipalId;
66
+ readonly seq: bigint;
67
+ readonly epoch: DataEpoch;
68
+ readonly controlHead: ControlRecordId;
69
+ readonly frontier: readonly ActorHave[];
70
+ readonly value: T;
71
+ } | SnapshotRejected
72
+ /** Client-local: no wire code (§29.1.4); surface it to the application. */
73
+ | {
74
+ readonly kind: "local-failure";
75
+ readonly reason: "NO_DEK" | "DEK_COMMITMENT_MISMATCH" | "AEAD" | "PROFILE_REJECTED";
76
+ readonly snapshotId: Hash32;
77
+ readonly message: string;
78
+ };
79
+ export interface ReceiveSnapshotOptions<T> extends SnapshotCheckOptions {
80
+ readonly dek: (epoch: DataEpoch) => ResourceDEK | undefined | Promise<ResourceDEK | undefined>;
81
+ /** The Data Profile's Snapshot codec: decodes and validates the Snapshot plaintext. */
82
+ readonly profile: DataProfileCodec<T>;
83
+ }
84
+ /** A received Snapshot end to end (steps 1-10). Only "accepted" may be loaded. */
85
+ export declare function receiveSnapshot<T>(view: ControlView, bytes: Uint8Array, options: ReceiveSnapshotOptions<T>): Promise<ReceivedSnapshot<T>>;
86
+ //# sourceMappingURL=snapshot.d.ts.map