@kya-os/mcp 1.6.1 → 1.9.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/CHANGELOG.md +117 -0
- package/README.md +228 -3
- package/dist/authz/index.d.ts +1 -0
- package/dist/authz/index.d.ts.map +1 -1
- package/dist/authz/index.js +1 -0
- package/dist/authz/index.js.map +1 -1
- package/dist/authz/oidc/oidc-adapter.d.ts +10 -2
- package/dist/authz/oidc/oidc-adapter.d.ts.map +1 -1
- package/dist/authz/oidc/oidc-adapter.js +18 -9
- package/dist/authz/oidc/oidc-adapter.js.map +1 -1
- package/dist/authz/oidc/pending-flow-store.d.ts +80 -0
- package/dist/authz/oidc/pending-flow-store.d.ts.map +1 -0
- package/dist/authz/oidc/pending-flow-store.js +82 -0
- package/dist/authz/oidc/pending-flow-store.js.map +1 -0
- package/dist/card/build.d.ts +41 -0
- package/dist/card/build.d.ts.map +1 -0
- package/dist/card/build.js +47 -0
- package/dist/card/build.js.map +1 -0
- package/dist/card/builder.d.ts +99 -0
- package/dist/card/builder.d.ts.map +1 -0
- package/dist/card/builder.js +147 -0
- package/dist/card/builder.js.map +1 -0
- package/dist/card/cimd.d.ts +92 -0
- package/dist/card/cimd.d.ts.map +1 -0
- package/dist/card/cimd.js +225 -0
- package/dist/card/cimd.js.map +1 -0
- package/dist/card/delegation.d.ts +145 -0
- package/dist/card/delegation.d.ts.map +1 -0
- package/dist/card/delegation.js +293 -0
- package/dist/card/delegation.js.map +1 -0
- package/dist/card/emit.d.ts +133 -0
- package/dist/card/emit.d.ts.map +1 -0
- package/dist/card/emit.js +127 -0
- package/dist/card/emit.js.map +1 -0
- package/dist/card/index.d.ts +43 -0
- package/dist/card/index.d.ts.map +1 -0
- package/dist/card/index.js +46 -0
- package/dist/card/index.js.map +1 -0
- package/dist/card/middleware.d.ts +112 -0
- package/dist/card/middleware.d.ts.map +1 -0
- package/dist/card/middleware.js +121 -0
- package/dist/card/middleware.js.map +1 -0
- package/dist/card/proof/build.d.ts +22 -0
- package/dist/card/proof/build.d.ts.map +1 -0
- package/dist/card/proof/build.js +55 -0
- package/dist/card/proof/build.js.map +1 -0
- package/dist/card/proof/canonical.d.ts +66 -0
- package/dist/card/proof/canonical.d.ts.map +1 -0
- package/dist/card/proof/canonical.js +131 -0
- package/dist/card/proof/canonical.js.map +1 -0
- package/dist/card/proof/http-sig.d.ts +69 -0
- package/dist/card/proof/http-sig.d.ts.map +1 -0
- package/dist/card/proof/http-sig.js +101 -0
- package/dist/card/proof/http-sig.js.map +1 -0
- package/dist/card/proof/index.d.ts +25 -0
- package/dist/card/proof/index.d.ts.map +1 -0
- package/dist/card/proof/index.js +25 -0
- package/dist/card/proof/index.js.map +1 -0
- package/dist/card/proof/nonce-cache.d.ts +67 -0
- package/dist/card/proof/nonce-cache.d.ts.map +1 -0
- package/dist/card/proof/nonce-cache.js +88 -0
- package/dist/card/proof/nonce-cache.js.map +1 -0
- package/dist/card/proof/signer.d.ts +32 -0
- package/dist/card/proof/signer.d.ts.map +1 -0
- package/dist/card/proof/signer.js +59 -0
- package/dist/card/proof/signer.js.map +1 -0
- package/dist/card/proof/types.d.ts +219 -0
- package/dist/card/proof/types.d.ts.map +1 -0
- package/dist/card/proof/types.js +87 -0
- package/dist/card/proof/types.js.map +1 -0
- package/dist/card/proof/verify.d.ts +27 -0
- package/dist/card/proof/verify.d.ts.map +1 -0
- package/dist/card/proof/verify.js +236 -0
- package/dist/card/proof/verify.js.map +1 -0
- package/dist/card/resolve.d.ts +97 -0
- package/dist/card/resolve.d.ts.map +1 -0
- package/dist/card/resolve.js +223 -0
- package/dist/card/resolve.js.map +1 -0
- package/dist/card/revocation.d.ts +96 -0
- package/dist/card/revocation.d.ts.map +1 -0
- package/dist/card/revocation.js +190 -0
- package/dist/card/revocation.js.map +1 -0
- package/dist/card/schema.d.ts +177 -0
- package/dist/card/schema.d.ts.map +1 -0
- package/dist/card/schema.js +119 -0
- package/dist/card/schema.js.map +1 -0
- package/dist/card/verify.d.ts +144 -0
- package/dist/card/verify.d.ts.map +1 -0
- package/dist/card/verify.js +132 -0
- package/dist/card/verify.js.map +1 -0
- package/dist/delegation/bitstring.d.ts +9 -7
- package/dist/delegation/bitstring.d.ts.map +1 -1
- package/dist/delegation/bitstring.js +70 -37
- package/dist/delegation/bitstring.js.map +1 -1
- package/dist/delegation/did-linkage.d.ts +23 -0
- package/dist/delegation/did-linkage.d.ts.map +1 -0
- package/dist/delegation/did-linkage.js +57 -0
- package/dist/delegation/did-linkage.js.map +1 -0
- package/dist/delegation/did-resolver-registry.d.ts +7 -0
- package/dist/delegation/did-resolver-registry.d.ts.map +1 -0
- package/dist/delegation/did-resolver-registry.js +20 -0
- package/dist/delegation/did-resolver-registry.js.map +1 -0
- package/dist/delegation/did-web-resolver.d.ts +29 -18
- package/dist/delegation/did-web-resolver.d.ts.map +1 -1
- package/dist/delegation/did-web-resolver.js +65 -35
- package/dist/delegation/did-web-resolver.js.map +1 -1
- package/dist/delegation/index.d.ts +3 -0
- package/dist/delegation/index.d.ts.map +1 -1
- package/dist/delegation/index.js +3 -0
- package/dist/delegation/index.js.map +1 -1
- package/dist/delegation/statuslist-manager.d.ts.map +1 -1
- package/dist/delegation/statuslist-manager.js +16 -1
- package/dist/delegation/statuslist-manager.js.map +1 -1
- package/dist/delegation/vc-jwt-verify.d.ts +61 -0
- package/dist/delegation/vc-jwt-verify.d.ts.map +1 -0
- package/dist/delegation/vc-jwt-verify.js +131 -0
- package/dist/delegation/vc-jwt-verify.js.map +1 -0
- package/dist/delegation/vc-verification-checks.d.ts +50 -0
- package/dist/delegation/vc-verification-checks.d.ts.map +1 -0
- package/dist/delegation/vc-verification-checks.js +212 -0
- package/dist/delegation/vc-verification-checks.js.map +1 -0
- package/dist/delegation/vc-verifier.d.ts +24 -61
- package/dist/delegation/vc-verifier.d.ts.map +1 -1
- package/dist/delegation/vc-verifier.js +57 -180
- package/dist/delegation/vc-verifier.js.map +1 -1
- package/dist/delegation/vc-verifier.types.d.ts +88 -0
- package/dist/delegation/vc-verifier.types.d.ts.map +1 -0
- package/dist/delegation/vc-verifier.types.js +9 -0
- package/dist/delegation/vc-verifier.types.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/cheqd/dlr.d.ts +44 -0
- package/dist/integrations/cheqd/dlr.d.ts.map +1 -0
- package/dist/integrations/cheqd/dlr.js +86 -0
- package/dist/integrations/cheqd/dlr.js.map +1 -0
- package/dist/integrations/cheqd/index.d.ts +5 -0
- package/dist/integrations/cheqd/index.d.ts.map +1 -0
- package/dist/integrations/cheqd/index.js +5 -0
- package/dist/integrations/cheqd/index.js.map +1 -0
- package/dist/integrations/cheqd/linkage.d.ts +20 -0
- package/dist/integrations/cheqd/linkage.d.ts.map +1 -0
- package/dist/integrations/cheqd/linkage.js +32 -0
- package/dist/integrations/cheqd/linkage.js.map +1 -0
- package/dist/integrations/cheqd/registrar.d.ts +85 -0
- package/dist/integrations/cheqd/registrar.d.ts.map +1 -0
- package/dist/integrations/cheqd/registrar.js +304 -0
- package/dist/integrations/cheqd/registrar.js.map +1 -0
- package/dist/integrations/cheqd/resolver.d.ts +38 -0
- package/dist/integrations/cheqd/resolver.d.ts.map +1 -0
- package/dist/integrations/cheqd/resolver.js +156 -0
- package/dist/integrations/cheqd/resolver.js.map +1 -0
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/kya-os-transport.d.ts +2 -1
- package/dist/middleware/kya-os-transport.d.ts.map +1 -1
- package/dist/middleware/kya-os-transport.js +2 -1
- package/dist/middleware/kya-os-transport.js.map +1 -1
- package/dist/middleware/with-kya-os-server.d.ts +21 -4
- package/dist/middleware/with-kya-os-server.d.ts.map +1 -1
- package/dist/middleware/with-kya-os-server.js +4 -0
- package/dist/middleware/with-kya-os-server.js.map +1 -1
- package/dist/middleware/with-kya-os.config-types.d.ts +139 -0
- package/dist/middleware/with-kya-os.config-types.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.config-types.js +9 -0
- package/dist/middleware/with-kya-os.config-types.js.map +1 -0
- package/dist/middleware/with-kya-os.d.ts +10 -238
- package/dist/middleware/with-kya-os.d.ts.map +1 -1
- package/dist/middleware/with-kya-os.delegation-gate.d.ts +19 -0
- package/dist/middleware/with-kya-os.delegation-gate.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.delegation-gate.js +175 -0
- package/dist/middleware/with-kya-os.delegation-gate.js.map +1 -0
- package/dist/middleware/with-kya-os.delegation-verify.d.ts +51 -0
- package/dist/middleware/with-kya-os.delegation-verify.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.delegation-verify.js +165 -0
- package/dist/middleware/with-kya-os.delegation-verify.js.map +1 -0
- package/dist/middleware/with-kya-os.deps.d.ts +40 -0
- package/dist/middleware/with-kya-os.deps.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.deps.js +9 -0
- package/dist/middleware/with-kya-os.deps.js.map +1 -0
- package/dist/middleware/with-kya-os.grants.d.ts +30 -0
- package/dist/middleware/with-kya-os.grants.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.grants.js +145 -0
- package/dist/middleware/with-kya-os.grants.js.map +1 -0
- package/dist/middleware/with-kya-os.helpers.d.ts +24 -0
- package/dist/middleware/with-kya-os.helpers.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.helpers.js +57 -0
- package/dist/middleware/with-kya-os.helpers.js.map +1 -0
- package/dist/middleware/with-kya-os.js +65 -703
- package/dist/middleware/with-kya-os.js.map +1 -1
- package/dist/middleware/with-kya-os.policy-gate.d.ts +16 -0
- package/dist/middleware/with-kya-os.policy-gate.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.policy-gate.js +98 -0
- package/dist/middleware/with-kya-os.policy-gate.js.map +1 -0
- package/dist/middleware/with-kya-os.protocol.d.ts +29 -0
- package/dist/middleware/with-kya-os.protocol.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.protocol.js +121 -0
- package/dist/middleware/with-kya-os.protocol.js.map +1 -0
- package/dist/middleware/with-kya-os.session.d.ts +32 -0
- package/dist/middleware/with-kya-os.session.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.session.js +201 -0
- package/dist/middleware/with-kya-os.session.js.map +1 -0
- package/dist/middleware/with-kya-os.types.d.ts +178 -0
- package/dist/middleware/with-kya-os.types.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.types.js +13 -0
- package/dist/middleware/with-kya-os.types.js.map +1 -0
- package/dist/proof/generator.d.ts +21 -0
- package/dist/proof/generator.d.ts.map +1 -1
- package/dist/proof/generator.js +21 -0
- package/dist/proof/generator.js.map +1 -1
- package/dist/proof/index.d.ts +1 -1
- package/dist/proof/index.d.ts.map +1 -1
- package/dist/proof/index.js +1 -1
- package/dist/proof/index.js.map +1 -1
- package/dist/proof/verifier.d.ts +26 -3
- package/dist/proof/verifier.d.ts.map +1 -1
- package/dist/proof/verifier.js +54 -24
- package/dist/proof/verifier.js.map +1 -1
- package/dist/providers/grant-store.d.ts +10 -1
- package/dist/providers/grant-store.d.ts.map +1 -1
- package/dist/providers/grant-store.js.map +1 -1
- package/dist/providers/runtime-fetch.d.ts +8 -0
- package/dist/providers/runtime-fetch.d.ts.map +1 -1
- package/dist/providers/runtime-fetch.js +17 -0
- package/dist/providers/runtime-fetch.js.map +1 -1
- package/dist/providers/web-crypto.d.ts.map +1 -1
- package/dist/providers/web-crypto.js +15 -7
- package/dist/providers/web-crypto.js.map +1 -1
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +1 -0
- package/dist/session/index.js.map +1 -1
- package/dist/session/manager.d.ts +17 -6
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +16 -26
- package/dist/session/manager.js.map +1 -1
- package/dist/session/session-store.d.ts +78 -0
- package/dist/session/session-store.d.ts.map +1 -0
- package/dist/session/session-store.js +79 -0
- package/dist/session/session-store.js.map +1 -0
- package/dist/types/protocol.d.ts +9 -3
- package/dist/types/protocol.d.ts.map +1 -1
- package/dist/types/protocol.js.map +1 -1
- package/dist/utils/guards.d.ts +2 -0
- package/dist/utils/guards.d.ts.map +1 -0
- package/dist/utils/guards.js +4 -0
- package/dist/utils/guards.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/ip-classifier.d.ts +12 -0
- package/dist/utils/ip-classifier.d.ts.map +1 -0
- package/dist/utils/ip-classifier.js +153 -0
- package/dist/utils/ip-classifier.js.map +1 -0
- package/dist/utils/safe-fetch-transports.d.ts +40 -0
- package/dist/utils/safe-fetch-transports.d.ts.map +1 -0
- package/dist/utils/safe-fetch-transports.js +121 -0
- package/dist/utils/safe-fetch-transports.js.map +1 -0
- package/dist/utils/safe-fetch-types.d.ts +66 -0
- package/dist/utils/safe-fetch-types.d.ts.map +1 -0
- package/dist/utils/safe-fetch-types.js +10 -0
- package/dist/utils/safe-fetch-types.js.map +1 -0
- package/dist/utils/safe-fetch.d.ts +40 -0
- package/dist/utils/safe-fetch.d.ts.map +1 -0
- package/dist/utils/safe-fetch.js +188 -0
- package/dist/utils/safe-fetch.js.map +1 -0
- package/dist/utils/statuslist-purpose.d.ts +12 -0
- package/dist/utils/statuslist-purpose.d.ts.map +1 -0
- package/dist/utils/statuslist-purpose.js +19 -0
- package/dist/utils/statuslist-purpose.js.map +1 -0
- package/dist/utils/url.d.ts +2 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +8 -0
- package/dist/utils/url.js.map +1 -0
- package/package.json +25 -5
- package/schemas/README.md +2 -1
- package/schemas/card-delegation-credential.json +239 -0
- package/schemas/kya-os-card.schema.json +284 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KYA-OS Entity Card — RESOLVE (discovery).
|
|
3
|
+
*
|
|
4
|
+
* Discover another entity's card — the conventional, UNAUTHENTICATED half. Discovery
|
|
5
|
+
* precedes the proof: you need the card's DID (the proof audience) before you can mint a
|
|
6
|
+
* proof against it.
|
|
7
|
+
*
|
|
8
|
+
* The card's canonical home is the `KyaOsEntityCard` service entry on the entity's
|
|
9
|
+
* `did:web` DID document, NOT a bespoke `/.well-known` file. `resolveCard` is multi-surface:
|
|
10
|
+
* - a `did:web` (or `{ did }`) resolves in TWO steps — `did.json` → the `KyaOsEntityCard`
|
|
11
|
+
* service entry → `card.json`;
|
|
12
|
+
* - `{ cardUrl }` fetches a `card.json` directly (the lazy-fetch target of a `cardRef`);
|
|
13
|
+
* - `{ serverMeta }` reads a server.json / catalog `_meta['org.kya-os/card']` entry — a
|
|
14
|
+
* `{ 'org.kya-os/cardRef' }` and a `did:web` inline summary DEREFERENCE the entity's canonical
|
|
15
|
+
* `card.json` (the summary is a discovery INDEX, its `id` names the DID, never trusted as an
|
|
16
|
+
* authoritative card); a `did:key` summary has no web home to dereference, so it is parsed
|
|
17
|
+
* directly — fail-closed because the summary carries the `revocation` kill switch and
|
|
18
|
+
* `delegationRef` (see `resolveFromServerMeta`);
|
|
19
|
+
* - `{ a2a }` follows an A2A AgentExtension's `params.cardUrl`;
|
|
20
|
+
* - `{ agentFacts }` two-steps from a NANDA AgentFacts `id` (a `did:web`).
|
|
21
|
+
*
|
|
22
|
+
* EVERY outbound fetch goes through the injected `SafeFetch` seam (SSRF-hardened, https-only,
|
|
23
|
+
* private-range-denied — see `src/utils/safe-fetch.ts`), because resolution now follows
|
|
24
|
+
* attacker-influenced URLs across four discovery surfaces.
|
|
25
|
+
*/
|
|
26
|
+
import type { SafeFetch } from '../utils/safe-fetch.js';
|
|
27
|
+
import { type EntityCard } from './schema.js';
|
|
28
|
+
/** DID-document `service[].type` string that anchors the card (we own only this string). */
|
|
29
|
+
export declare const KYA_OS_CARD_SERVICE_TYPE = "KyaOsEntityCard";
|
|
30
|
+
/** DID-document `service[].id` fragment for the card service entry. */
|
|
31
|
+
export declare const KYA_OS_CARD_SERVICE_ID = "#kya-os-card";
|
|
32
|
+
/** Reverse-DNS `_meta` key carrying the card (inline summary or a `cardRef`) on MCP surfaces. */
|
|
33
|
+
export declare const KYA_OS_CARD_META_KEY = "org.kya-os/card";
|
|
34
|
+
/** Reverse-DNS key whose value is a lazy-fetch `card.json` URL inside `org.kya-os/card`. */
|
|
35
|
+
export declare const KYA_OS_CARD_REF_KEY = "org.kya-os/cardRef";
|
|
36
|
+
/**
|
|
37
|
+
* Bare-domain (org-root) card convention. A path-form `did:web:host:a:b` anchors its card at
|
|
38
|
+
* `/a/b/card.json`, but a BARE `did:web:host` (an org root — e.g. the default trusted issuer
|
|
39
|
+
* `did:web:example.com`) has no path segment, so its card lives at this well-known path. Defined
|
|
40
|
+
* ONCE here so `didWebToCardUrl` (emit) and the summary-derive resolve path agree on one URL —
|
|
41
|
+
* closing the emit/resolve asymmetry that previously locked bare org roots out of the helpers.
|
|
42
|
+
*/
|
|
43
|
+
export declare const KYA_OS_CARD_WELL_KNOWN_PATH = ".well-known/kya-os-card.json";
|
|
44
|
+
/** A non-DID reference form `resolveCard` accepts in addition to a bare `did:web` string. */
|
|
45
|
+
export type CardRef = {
|
|
46
|
+
did: string;
|
|
47
|
+
} | {
|
|
48
|
+
cardUrl: string;
|
|
49
|
+
} | {
|
|
50
|
+
serverMeta: unknown;
|
|
51
|
+
} | {
|
|
52
|
+
a2a: unknown;
|
|
53
|
+
} | {
|
|
54
|
+
agentFacts: unknown;
|
|
55
|
+
};
|
|
56
|
+
export type ResolveCardInput = string | CardRef;
|
|
57
|
+
export interface ResolveCardDeps {
|
|
58
|
+
/** SSRF-hardened fetch seam — every outbound request is routed through it. */
|
|
59
|
+
fetch: SafeFetch;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Discover an entity's card from any supported reference form. A `did:web` resolves in two
|
|
63
|
+
* steps (`did.json` → `KyaOsEntityCard` service → `card.json`); the reference forms short-cut
|
|
64
|
+
* to the `card.json` URL (or an inline summary). `did:key` has no domain anchor, so a
|
|
65
|
+
* `did:key` card must be supplied directly (e.g. via `parseCard`), never resolved.
|
|
66
|
+
*
|
|
67
|
+
* THROW CONTRACT (fail-closed — deliberately UNLIKE the DID-METHOD resolvers, which return
|
|
68
|
+
* null so a failed method can be retried): every failure path REJECTS with a reasoned `Error`
|
|
69
|
+
* (message prefixed `resolveCard:`, or a `ZodError` from `parseCard` on an invalid card body).
|
|
70
|
+
* `resolveCard` NEVER resolves to `null`/`undefined`. Rationale: this is a security-relevant
|
|
71
|
+
* verify surface — a silently-null card would let an integrator mistake "couldn't resolve" for
|
|
72
|
+
* "no card = allow" (fail-open). Integrators MUST wrap the call and treat a throw as a hard
|
|
73
|
+
* deny. See CONTRIBUTING.md → Code Style for why this surface is scoped out of the null-on-
|
|
74
|
+
* failure resolver rule.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveCard(input: ResolveCardInput, deps: ResolveCardDeps): Promise<EntityCard>;
|
|
77
|
+
/**
|
|
78
|
+
* Map a `did:web` to its per-entity card URL:
|
|
79
|
+
* did:web:host:a:b → https://host/a/b/card.json
|
|
80
|
+
* did:web:host → https://host/.well-known/kya-os-card.json (the bare org-root convention)
|
|
81
|
+
*
|
|
82
|
+
* A bare `did:web:host` (an org root, incl. the default trusted issuer) resolves to the
|
|
83
|
+
* well-known card path so the trust anchors can publish their own card via the shipped helpers,
|
|
84
|
+
* and the emit projections agree with the summary-derive resolve path on this one URL. A
|
|
85
|
+
* non-`did:web` DID (e.g. `did:key`) has no web home, so this throws with actionable guidance:
|
|
86
|
+
* emit it onto the inline summary + AgentFacts surfaces, or supply an explicit `serviceEndpoint`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function didWebToCardUrl(did: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* Map a `did:web` to its DID document URL (W3C did:web):
|
|
91
|
+
* did:web:host:a:b → https://host/a/b/did.json
|
|
92
|
+
* did:web:host → https://host/.well-known/did.json
|
|
93
|
+
*/
|
|
94
|
+
export declare function didWebToDidDocUrl(did: string): string;
|
|
95
|
+
/** Validate + parse an unknown value into an EntityCard (throws a ZodError on mismatch). */
|
|
96
|
+
export declare function parseCard(value: unknown): EntityCard;
|
|
97
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/card/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAKhE,4FAA4F;AAC5F,eAAO,MAAM,wBAAwB,oBAAoB,CAAC;AAC1D,uEAAuE;AACvE,eAAO,MAAM,sBAAsB,iBAAiB,CAAC;AACrD,iGAAiG;AACjG,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AACxD;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,iCAAiC,CAAC;AAE1E,6FAA6F;AAC7F,MAAM,MAAM,OAAO,GACf;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GACf;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GACnB;IAAE,UAAU,EAAE,OAAO,CAAA;CAAE,GACvB;IAAE,GAAG,EAAE,OAAO,CAAA;CAAE,GAChB;IAAE,UAAU,EAAE,OAAO,CAAA;CAAE,CAAC;AAE5B,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,UAAU,CAAC,CASrB;AAmID;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED,4FAA4F;AAC5F,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAEpD"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KYA-OS Entity Card — RESOLVE (discovery).
|
|
3
|
+
*
|
|
4
|
+
* Discover another entity's card — the conventional, UNAUTHENTICATED half. Discovery
|
|
5
|
+
* precedes the proof: you need the card's DID (the proof audience) before you can mint a
|
|
6
|
+
* proof against it.
|
|
7
|
+
*
|
|
8
|
+
* The card's canonical home is the `KyaOsEntityCard` service entry on the entity's
|
|
9
|
+
* `did:web` DID document, NOT a bespoke `/.well-known` file. `resolveCard` is multi-surface:
|
|
10
|
+
* - a `did:web` (or `{ did }`) resolves in TWO steps — `did.json` → the `KyaOsEntityCard`
|
|
11
|
+
* service entry → `card.json`;
|
|
12
|
+
* - `{ cardUrl }` fetches a `card.json` directly (the lazy-fetch target of a `cardRef`);
|
|
13
|
+
* - `{ serverMeta }` reads a server.json / catalog `_meta['org.kya-os/card']` entry — a
|
|
14
|
+
* `{ 'org.kya-os/cardRef' }` and a `did:web` inline summary DEREFERENCE the entity's canonical
|
|
15
|
+
* `card.json` (the summary is a discovery INDEX, its `id` names the DID, never trusted as an
|
|
16
|
+
* authoritative card); a `did:key` summary has no web home to dereference, so it is parsed
|
|
17
|
+
* directly — fail-closed because the summary carries the `revocation` kill switch and
|
|
18
|
+
* `delegationRef` (see `resolveFromServerMeta`);
|
|
19
|
+
* - `{ a2a }` follows an A2A AgentExtension's `params.cardUrl`;
|
|
20
|
+
* - `{ agentFacts }` two-steps from a NANDA AgentFacts `id` (a `did:web`).
|
|
21
|
+
*
|
|
22
|
+
* EVERY outbound fetch goes through the injected `SafeFetch` seam (SSRF-hardened, https-only,
|
|
23
|
+
* private-range-denied — see `src/utils/safe-fetch.ts`), because resolution now follows
|
|
24
|
+
* attacker-influenced URLs across four discovery surfaces.
|
|
25
|
+
*/
|
|
26
|
+
import { isRecord } from '../utils/guards.js';
|
|
27
|
+
import { EntityCardSchema } from './schema.js';
|
|
28
|
+
// ── Canonical discovery constants (co-located in the lowest module so `emit.ts`, which
|
|
29
|
+
// builds these surfaces, can depend on `resolve.ts` one-way without an import cycle) ──
|
|
30
|
+
/** DID-document `service[].type` string that anchors the card (we own only this string). */
|
|
31
|
+
export const KYA_OS_CARD_SERVICE_TYPE = 'KyaOsEntityCard';
|
|
32
|
+
/** DID-document `service[].id` fragment for the card service entry. */
|
|
33
|
+
export const KYA_OS_CARD_SERVICE_ID = '#kya-os-card';
|
|
34
|
+
/** Reverse-DNS `_meta` key carrying the card (inline summary or a `cardRef`) on MCP surfaces. */
|
|
35
|
+
export const KYA_OS_CARD_META_KEY = 'org.kya-os/card';
|
|
36
|
+
/** Reverse-DNS key whose value is a lazy-fetch `card.json` URL inside `org.kya-os/card`. */
|
|
37
|
+
export const KYA_OS_CARD_REF_KEY = 'org.kya-os/cardRef';
|
|
38
|
+
/**
|
|
39
|
+
* Bare-domain (org-root) card convention. A path-form `did:web:host:a:b` anchors its card at
|
|
40
|
+
* `/a/b/card.json`, but a BARE `did:web:host` (an org root — e.g. the default trusted issuer
|
|
41
|
+
* `did:web:example.com`) has no path segment, so its card lives at this well-known path. Defined
|
|
42
|
+
* ONCE here so `didWebToCardUrl` (emit) and the summary-derive resolve path agree on one URL —
|
|
43
|
+
* closing the emit/resolve asymmetry that previously locked bare org roots out of the helpers.
|
|
44
|
+
*/
|
|
45
|
+
export const KYA_OS_CARD_WELL_KNOWN_PATH = '.well-known/kya-os-card.json';
|
|
46
|
+
/**
|
|
47
|
+
* Discover an entity's card from any supported reference form. A `did:web` resolves in two
|
|
48
|
+
* steps (`did.json` → `KyaOsEntityCard` service → `card.json`); the reference forms short-cut
|
|
49
|
+
* to the `card.json` URL (or an inline summary). `did:key` has no domain anchor, so a
|
|
50
|
+
* `did:key` card must be supplied directly (e.g. via `parseCard`), never resolved.
|
|
51
|
+
*
|
|
52
|
+
* THROW CONTRACT (fail-closed — deliberately UNLIKE the DID-METHOD resolvers, which return
|
|
53
|
+
* null so a failed method can be retried): every failure path REJECTS with a reasoned `Error`
|
|
54
|
+
* (message prefixed `resolveCard:`, or a `ZodError` from `parseCard` on an invalid card body).
|
|
55
|
+
* `resolveCard` NEVER resolves to `null`/`undefined`. Rationale: this is a security-relevant
|
|
56
|
+
* verify surface — a silently-null card would let an integrator mistake "couldn't resolve" for
|
|
57
|
+
* "no card = allow" (fail-open). Integrators MUST wrap the call and treat a throw as a hard
|
|
58
|
+
* deny. See CONTRIBUTING.md → Code Style for why this surface is scoped out of the null-on-
|
|
59
|
+
* failure resolver rule.
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveCard(input, deps) {
|
|
62
|
+
const { fetch } = deps;
|
|
63
|
+
if (typeof input === 'string')
|
|
64
|
+
return resolveDidWeb(input, fetch);
|
|
65
|
+
if ('did' in input)
|
|
66
|
+
return resolveDidWeb(input.did, fetch);
|
|
67
|
+
if ('cardUrl' in input)
|
|
68
|
+
return fetchCard(input.cardUrl, fetch);
|
|
69
|
+
if ('serverMeta' in input)
|
|
70
|
+
return resolveFromServerMeta(input.serverMeta, fetch);
|
|
71
|
+
if ('a2a' in input)
|
|
72
|
+
return fetchCard(cardUrlFromA2A(input.a2a), fetch);
|
|
73
|
+
if ('agentFacts' in input)
|
|
74
|
+
return resolveDidWeb(didFromAgentFacts(input.agentFacts), fetch);
|
|
75
|
+
throw new Error('resolveCard: unrecognized reference form');
|
|
76
|
+
}
|
|
77
|
+
/** Two-step `did:web` resolution: `did.json` → `KyaOsEntityCard` service → `card.json`. */
|
|
78
|
+
async function resolveDidWeb(did, fetch) {
|
|
79
|
+
if (!did.startsWith('did:web:')) {
|
|
80
|
+
throw new Error(`resolveCard: only did:web is resolvable (got "${did}"); supply a did:key card directly`);
|
|
81
|
+
}
|
|
82
|
+
const didDoc = await fetchJson(didWebToDidDocUrl(did), fetch);
|
|
83
|
+
return fetchCard(cardEndpointFromDidDoc(didDoc, did), fetch, did);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read a server.json / catalog `_meta` block and resolve it fail-closed. An explicit
|
|
87
|
+
* `org.kya-os/cardRef` is a lazy-fetch URL; an inline summary is a discovery INDEX handled by
|
|
88
|
+
* `resolveInlineSummary` (dereference for `did:web`, parse-in-place for `did:key`).
|
|
89
|
+
*/
|
|
90
|
+
async function resolveFromServerMeta(serverMeta, fetch) {
|
|
91
|
+
const entry = isRecord(serverMeta) ? serverMeta[KYA_OS_CARD_META_KEY] : undefined;
|
|
92
|
+
if (entry === undefined) {
|
|
93
|
+
throw new Error(`resolveCard: serverMeta has no "${KYA_OS_CARD_META_KEY}" entry`);
|
|
94
|
+
}
|
|
95
|
+
const ref = isRecord(entry) ? entry[KYA_OS_CARD_REF_KEY] : undefined;
|
|
96
|
+
if (typeof ref === 'string')
|
|
97
|
+
return fetchCard(ref, fetch);
|
|
98
|
+
return resolveInlineSummary(entry, fetch);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve an inline `_meta` summary fail-closed. A `did:web` summary is a discovery INDEX: its
|
|
102
|
+
* `id` names the DID, so we DEREFERENCE the canonical `card.json` and never trust the (claim-
|
|
103
|
+
* minimal) summary as a first-class card — landing back on the one authoritative source that
|
|
104
|
+
* carries `revocation`/`delegationRef`/`attestations`, so a revoked card cannot fail open. A
|
|
105
|
+
* `did:key` id has NO web home to dereference, so the summary is parsed directly; this stays
|
|
106
|
+
* fail-closed because the summary now carries the `revocation` kill switch and `delegationRef`
|
|
107
|
+
* (the projection is self-verifiable), so `verifyCard`'s status-list checker is still consulted
|
|
108
|
+
* and a revoked `did:key` card verifies `ok:false`.
|
|
109
|
+
*
|
|
110
|
+
* SECURITY (§12.6): the `did:key` branch trusts the *presented* summary — a `did:key` has no
|
|
111
|
+
* origin-served `card.json` to dereference, so an intermediary that strips `revocation` from an
|
|
112
|
+
* unsigned `did:key` summary can hide a revocation. `did:web` is REQUIRED for revocable Entities.
|
|
113
|
+
*/
|
|
114
|
+
async function resolveInlineSummary(entry, fetch) {
|
|
115
|
+
const id = isRecord(entry) ? entry.id : undefined;
|
|
116
|
+
if (typeof id !== 'string') {
|
|
117
|
+
throw new Error(`resolveCard: inline "${KYA_OS_CARD_META_KEY}" summary has no string id (DID)`);
|
|
118
|
+
}
|
|
119
|
+
if (id.startsWith('did:web:'))
|
|
120
|
+
return fetchCard(didWebToCardUrl(id), fetch, id);
|
|
121
|
+
return parseCard(entry);
|
|
122
|
+
}
|
|
123
|
+
/** Locate the `KyaOsEntityCard` service entry's endpoint on a DID document (fail-closed). */
|
|
124
|
+
function cardEndpointFromDidDoc(didDoc, did) {
|
|
125
|
+
const services = isRecord(didDoc) && Array.isArray(didDoc.service) ? didDoc.service : [];
|
|
126
|
+
for (const svc of services) {
|
|
127
|
+
if (isRecord(svc) &&
|
|
128
|
+
svc.type === KYA_OS_CARD_SERVICE_TYPE &&
|
|
129
|
+
typeof svc.serviceEndpoint === 'string') {
|
|
130
|
+
return svc.serviceEndpoint;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`resolveCard: DID document for "${did}" has no ${KYA_OS_CARD_SERVICE_TYPE} service entry`);
|
|
134
|
+
}
|
|
135
|
+
/** Pull the `card.json` URL out of an A2A AgentExtension's `params.cardUrl` (fail-closed). */
|
|
136
|
+
function cardUrlFromA2A(a2a) {
|
|
137
|
+
const params = isRecord(a2a) ? a2a.params : undefined;
|
|
138
|
+
const cardUrl = isRecord(params) ? params.cardUrl : undefined;
|
|
139
|
+
if (typeof cardUrl !== 'string') {
|
|
140
|
+
throw new Error('resolveCard: A2A extension has no string params.cardUrl');
|
|
141
|
+
}
|
|
142
|
+
return cardUrl;
|
|
143
|
+
}
|
|
144
|
+
/** Pull the agent DID out of a NANDA AgentFacts document's `id` (fail-closed). */
|
|
145
|
+
function didFromAgentFacts(agentFacts) {
|
|
146
|
+
const id = isRecord(agentFacts) ? agentFacts.id : undefined;
|
|
147
|
+
if (typeof id !== 'string') {
|
|
148
|
+
throw new Error('resolveCard: AgentFacts has no string id (DID)');
|
|
149
|
+
}
|
|
150
|
+
return id;
|
|
151
|
+
}
|
|
152
|
+
/** GET a URL through the SafeFetch seam and return its parsed JSON (throws on non-2xx). */
|
|
153
|
+
async function fetchJson(url, fetch) {
|
|
154
|
+
const res = await fetch(url);
|
|
155
|
+
if (!res.ok)
|
|
156
|
+
throw new Error(`resolveCard: GET ${url} → ${res.status}`);
|
|
157
|
+
return res.json();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* GET a `card.json` URL and validate it into an `EntityCard`. When `expectedDid` is supplied — every
|
|
161
|
+
* DID-anchored resolve path (`did:web` two-step, inline `did:web` summary, AgentFacts) — BIND the
|
|
162
|
+
* fetched card to it: its `id` MUST equal that DID (normalized), else FAIL CLOSED. This closes the
|
|
163
|
+
* cross-origin confusion where a DID document's `KyaOsEntityCard` service entry points at a card for
|
|
164
|
+
* a DIFFERENT DID (or a compromised third-party endpoint), which would otherwise attribute another
|
|
165
|
+
* entity's claims/capabilities to the resolved DID. The `{ cardUrl }` and A2A paths carry no
|
|
166
|
+
* independent DID to bind against, so they pass no `expectedDid` (the URL itself is the trust root).
|
|
167
|
+
*/
|
|
168
|
+
async function fetchCard(url, fetch, expectedDid) {
|
|
169
|
+
const card = parseCard(await fetchJson(url, fetch));
|
|
170
|
+
if (expectedDid !== undefined && normalizeDid(card.id) !== normalizeDid(expectedDid)) {
|
|
171
|
+
throw new Error(`resolveCard: resolved card id "${card.id}" ≠ requested DID "${expectedDid}" (identity mismatch) — fail-closed`);
|
|
172
|
+
}
|
|
173
|
+
return card;
|
|
174
|
+
}
|
|
175
|
+
/** Decompose a `did:web` into its host + slash-joined path segments. */
|
|
176
|
+
function parseDidWeb(did) {
|
|
177
|
+
const parts = did.slice('did:web:'.length).split(':').map(decodeURIComponent);
|
|
178
|
+
return { host: parts[0] ?? '', path: parts.slice(1).join('/') };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Canonicalize a DID for identity comparison. For `did:web` the DNS host is case-INsensitive and the
|
|
182
|
+
* segments are percent-decoded (reusing {@link parseDidWeb}), so `did:web:Example.com` binds to
|
|
183
|
+
* `did:web:example.com`; other DID methods (e.g. `did:key`, case-sensitive base58) compare verbatim.
|
|
184
|
+
*/
|
|
185
|
+
function normalizeDid(did) {
|
|
186
|
+
if (!did.startsWith('did:web:'))
|
|
187
|
+
return did;
|
|
188
|
+
const { host, path } = parseDidWeb(did);
|
|
189
|
+
const base = `did:web:${host.toLowerCase()}`;
|
|
190
|
+
return path ? `${base}:${path.split('/').join(':')}` : base;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Map a `did:web` to its per-entity card URL:
|
|
194
|
+
* did:web:host:a:b → https://host/a/b/card.json
|
|
195
|
+
* did:web:host → https://host/.well-known/kya-os-card.json (the bare org-root convention)
|
|
196
|
+
*
|
|
197
|
+
* A bare `did:web:host` (an org root, incl. the default trusted issuer) resolves to the
|
|
198
|
+
* well-known card path so the trust anchors can publish their own card via the shipped helpers,
|
|
199
|
+
* and the emit projections agree with the summary-derive resolve path on this one URL. A
|
|
200
|
+
* non-`did:web` DID (e.g. `did:key`) has no web home, so this throws with actionable guidance:
|
|
201
|
+
* emit it onto the inline summary + AgentFacts surfaces, or supply an explicit `serviceEndpoint`.
|
|
202
|
+
*/
|
|
203
|
+
export function didWebToCardUrl(did) {
|
|
204
|
+
if (!did.startsWith('did:web:')) {
|
|
205
|
+
throw new Error(`didWebToCardUrl: only a did:web has a web card URL (got "${did}"); a did:key card has no web home — emit it onto the inline summary + AgentFacts surfaces, or supply an explicit serviceEndpoint`);
|
|
206
|
+
}
|
|
207
|
+
const { host, path } = parseDidWeb(did);
|
|
208
|
+
return path ? `https://${host}/${path}/card.json` : `https://${host}/${KYA_OS_CARD_WELL_KNOWN_PATH}`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Map a `did:web` to its DID document URL (W3C did:web):
|
|
212
|
+
* did:web:host:a:b → https://host/a/b/did.json
|
|
213
|
+
* did:web:host → https://host/.well-known/did.json
|
|
214
|
+
*/
|
|
215
|
+
export function didWebToDidDocUrl(did) {
|
|
216
|
+
const { host, path } = parseDidWeb(did);
|
|
217
|
+
return path ? `https://${host}/${path}/did.json` : `https://${host}/.well-known/did.json`;
|
|
218
|
+
}
|
|
219
|
+
/** Validate + parse an unknown value into an EntityCard (throws a ZodError on mismatch). */
|
|
220
|
+
export function parseCard(value) {
|
|
221
|
+
return EntityCardSchema.parse(value);
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/card/resolve.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KYA-OS Entity Card — REVOCATION seam (W3C Bitstring Status List v1.0).
|
|
3
|
+
*
|
|
4
|
+
* Capability + delegation credentials are revocable, so a verifier must be able to ask "is
|
|
5
|
+
* THIS credential still live?" without the question's shape leaking into callers. This module
|
|
6
|
+
* ships that question as a pluggable seam — `RevocationChecker` — so the VC-2.0 / status-list
|
|
7
|
+
* churn stays behind one interface (the same injected-seam discipline `verifyCard` already
|
|
8
|
+
* uses for its capability / attestation / accountability verifiers; NO `mcp-i-core` runtime
|
|
9
|
+
* dependency leaks into `@kya-os/mcp`).
|
|
10
|
+
*
|
|
11
|
+
* `createRevocationChecker` is the DEFAULT implementation over W3C Bitstring Status List v1.0
|
|
12
|
+
* (`BitstringStatusListEntry` — the StatusList2021 SUCCESSOR). It resolves the entry's
|
|
13
|
+
* `statusListCredential` through the injected, SSRF-hardened {@link SafeFetch} seam, inflates
|
|
14
|
+
* the multibase / GZIP `encodedList` bitstring, and reads the bit at `statusListIndex`.
|
|
15
|
+
*
|
|
16
|
+
* FAIL-CLOSED is the invariant: an unreachable status list, a malformed credential, a
|
|
17
|
+
* mismatched `statusPurpose`, or an out-of-range index all resolve to `{ revoked: true }` —
|
|
18
|
+
* the absence of proof-of-liveness is treated as revoked, never as "probably fine".
|
|
19
|
+
*
|
|
20
|
+
* Each verdict also reports `fresh`: `true` only when read from a LIVE, in-validity-window
|
|
21
|
+
* status list. A stale (past `validUntil`) or not-yet-valid list still yields a readable bit
|
|
22
|
+
* but `fresh: false`. {@link evaluateRevocationChain} walks a delegation chain root→leaf and
|
|
23
|
+
* fail-closes (cascading: a revoked parent invalidates the whole subtree) on the first
|
|
24
|
+
* revoked/unresolvable hop, threading `fresh` through every hop. The caller decides the gate:
|
|
25
|
+
* L2 accepts a non-revoked chain (cached / offline-verifiable); L3 additionally requires
|
|
26
|
+
* `fresh` (a live status check). NOT wired into `verifyCard` here — a later step injects it.
|
|
27
|
+
*/
|
|
28
|
+
import type { SafeFetch } from '../utils/safe-fetch.js';
|
|
29
|
+
import type { BitstringStatusListEntry } from './schema.js';
|
|
30
|
+
/** The `statusPurpose` this checker evaluates by default (revocation, not suspension). */
|
|
31
|
+
export declare const STATUS_PURPOSE_REVOCATION = "revocation";
|
|
32
|
+
/**
|
|
33
|
+
* Inflate a GZIP `encodedList` payload to the raw status bitstring. The seam that keeps this
|
|
34
|
+
* verifier ISOMORPHIC: the default is Node-only (`node:zlib`), but a Workers / Deno / browser
|
|
35
|
+
* deployment injects a `DecompressionStream`-based impl so the bundle never pulls `node:zlib`.
|
|
36
|
+
* MAY be sync or async; MUST reject (throw / reject) on non-inflatable input (fail-closed).
|
|
37
|
+
*/
|
|
38
|
+
export type Decompress = (bytes: Uint8Array) => Uint8Array | Promise<Uint8Array>;
|
|
39
|
+
/** Verdict for ONE status entry. */
|
|
40
|
+
export interface RevocationStatus {
|
|
41
|
+
/** True iff the status bit is set OR the status could not be proven clear (fail-closed). */
|
|
42
|
+
revoked: boolean;
|
|
43
|
+
/** True iff read from a LIVE, in-validity-window status list (drives the L2/L3 gate). */
|
|
44
|
+
fresh: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The injected revocation seam: resolve a `BitstringStatusListEntry` to a {@link RevocationStatus}.
|
|
48
|
+
* A runtime supplies an implementation (the default is {@link createRevocationChecker}); a
|
|
49
|
+
* caller may inject a cache-backed or offline variant. Implementations MUST fail closed.
|
|
50
|
+
*/
|
|
51
|
+
export type RevocationChecker = (entry: BitstringStatusListEntry) => Promise<RevocationStatus>;
|
|
52
|
+
/** Result of a cascading root→leaf chain walk. */
|
|
53
|
+
export interface RevocationChainResult {
|
|
54
|
+
/** True iff EVERY hop is non-revoked (fail-closed: any revoked/unresolvable hop ⇒ false). */
|
|
55
|
+
ok: boolean;
|
|
56
|
+
/** True iff EVERY checked hop was read from a live, in-window status list. */
|
|
57
|
+
fresh: boolean;
|
|
58
|
+
/** Fail-closed reasons (empty ⇒ ok). */
|
|
59
|
+
reasons: string[];
|
|
60
|
+
}
|
|
61
|
+
/** Dependencies for the default Bitstring Status List v1.0 checker. */
|
|
62
|
+
export interface RevocationCheckerDeps {
|
|
63
|
+
/** SSRF-hardened fetch seam used to resolve the `statusListCredential`. */
|
|
64
|
+
fetch: SafeFetch;
|
|
65
|
+
/** Injectable clock returning epoch MILLISECONDS (deterministic freshness tests). */
|
|
66
|
+
now?: () => number;
|
|
67
|
+
/** `statusPurpose` to honour; default {@link STATUS_PURPOSE_REVOCATION}. */
|
|
68
|
+
statusPurpose?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Injectable GZIP inflation seam ({@link Decompress}). DEFAULT = `node:zlib` `gunzipSync` bounded
|
|
71
|
+
* to the 16 MiB {@link MAX_STATUS_LIST_BYTES} `maxOutputLength` cap (so a decompression bomb
|
|
72
|
+
* THROWS mid-inflation ⇒ fail-closed, never allocating the whole payload). Non-Node runtimes
|
|
73
|
+
* inject a `DecompressionStream`-based impl; when they do, the default is never referenced so a
|
|
74
|
+
* bundler tree-shakes `node:zlib` out of the graph. An injected impl SHOULD bound its own output
|
|
75
|
+
* — {@link decodeStatusList} still applies the cap as a post-inflation backstop regardless.
|
|
76
|
+
*/
|
|
77
|
+
decompress?: Decompress;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build the default `RevocationChecker` over W3C Bitstring Status List v1.0. Every outbound
|
|
81
|
+
* request rides the injected {@link SafeFetch}; any failure on the path (unreachable, non-2xx,
|
|
82
|
+
* malformed, wrong purpose, out-of-range index) resolves {@link FAIL_CLOSED}.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createRevocationChecker(deps: RevocationCheckerDeps): RevocationChecker;
|
|
85
|
+
/**
|
|
86
|
+
* Walk a delegation/capability chain root→leaf and fail-close on the first revoked or
|
|
87
|
+
* unresolvable hop (cascading — a revoked ancestor invalidates the subtree, so the walk
|
|
88
|
+
* short-circuits). `fresh` is the AND of every checked hop's liveness.
|
|
89
|
+
*
|
|
90
|
+
* An empty set of status entries is trivially `ok` (nothing is revoked — an offline/L2-acceptable
|
|
91
|
+
* verdict) but is NOT `fresh`: freshness is a LIVE-check signal, and zero checks obtained no live
|
|
92
|
+
* signal. An L3 liveness gate that requires `fresh` therefore cannot be satisfied by a chain that
|
|
93
|
+
* simply omits all `credentialStatus` entries — it must consult at least one live status list.
|
|
94
|
+
*/
|
|
95
|
+
export declare function evaluateRevocationChain(entries: readonly BitstringStatusListEntry[], check: RevocationChecker): Promise<RevocationChainResult>;
|
|
96
|
+
//# sourceMappingURL=revocation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revocation.d.ts","sourceRoot":"","sources":["../../src/card/revocation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAE5D,0FAA0F;AAC1F,eAAO,MAAM,yBAAyB,eAAe,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAcjF,oCAAoC;AACpC,MAAM,WAAW,gBAAgB;IAC/B,4FAA4F;IAC5F,OAAO,EAAE,OAAO,CAAC;IACjB,yFAAyF;IACzF,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,wBAAwB,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE/F,kDAAkD;AAClD,MAAM,WAAW,qBAAqB;IACpC,6FAA6F;IAC7F,EAAE,EAAE,OAAO,CAAC;IACZ,8EAA8E;IAC9E,KAAK,EAAE,OAAO,CAAC;IACf,wCAAwC;IACxC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,uEAAuE;AACvE,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,KAAK,EAAE,SAAS,CAAC;IACjB,qFAAqF;IACrF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAKD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CAetF;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,SAAS,wBAAwB,EAAE,EAC5C,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,qBAAqB,CAAC,CAiBhC"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KYA-OS Entity Card — REVOCATION seam (W3C Bitstring Status List v1.0).
|
|
3
|
+
*
|
|
4
|
+
* Capability + delegation credentials are revocable, so a verifier must be able to ask "is
|
|
5
|
+
* THIS credential still live?" without the question's shape leaking into callers. This module
|
|
6
|
+
* ships that question as a pluggable seam — `RevocationChecker` — so the VC-2.0 / status-list
|
|
7
|
+
* churn stays behind one interface (the same injected-seam discipline `verifyCard` already
|
|
8
|
+
* uses for its capability / attestation / accountability verifiers; NO `mcp-i-core` runtime
|
|
9
|
+
* dependency leaks into `@kya-os/mcp`).
|
|
10
|
+
*
|
|
11
|
+
* `createRevocationChecker` is the DEFAULT implementation over W3C Bitstring Status List v1.0
|
|
12
|
+
* (`BitstringStatusListEntry` — the StatusList2021 SUCCESSOR). It resolves the entry's
|
|
13
|
+
* `statusListCredential` through the injected, SSRF-hardened {@link SafeFetch} seam, inflates
|
|
14
|
+
* the multibase / GZIP `encodedList` bitstring, and reads the bit at `statusListIndex`.
|
|
15
|
+
*
|
|
16
|
+
* FAIL-CLOSED is the invariant: an unreachable status list, a malformed credential, a
|
|
17
|
+
* mismatched `statusPurpose`, or an out-of-range index all resolve to `{ revoked: true }` —
|
|
18
|
+
* the absence of proof-of-liveness is treated as revoked, never as "probably fine".
|
|
19
|
+
*
|
|
20
|
+
* Each verdict also reports `fresh`: `true` only when read from a LIVE, in-validity-window
|
|
21
|
+
* status list. A stale (past `validUntil`) or not-yet-valid list still yields a readable bit
|
|
22
|
+
* but `fresh: false`. {@link evaluateRevocationChain} walks a delegation chain root→leaf and
|
|
23
|
+
* fail-closes (cascading: a revoked parent invalidates the whole subtree) on the first
|
|
24
|
+
* revoked/unresolvable hop, threading `fresh` through every hop. The caller decides the gate:
|
|
25
|
+
* L2 accepts a non-revoked chain (cached / offline-verifiable); L3 additionally requires
|
|
26
|
+
* `fresh` (a live status check). NOT wired into `verifyCard` here — a later step injects it.
|
|
27
|
+
*/
|
|
28
|
+
import { base64urlDecodeToBytes } from '../utils/base64.js';
|
|
29
|
+
import { isRecord } from '../utils/guards.js';
|
|
30
|
+
import { assertStatusPurpose } from '../utils/statuslist-purpose.js';
|
|
31
|
+
/** The `statusPurpose` this checker evaluates by default (revocation, not suspension). */
|
|
32
|
+
export const STATUS_PURPOSE_REVOCATION = 'revocation';
|
|
33
|
+
/** Multibase prefix for an unpadded base64url payload (W3C Bitstring Status List `encodedList`). */
|
|
34
|
+
const MULTIBASE_BASE64URL = 'u';
|
|
35
|
+
/**
|
|
36
|
+
* Hard ceiling on the INFLATED status bitstring (16 MiB ⇒ ~134M entries — far beyond any
|
|
37
|
+
* herd-privacy list). A status bitstring is fixed-size, so a generous fixed cap costs nothing
|
|
38
|
+
* legitimate while stopping a decompression bomb: without it, the ~786 KB of GZIP that fits
|
|
39
|
+
* inside SafeFetch's 1 MiB body cap inflates to ~800 MB, blocking the event loop on a synchronous
|
|
40
|
+
* `gunzipSync`. Exceeding the cap makes `gunzipSync` throw, which the checker converts to
|
|
41
|
+
* FAIL_CLOSED — preserving the module's uniform fail-closed posture. */
|
|
42
|
+
const MAX_STATUS_LIST_BYTES = 16 * 1024 * 1024;
|
|
43
|
+
/** Fail-closed verdict: status indeterminate ⇒ treat as revoked AND not fresh. */
|
|
44
|
+
const FAIL_CLOSED = { revoked: true, fresh: false };
|
|
45
|
+
/**
|
|
46
|
+
* Build the default `RevocationChecker` over W3C Bitstring Status List v1.0. Every outbound
|
|
47
|
+
* request rides the injected {@link SafeFetch}; any failure on the path (unreachable, non-2xx,
|
|
48
|
+
* malformed, wrong purpose, out-of-range index) resolves {@link FAIL_CLOSED}.
|
|
49
|
+
*/
|
|
50
|
+
export function createRevocationChecker(deps) {
|
|
51
|
+
const clock = deps.now ?? (() => Date.now());
|
|
52
|
+
const wantedPurpose = deps.statusPurpose ?? STATUS_PURPOSE_REVOCATION;
|
|
53
|
+
const decompress = deps.decompress ?? defaultNodeGunzip;
|
|
54
|
+
return async function check(entry) {
|
|
55
|
+
try {
|
|
56
|
+
const credential = await fetchStatusList(entry.statusListCredential, deps.fetch);
|
|
57
|
+
assertStatusPurpose(statusListSubject(credential).statusPurpose, wantedPurpose);
|
|
58
|
+
const bits = await decodeStatusList(credential, decompress);
|
|
59
|
+
const revoked = readBit(bits, parseIndex(entry.statusListIndex));
|
|
60
|
+
return { revoked, fresh: isLive(credential, clock()) };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return FAIL_CLOSED;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Walk a delegation/capability chain root→leaf and fail-close on the first revoked or
|
|
69
|
+
* unresolvable hop (cascading — a revoked ancestor invalidates the subtree, so the walk
|
|
70
|
+
* short-circuits). `fresh` is the AND of every checked hop's liveness.
|
|
71
|
+
*
|
|
72
|
+
* An empty set of status entries is trivially `ok` (nothing is revoked — an offline/L2-acceptable
|
|
73
|
+
* verdict) but is NOT `fresh`: freshness is a LIVE-check signal, and zero checks obtained no live
|
|
74
|
+
* signal. An L3 liveness gate that requires `fresh` therefore cannot be satisfied by a chain that
|
|
75
|
+
* simply omits all `credentialStatus` entries — it must consult at least one live status list.
|
|
76
|
+
*/
|
|
77
|
+
export async function evaluateRevocationChain(entries, check) {
|
|
78
|
+
let fresh = entries.length > 0; // no entries ⇒ no live signal ⇒ not fresh (fail-closed for L3)
|
|
79
|
+
for (const [hop, entry] of entries.entries()) {
|
|
80
|
+
const status = await check(entry);
|
|
81
|
+
fresh = fresh && status.fresh;
|
|
82
|
+
if (status.revoked) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
fresh: false,
|
|
86
|
+
reasons: [
|
|
87
|
+
`revocation: hop ${hop} (${entry.statusListCredential}#${entry.statusListIndex}) ` +
|
|
88
|
+
'is revoked or unresolvable — fail-closed, subtree invalidated',
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, fresh, reasons: [] };
|
|
94
|
+
}
|
|
95
|
+
// ── Internals ──────────────────────────────────────────────────────────────────
|
|
96
|
+
/** GET the status-list credential through SafeFetch (throws on non-2xx / non-object body). */
|
|
97
|
+
async function fetchStatusList(url, fetch) {
|
|
98
|
+
const res = await fetch(url);
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
throw new Error(`revocation: status-list GET ${url} → ${res.status}`);
|
|
101
|
+
const body = await res.json();
|
|
102
|
+
if (!isRecord(body))
|
|
103
|
+
throw new Error('revocation: status-list credential is not an object');
|
|
104
|
+
return body;
|
|
105
|
+
}
|
|
106
|
+
/** The `credentialSubject` of a status-list credential (throws if absent). */
|
|
107
|
+
function statusListSubject(credential) {
|
|
108
|
+
const subject = credential.credentialSubject;
|
|
109
|
+
if (!isRecord(subject))
|
|
110
|
+
throw new Error('revocation: status-list has no credentialSubject');
|
|
111
|
+
return subject;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* DEFAULT {@link Decompress}: `node:zlib` `gunzipSync` bounded to {@link MAX_STATUS_LIST_BYTES}
|
|
115
|
+
* so an oversized inflation THROWS mid-stream (fail-closed) instead of allocating the whole bomb.
|
|
116
|
+
* Pulled in via a DYNAMIC `import('node:zlib')` — only when no `decompress` was injected — so a
|
|
117
|
+
* Workers / Deno / browser bundle that supplies its own seam keeps `node:zlib` out of the graph.
|
|
118
|
+
*/
|
|
119
|
+
async function defaultNodeGunzip(bytes) {
|
|
120
|
+
const { gunzipSync } = await import('node:zlib');
|
|
121
|
+
return new Uint8Array(gunzipSync(bytes, { maxOutputLength: MAX_STATUS_LIST_BYTES }));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Inflate the multibase / GZIP `encodedList` into the raw status bitstring bytes through the
|
|
125
|
+
* injected {@link Decompress} seam. The 16 MiB cap is re-checked here as a POST-inflation backstop:
|
|
126
|
+
* the default gunzip caps mid-stream, but an injected (edge) decompressor that does not bound its
|
|
127
|
+
* own output is still fail-closed against an oversized bitstring at this seam.
|
|
128
|
+
*/
|
|
129
|
+
async function decodeStatusList(credential, decompress) {
|
|
130
|
+
const encoded = statusListSubject(credential).encodedList;
|
|
131
|
+
if (typeof encoded !== 'string' || encoded.length === 0) {
|
|
132
|
+
throw new Error('revocation: status-list has no encodedList');
|
|
133
|
+
}
|
|
134
|
+
const base64url = encoded[0] === MULTIBASE_BASE64URL ? encoded.slice(1) : encoded;
|
|
135
|
+
const inflated = await decompress(base64urlDecodeToBytes(base64url));
|
|
136
|
+
if (inflated.length > MAX_STATUS_LIST_BYTES) {
|
|
137
|
+
throw new Error(`revocation: status list too large: ${inflated.length} bytes exceeds ${MAX_STATUS_LIST_BYTES}`);
|
|
138
|
+
}
|
|
139
|
+
return inflated instanceof Uint8Array ? inflated : new Uint8Array(inflated);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse the decimal `statusListIndex` (a non-negative integer expressed as a string), fail-closed.
|
|
143
|
+
* MUST be canonical decimal only: `Number()` alone would coerce whitespace (`" "` → 0), hex
|
|
144
|
+
* (`"0x2A"` → 42), octal, `"+42"`, and `"1e1"` → 10 — silently reading a DIFFERENT (often clear) bit
|
|
145
|
+
* than the credential names, so a revoked credential could read as live. Reject anything that is not
|
|
146
|
+
* `[0-9]+`.
|
|
147
|
+
*/
|
|
148
|
+
function parseIndex(raw) {
|
|
149
|
+
if (!/^[0-9]+$/.test(raw)) {
|
|
150
|
+
throw new Error(`revocation: statusListIndex must be a canonical non-negative decimal (got "${raw}")`);
|
|
151
|
+
}
|
|
152
|
+
const index = Number(raw);
|
|
153
|
+
if (!Number.isSafeInteger(index) || index < 0) {
|
|
154
|
+
throw new Error(`revocation: statusListIndex out of range "${raw}"`);
|
|
155
|
+
}
|
|
156
|
+
return index;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Read the status bit at `index` — MSB-first within each byte (W3C Bitstring encoding).
|
|
160
|
+
* Uses FULL-PRECISION arithmetic (`Math.floor(index / 8)`, `index % 8`) rather than the 32-bit
|
|
161
|
+
* bitwise ops `>>>`/`&`, which coerce their operand to uint32 first: an index ≥ 2^32 would
|
|
162
|
+
* silently wrap onto a low byte (e.g. 2^32+5 ⇒ byte 0, bit 5) and read the WRONG — often clear —
|
|
163
|
+
* bit, turning an out-of-range index that MUST fail-closed into a fail-OPEN "not revoked". With
|
|
164
|
+
* full-precision arithmetic an out-of-range byte offset indexes past the bitstring, yielding
|
|
165
|
+
* `undefined` ⇒ throw ⇒ FAIL_CLOSED. */
|
|
166
|
+
function readBit(bits, index) {
|
|
167
|
+
const byte = bits[Math.floor(index / 8)];
|
|
168
|
+
if (byte === undefined) {
|
|
169
|
+
throw new Error(`revocation: statusListIndex ${index} is out of range for the status list`);
|
|
170
|
+
}
|
|
171
|
+
return (byte & (0x80 >> (index % 8))) !== 0;
|
|
172
|
+
}
|
|
173
|
+
/** True iff `nowMs` falls inside the credential's `[validFrom, validUntil]` window (if declared). */
|
|
174
|
+
function isLive(credential, nowMs) {
|
|
175
|
+
const validUntil = parseDate(credential.validUntil ?? credential.expirationDate);
|
|
176
|
+
if (validUntil !== undefined && validUntil < nowMs)
|
|
177
|
+
return false; // stale → cached-OK at L2 only
|
|
178
|
+
const validFrom = parseDate(credential.validFrom ?? credential.issuanceDate);
|
|
179
|
+
if (validFrom !== undefined && validFrom > nowMs)
|
|
180
|
+
return false; // not yet valid
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
/** Parse an ISO-8601 date string to epoch ms, or `undefined` when absent / unparseable. */
|
|
184
|
+
function parseDate(value) {
|
|
185
|
+
if (typeof value !== 'string')
|
|
186
|
+
return undefined;
|
|
187
|
+
const ms = Date.parse(value);
|
|
188
|
+
return Number.isNaN(ms) ? undefined : ms;
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=revocation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revocation.js","sourceRoot":"","sources":["../../src/card/revocation.ts"],"names":[],"mappings":"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"}
|