@openlfcp/wire 0.1.0 → 0.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.
Files changed (52) hide show
  1. package/README.md +3 -3
  2. package/package.json +3 -3
  3. package/dist/capability.d.ts +0 -143
  4. package/dist/capability.js +0 -409
  5. package/dist/cbor/decode.d.ts +0 -23
  6. package/dist/cbor/decode.js +0 -163
  7. package/dist/cbor/encode.d.ts +0 -10
  8. package/dist/cbor/encode.js +0 -149
  9. package/dist/cbor/index.d.ts +0 -10
  10. package/dist/cbor/index.js +0 -10
  11. package/dist/cbor/text.d.ts +0 -11
  12. package/dist/cbor/text.js +0 -5
  13. package/dist/cbor/value.d.ts +0 -30
  14. package/dist/cbor/value.js +0 -23
  15. package/dist/chain.d.ts +0 -140
  16. package/dist/chain.js +0 -339
  17. package/dist/control-sync.d.ts +0 -69
  18. package/dist/control-sync.js +0 -44
  19. package/dist/control.d.ts +0 -173
  20. package/dist/control.js +0 -369
  21. package/dist/cose.d.ts +0 -81
  22. package/dist/cose.js +0 -133
  23. package/dist/data-unit.d.ts +0 -204
  24. package/dist/data-unit.js +0 -314
  25. package/dist/endpoint.d.ts +0 -51
  26. package/dist/endpoint.js +0 -116
  27. package/dist/epoch.d.ts +0 -109
  28. package/dist/epoch.js +0 -128
  29. package/dist/fields.d.ts +0 -18
  30. package/dist/fields.js +0 -78
  31. package/dist/handshake.d.ts +0 -175
  32. package/dist/handshake.js +0 -297
  33. package/dist/have.d.ts +0 -101
  34. package/dist/have.js +0 -268
  35. package/dist/index.d.ts +0 -21
  36. package/dist/index.js +0 -21
  37. package/dist/invite.d.ts +0 -80
  38. package/dist/invite.js +0 -247
  39. package/dist/key-package.d.ts +0 -92
  40. package/dist/key-package.js +0 -132
  41. package/dist/message.d.ts +0 -367
  42. package/dist/message.js +0 -690
  43. package/dist/objects.d.ts +0 -154
  44. package/dist/objects.js +0 -156
  45. package/dist/principal.d.ts +0 -43
  46. package/dist/principal.js +0 -81
  47. package/dist/session-state.d.ts +0 -47
  48. package/dist/session-state.js +0 -49
  49. package/dist/snapshot.d.ts +0 -86
  50. package/dist/snapshot.js +0 -189
  51. package/dist/transition.d.ts +0 -97
  52. package/dist/transition.js +0 -130
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  LFCP-WIRE-01 structures and codecs: deterministic CBOR (`@openlfcp/wire/cbor`), COSE_Sign1, Principal Descriptors, Control Records and chain validation, Data Units, Key Packages, Snapshots, invitations and the wire messages with the session handshake.
4
4
 
5
5
  ```sh
6
- npm install @openlfcp/wire@next
6
+ npm install @openlfcp/wire
7
7
  ```
8
8
 
9
9
  ESM only; Node.js 24 or later (browsers and Electron for every package except `@openlfcp/storage-node`).
