wire-mesh-core 3.7.2 → 3.8.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/adapters/byte-stream-connection.d.cts +2 -2
- package/dist/adapters/byte-stream-connection.d.mts +2 -2
- package/dist/adapters/frame-codec.d.cts +1 -1
- package/dist/adapters/frame-codec.d.mts +1 -1
- package/dist/adapters/node-identity.d.cts +2 -2
- package/dist/adapters/node-identity.d.mts +2 -2
- package/dist/adapters/tcp-transport.d.cts +1 -1
- package/dist/adapters/tcp-transport.d.mts +1 -1
- package/dist/adapters/threshold-identity.d.cts +2 -2
- package/dist/adapters/threshold-identity.d.mts +2 -2
- package/dist/adapters/threshold-wasm.d.cts +1 -1
- package/dist/adapters/threshold-wasm.d.mts +1 -1
- package/dist/adapters/tls-transport.d.cts +1 -1
- package/dist/adapters/tls-transport.d.mts +1 -1
- package/dist/domain/advert-extension-policy.d.cts +1 -1
- package/dist/domain/advert-extension-policy.d.mts +1 -1
- package/dist/domain/bulk.d.cts +2 -2
- package/dist/domain/bulk.d.mts +2 -2
- package/dist/domain/capability-grant.cjs +1 -1
- package/dist/domain/capability-grant.d.cts +3 -3
- package/dist/domain/capability-grant.d.mts +3 -3
- package/dist/domain/capability-grant.mjs +1 -1
- package/dist/domain/capability-request.cjs +52 -4
- package/dist/domain/capability-request.d.cts +14 -3
- package/dist/domain/capability-request.d.mts +14 -3
- package/dist/domain/capability-request.mjs +52 -5
- package/dist/domain/coordinator-election.d.cts +1 -1
- package/dist/domain/coordinator-election.d.mts +1 -1
- package/dist/domain/data-sync.d.cts +2 -2
- package/dist/domain/data-sync.d.mts +2 -2
- package/dist/domain/device-id.d.cts +1 -1
- package/dist/domain/device-id.d.mts +1 -1
- package/dist/domain/direct-manage-request.d.cts +2 -2
- package/dist/domain/direct-manage-request.d.mts +2 -2
- package/dist/domain/gossip-expansion.d.cts +1 -1
- package/dist/domain/gossip-expansion.d.mts +1 -1
- package/dist/domain/grant-candidates.d.cts +1 -1
- package/dist/domain/grant-candidates.d.mts +1 -1
- package/dist/domain/handshake.d.cts +1 -1
- package/dist/domain/handshake.d.mts +1 -1
- package/dist/domain/hub-mailbox.d.cts +1 -1
- package/dist/domain/hub-mailbox.d.mts +1 -1
- package/dist/domain/mesh-session.cjs +1 -1
- package/dist/domain/mesh-session.d.cts +3 -3
- package/dist/domain/mesh-session.d.mts +3 -3
- package/dist/domain/mesh-session.mjs +1 -1
- package/dist/domain/notice-board.d.cts +3 -3
- package/dist/domain/notice-board.d.mts +3 -3
- package/dist/domain/path-trace.d.cts +1 -1
- package/dist/domain/path-trace.d.mts +1 -1
- package/dist/domain/peer-advert.d.cts +2 -2
- package/dist/domain/peer-advert.d.mts +2 -2
- package/dist/domain/relay-advert.d.cts +1 -1
- package/dist/domain/relay-advert.d.mts +1 -1
- package/dist/domain/relay-hub.d.cts +2 -2
- package/dist/domain/relay-hub.d.mts +2 -2
- package/dist/domain/relay-use-gate.d.cts +2 -2
- package/dist/domain/relay-use-gate.d.mts +2 -2
- package/dist/domain/revocation-view.cjs +1 -1
- package/dist/domain/revocation-view.d.cts +2 -2
- package/dist/domain/revocation-view.d.mts +2 -2
- package/dist/domain/revocation-view.mjs +1 -1
- package/dist/domain/room-rekey.d.cts +3 -3
- package/dist/domain/room-rekey.d.mts +3 -3
- package/dist/domain/room-token-verification.cjs +1 -1
- package/dist/domain/room-token-verification.d.cts +1 -1
- package/dist/domain/room-token-verification.d.mts +1 -1
- package/dist/domain/room-token-verification.mjs +1 -1
- package/dist/domain/room.d.cts +4 -4
- package/dist/domain/room.d.mts +4 -4
- package/dist/domain/shard-manifest.d.cts +1 -1
- package/dist/domain/shard-manifest.d.mts +1 -1
- package/dist/domain/threshold-subject.cjs +1 -1
- package/dist/domain/threshold-subject.mjs +1 -1
- package/dist/domain/tokens.cjs +3 -1
- package/dist/domain/tokens.d.cts +2 -2
- package/dist/domain/tokens.d.mts +2 -2
- package/dist/domain/tokens.mjs +2 -2
- package/dist/domain/topology-snapshot.d.cts +1 -1
- package/dist/domain/topology-snapshot.d.mts +1 -1
- package/dist/domain/topology.d.cts +1 -1
- package/dist/domain/topology.d.mts +1 -1
- package/dist/domain/webrtc-signaling.cjs +1 -1
- package/dist/domain/webrtc-signaling.d.cts +2 -2
- package/dist/domain/webrtc-signaling.d.mts +2 -2
- package/dist/domain/webrtc-signaling.mjs +1 -1
- package/dist/generated/protocol.cjs +4 -1
- package/dist/generated/protocol.d.cts +1 -1
- package/dist/generated/protocol.d.mts +1 -1
- package/dist/generated/protocol.mjs +4 -1
- package/dist/{identity-DVCNuFLG.d.cts → identity-DJbr96J3.d.cts} +1 -1
- package/dist/{identity-DcaYgjXT.d.mts → identity-ViYdZrIU.d.mts} +1 -1
- package/dist/ports/identity.d.cts +1 -1
- package/dist/ports/identity.d.mts +1 -1
- package/dist/ports/transport.d.cts +1 -1
- package/dist/ports/transport.d.mts +1 -1
- package/dist/{protocol-DCDae3z4.d.cts → protocol-BgR1e_NI.d.cts} +3 -0
- package/dist/{protocol-DCDae3z4.d.mts → protocol-BgR1e_NI.d.mts} +3 -0
- package/dist/{room-token-verification-CkGnFuQc.d.mts → room-token-verification-7zD4ib-A.d.mts} +2 -2
- package/dist/{room-token-verification-PSv4eQ1x.d.cts → room-token-verification-hFtBDZPO.d.cts} +2 -2
- package/dist/{tokens-DqFu7iMX.d.cts → tokens-BMWCnQEF.d.cts} +37 -5
- package/dist/{tokens-CMdWiRBb.mjs → tokens-D8JlTJNA.mjs} +175 -17
- package/dist/{tokens-CBiSYuzp.d.mts → tokens-DRkm_QOR.d.mts} +37 -5
- package/dist/{tokens-DSG3SDr8.cjs → tokens-tPJQ7Vzt.cjs} +186 -16
- package/dist/{transport-B0GywEzG.d.mts → transport-DP3hdehk.d.cts} +1 -1
- package/dist/{transport-DIXjoQKn.d.cts → transport-Dv_LTCsF.d.mts} +1 -1
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as Listener, r as Transport, t as Connection } from "../transport-
|
|
1
|
+
import { n as Listener, r as Transport, t as Connection } from "../transport-Dv_LTCsF.mjs";
|
|
2
2
|
export { Connection, Listener, Transport };
|
|
@@ -1564,9 +1564,12 @@ declare const tokenClaimsSchema: z.ZodLazy<z.ZodObject<{
|
|
|
1564
1564
|
expires: z.ZodNumber;
|
|
1565
1565
|
"not-before": z.ZodOptional<z.ZodNumber>;
|
|
1566
1566
|
parent: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
|
|
1567
|
+
"authorised-by": z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
|
|
1567
1568
|
"delegations-remaining": z.ZodOptional<z.ZodNumber>;
|
|
1568
1569
|
"valid-until": z.ZodOptional<z.ZodNumber>;
|
|
1569
1570
|
conditions: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
|
|
1571
|
+
"grants-capability": z.ZodOptional<z.ZodLazy<z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>]>>>>;
|
|
1572
|
+
"requests-capability": z.ZodOptional<z.ZodLazy<z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>]>>>>;
|
|
1570
1573
|
}, z.core.$catchall<z.ZodUnknown>>>;
|
|
1571
1574
|
declare const pingFrameSchema: z.ZodLazy<z.ZodObject<{
|
|
1572
1575
|
type: z.ZodLiteral<"ping">;
|
|
@@ -1564,9 +1564,12 @@ declare const tokenClaimsSchema: z.ZodLazy<z.ZodObject<{
|
|
|
1564
1564
|
expires: z.ZodNumber;
|
|
1565
1565
|
"not-before": z.ZodOptional<z.ZodNumber>;
|
|
1566
1566
|
parent: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
|
|
1567
|
+
"authorised-by": z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
|
|
1567
1568
|
"delegations-remaining": z.ZodOptional<z.ZodNumber>;
|
|
1568
1569
|
"valid-until": z.ZodOptional<z.ZodNumber>;
|
|
1569
1570
|
conditions: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
|
|
1571
|
+
"grants-capability": z.ZodOptional<z.ZodLazy<z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>]>>>>;
|
|
1572
|
+
"requests-capability": z.ZodOptional<z.ZodLazy<z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>]>>>>;
|
|
1570
1573
|
}, z.core.$catchall<z.ZodUnknown>>>;
|
|
1571
1574
|
declare const pingFrameSchema: z.ZodLazy<z.ZodObject<{
|
|
1572
1575
|
type: z.ZodLiteral<"ping">;
|
package/dist/{room-token-verification-CkGnFuQc.d.mts → room-token-verification-7zD4ib-A.d.mts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as IdentityKey, D as DeviceId, en as TokenClaims, p as CapabilityToken } from "./protocol-
|
|
2
|
-
import {
|
|
1
|
+
import { B as IdentityKey, D as DeviceId, en as TokenClaims, p as CapabilityToken } from "./protocol-BgR1e_NI.mjs";
|
|
2
|
+
import { d as VerifyCapabilityTokenOptions, u as TokenVerdictReason } from "./tokens-DRkm_QOR.mjs";
|
|
3
3
|
//#region src/domain/room-token-verification.d.ts
|
|
4
4
|
/** The one capability every core/room verb (room.send/read/leave/members) is gated by, per spec/room.cddl -- room.join/room.invite are deliberately ungated instead and need no token check at all. */
|
|
5
5
|
declare const ROOM_MEMBER_CAPABILITY = "room:member";
|
package/dist/{room-token-verification-PSv4eQ1x.d.cts → room-token-verification-hFtBDZPO.d.cts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as IdentityKey, D as DeviceId, en as TokenClaims, p as CapabilityToken } from "./protocol-
|
|
2
|
-
import {
|
|
1
|
+
import { B as IdentityKey, D as DeviceId, en as TokenClaims, p as CapabilityToken } from "./protocol-BgR1e_NI.cjs";
|
|
2
|
+
import { d as VerifyCapabilityTokenOptions, u as TokenVerdictReason } from "./tokens-BMWCnQEF.cjs";
|
|
3
3
|
//#region src/domain/room-token-verification.d.ts
|
|
4
4
|
/** The one capability every core/room verb (room.send/read/leave/members) is gated by, per spec/room.cddl -- room.join/room.invite are deliberately ungated instead and need no token check at all. */
|
|
5
5
|
declare const ROOM_MEMBER_CAPABILITY = "room:member";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "./protocol-
|
|
2
|
-
import { t as IdentityPort } from "./identity-
|
|
1
|
+
import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "./protocol-BgR1e_NI.cjs";
|
|
2
|
+
import { t as IdentityPort } from "./identity-DJbr96J3.cjs";
|
|
3
3
|
import { t as Clock } from "./clock-DiSx-WKM.cjs";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { JsonValue, PredicateNode, Resolution } from "trilean";
|
|
@@ -15,11 +15,23 @@ interface NarrowingCandidate {
|
|
|
15
15
|
interface ConditionsContext {
|
|
16
16
|
readonly clock: Clock;
|
|
17
17
|
readonly claims: Readonly<TokenClaims>;
|
|
18
|
+
/** The minted child a grant-capability's conditions are being evaluated against (wire-mesh#323 subject mode): present only inside an authorised-by verification walk or a mint-time authorisation check, never when a token's conditions are evaluated against its own claims. */
|
|
19
|
+
readonly subject?: Readonly<GrantSubject>;
|
|
20
|
+
}
|
|
21
|
+
/** The child half of a subject-mode conditions evaluation: the claims of the token a grant-capability authorises, structurally what NarrowingCandidate already is plus the bearer the mint named. Satisfied by a full TokenClaims, so the verifier passes the child's decoded claims directly. */
|
|
22
|
+
interface GrantSubject {
|
|
23
|
+
capability: TokenClaims["capability"];
|
|
24
|
+
scope: TokenClaims["scope"];
|
|
25
|
+
bearer: DeviceId;
|
|
26
|
+
expires: number;
|
|
27
|
+
delegationsRemaining?: number;
|
|
18
28
|
}
|
|
19
29
|
/** One delegate system's own handler: given the delegate node's payload and the concrete context it was invoked with, resolves to a boolean-valued Resolution. Handlers never throw for a data-quality problem -- an op that cannot make sense of its own payload/context returns `{found: false}`, which the evaluator turns into a fail-closed `indeterminate`. */
|
|
20
30
|
type TokenDelegateHandler<TContext> = (payload: JsonValue, context: TContext) => Resolution | Promise<Resolution>;
|
|
21
31
|
//#endregion
|
|
22
32
|
//#region src/domain/tokens.d.ts
|
|
33
|
+
/** The capability verb of a grant-capability (wire-mesh#323): authority to mint other capability tokens, the same register manage:revoke already established for authority over tokens. */
|
|
34
|
+
declare const MANAGE_GRANT_CAPABILITY = "manage:grant";
|
|
23
35
|
/**
|
|
24
36
|
* The revocation view a verifier consults. Returns every recorded, already-signature-verified revocation-claims for tokenId, across every issuer that has ever submitted one -- unfiltered by the store itself. The actual verifier obligation (an entry counts against a token when its own issuer matches the token's own issuer, OR its optional `authorization` grants delegated revoke authority -- management.cddl) is checked by the caller (verifyTokenChain), not here, since that check needs the target token's own scope plus identity/clock to verify a nested authorization token, none of which a store constructed ahead of time has access to. Implementations ingest gossiped revocation-announce frames via verifyRevocationEntry (which enforces each entry's own signature and self-certification) and key the resulting claims by token-id alone -- a token-id can legitimately carry multiple recorded entries from different issuers.
|
|
25
37
|
*/
|
|
@@ -27,6 +39,12 @@ interface RevocationCheck {
|
|
|
27
39
|
entriesFor: (tokenId: Uint8Array) => Promise<readonly RevocationClaims[]>;
|
|
28
40
|
}
|
|
29
41
|
type TokenVerdictReason = "malformed" | "bad_signature" | "wrong_issuer" | "bearer_mismatch" | "expired" | "not_yet_valid" | "content_expired" | "revoked" | "delegation_exceeds_parent" | "parent_invalid" |
|
|
42
|
+
/** The authorised-by link's own obligations failed: the cited grant-capability did not decode or fully verify, its bearer is not this token's issuer, its capability is not manage:grant, its grants-capability does not match, the scope or expiry widened, the depth bound was exceeded, or its conditions (evaluated against THIS token's claims) did not hold. Collapsed to one reason the same way every narrowing failure already collapses to delegation_exceeds_parent. */
|
|
43
|
+
"authorisation_invalid" |
|
|
44
|
+
/** The parent/authorised-by walk exceeded MAX_CHAIN_DEPTH. */
|
|
45
|
+
"chain_too_deep" |
|
|
46
|
+
/** The walk revisited a payload already on the current path: a cycle two colluding issuers constructed. */
|
|
47
|
+
"chain_cycle" |
|
|
30
48
|
/** claims.conditions is present but its bstr fails CBOR decode, or decodes to something that is not a JSON array of valid trilean PredicateNodes -- fail-closed per CONVENTIONS.md's verifier-obligations glossary, the same treatment "malformed" already gives an undecodable payload. */
|
|
31
49
|
"conditions_invalid" |
|
|
32
50
|
/** claims.conditions decoded and validated, but at least one entry did not evaluate to a definite `true` (indeterminate or false) -- the issuer's own additional restriction was not met. */
|
|
@@ -75,7 +93,13 @@ interface VerifyRevocationEntryOptions {
|
|
|
75
93
|
* Verifies one gossiped revocation-entry (management.cddl): a well-formed COSE_Sign1 whose signature verifies against its own embedded issuer-key, where that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id). A verifier that ingests a revocation-announce frame runs each entry through this before recording it in its revocation view; entries failing here are dropped, not stored. The issuer-match against a specific token's own issuer (only a token's own issuer may revoke it) is deliberately NOT checked here -- it happens at lookup time in RevocationCheck, against whichever token is being verified.
|
|
76
94
|
*/
|
|
77
95
|
declare function verifyRevocationEntry(entry: RevocationEntry, options: VerifyRevocationEntryOptions): Promise<RevocationEntryVerdict>;
|
|
78
|
-
type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent"
|
|
96
|
+
type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent" |
|
|
97
|
+
/** parent and authorised-by are mutually exclusive link kinds; a manage:grant capability must be minted through authorised-by only. */
|
|
98
|
+
"links_exclusive" |
|
|
99
|
+
/** The cited grant-capability did not decode, or its own conditions bytes did not. */
|
|
100
|
+
"authorisation_malformed" | "authorisation_parent_forbidden" | "authorisation_bearer_mismatch" | "authorisation_capability_mismatch" | "authorisation_scope_does_not_narrow" | "authorisation_exceeds_authoriser" | "authorisation_delegation_exceeds" |
|
|
101
|
+
/** The authoriser's conditions, evaluated against the candidate being minted, did not all hold (wire-mesh#323 subject mode). */
|
|
102
|
+
"authorisation_conditions_not_satisfied";
|
|
79
103
|
type MintVerdict = {
|
|
80
104
|
ok: true;
|
|
81
105
|
token: CapabilityToken;
|
|
@@ -84,13 +108,15 @@ type MintVerdict = {
|
|
|
84
108
|
reason: MintRefusalReason;
|
|
85
109
|
};
|
|
86
110
|
/**
|
|
87
|
-
* A pure query: could deviceId, presenting heldToken as its own delegation authority, successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
|
|
111
|
+
* A pure query: could deviceId, presenting heldToken as its own delegation authority," successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
|
|
88
112
|
*
|
|
89
113
|
* Deliberately narrower than a full mint attempt in one respect: this checks only the narrowing rules tokens.cddl's own delegation obligations require (bearer match, expiry, scope, capability, delegations-remaining), the same scope mintCapabilityToken itself checks a *parent* against -- it does not verify heldToken's own signature or revocation status, exactly as mintCapabilityToken never re-verifies its own parent's signature either. A caller that also needs heldToken's cryptographic validity confirmed calls verifyCapabilityToken separately.
|
|
90
114
|
*
|
|
91
115
|
* Async as of the predicate-list evaluator (issue #85): checkNarrowing now routes through trilean's own evaluatePredicate, which is asynchronous throughout -- there is no synchronous path through a real evaluator call, so this is a genuine, deliberate breaking change to what was previously a synchronous pure function. No existing consumer of wire-mesh-core calls canGrant (confirmed against agent-comms, the only other repo depending on this package), so there is nothing to migrate.
|
|
92
116
|
*/
|
|
93
117
|
declare function canGrant(heldToken: CapabilityToken, deviceId: DeviceId, candidate: Readonly<NarrowingCandidate>, now: number): Promise<boolean>;
|
|
118
|
+
/** A pure query: could deviceId, presenting heldGrantCapability as its minting authority, mint a token matching candidate right now (wire-mesh#323) -- canGrant's authorised-by analogue, with the same deliberately-narrower-than-verify caveat canGrant's own doc comment states: the held token's signature and revocation status are not checked here; a caller needing those confirmed calls verifyCapabilityToken separately. The candidate carries a bearer because the authoriser's subject-mode conditions read it (the no-self-grant bar). */
|
|
119
|
+
declare function canGrantVia(heldGrantCapability: CapabilityToken, deviceId: DeviceId, candidate: Readonly<GrantSubject>, now: number): Promise<boolean>;
|
|
94
120
|
interface MintCapabilityTokenOptions {
|
|
95
121
|
/** The issuer -- signs the token, and supplies the self-certifying issuer/issuer-key claims. */
|
|
96
122
|
identity: IdentityPort;
|
|
@@ -107,6 +133,12 @@ interface MintCapabilityTokenOptions {
|
|
|
107
133
|
parent?: CapabilityToken;
|
|
108
134
|
/** Additional, issuer-chosen restrictions beyond the five mandatory narrowing checks (issue #85) -- CBOR-encoded into claims.conditions verbatim, evaluated by every verifier via evaluateConditions. Strictly additive: has no bearing on narrowing, which mint enforces separately above regardless of what's given here. Not re-validated against trilean's own schema before encoding -- the TS type already guarantees a well-formed PredicateNode[] at this call site, unlike the bytes a verifier decodes from an untrusted wire token, which always are. */
|
|
109
135
|
conditions?: PredicateNode[];
|
|
136
|
+
/** The grant-capability authorising this mint (wire-mesh#323): the resulting token carries it as authorised-by, and every link obligation (bearer match, manage:grant verb, grants-capability match, scope and expiry narrowing, depth consumption, and the authoriser's subject-mode conditions against this very candidate) is enforced here, at issuance, rather than left for the far end as a bare authorisation_invalid. Mutually exclusive with parent. */
|
|
137
|
+
authorisedBy?: CapabilityToken;
|
|
138
|
+
/** On a manage:grant token: the single verb it authorises minting. Absent means any verb within scope. */
|
|
139
|
+
grantsCapability?: TokenClaims["capability"];
|
|
140
|
+
/** On a manage:request token (wire-mesh#324): the single verb it authorises requesting. Absent means any verb within scope. */
|
|
141
|
+
requestsCapability?: TokenClaims["capability"];
|
|
110
142
|
}
|
|
111
143
|
/**
|
|
112
144
|
* Mints one capability token: builds token-claims from the given fields, signs it as a COSE_Sign1 under `identity`'s own key, with a protected header matching what the frozen conformance vectors actually encode (`{1: alg, 4: issuer device-id}`, not the empty header a token merely needs to verify against itself).
|
|
@@ -123,4 +155,4 @@ interface MintRevocationEntryOptions {
|
|
|
123
155
|
/** Mints one revocation-entry (management.cddl): a COSE_Sign1 over revocation-claims, signed the same way mintCapabilityToken signs a token. No narrowing chain to check -- a revocation entry has no parent and cannot fail to be issuable the way a delegated token can, so this returns the entry directly rather than a verdict. */
|
|
124
156
|
declare function mintRevocationEntry(options: MintRevocationEntryOptions): Promise<RevocationEntry>;
|
|
125
157
|
//#endregion
|
|
126
|
-
export {
|
|
158
|
+
export { sig1ToBeSigned as _, MintVerdict as a, ConditionsContext as b, RevocationEntryVerdictReason as c, VerifyCapabilityTokenOptions as d, VerifyRevocationEntryOptions as f, mintRevocationEntry as g, mintCapabilityToken as h, MintRevocationEntryOptions as i, TokenVerdict as l, canGrantVia as m, MintCapabilityTokenOptions as n, RevocationCheck as o, canGrant as p, MintRefusalReason as r, RevocationEntryVerdict as s, MANAGE_GRANT_CAPABILITY as t, TokenVerdictReason as u, verifyCapabilityToken as v, TokenDelegateHandler as x, verifyRevocationEntry as y };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { capabilityTokenSchema, revocationClaimsSchema, tokenClaimsSchema } from "./generated/protocol.mjs";
|
|
2
|
+
import { bytesToHex, deviceIdToHex } from "./domain/device-id.mjs";
|
|
2
3
|
import { n as scopeNarrows, t as bytesEqual } from "./token-scope-Z4bmci4M.mjs";
|
|
3
4
|
import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -24,6 +25,24 @@ const NARROWING_SYSTEMS = [
|
|
|
24
25
|
function isNarrowingSystem(system) {
|
|
25
26
|
return system === "bearer-is" || system === "expires-at" || system === "scope-narrows" || system === "capability-is" || system === "depth-remaining";
|
|
26
27
|
}
|
|
28
|
+
/** The two subject-mode delegate systems (wire-mesh#323), registered only when a ConditionsContext carries a subject, and indeterminate (fail-closed) whenever one does not. */
|
|
29
|
+
const GRANTEE_IS = "grantee-is";
|
|
30
|
+
const GRANTED_CAPABILITY_IS = "granted-capability-is";
|
|
31
|
+
function isSubjectSystem(system) {
|
|
32
|
+
return system === "grantee-is" || system === "granted-capability-is";
|
|
33
|
+
}
|
|
34
|
+
const subjectHandlers = {
|
|
35
|
+
[GRANTEE_IS]: (payload, context) => {
|
|
36
|
+
if (context.subject === void 0) return { found: false };
|
|
37
|
+
if (typeof payload !== "string") return { found: false };
|
|
38
|
+
return booleanFound(deviceIdToHex(context.subject.bearer) === payload);
|
|
39
|
+
},
|
|
40
|
+
[GRANTED_CAPABILITY_IS]: (payload, context) => {
|
|
41
|
+
if (context.subject === void 0) return { found: false };
|
|
42
|
+
if (typeof payload !== "string") return { found: false };
|
|
43
|
+
return booleanFound(context.subject.capability === payload);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
27
46
|
function booleanFound(value) {
|
|
28
47
|
return {
|
|
29
48
|
found: true,
|
|
@@ -58,15 +77,15 @@ const narrowingHandlers = {
|
|
|
58
77
|
return booleanFound(context.candidate.delegationsRemaining !== void 0 && context.candidate.delegationsRemaining < parentRemaining);
|
|
59
78
|
}
|
|
60
79
|
};
|
|
61
|
-
/** `{kind:"compare", op:"eq", left:{kind:"delegate", system, payload
|
|
62
|
-
function delegateIsTrue(system) {
|
|
80
|
+
/** `{kind:"compare", op:"eq", left:{kind:"delegate", system, payload}, right:{kind:"booleanLiteral", value:true}}` -- delegate is an ExpressionNode, not itself a PredicateNode (trilean's PredicateNodeSchema has no `delegate` member), so every predicate op is expressed this way: resolve the delegate to a boolean ComputedValue, then compare it against the literal `true`. The narrowing checks pass no payload (every fact they need arrives via context); the subject-mode systems take their comparison value as the payload. */
|
|
81
|
+
function delegateIsTrue(system, payload = null) {
|
|
63
82
|
return {
|
|
64
83
|
kind: "compare",
|
|
65
84
|
op: "eq",
|
|
66
85
|
left: {
|
|
67
86
|
kind: "delegate",
|
|
68
87
|
system,
|
|
69
|
-
payload
|
|
88
|
+
payload
|
|
70
89
|
},
|
|
71
90
|
right: {
|
|
72
91
|
kind: "booleanLiteral",
|
|
@@ -104,16 +123,25 @@ async function evaluateNarrowing(parentClaims, childIssuer, candidate) {
|
|
|
104
123
|
/**
|
|
105
124
|
* Evaluates token-claims' own `conditions` field (issue #85): additional, issuer-chosen predicates strictly additive to the five mandatory narrowing checks above, which are computed structurally (from a delegated token's own claims and its parent's) and never read from this field -- the five built-ins are inherently about a parent/child relationship, so they are not meaningfully reusable inside a bare, parent-less conditions evaluation, unlike a genuine domain-specific condition (a message TTL, #84's future revoke-authorization check) which reasons about the presented token's own claims directly. `extraHandlers` is the registration point for those: none exist yet, so a `conditions` entry naming any system with no registered handler resolves `{found: false}` -- indeterminate, fail-closed. Absent `nodes` (an empty array, the caller's own signal for "no conditions field present") is trivially `{ok: true}` -- no extra restrictions, exactly today's behaviour. Every entry MUST evaluate to a definite `true`; the loop short-circuits on the first indeterminate or false result.
|
|
106
125
|
*/
|
|
107
|
-
async function evaluateConditions(nodes, claims, clock, extraHandlers = {}) {
|
|
108
|
-
const context = {
|
|
126
|
+
async function evaluateConditions(nodes, claims, clock, extraHandlers = {}, subject) {
|
|
127
|
+
const context = subject === void 0 ? {
|
|
109
128
|
clock,
|
|
110
129
|
claims
|
|
130
|
+
} : {
|
|
131
|
+
clock,
|
|
132
|
+
claims,
|
|
133
|
+
subject
|
|
111
134
|
};
|
|
112
135
|
const resolvers = {
|
|
113
136
|
resolveValue: unusedResolveValue,
|
|
114
137
|
resolveLookup: unusedResolveLookup,
|
|
115
138
|
resolveCollection: unusedResolveCollection,
|
|
116
139
|
resolveDelegate: async (system, payload) => {
|
|
140
|
+
if (subject !== void 0 && isSubjectSystem(system)) try {
|
|
141
|
+
return await subjectHandlers[system](payload, context);
|
|
142
|
+
} catch {
|
|
143
|
+
return { found: false };
|
|
144
|
+
}
|
|
117
145
|
const handler = Object.hasOwn(extraHandlers, system) ? extraHandlers[system] : void 0;
|
|
118
146
|
if (handler === void 0) return { found: false };
|
|
119
147
|
try {
|
|
@@ -134,6 +162,10 @@ async function evaluateConditions(nodes, claims, clock, extraHandlers = {}) {
|
|
|
134
162
|
}
|
|
135
163
|
//#endregion
|
|
136
164
|
//#region src/domain/tokens.ts
|
|
165
|
+
/** The capability verb of a grant-capability (wire-mesh#323): authority to mint other capability tokens, the same register manage:revoke already established for authority over tokens. */
|
|
166
|
+
const MANAGE_GRANT_CAPABILITY = "manage:grant";
|
|
167
|
+
/** Both link kinds walk at most this many hops before the verifier gives up, mirroring Rust's own MAX_CHAIN_DEPTH. Two colluding issuers can construct a mutually-bearing parent cycle that would otherwise recurse forever, and the authorised-by link makes the attack cheaper to mount, so TS gains the bound rather than relying on JS recursion limits. */
|
|
168
|
+
const MAX_CHAIN_DEPTH = 64;
|
|
137
169
|
/** RFC 9052 §4.4 Sig_structure for a COSE_Sign1 with no external AAD: ["Signature1", protected, external_aad, payload]. Exported (not just used internally by verifyTokenChain) so any other self-certifying COSE_Sign1 construction in this codebase -- e.g. threshold-subject.ts's own to-be-signed bytes for a FROST-signed capability-token/revocation-entry/handle-record/room-notice -- reuses this single construction rather than a second hand-rolled one. */
|
|
138
170
|
function sig1ToBeSigned(protectedHeader, payload) {
|
|
139
171
|
return encode([
|
|
@@ -207,12 +239,22 @@ async function revocationEntryGrantsRevoke(entry, targetClaims, options) {
|
|
|
207
239
|
});
|
|
208
240
|
return authorizationVerdict.ok && authorizationVerdict.claims.capability === "manage:revoke" && scopeNarrows(authorizationVerdict.claims.scope, targetClaims.scope);
|
|
209
241
|
}
|
|
210
|
-
async function verifyTokenChain(token, options) {
|
|
242
|
+
async function verifyTokenChain(token, options, subjectClaims, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
211
243
|
const [protectedHeader, , payload, signature] = token;
|
|
212
244
|
if (payload === null) return {
|
|
213
245
|
ok: false,
|
|
214
246
|
reason: "malformed"
|
|
215
247
|
};
|
|
248
|
+
if (depth >= MAX_CHAIN_DEPTH) return {
|
|
249
|
+
ok: false,
|
|
250
|
+
reason: "chain_too_deep"
|
|
251
|
+
};
|
|
252
|
+
const payloadHex = bytesToHex(payload);
|
|
253
|
+
if (seen.has(payloadHex)) return {
|
|
254
|
+
ok: false,
|
|
255
|
+
reason: "chain_cycle"
|
|
256
|
+
};
|
|
257
|
+
const deeperSeen = new Set(seen).add(payloadHex);
|
|
216
258
|
let decodedClaims;
|
|
217
259
|
try {
|
|
218
260
|
decodedClaims = decode(payload, cdeDecodeOptions);
|
|
@@ -271,15 +313,25 @@ async function verifyTokenChain(token, options) {
|
|
|
271
313
|
ok: false,
|
|
272
314
|
reason: "conditions_invalid"
|
|
273
315
|
};
|
|
274
|
-
if (!(await evaluateConditions(conditionsResult.data, claims, options.clock, options.extraPredicateResolvers)).ok) return {
|
|
316
|
+
if (!(await evaluateConditions(conditionsResult.data, claims, options.clock, options.extraPredicateResolvers, subjectClaims)).ok) return {
|
|
275
317
|
ok: false,
|
|
276
318
|
reason: "conditions_not_satisfied"
|
|
277
319
|
};
|
|
278
320
|
}
|
|
279
|
-
|
|
321
|
+
const parentBytes = claims.parent;
|
|
322
|
+
const authorisationBytes = claims["authorised-by"];
|
|
323
|
+
if (claims.capability === "manage:grant" && parentBytes !== void 0) return {
|
|
324
|
+
ok: false,
|
|
325
|
+
reason: "authorisation_invalid"
|
|
326
|
+
};
|
|
327
|
+
if (parentBytes !== void 0 && authorisationBytes !== void 0) return {
|
|
328
|
+
ok: false,
|
|
329
|
+
reason: "malformed"
|
|
330
|
+
};
|
|
331
|
+
if (parentBytes !== void 0) {
|
|
280
332
|
let decodedParent;
|
|
281
333
|
try {
|
|
282
|
-
decodedParent = decode(
|
|
334
|
+
decodedParent = decode(parentBytes, cdeDecodeOptions);
|
|
283
335
|
} catch {
|
|
284
336
|
return {
|
|
285
337
|
ok: false,
|
|
@@ -291,11 +343,17 @@ async function verifyTokenChain(token, options) {
|
|
|
291
343
|
ok: false,
|
|
292
344
|
reason: "parent_invalid"
|
|
293
345
|
};
|
|
294
|
-
const parentVerdict = await verifyTokenChain(parentResult.data, options);
|
|
295
|
-
if (!parentVerdict.ok)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
346
|
+
const parentVerdict = await verifyTokenChain(parentResult.data, options, void 0, depth + 1, deeperSeen);
|
|
347
|
+
if (!parentVerdict.ok) {
|
|
348
|
+
if (parentVerdict.reason === "chain_too_deep" || parentVerdict.reason === "chain_cycle") return {
|
|
349
|
+
ok: false,
|
|
350
|
+
reason: parentVerdict.reason
|
|
351
|
+
};
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
reason: "parent_invalid"
|
|
355
|
+
};
|
|
356
|
+
}
|
|
299
357
|
const candidate = {
|
|
300
358
|
capability: claims.capability,
|
|
301
359
|
scope: claims.scope,
|
|
@@ -314,6 +372,46 @@ async function verifyTokenChain(token, options) {
|
|
|
314
372
|
depth: parentVerdict.depth + 1
|
|
315
373
|
};
|
|
316
374
|
}
|
|
375
|
+
if (authorisationBytes !== void 0) {
|
|
376
|
+
let decodedAuthoriser;
|
|
377
|
+
try {
|
|
378
|
+
decodedAuthoriser = decode(authorisationBytes, cdeDecodeOptions);
|
|
379
|
+
} catch {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
reason: "authorisation_invalid"
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const authoriserResult = capabilityTokenSchema.safeParse(decodedAuthoriser);
|
|
386
|
+
if (!authoriserResult.success) return {
|
|
387
|
+
ok: false,
|
|
388
|
+
reason: "authorisation_invalid"
|
|
389
|
+
};
|
|
390
|
+
const authoriserVerdict = await verifyTokenChain(authoriserResult.data, options, claims, depth + 1, deeperSeen);
|
|
391
|
+
if (!authoriserVerdict.ok) {
|
|
392
|
+
if (authoriserVerdict.reason === "chain_too_deep" || authoriserVerdict.reason === "chain_cycle") return {
|
|
393
|
+
ok: false,
|
|
394
|
+
reason: authoriserVerdict.reason
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
reason: "authorisation_invalid"
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const authoriser = authoriserVerdict.claims;
|
|
402
|
+
const grantsCapability = authoriser["grants-capability"];
|
|
403
|
+
if (!(bytesEqual(authoriser.bearer, claims.issuer) && authoriser.capability === "manage:grant" && (grantsCapability === void 0 || grantsCapability === claims.capability) && scopeNarrows(authoriser.scope, claims.scope) && claims.expires <= authoriser.expires && (authoriser["delegations-remaining"] === void 0 || claims["delegations-remaining"] !== void 0 && claims["delegations-remaining"] < authoriser["delegations-remaining"]))) return {
|
|
404
|
+
ok: false,
|
|
405
|
+
reason: "authorisation_invalid"
|
|
406
|
+
};
|
|
407
|
+
return {
|
|
408
|
+
ok: true,
|
|
409
|
+
claims,
|
|
410
|
+
rootIssuer: authoriserVerdict.rootIssuer,
|
|
411
|
+
rootIssuerKey: authoriserVerdict.rootIssuerKey,
|
|
412
|
+
depth: authoriserVerdict.depth + 1
|
|
413
|
+
};
|
|
414
|
+
}
|
|
317
415
|
return {
|
|
318
416
|
ok: true,
|
|
319
417
|
claims,
|
|
@@ -373,8 +471,30 @@ async function checkNarrowing(parentClaims, granterDeviceId, candidate) {
|
|
|
373
471
|
const failedSystem = await evaluateNarrowing(parentClaims, granterDeviceId, candidate);
|
|
374
472
|
return failedSystem === void 0 ? void 0 : NARROWING_SYSTEM_TO_MINT_REFUSAL[failedSystem];
|
|
375
473
|
}
|
|
474
|
+
/** The authorised-by link's own arithmetic (wire-mesh#323), the analogue checkNarrowing already is for the parent link: everything tokens.cddl's authorised-by obligations require, checked against a not-yet-minted candidate, shared between mintCapabilityToken and canGrantVia so the two can never drift. The authoriser's conditions are evaluated in subject mode against the candidate, the same evaluation the verifier performs against the minted child. Returns the specific refusal reason, or undefined when the authorisation holds. */
|
|
475
|
+
async function checkAuthorisation(authorisingClaims, minterDeviceId, candidate) {
|
|
476
|
+
if (authorisingClaims.parent !== void 0) return "authorisation_parent_forbidden";
|
|
477
|
+
if (!bytesEqual(authorisingClaims.bearer, minterDeviceId)) return "authorisation_bearer_mismatch";
|
|
478
|
+
const grantsCapability = authorisingClaims["grants-capability"];
|
|
479
|
+
if (authorisingClaims.capability !== "manage:grant" || grantsCapability !== void 0 && grantsCapability !== candidate.capability) return "authorisation_capability_mismatch";
|
|
480
|
+
if (!scopeNarrows(authorisingClaims.scope, candidate.scope)) return "authorisation_scope_does_not_narrow";
|
|
481
|
+
if (candidate.expires > authorisingClaims.expires) return "authorisation_exceeds_authoriser";
|
|
482
|
+
const remaining = authorisingClaims["delegations-remaining"];
|
|
483
|
+
if (remaining !== void 0 && (candidate.delegationsRemaining === void 0 || candidate.delegationsRemaining >= remaining)) return "authorisation_delegation_exceeds";
|
|
484
|
+
if (authorisingClaims.conditions !== void 0) {
|
|
485
|
+
let decodedConditions;
|
|
486
|
+
try {
|
|
487
|
+
decodedConditions = decode(authorisingClaims.conditions, cdeDecodeOptions);
|
|
488
|
+
} catch {
|
|
489
|
+
return "authorisation_malformed";
|
|
490
|
+
}
|
|
491
|
+
const conditionsResult = conditionsListSchema.safeParse(decodedConditions);
|
|
492
|
+
if (!conditionsResult.success) return "authorisation_malformed";
|
|
493
|
+
if (!(await evaluateConditions(conditionsResult.data, authorisingClaims, { now: () => 0 }, {}, candidate)).ok) return "authorisation_conditions_not_satisfied";
|
|
494
|
+
}
|
|
495
|
+
}
|
|
376
496
|
/**
|
|
377
|
-
* A pure query: could deviceId, presenting heldToken as its own delegation authority, successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
|
|
497
|
+
* A pure query: could deviceId, presenting heldToken as its own delegation authority," successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
|
|
378
498
|
*
|
|
379
499
|
* Deliberately narrower than a full mint attempt in one respect: this checks only the narrowing rules tokens.cddl's own delegation obligations require (bearer match, expiry, scope, capability, delegations-remaining), the same scope mintCapabilityToken itself checks a *parent* against -- it does not verify heldToken's own signature or revocation status, exactly as mintCapabilityToken never re-verifies its own parent's signature either. A caller that also needs heldToken's cryptographic validity confirmed calls verifyCapabilityToken separately.
|
|
380
500
|
*
|
|
@@ -386,6 +506,13 @@ async function canGrant(heldToken, deviceId, candidate, now) {
|
|
|
386
506
|
if (heldClaims === void 0) return false;
|
|
387
507
|
return await checkNarrowing(heldClaims, deviceId, candidate) === void 0;
|
|
388
508
|
}
|
|
509
|
+
/** A pure query: could deviceId, presenting heldGrantCapability as its minting authority, mint a token matching candidate right now (wire-mesh#323) -- canGrant's authorised-by analogue, with the same deliberately-narrower-than-verify caveat canGrant's own doc comment states: the held token's signature and revocation status are not checked here; a caller needing those confirmed calls verifyCapabilityToken separately. The candidate carries a bearer because the authoriser's subject-mode conditions read it (the no-self-grant bar). */
|
|
510
|
+
async function canGrantVia(heldGrantCapability, deviceId, candidate, now) {
|
|
511
|
+
if (candidate.expires <= now) return false;
|
|
512
|
+
const heldClaims = decodeTokenClaims(heldGrantCapability);
|
|
513
|
+
if (heldClaims === void 0) return false;
|
|
514
|
+
return await checkAuthorisation(heldClaims, deviceId, candidate) === void 0;
|
|
515
|
+
}
|
|
389
516
|
/**
|
|
390
517
|
* Mints one capability token: builds token-claims from the given fields, signs it as a COSE_Sign1 under `identity`'s own key, with a protected header matching what the frozen conformance vectors actually encode (`{1: alg, 4: issuer device-id}`, not the empty header a token merely needs to verify against itself).
|
|
391
518
|
*
|
|
@@ -396,6 +523,14 @@ async function mintCapabilityToken(options) {
|
|
|
396
523
|
ok: false,
|
|
397
524
|
reason: "already_expired"
|
|
398
525
|
};
|
|
526
|
+
if (options.parent !== void 0 && options.authorisedBy !== void 0) return {
|
|
527
|
+
ok: false,
|
|
528
|
+
reason: "links_exclusive"
|
|
529
|
+
};
|
|
530
|
+
if (options.capability === "manage:grant" && options.parent !== void 0) return {
|
|
531
|
+
ok: false,
|
|
532
|
+
reason: "authorisation_parent_forbidden"
|
|
533
|
+
};
|
|
399
534
|
let parentBytes;
|
|
400
535
|
if (options.parent !== void 0) {
|
|
401
536
|
const parentClaims = decodeTokenClaims(options.parent);
|
|
@@ -415,6 +550,26 @@ async function mintCapabilityToken(options) {
|
|
|
415
550
|
};
|
|
416
551
|
parentBytes = encodeBuf(options.parent);
|
|
417
552
|
}
|
|
553
|
+
let authorisationBytes;
|
|
554
|
+
if (options.authorisedBy !== void 0) {
|
|
555
|
+
const authorisingClaims = decodeTokenClaims(options.authorisedBy);
|
|
556
|
+
if (authorisingClaims === void 0) return {
|
|
557
|
+
ok: false,
|
|
558
|
+
reason: "authorisation_malformed"
|
|
559
|
+
};
|
|
560
|
+
const refusal = await checkAuthorisation(authorisingClaims, options.identity.deviceId, {
|
|
561
|
+
capability: options.capability,
|
|
562
|
+
scope: options.scope,
|
|
563
|
+
bearer: options.bearer,
|
|
564
|
+
expires: options.expires,
|
|
565
|
+
...options.delegationsRemaining !== void 0 ? { delegationsRemaining: options.delegationsRemaining } : {}
|
|
566
|
+
});
|
|
567
|
+
if (refusal !== void 0) return {
|
|
568
|
+
ok: false,
|
|
569
|
+
reason: refusal
|
|
570
|
+
};
|
|
571
|
+
authorisationBytes = encodeBuf(options.authorisedBy);
|
|
572
|
+
}
|
|
418
573
|
const payload = encodeBuf({
|
|
419
574
|
"token-id": options.tokenId,
|
|
420
575
|
issuer: options.identity.deviceId,
|
|
@@ -426,7 +581,10 @@ async function mintCapabilityToken(options) {
|
|
|
426
581
|
...options.notBefore !== void 0 ? { "not-before": options.notBefore } : {},
|
|
427
582
|
...parentBytes !== void 0 ? { parent: parentBytes } : {},
|
|
428
583
|
...options.delegationsRemaining !== void 0 ? { "delegations-remaining": options.delegationsRemaining } : {},
|
|
429
|
-
...options.conditions !== void 0 ? { conditions: encodeBuf(options.conditions) } : {}
|
|
584
|
+
...options.conditions !== void 0 ? { conditions: encodeBuf(options.conditions) } : {},
|
|
585
|
+
...authorisationBytes !== void 0 ? { "authorised-by": authorisationBytes } : {},
|
|
586
|
+
...options.grantsCapability !== void 0 ? { "grants-capability": options.grantsCapability } : {},
|
|
587
|
+
...options.requestsCapability !== void 0 ? { "requests-capability": options.requestsCapability } : {}
|
|
430
588
|
});
|
|
431
589
|
const protectedHeader = protectedHeaderFor(options.identity);
|
|
432
590
|
return {
|
|
@@ -456,4 +614,4 @@ async function mintRevocationEntry(options) {
|
|
|
456
614
|
];
|
|
457
615
|
}
|
|
458
616
|
//#endregion
|
|
459
|
-
export {
|
|
617
|
+
export { mintRevocationEntry as a, verifyRevocationEntry as c, mintCapabilityToken as i, canGrant as n, sig1ToBeSigned as o, canGrantVia as r, verifyCapabilityToken as s, MANAGE_GRANT_CAPABILITY as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "./protocol-
|
|
2
|
-
import { t as IdentityPort } from "./identity-
|
|
1
|
+
import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "./protocol-BgR1e_NI.mjs";
|
|
2
|
+
import { t as IdentityPort } from "./identity-ViYdZrIU.mjs";
|
|
3
3
|
import { t as Clock } from "./clock-DiSx-WKM.mjs";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { JsonValue, PredicateNode, Resolution } from "trilean";
|
|
@@ -15,11 +15,23 @@ interface NarrowingCandidate {
|
|
|
15
15
|
interface ConditionsContext {
|
|
16
16
|
readonly clock: Clock;
|
|
17
17
|
readonly claims: Readonly<TokenClaims>;
|
|
18
|
+
/** The minted child a grant-capability's conditions are being evaluated against (wire-mesh#323 subject mode): present only inside an authorised-by verification walk or a mint-time authorisation check, never when a token's conditions are evaluated against its own claims. */
|
|
19
|
+
readonly subject?: Readonly<GrantSubject>;
|
|
20
|
+
}
|
|
21
|
+
/** The child half of a subject-mode conditions evaluation: the claims of the token a grant-capability authorises, structurally what NarrowingCandidate already is plus the bearer the mint named. Satisfied by a full TokenClaims, so the verifier passes the child's decoded claims directly. */
|
|
22
|
+
interface GrantSubject {
|
|
23
|
+
capability: TokenClaims["capability"];
|
|
24
|
+
scope: TokenClaims["scope"];
|
|
25
|
+
bearer: DeviceId;
|
|
26
|
+
expires: number;
|
|
27
|
+
delegationsRemaining?: number;
|
|
18
28
|
}
|
|
19
29
|
/** One delegate system's own handler: given the delegate node's payload and the concrete context it was invoked with, resolves to a boolean-valued Resolution. Handlers never throw for a data-quality problem -- an op that cannot make sense of its own payload/context returns `{found: false}`, which the evaluator turns into a fail-closed `indeterminate`. */
|
|
20
30
|
type TokenDelegateHandler<TContext> = (payload: JsonValue, context: TContext) => Resolution | Promise<Resolution>;
|
|
21
31
|
//#endregion
|
|
22
32
|
//#region src/domain/tokens.d.ts
|
|
33
|
+
/** The capability verb of a grant-capability (wire-mesh#323): authority to mint other capability tokens, the same register manage:revoke already established for authority over tokens. */
|
|
34
|
+
declare const MANAGE_GRANT_CAPABILITY = "manage:grant";
|
|
23
35
|
/**
|
|
24
36
|
* The revocation view a verifier consults. Returns every recorded, already-signature-verified revocation-claims for tokenId, across every issuer that has ever submitted one -- unfiltered by the store itself. The actual verifier obligation (an entry counts against a token when its own issuer matches the token's own issuer, OR its optional `authorization` grants delegated revoke authority -- management.cddl) is checked by the caller (verifyTokenChain), not here, since that check needs the target token's own scope plus identity/clock to verify a nested authorization token, none of which a store constructed ahead of time has access to. Implementations ingest gossiped revocation-announce frames via verifyRevocationEntry (which enforces each entry's own signature and self-certification) and key the resulting claims by token-id alone -- a token-id can legitimately carry multiple recorded entries from different issuers.
|
|
25
37
|
*/
|
|
@@ -27,6 +39,12 @@ interface RevocationCheck {
|
|
|
27
39
|
entriesFor: (tokenId: Uint8Array) => Promise<readonly RevocationClaims[]>;
|
|
28
40
|
}
|
|
29
41
|
type TokenVerdictReason = "malformed" | "bad_signature" | "wrong_issuer" | "bearer_mismatch" | "expired" | "not_yet_valid" | "content_expired" | "revoked" | "delegation_exceeds_parent" | "parent_invalid" |
|
|
42
|
+
/** The authorised-by link's own obligations failed: the cited grant-capability did not decode or fully verify, its bearer is not this token's issuer, its capability is not manage:grant, its grants-capability does not match, the scope or expiry widened, the depth bound was exceeded, or its conditions (evaluated against THIS token's claims) did not hold. Collapsed to one reason the same way every narrowing failure already collapses to delegation_exceeds_parent. */
|
|
43
|
+
"authorisation_invalid" |
|
|
44
|
+
/** The parent/authorised-by walk exceeded MAX_CHAIN_DEPTH. */
|
|
45
|
+
"chain_too_deep" |
|
|
46
|
+
/** The walk revisited a payload already on the current path: a cycle two colluding issuers constructed. */
|
|
47
|
+
"chain_cycle" |
|
|
30
48
|
/** claims.conditions is present but its bstr fails CBOR decode, or decodes to something that is not a JSON array of valid trilean PredicateNodes -- fail-closed per CONVENTIONS.md's verifier-obligations glossary, the same treatment "malformed" already gives an undecodable payload. */
|
|
31
49
|
"conditions_invalid" |
|
|
32
50
|
/** claims.conditions decoded and validated, but at least one entry did not evaluate to a definite `true` (indeterminate or false) -- the issuer's own additional restriction was not met. */
|
|
@@ -75,7 +93,13 @@ interface VerifyRevocationEntryOptions {
|
|
|
75
93
|
* Verifies one gossiped revocation-entry (management.cddl): a well-formed COSE_Sign1 whose signature verifies against its own embedded issuer-key, where that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id). A verifier that ingests a revocation-announce frame runs each entry through this before recording it in its revocation view; entries failing here are dropped, not stored. The issuer-match against a specific token's own issuer (only a token's own issuer may revoke it) is deliberately NOT checked here -- it happens at lookup time in RevocationCheck, against whichever token is being verified.
|
|
76
94
|
*/
|
|
77
95
|
declare function verifyRevocationEntry(entry: RevocationEntry, options: VerifyRevocationEntryOptions): Promise<RevocationEntryVerdict>;
|
|
78
|
-
type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent"
|
|
96
|
+
type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent" |
|
|
97
|
+
/** parent and authorised-by are mutually exclusive link kinds; a manage:grant capability must be minted through authorised-by only. */
|
|
98
|
+
"links_exclusive" |
|
|
99
|
+
/** The cited grant-capability did not decode, or its own conditions bytes did not. */
|
|
100
|
+
"authorisation_malformed" | "authorisation_parent_forbidden" | "authorisation_bearer_mismatch" | "authorisation_capability_mismatch" | "authorisation_scope_does_not_narrow" | "authorisation_exceeds_authoriser" | "authorisation_delegation_exceeds" |
|
|
101
|
+
/** The authoriser's conditions, evaluated against the candidate being minted, did not all hold (wire-mesh#323 subject mode). */
|
|
102
|
+
"authorisation_conditions_not_satisfied";
|
|
79
103
|
type MintVerdict = {
|
|
80
104
|
ok: true;
|
|
81
105
|
token: CapabilityToken;
|
|
@@ -84,13 +108,15 @@ type MintVerdict = {
|
|
|
84
108
|
reason: MintRefusalReason;
|
|
85
109
|
};
|
|
86
110
|
/**
|
|
87
|
-
* A pure query: could deviceId, presenting heldToken as its own delegation authority, successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
|
|
111
|
+
* A pure query: could deviceId, presenting heldToken as its own delegation authority," successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
|
|
88
112
|
*
|
|
89
113
|
* Deliberately narrower than a full mint attempt in one respect: this checks only the narrowing rules tokens.cddl's own delegation obligations require (bearer match, expiry, scope, capability, delegations-remaining), the same scope mintCapabilityToken itself checks a *parent* against -- it does not verify heldToken's own signature or revocation status, exactly as mintCapabilityToken never re-verifies its own parent's signature either. A caller that also needs heldToken's cryptographic validity confirmed calls verifyCapabilityToken separately.
|
|
90
114
|
*
|
|
91
115
|
* Async as of the predicate-list evaluator (issue #85): checkNarrowing now routes through trilean's own evaluatePredicate, which is asynchronous throughout -- there is no synchronous path through a real evaluator call, so this is a genuine, deliberate breaking change to what was previously a synchronous pure function. No existing consumer of wire-mesh-core calls canGrant (confirmed against agent-comms, the only other repo depending on this package), so there is nothing to migrate.
|
|
92
116
|
*/
|
|
93
117
|
declare function canGrant(heldToken: CapabilityToken, deviceId: DeviceId, candidate: Readonly<NarrowingCandidate>, now: number): Promise<boolean>;
|
|
118
|
+
/** A pure query: could deviceId, presenting heldGrantCapability as its minting authority, mint a token matching candidate right now (wire-mesh#323) -- canGrant's authorised-by analogue, with the same deliberately-narrower-than-verify caveat canGrant's own doc comment states: the held token's signature and revocation status are not checked here; a caller needing those confirmed calls verifyCapabilityToken separately. The candidate carries a bearer because the authoriser's subject-mode conditions read it (the no-self-grant bar). */
|
|
119
|
+
declare function canGrantVia(heldGrantCapability: CapabilityToken, deviceId: DeviceId, candidate: Readonly<GrantSubject>, now: number): Promise<boolean>;
|
|
94
120
|
interface MintCapabilityTokenOptions {
|
|
95
121
|
/** The issuer -- signs the token, and supplies the self-certifying issuer/issuer-key claims. */
|
|
96
122
|
identity: IdentityPort;
|
|
@@ -107,6 +133,12 @@ interface MintCapabilityTokenOptions {
|
|
|
107
133
|
parent?: CapabilityToken;
|
|
108
134
|
/** Additional, issuer-chosen restrictions beyond the five mandatory narrowing checks (issue #85) -- CBOR-encoded into claims.conditions verbatim, evaluated by every verifier via evaluateConditions. Strictly additive: has no bearing on narrowing, which mint enforces separately above regardless of what's given here. Not re-validated against trilean's own schema before encoding -- the TS type already guarantees a well-formed PredicateNode[] at this call site, unlike the bytes a verifier decodes from an untrusted wire token, which always are. */
|
|
109
135
|
conditions?: PredicateNode[];
|
|
136
|
+
/** The grant-capability authorising this mint (wire-mesh#323): the resulting token carries it as authorised-by, and every link obligation (bearer match, manage:grant verb, grants-capability match, scope and expiry narrowing, depth consumption, and the authoriser's subject-mode conditions against this very candidate) is enforced here, at issuance, rather than left for the far end as a bare authorisation_invalid. Mutually exclusive with parent. */
|
|
137
|
+
authorisedBy?: CapabilityToken;
|
|
138
|
+
/** On a manage:grant token: the single verb it authorises minting. Absent means any verb within scope. */
|
|
139
|
+
grantsCapability?: TokenClaims["capability"];
|
|
140
|
+
/** On a manage:request token (wire-mesh#324): the single verb it authorises requesting. Absent means any verb within scope. */
|
|
141
|
+
requestsCapability?: TokenClaims["capability"];
|
|
110
142
|
}
|
|
111
143
|
/**
|
|
112
144
|
* Mints one capability token: builds token-claims from the given fields, signs it as a COSE_Sign1 under `identity`'s own key, with a protected header matching what the frozen conformance vectors actually encode (`{1: alg, 4: issuer device-id}`, not the empty header a token merely needs to verify against itself).
|
|
@@ -123,4 +155,4 @@ interface MintRevocationEntryOptions {
|
|
|
123
155
|
/** Mints one revocation-entry (management.cddl): a COSE_Sign1 over revocation-claims, signed the same way mintCapabilityToken signs a token. No narrowing chain to check -- a revocation entry has no parent and cannot fail to be issuable the way a delegated token can, so this returns the entry directly rather than a verdict. */
|
|
124
156
|
declare function mintRevocationEntry(options: MintRevocationEntryOptions): Promise<RevocationEntry>;
|
|
125
157
|
//#endregion
|
|
126
|
-
export {
|
|
158
|
+
export { sig1ToBeSigned as _, MintVerdict as a, ConditionsContext as b, RevocationEntryVerdictReason as c, VerifyCapabilityTokenOptions as d, VerifyRevocationEntryOptions as f, mintRevocationEntry as g, mintCapabilityToken as h, MintRevocationEntryOptions as i, TokenVerdict as l, canGrantVia as m, MintCapabilityTokenOptions as n, RevocationCheck as o, canGrant as p, MintRefusalReason as r, RevocationEntryVerdict as s, MANAGE_GRANT_CAPABILITY as t, TokenVerdictReason as u, verifyCapabilityToken as v, TokenDelegateHandler as x, verifyRevocationEntry as y };
|