@@ -12,9 +12,9 @@ ESM only; Node.js 24 or later (browsers and Electron for every package except `@
12
12
 
13
13
  Part of the OpenLFCP TypeScript SDK, which implements the OpenLFCP MVP 0.1
14
14
  subset of [LFCP-WIRE-01](https://github.com/openlfcp/spec/blob/main/wire/LFCP-WIRE-01.md)
15
- at `mvp-0.1-baseline.8`, not every deferred WIRE-01 feature. It does not
15
+ at `mvp-0.1-baseline.9`, not every deferred WIRE-01 feature. It does not
16
16
  claim full LFCP-WIRE-01 conformance. This is the MVP 0.1 release
17
- (`0.1.0`, npm dist-tag `latest`). Until 1.0, minor versions may change APIs.
17
+ (`0.1.2`, npm dist-tag `latest`). Until 1.0, minor versions may change APIs.
18
18
 
19
19
  ## Links
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlfcp/wire",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Deterministic CBOR, COSE and LFCP Wire structures and codecs.",
5
5
  "keywords": [
6
6
  "openlfcp",
@@ -47,7 +47,7 @@
47
47
  "tag": "latest"
48
48
  },
49
49
  "dependencies": {
50
- "@openlfcp/core": "^0.1.0",
51
- "@openlfcp/crypto": "^0.1.0"
50
+ "@openlfcp/core": "^0.1.2",
51
+ "@openlfcp/crypto": "^0.1.2"
52
52
  }
53
53
  }
@@ -1,143 +0,0 @@
1
- import { type ControlRecordId, type DataEpoch, type PrincipalId } from "@openlfcp/core";
2
- import type { ControlState } from "./chain.js";
3
- import { type ControlRecord } from "./control.js";
4
- import type { PrincipalDescriptor } from "./principal.js";
5
- /**
6
- * The LFCP capability engine (LFCP-WIRE-01 §17, §18, §20, §19, §25.2).
7
- *
8
- * Authority comes only from the validated Control Chain: a Principal ID
9
- * holds an ability at a Control Head H because of the owner rule or of
10
- * grants and claims committed through H. There is no server account,
11
- * user table or hosting identity anywhere in this module, and nothing
12
- * here takes one.
13
- *
14
- * Everything is a pure function of a ControlState (the state at some head
15
- * H). Evaluating at an older H means using the state at that H (see
16
- * `stateAt` on a linear chain result), so a later revocation never
17
- * changes an earlier answer.
18
- */
19
- /** §17.1 standard ability codes. */
20
- export declare const ABILITY: Readonly<{
21
- DATA_READ: 1n;
22
- DATA_WRITE: 2n;
23
- SNAPSHOT_PUBLISH: 3n;
24
- CAPABILITY_GRANT: 4n;
25
- CAPABILITY_REVOKE: 5n;
26
- KEY_DISTRIBUTE: 6n;
27
- KEY_ROTATE: 7n;
28
- ROUTE_UPDATE: 8n;
29
- OWNER_TRANSFER_OFFER: 9n;
30
- RESOURCE_TOMBSTONE: 10n;
31
- INVITE_CLAIM: 11n;
32
- }>;
33
- /** §17.1 names of the standard codes. */
34
- export declare const ABILITY_NAMES: ReadonlyMap<bigint, string>;
35
- /**
36
- * Whether `ability` is a §17.1 standard code. §17.1: "A code that is not in
37
- * the table above is kept as received and confers nothing", so an unknown
38
- * code is kept in its record but is never held.
39
- */
40
- export declare const isStandardAbility: (ability: bigint) => boolean;
41
- /** A grant created by a CAPABILITY_GRANT record or by a successful claim (§17.2, §18.1). */
42
- export interface Grant {
43
- /** The ID of the Control Record that created it (§17.2). */
44
- readonly id: ControlRecordId;
45
- readonly source: "grant" | "claim";
46
- readonly issuer: PrincipalId;
47
- readonly subject: PrincipalId;
48
- readonly abilities: readonly bigint[];
49
- readonly delegable: readonly bigint[];
50
- readonly parentGrantId: ControlRecordId | null;
51
- /** §17.2 claim_limit, for invitation grants; null when absent. */
52
- readonly claimLimit: bigint | null;
53
- /** Claims consumed so far, counted in linear chain order (§18.1). */
54
- readonly claimsUsed: bigint;
55
- /** The revoking record, or null while not revoked (§17.3). */
56
- readonly revokedBy: ControlRecordId | null;
57
- }
58
- /** The capability part of ControlState. */
59
- export interface CapabilityState {
60
- /** Every grant created through this head, by grant ID hex, active or not. */
61
- readonly grants: ReadonlyMap<string, Grant>;
62
- }
63
- /**
64
- * Whether a grant is active at this state. §17.2: "A grant is active while
65
- * it has not been revoked and, when it has a parent grant, while that
66
- * parent is active. Revoking a grant therefore also deactivates every grant
67
- * delegated from it, directly or through further delegations."
68
- */
69
- export declare function isGrantActive(state: CapabilityState, grantId: Uint8Array): boolean;
70
- /**
71
- * Whether `principal` holds `ability` at the head of `state`: the owner
72
- * holds every standard ability implicitly (§17.1, no self-grant); anyone
73
- * else holds it through an active grant that confers it. Unknown codes and
74
- * the reserved ability 9 are held by no one (§17.1, §23.1).
75
- */
76
- export declare function hasAbility(state: ControlState, principal: PrincipalId, ability: bigint): boolean;
77
- /** Every standard ability `principal` holds at the head of `state`, ascending. */
78
- export declare function abilitiesOf(state: ControlState, principal: PrincipalId): readonly bigint[];
79
- /**
80
- * An authorization decision; a refusal names the rule. `code` marks the
81
- * refusals callers must tell apart: INVITE_CLAIM_EXHAUSTED (§18.1 rule 3,
82
- * which LFCP-022 reports separately; still AUTHORIZATION_FAILED on the
83
- * wire) and INVALID_SIGNATURE (§23.3: a transfer offer or acceptance whose
84
- * signature does not verify, INVALID_SIGNATURE on the wire).
85
- */
86
- export type Authorization = {
87
- readonly allowed: true;
88
- } | {
89
- readonly allowed: false;
90
- readonly reason: string;
91
- readonly code?: "INVITE_CLAIM_EXHAUSTED" | "INVALID_SIGNATURE";
92
- };
93
- /**
94
- * Whether the issuer of `record` had the authority the record needs, at
95
- * the state before it (the chain's previous head). Genesis is authorized by
96
- * verifyGenesis. Coordinator Recovery and Resource Tombstone records never
97
- * get here: the chain refuses them first (DV1).
98
- */
99
- export declare function authorizeControlRecord(record: ControlRecord, state: ControlState): Authorization;
100
- /** The capability transitions of an applied record (called by applyRecord). */
101
- export declare function applyCapabilities(grants: ReadonlyMap<string, Grant>, record: ControlRecord): ReadonlyMap<string, Grant>;
102
- /**
103
- * §25.2 Key Package authority at the package's referenced Control Head:
104
- * the sender must hold key/distribute, and the recipient must hold
105
- * data/read, "except for an Invitation Principal explicitly authorized by
106
- * an active invite grant: a recipient that is the subject of an active
107
- * grant that includes and still confers invite/claim". `epoch` is the
108
- * package's Data Epoch; whether that epoch is recognized at the head is
109
- * checked with the epoch rules (LFCP-023, LFCP-024).
110
- */
111
- export declare function canDistributeKey(state: ControlState, sender: PrincipalId, recipient: PrincipalId, _epoch: DataEpoch): Authorization;
112
- /** A verified ownership transfer: the new owner's descriptor, from the offer. */
113
- export interface VerifiedOwnerTransfer {
114
- readonly authorization: Authorization;
115
- readonly newOwner?: PrincipalDescriptor;
116
- }
117
- /**
118
- * The §23.3 verifier for an OWNER_TRANSFER_COMMIT, at the state before it.
119
- * Verification only: there is no transfer UI or flow (deferred from MVP 0.1).
120
- *
121
- * §23.3 "A verifier MUST confirm":
122
- * 1. the offer is signed by the current owner;
123
- * 2. the offer references the current Control Head;
124
- * 3. the offer names the accepting Principal;
125
- * 4. the acceptance is signed by that Principal;
126
- * 5. the commit itself is signed by that Principal (here: issued by it;
127
- * the chain verifies the signature against the issuer);
128
- * 6. the expected next Control Sequence matches the commit sequence.
129
- * Also, from §23.1 and §23.2: the offer and accept are canonical signed
130
- * objects of this Resource, and the accept names the exact offer ID (the
131
- * §10.6 object ID of the offer bytes). §23.3: "A commit that fails any of
132
- * these checks is rejected with AUTHORIZATION_FAILED, except that an offer
133
- * or acceptance whose signature does not verify is rejected with
134
- * INVALID_SIGNATURE" (the refusal's code).
135
- */
136
- export declare function verifyOwnerTransfer(record: ControlRecord, state: ControlState): VerifiedOwnerTransfer;
137
- /**
138
- * The descriptor a transfer commit carries for its own issuer: the offer's
139
- * proposed owner (self-certifying, §7), so a new owner never granted
140
- * before can still be verified. Undefined for other records.
141
- */
142
- export declare function transferIssuerDescriptor(record: ControlRecord): PrincipalDescriptor | undefined;
143
- //# sourceMappingURL=capability.d.ts.map
@@ -1,409 +0,0 @@
1
- import { bytesEqual, controlRecordId, toHex, } from "@openlfcp/core";
2
- import { parseOwnerTransferAccept, parseOwnerTransferOffer, } from "./control.js";
3
- import { objectId, verifySignedObject } from "./cose.js";
4
- /**
5
- * The LFCP capability engine (LFCP-WIRE-01 §17, §18, §20, §19, §25.2).
6
- *
7
- * Authority comes only from the validated Control Chain: a Principal ID
8
- * holds an ability at a Control Head H because of the owner rule or of
9
- * grants and claims committed through H. There is no server account,
10
- * user table or hosting identity anywhere in this module, and nothing
11
- * here takes one.
12
- *
13
- * Everything is a pure function of a ControlState (the state at some head
14
- * H). Evaluating at an older H means using the state at that H (see
15
- * `stateAt` on a linear chain result), so a later revocation never
16
- * changes an earlier answer.
17
- */
18
- /** §17.1 standard ability codes. */
19
- export const ABILITY = Object.freeze({
20
- DATA_READ: 1n,
21
- DATA_WRITE: 2n,
22
- SNAPSHOT_PUBLISH: 3n,
23
- CAPABILITY_GRANT: 4n,
24
- CAPABILITY_REVOKE: 5n,
25
- KEY_DISTRIBUTE: 6n,
26
- KEY_ROTATE: 7n,
27
- ROUTE_UPDATE: 8n,
28
- OWNER_TRANSFER_OFFER: 9n,
29
- RESOURCE_TOMBSTONE: 10n,
30
- INVITE_CLAIM: 11n,
31
- });
32
- /** §17.1 names of the standard codes. */
33
- export const ABILITY_NAMES = new Map([
34
- [1n, "data/read"],
35
- [2n, "data/write"],
36
- [3n, "snapshot/publish"],
37
- [4n, "capability/grant"],
38
- [5n, "capability/revoke"],
39
- [6n, "key/distribute"],
40
- [7n, "key/rotate"],
41
- [8n, "route/update"],
42
- [9n, "owner/transfer-offer"],
43
- [10n, "resource/tombstone"],
44
- [11n, "invite/claim"],
45
- ]);
46
- /**
47
- * Whether `ability` is a §17.1 standard code. §17.1: "A code that is not in
48
- * the table above is kept as received and confers nothing", so an unknown
49
- * code is kept in its record but is never held.
50
- */
51
- export const isStandardAbility = (ability) => ABILITY_NAMES.has(ability);
52
- /**
53
- * Whether holding `ability` can mean anything: a standard code other than
54
- * 9. §17.1: "9 | owner/transfer-offer (reserved; confers nothing in
55
- * WIRE-01)"; §23.1: "Only the current owner creates offers."
56
- */
57
- const confers = (ability) => isStandardAbility(ability) && ability !== ABILITY.OWNER_TRANSFER_OFFER;
58
- const key = (id) => toHex(id);
59
- /**
60
- * Whether a grant is active at this state. §17.2: "A grant is active while
61
- * it has not been revoked and, when it has a parent grant, while that
62
- * parent is active. Revoking a grant therefore also deactivates every grant
63
- * delegated from it, directly or through further delegations."
64
- */
65
- export function isGrantActive(state, grantId) {
66
- const seen = new Set();
67
- let grant = state.grants.get(key(grantId));
68
- while (grant !== undefined) {
69
- if (grant.revokedBy !== null)
70
- return false;
71
- if (grant.parentGrantId === null)
72
- return true;
73
- if (seen.has(key(grant.id)))
74
- return false;
75
- seen.add(key(grant.id));
76
- grant = state.grants.get(key(grant.parentGrantId));
77
- }
78
- return false;
79
- }
80
- const isOwner = (state, principal) => bytesEqual(state.owner.principalId, principal);
81
- /**
82
- * Whether an invitation grant's claims are used up. §18.1: "An Invitation
83
- * Grant whose claims are used up confers no invite/claim; its other
84
- * abilities stay active until it is revoked."
85
- */
86
- const claimsExhausted = (g) => g.claimLimit !== null && g.claimsUsed >= g.claimLimit;
87
- /** Whether grant `g` confers `ability` at this state: it lists it, is active, and (for invite/claim) is not used up. */
88
- function grantConfers(state, g, ability) {
89
- if (!g.abilities.includes(ability) || !isGrantActive(state, g.id))
90
- return false;
91
- return ability !== ABILITY.INVITE_CLAIM || !claimsExhausted(g);
92
- }
93
- /**
94
- * Whether `principal` holds `ability` at the head of `state`: the owner
95
- * holds every standard ability implicitly (§17.1, no self-grant); anyone
96
- * else holds it through an active grant that confers it. Unknown codes and
97
- * the reserved ability 9 are held by no one (§17.1, §23.1).
98
- */
99
- export function hasAbility(state, principal, ability) {
100
- if (!confers(ability))
101
- return false;
102
- if (isOwner(state, principal))
103
- return true;
104
- for (const g of state.grants.values())
105
- if (bytesEqual(g.subject, principal) && grantConfers(state, g, ability))
106
- return true;
107
- return false;
108
- }
109
- /** Every standard ability `principal` holds at the head of `state`, ascending. */
110
- export function abilitiesOf(state, principal) {
111
- return [...ABILITY_NAMES.keys()].filter((a) => hasAbility(state, principal, a));
112
- }
113
- const ALLOW = Object.freeze({ allowed: true });
114
- const deny = (reason) => Object.freeze({ allowed: false, reason });
115
- const badSignature = (reason) => Object.freeze({ allowed: false, reason, code: "INVALID_SIGNATURE" });
116
- const subset = (list, of) => list.every((a) => of.includes(a));
117
- /**
118
- * §17.3: revoke authority "covers a grant when the revoker issued it, or
119
- * when it was delegated, directly or through further delegations, from a
120
- * grant the revoker issued. A grant the revoker received is not covered
121
- * unless the revoker also issued one of its ancestors."
122
- */
123
- function covers(state, target, revoker) {
124
- const seen = new Set();
125
- let grant = target;
126
- while (grant !== undefined && !seen.has(key(grant.id))) {
127
- if (bytesEqual(grant.issuer, revoker))
128
- return true;
129
- seen.add(key(grant.id));
130
- grant = grant.parentGrantId === null ? undefined : state.grants.get(key(grant.parentGrantId));
131
- }
132
- return false;
133
- }
134
- /**
135
- * Whether the issuer of `record` had the authority the record needs, at
136
- * the state before it (the chain's previous head). Genesis is authorized by
137
- * verifyGenesis. Coordinator Recovery and Resource Tombstone records never
138
- * get here: the chain refuses them first (DV1).
139
- */
140
- export function authorizeControlRecord(record, state) {
141
- const issuer = record.payload.issuer;
142
- const body = record.body;
143
- switch (body.type) {
144
- case "GENESIS":
145
- return ALLOW;
146
- case "CAPABILITY_GRANT": {
147
- // §17.2: "The owner MAY grant any abilities without a parent grant. A
148
- // non-owner issuer MUST hold capability/grant and MUST reference a
149
- // parent grant." "If parent grant id is present: the parent grant
150
- // MUST be active; the issuer MUST be the subject of the parent grant;
151
- // every granted ability MUST be included in the parent's delegable
152
- // abilities; every delegable ability of the new grant MUST also be
153
- // included in the parent's delegable abilities."
154
- if (body.parentGrantId !== undefined) {
155
- const parent = state.grants.get(key(body.parentGrantId));
156
- if (parent === undefined)
157
- return deny("the parent grant does not exist (§17.2)");
158
- if (!isGrantActive(state, parent.id))
159
- return deny("the parent grant is not active (§17.2)");
160
- if (!bytesEqual(parent.subject, issuer))
161
- return deny("the issuer is not the subject of the parent grant (§17.2)");
162
- if (!subset(body.abilities, parent.delegable))
163
- return deny("a granted ability is not delegable under the parent grant (§17.2)");
164
- if (!subset(body.delegable, parent.delegable))
165
- return deny("a delegable ability is not delegable under the parent grant (§17.2)");
166
- }
167
- else if (!isOwner(state, issuer)) {
168
- return deny("a non-owner grant must prove its authority through a parent grant (§17.2)");
169
- }
170
- if (!isOwner(state, issuer) && !hasAbility(state, issuer, ABILITY.CAPABILITY_GRANT))
171
- return deny("the issuer does not hold capability/grant (§17.2)");
172
- return ALLOW;
173
- }
174
- case "CAPABILITY_REVOKE": {
175
- // §17.3, in order: the target exists, the issuer's authority covers
176
- // it, it is not already revoked. Each failure is AUTHORIZATION_FAILED.
177
- const target = state.grants.get(key(body.grantId));
178
- if (target === undefined)
179
- return deny("the revoked grant does not exist (§17.3)");
180
- // "The owner may revoke any grant." Otherwise the issuer MUST "possess
181
- // capability/revoke authority that covers the target grant".
182
- if (!isOwner(state, issuer)) {
183
- if (!hasAbility(state, issuer, ABILITY.CAPABILITY_REVOKE))
184
- return deny("the issuer does not hold capability/revoke (§17.3)");
185
- if (!covers(state, target, issuer))
186
- return deny("the issuer's capability/revoke authority does not cover the grant (§17.3)");
187
- }
188
- if (target.revokedBy !== null)
189
- return deny("the grant is already revoked (§17.3)");
190
- return ALLOW;
191
- }
192
- case "CAPABILITY_CLAIM": {
193
- // §18.1 validation rules 1-5 (rule 6, coordinator serialization, is LFCP-022).
194
- const invitation = state.grants.get(key(body.invitationGrantId));
195
- if (invitation === undefined)
196
- return deny("the invitation grant does not exist (§18.1)");
197
- if (!isGrantActive(state, invitation.id))
198
- return deny("the invitation grant is not active (§18.1 rule 1)");
199
- if (!invitation.abilities.includes(ABILITY.INVITE_CLAIM))
200
- return deny("the invitation grant does not grant invite/claim (§18.1 rule 2)");
201
- // §18: "Only an invitation grant with a claim_limit can be claimed: one
202
- // without claim_limit is not claimable."
203
- if (invitation.claimLimit === null)
204
- return deny("the invitation grant has no claim_limit and is not claimable (§18)");
205
- if (claimsExhausted(invitation))
206
- return Object.freeze({
207
- allowed: false,
208
- reason: "the invitation grant has no claims left (§18.1 rule 3)",
209
- code: "INVITE_CLAIM_EXHAUSTED",
210
- });
211
- if (!bytesEqual(invitation.subject, issuer))
212
- return deny("the claim issuer is not the Invitation Principal (§18.1 rule 5)");
213
- // Rule 4: a subset of the invitation's abilities, excluding invite/claim
214
- // unless the invitation explicitly delegates it. Unknown codes confer
215
- // nothing (§17.1), so they request nothing and are not checked.
216
- const transferable = invitation.abilities.filter((a) => a !== ABILITY.INVITE_CLAIM || invitation.delegable.includes(ABILITY.INVITE_CLAIM));
217
- if (!subset(body.abilities.filter(isStandardAbility), transferable))
218
- return deny("the claim requests abilities beyond the invitation grant (§18.1 rule 4)");
219
- return ALLOW;
220
- }
221
- case "KEY_EPOCH":
222
- // §19: "The issuer MUST possess key/rotate."
223
- return hasAbility(state, issuer, ABILITY.KEY_ROTATE)
224
- ? ALLOW
225
- : deny("the issuer does not hold key/rotate (§19)");
226
- case "OWNER_TRANSFER_COMMIT":
227
- return verifyOwnerTransfer(record, state).authorization;
228
- case "ROUTE_UPDATE":
229
- // §20: "The issuer MUST possess route/update"; "Each Route Update MUST
230
- // carry a route version strictly greater than the current one: 0 after
231
- // Genesis (Section 15), otherwise the version of the last committed
232
- // Route Update." §20 names no code: AUTHORIZATION_FAILED here.
233
- if (!hasAbility(state, issuer, ABILITY.ROUTE_UPDATE))
234
- return deny("the issuer does not hold route/update (§20)");
235
- return body.routeVersion > state.routeVersion
236
- ? ALLOW
237
- : deny("the route version does not increase (§20)");
238
- case "EXTENSION":
239
- // §14: "A Control Record of an extension type (32 or above) requires
240
- // owner authority: its issuer MUST be the Resource owner at the
241
- // record's position in the chain, whether or not the receiver supports
242
- // the extension." §14 names no code: AUTHORIZATION_FAILED here.
243
- return isOwner(state, issuer)
244
- ? ALLOW
245
- : deny("an extension record must be issued by the owner (§14)");
246
- case "COORDINATOR_RECOVERY":
247
- case "RESOURCE_TOMBSTONE":
248
- return deny(`${body.type} is refused in MVP 0.1 (scope §4, DV1)`);
249
- }
250
- }
251
- /** The capability transitions of an applied record (called by applyRecord). */
252
- export function applyCapabilities(grants, record) {
253
- const body = record.body;
254
- const id = controlRecordId(record.signed.id);
255
- switch (body.type) {
256
- case "CAPABILITY_GRANT": {
257
- const next = new Map(grants);
258
- next.set(key(id), Object.freeze({
259
- id,
260
- source: "grant",
261
- issuer: record.payload.issuer,
262
- subject: body.subject.principalId,
263
- abilities: body.abilities,
264
- delegable: body.delegable,
265
- parentGrantId: body.parentGrantId ?? null,
266
- claimLimit: body.claimLimit ?? null,
267
- claimsUsed: 0n,
268
- revokedBy: null,
269
- }));
270
- return next;
271
- }
272
- case "CAPABILITY_REVOKE": {
273
- const target = grants.get(key(body.grantId));
274
- if (target === undefined)
275
- return grants;
276
- const next = new Map(grants);
277
- next.set(key(target.id), Object.freeze({ ...target, revokedBy: id }));
278
- return next;
279
- }
280
- case "CAPABILITY_CLAIM": {
281
- // §18.1: "A successful claim creates a new capability grant to the
282
- // claimant [and] consumes one claim from the Invitation Grant." "The
283
- // grant a claim creates is identified by the claim record's Control
284
- // Record ID. Its subject is the claimant and its abilities are the
285
- // claimed abilities; it has no parent grant and an empty delegable
286
- // list, so revoking the Invitation Grant later does not revoke it."
287
- const invitation = grants.get(key(body.invitationGrantId));
288
- const next = new Map(grants);
289
- if (invitation !== undefined)
290
- next.set(key(invitation.id), Object.freeze({ ...invitation, claimsUsed: invitation.claimsUsed + 1n }));
291
- next.set(key(id), Object.freeze({
292
- id,
293
- source: "claim",
294
- issuer: record.payload.issuer,
295
- subject: body.claimant.principalId,
296
- abilities: body.abilities,
297
- delegable: [],
298
- parentGrantId: null,
299
- claimLimit: null,
300
- claimsUsed: 0n,
301
- revokedBy: null,
302
- }));
303
- return next;
304
- }
305
- default:
306
- return grants;
307
- }
308
- }
309
- /**
310
- * §25.2 Key Package authority at the package's referenced Control Head:
311
- * the sender must hold key/distribute, and the recipient must hold
312
- * data/read, "except for an Invitation Principal explicitly authorized by
313
- * an active invite grant: a recipient that is the subject of an active
314
- * grant that includes and still confers invite/claim". `epoch` is the
315
- * package's Data Epoch; whether that epoch is recognized at the head is
316
- * checked with the epoch rules (LFCP-023, LFCP-024).
317
- */
318
- export function canDistributeKey(state, sender, recipient, _epoch) {
319
- if (!hasAbility(state, sender, ABILITY.KEY_DISTRIBUTE))
320
- return deny("the sender does not hold key/distribute (§25.2)");
321
- if (hasAbility(state, recipient, ABILITY.DATA_READ))
322
- return ALLOW;
323
- if (hasAbility(state, recipient, ABILITY.INVITE_CLAIM))
324
- return ALLOW;
325
- return deny("the recipient holds neither data/read nor an active invite grant (§25.2)");
326
- }
327
- /**
328
- * The §23.3 verifier for an OWNER_TRANSFER_COMMIT, at the state before it.
329
- * Verification only: there is no transfer UI or flow (deferred from MVP 0.1).
330
- *
331
- * §23.3 "A verifier MUST confirm":
332
- * 1. the offer is signed by the current owner;
333
- * 2. the offer references the current Control Head;
334
- * 3. the offer names the accepting Principal;
335
- * 4. the acceptance is signed by that Principal;
336
- * 5. the commit itself is signed by that Principal (here: issued by it;
337
- * the chain verifies the signature against the issuer);
338
- * 6. the expected next Control Sequence matches the commit sequence.
339
- * Also, from §23.1 and §23.2: the offer and accept are canonical signed
340
- * objects of this Resource, and the accept names the exact offer ID (the
341
- * §10.6 object ID of the offer bytes). §23.3: "A commit that fails any of
342
- * these checks is rejected with AUTHORIZATION_FAILED, except that an offer
343
- * or acceptance whose signature does not verify is rejected with
344
- * INVALID_SIGNATURE" (the refusal's code).
345
- */
346
- export function verifyOwnerTransfer(record, state) {
347
- const body = record.body;
348
- if (body.type !== "OWNER_TRANSFER_COMMIT")
349
- return { authorization: deny("not an ownership transfer commit") };
350
- let offer;
351
- let accept;
352
- try {
353
- offer = parseOwnerTransferOffer(body.offer);
354
- accept = parseOwnerTransferAccept(body.accept);
355
- }
356
- catch {
357
- return {
358
- authorization: deny("the offer or accept is not a valid signed object (§23.1, §23.2)"),
359
- };
360
- }
361
- const o = offer.payload;
362
- const a = accept.payload;
363
- if (!bytesEqual(o.resourceId, state.resourceId) || !bytesEqual(a.resourceId, state.resourceId))
364
- return { authorization: deny("the offer or accept is for another Resource (§23.1, §23.2)") };
365
- const offerSigned = verifySignedObject(offer.signed, state.owner);
366
- if (!offerSigned.valid)
367
- return {
368
- authorization: badSignature(`the offer is not signed by the current owner (${offerSigned.reason}, §23.3 rule 1)`),
369
- };
370
- if (!bytesEqual(o.controlHead, state.head))
371
- return {
372
- authorization: deny("the offer does not reference the current Control Head (§23.3 rule 2)"),
373
- };
374
- if (!bytesEqual(a.newOwner, o.proposedOwner.principalId))
375
- return {
376
- authorization: deny("the accept is not by the Principal the offer names (§23.3 rule 3)"),
377
- };
378
- if (!bytesEqual(a.offerId, objectId(offer.signed.bytes)))
379
- return { authorization: deny("the accept does not name this offer (§23.2)") };
380
- const acceptSigned = verifySignedObject(accept.signed, o.proposedOwner);
381
- if (!acceptSigned.valid)
382
- return {
383
- authorization: badSignature(`the accept is not signed by the new owner (${acceptSigned.reason}, §23.3 rule 4)`),
384
- };
385
- if (!bytesEqual(record.payload.issuer, o.proposedOwner.principalId))
386
- return { authorization: deny("the commit is not issued by the new owner (§23.3 rule 5)") };
387
- if (o.expectedControlSeq !== record.payload.controlSeq)
388
- return {
389
- authorization: deny("the offer's expected Control Sequence is not the commit's (§23.3 rule 6)"),
390
- };
391
- return { authorization: ALLOW, newOwner: o.proposedOwner };
392
- }
393
- /**
394
- * The descriptor a transfer commit carries for its own issuer: the offer's
395
- * proposed owner (self-certifying, §7), so a new owner never granted
396
- * before can still be verified. Undefined for other records.
397
- */
398
- export function transferIssuerDescriptor(record) {
399
- if (record.body.type !== "OWNER_TRANSFER_COMMIT")
400
- return undefined;
401
- try {
402
- const proposed = parseOwnerTransferOffer(record.body.offer).payload.proposedOwner;
403
- return bytesEqual(proposed.principalId, record.payload.issuer) ? proposed : undefined;
404
- }
405
- catch {
406
- return undefined;
407
- }
408
- }
409
- //# sourceMappingURL=capability.js.map
@@ -1,23 +0,0 @@
1
- import { type CborMap, type CborValue } from "./value.js";
2
- /**
3
- * Decodes exactly one deterministic CBOR item (LFCP-WIRE-01 §5.2) and
4
- * rejects anything else: indefinite lengths, non-shortest heads, unsorted
5
- * or duplicate map keys, tags, floats and other simple values, invalid
6
- * UTF-8, truncated input and trailing bytes.
7
- */
8
- export declare function decodeStrict(bytes: Uint8Array): CborValue;
9
- /**
10
- * The §5.2 receiver check: `bytes` decode strictly and re-encode to exactly
11
- * the same bytes. Signature and object-ID checks still use the received
12
- * bytes; this only decides whether they are the deterministic encoding.
13
- */
14
- /**
15
- * Strict decode plus the §5.2 re-encode comparison (N7): the bytes must be
16
- * well-formed (CBOR_* errors from `decodeStrict` otherwise) and exactly the
17
- * deterministic encoding of their value (CBOR_NON_CANONICAL otherwise). The
18
- * re-encoding is only compared, never returned.
19
- */
20
- export declare function decodeDeterministic(bytes: Uint8Array): CborValue;
21
- export declare function isDeterministic(bytes: Uint8Array): boolean;
22
- export type { CborMap };
23
- //# sourceMappingURL=decode.d.ts.map