@fora-protocol/sdk 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +37 -0
  3. package/gen/ts/vocab/functiontokens.js +78 -0
  4. package/gen/ts/vocab/functiontokens.ts +84 -0
  5. package/gen/ts/vocab/geographytokens.js +30 -0
  6. package/gen/ts/vocab/geographytokens.ts +37 -0
  7. package/gen/ts/vocab/pricingunits.js +56 -0
  8. package/gen/ts/vocab/pricingunits.ts +63 -0
  9. package/gen/ts/vocab/quotametrics.js +40 -0
  10. package/gen/ts/vocab/quotametrics.ts +47 -0
  11. package/gen/ts/vocab/usertypes.js +40 -0
  12. package/gen/ts/vocab/usertypes.ts +46 -0
  13. package/gen/ts/wire/base.js +245 -0
  14. package/gen/ts/wire/base.ts +255 -0
  15. package/gen/ts/wire/names.js +41 -0
  16. package/gen/ts/wire/names.ts +42 -0
  17. package/gen/ts/wire/schemas.js +111 -0
  18. package/gen/ts/wire/schemas.ts +216 -0
  19. package/package.json +164 -0
  20. package/sdk/ts/client/content.d.ts +74 -0
  21. package/sdk/ts/client/content.js +466 -0
  22. package/sdk/ts/client/errors.d.ts +79 -0
  23. package/sdk/ts/client/errors.js +148 -0
  24. package/sdk/ts/client/index.d.ts +241 -0
  25. package/sdk/ts/client/index.js +740 -0
  26. package/sdk/ts/client/route.d.ts +41 -0
  27. package/sdk/ts/client/route.js +107 -0
  28. package/sdk/ts/client/send.d.ts +64 -0
  29. package/sdk/ts/client/send.js +142 -0
  30. package/sdk/ts/client/transport.d.ts +187 -0
  31. package/sdk/ts/client/transport.js +381 -0
  32. package/sdk/ts/core/multisig-parse.d.ts +47 -0
  33. package/sdk/ts/core/multisig-parse.js +234 -0
  34. package/sdk/ts/core/sign-request.d.ts +83 -0
  35. package/sdk/ts/core/sign-request.js +146 -0
  36. package/sdk/ts/core/sign.d.ts +31 -0
  37. package/sdk/ts/core/sign.js +82 -0
  38. package/sdk/ts/core/signing-transport.d.ts +75 -0
  39. package/sdk/ts/core/signing-transport.js +172 -0
  40. package/sdk/ts/core/verifier.d.ts +177 -0
  41. package/sdk/ts/core/verifier.js +269 -0
  42. package/sdk/ts/core/verify-multisig-request.d.ts +72 -0
  43. package/sdk/ts/core/verify-multisig-request.js +112 -0
  44. package/sdk/ts/core/verify-request.d.ts +181 -0
  45. package/sdk/ts/core/verify-request.js +284 -0
  46. package/sdk/ts/core/window.d.ts +22 -0
  47. package/sdk/ts/core/window.js +43 -0
  48. package/sdk/ts/core/wire-canon.d.ts +20 -0
  49. package/sdk/ts/core/wire-canon.js +174 -0
  50. package/sdk/ts/hono/middleware.d.ts +38 -0
  51. package/sdk/ts/hono/middleware.js +64 -0
  52. package/sdk/ts/resolvers/errors.d.ts +130 -0
  53. package/sdk/ts/resolvers/errors.js +150 -0
  54. package/sdk/ts/resolvers/http.d.ts +98 -0
  55. package/sdk/ts/resolvers/http.js +274 -0
  56. package/sdk/ts/resolvers/index.d.ts +9 -0
  57. package/sdk/ts/resolvers/index.js +15 -0
  58. package/sdk/ts/resolvers/jwks.d.ts +2 -0
  59. package/sdk/ts/resolvers/jwks.js +43 -0
  60. package/sdk/ts/resolvers/offer-key-cache.d.ts +68 -0
  61. package/sdk/ts/resolvers/offer-key-cache.js +152 -0
  62. package/sdk/ts/resolvers/registration-requirements.d.ts +47 -0
  63. package/sdk/ts/resolvers/registration-requirements.js +220 -0
  64. package/sdk/ts/resolvers/ssrf.d.ts +28 -0
  65. package/sdk/ts/resolvers/ssrf.js +235 -0
  66. package/sdk/ts/resolvers/static.d.ts +7 -0
  67. package/sdk/ts/resolvers/static.js +16 -0
  68. package/sdk/ts/resolvers/wba.d.ts +134 -0
  69. package/sdk/ts/resolvers/wba.js +506 -0
  70. package/sdk/ts/resolvers/wellknown.d.ts +28 -0
  71. package/sdk/ts/resolvers/wellknown.js +169 -0
  72. package/sdk/ts/src/acceptance.d.ts +46 -0
  73. package/sdk/ts/src/acceptance.js +162 -0
  74. package/sdk/ts/src/base64url.d.ts +19 -0
  75. package/sdk/ts/src/base64url.js +55 -0
  76. package/sdk/ts/src/crossfield.d.ts +28 -0
  77. package/sdk/ts/src/crossfield.js +215 -0
  78. package/sdk/ts/src/endpoint-rule.d.ts +30 -0
  79. package/sdk/ts/src/endpoint-rule.js +63 -0
  80. package/sdk/ts/src/errordetail.d.ts +115 -0
  81. package/sdk/ts/src/errordetail.js +256 -0
  82. package/sdk/ts/src/hashurl.d.ts +5 -0
  83. package/sdk/ts/src/hashurl.js +15 -0
  84. package/sdk/ts/src/host-ref.d.ts +70 -0
  85. package/sdk/ts/src/host-ref.js +320 -0
  86. package/sdk/ts/src/hosts.d.ts +138 -0
  87. package/sdk/ts/src/hosts.js +214 -0
  88. package/sdk/ts/src/idempotency.d.ts +11 -0
  89. package/sdk/ts/src/idempotency.js +27 -0
  90. package/sdk/ts/src/jsondepth.d.ts +27 -0
  91. package/sdk/ts/src/jsondepth.js +77 -0
  92. package/sdk/ts/src/licenseterm.d.ts +113 -0
  93. package/sdk/ts/src/licenseterm.js +433 -0
  94. package/sdk/ts/src/money.d.ts +21 -0
  95. package/sdk/ts/src/money.js +60 -0
  96. package/sdk/ts/src/offer-sign.d.ts +11 -0
  97. package/sdk/ts/src/offer-sign.js +31 -0
  98. package/sdk/ts/src/opaque-url.d.ts +3 -0
  99. package/sdk/ts/src/opaque-url.js +20 -0
  100. package/sdk/ts/src/pop.d.ts +51 -0
  101. package/sdk/ts/src/pop.js +166 -0
  102. package/sdk/ts/src/regschema.d.ts +229 -0
  103. package/sdk/ts/src/regschema.js +1315 -0
  104. package/sdk/ts/src/scopes.d.ts +20 -0
  105. package/sdk/ts/src/scopes.js +45 -0
  106. package/sdk/ts/src/signurl.d.ts +23 -0
  107. package/sdk/ts/src/signurl.js +107 -0
  108. package/sdk/ts/src/thumbprint.d.ts +14 -0
  109. package/sdk/ts/src/thumbprint.js +24 -0
  110. package/sdk/ts/src/verify.d.ts +49 -0
  111. package/sdk/ts/src/verify.js +109 -0
  112. package/sdk/ts/src/wire-names.d.ts +1 -0
  113. package/sdk/ts/src/wire-names.js +16 -0
  114. package/sdk/ts/src/wire.d.ts +68 -0
  115. package/sdk/ts/src/wire.js +114 -0
@@ -0,0 +1,174 @@
1
+ // sdk/ts/core — the wire→canonical offer inversion (the from-wire canonicalizer).
2
+ // Companion to the Verifier's canonicalOfferPayload: that face clears the two
3
+ // signature fields and JCS-es an ALREADY-canonical offer; THIS face reconstructs
4
+ // the canonical form from the FORA Connect WIRE offer (proto3-JSON with
5
+ // EmitUnpopulated — zero-valued scalars, empty repeateds, null messages, and
6
+ // *_UNSPECIFIED enums all present). Every TS client verifying a wire offer must
7
+ // invert the wire emission BEFORE canonicalOfferPayload, never call it on raw wire.
8
+ //
9
+ // Mirror of sdk/python fora_sdk.wire_canon.from_wire_offer. Its presence rules are
10
+ // SCHEMA-AWARE, driven by the generated gen/ts OfferSchema exactly as the Python
11
+ // side reads them from the gen/python model field defaults:
12
+ //
13
+ // .optional() → proto3 presence-tracked → a set-but-zero scalar is KEPT
14
+ // (an optional string deliberately "" survives, e.g. pricing.unit).
15
+ // .default("")/.default(0)/bare-required → non-optional → the zero value is DROPPED.
16
+ // z.object → message: recurse (a set-but-empty {} stays, a null message drops).
17
+ // z.array → repeated: drop when empty, else canonicalize each element.
18
+ // z.record → map/Struct: keep VERBATIM (keys are data, never pruned or cased),
19
+ // drop only when empty.
20
+ // *_UNSPECIFIED enum values are zero values and drop.
21
+ //
22
+ // Byte-parity with the Go oracle is pinned by tests/wire-canonical.parity.test.ts
23
+ // over the shared sdk/go/helpers/testdata/wire-canonical-vectors.json corpus.
24
+ //
25
+ // FAIL-CLOSED: a wire field newer than the pinned OfferSchema is kept verbatim, so a
26
+ // signature covering bytes this pin cannot reconstruct verifies FALSE (rejected),
27
+ // never silently accepted.
28
+ import { OfferSchema } from "../../../gen/ts/wire/schemas.js";
29
+ // Sentinel: this field is omitted entirely by the canonical marshal.
30
+ const OMIT = Symbol("fora.core.wire-canon.omit");
31
+ const CAMEL_BOUNDARY = /([a-z0-9])([A-Z])/g;
32
+ const UNSPECIFIED_ENUM = /^[A-Z][A-Z0-9_]*_UNSPECIFIED$/;
33
+ // Wrapper node types that decorate a field without changing its wire identity.
34
+ const WRAPPERS = new Set(["ZodOptional", "ZodDefault", "ZodNullable", "ZodBranded", "ZodEffects", "ZodPipe"]);
35
+ function zdef(schema) {
36
+ return schema._def;
37
+ }
38
+ // kindOf reads the node kind in Zod 3 spelling for both majors: Zod 4's "optional"
39
+ // becomes "ZodOptional", so one vocabulary drives the walk. A node with neither
40
+ // spelling is a Zod this module does not read; it throws rather than returning a
41
+ // kind nothing matches, which would silently drop presence-tracked zero values
42
+ // from the canonical form and fail signature verification without a cause.
43
+ export function kindOf(schema) {
44
+ const d = zdef(schema);
45
+ if (d.typeName !== undefined)
46
+ return d.typeName;
47
+ if (typeof d.type !== "string") {
48
+ throw new Error("fora/core: zod node has neither typeName (Zod 3) nor a string type (Zod 4)");
49
+ }
50
+ return `Zod${d.type.charAt(0).toUpperCase()}${d.type.slice(1)}`;
51
+ }
52
+ // innerOf peels one wrapper: ZodOptional/ZodDefault/ZodNullable expose `innerType`,
53
+ // Zod 3 ZodArray/ZodBranded expose `type`, Zod 3 ZodEffects exposes `schema`, Zod 4
54
+ // arrays expose `element` and Zod 4 pipes `in` — mutually exclusive.
55
+ function innerOf(schema) {
56
+ const d = zdef(schema);
57
+ const inner = d.innerType ?? d.schema ?? d.element ?? d.in ?? (typeof d.type === "object" ? d.type : undefined);
58
+ if (inner === undefined)
59
+ throw new Error("fora/core: zod wrapper has no inner type");
60
+ return inner;
61
+ }
62
+ function shapeOf(schema) {
63
+ return schema.shape;
64
+ }
65
+ // coreType strips presence/decoration wrappers to the underlying kind (ZodObject,
66
+ // ZodArray, ZodRecord, or a scalar). It STOPS at ZodArray so a repeated field is
67
+ // still detectable as repeated (the element is unwrapped separately).
68
+ function coreType(schema) {
69
+ let s = schema;
70
+ while (WRAPPERS.has(kindOf(s)))
71
+ s = innerOf(s);
72
+ return s;
73
+ }
74
+ // isPresenceTracked mirrors Python's `field.default is None`: a `.optional()` field
75
+ // is proto3 presence-tracked (keep its zero value); `.default(...)` or a bare
76
+ // required field is non-optional (drop its zero value).
77
+ function isPresenceTracked(schema) {
78
+ const t = kindOf(schema);
79
+ if (t === "ZodOptional")
80
+ return true;
81
+ if (t === "ZodDefault")
82
+ return false;
83
+ if (t === "ZodNullable" || t === "ZodBranded" || t === "ZodEffects" || t === "ZodPipe") {
84
+ return isPresenceTracked(innerOf(schema));
85
+ }
86
+ return false;
87
+ }
88
+ function isPlainObject(value) {
89
+ return typeof value === "object" && value !== null && !Array.isArray(value);
90
+ }
91
+ // snake inverts protojson's lowerCamel json_name back to the proto name. It is
92
+ // idempotent on already-snake keys (the shared corpus is snake_case wire).
93
+ function snake(key) {
94
+ return key.replace(CAMEL_BOUNDARY, "$1_$2").toLowerCase();
95
+ }
96
+ function canonScalar(value, presence) {
97
+ if (presence)
98
+ return value; // set (on the wire) → part of the signed bytes
99
+ const zeroish = value === "" || value === false || (typeof value === "number" && value === 0);
100
+ if (zeroish)
101
+ return OMIT;
102
+ if (typeof value === "string" && UNSPECIFIED_ENUM.test(value))
103
+ return OMIT;
104
+ return value;
105
+ }
106
+ function canonField(field, value) {
107
+ if (value === null)
108
+ return OMIT; // unset message/Struct (EmitUnpopulated renders null)
109
+ const core = coreType(field);
110
+ const kind = kindOf(core);
111
+ if (Array.isArray(value)) {
112
+ const elem = kind === "ZodArray" ? coreType(innerOf(core)) : undefined;
113
+ const elemIsMessage = elem !== undefined && kindOf(elem) === "ZodObject";
114
+ const items = value.map((v) => elemIsMessage && elem !== undefined && isPlainObject(v) ? canonMessage(elem, v) : v);
115
+ return items.length > 0 ? items : OMIT; // canonical omits empty repeated
116
+ }
117
+ if (isPlainObject(value)) {
118
+ if (kind === "ZodObject")
119
+ return canonMessage(core, value); // set-but-empty {} stays
120
+ if (kind === "ZodRecord")
121
+ return Object.keys(value).length > 0 ? value : OMIT; // map keys verbatim
122
+ return value;
123
+ }
124
+ return canonScalar(value, isPresenceTracked(field));
125
+ }
126
+ // keep writes a member the canonical form must carry, including one whose name is an
127
+ // inherited property of every object. A plain assignment to "__proto__" replaces the
128
+ // object's prototype and creates no member; defineProperty creates the member, which is
129
+ // what JSON.stringify — and therefore JCS, and therefore the signature — reads.
130
+ function keep(out, name, value) {
131
+ Object.defineProperty(out, name, {
132
+ value,
133
+ enumerable: true,
134
+ writable: true,
135
+ configurable: true,
136
+ });
137
+ }
138
+ function canonMessage(objectSchema, wire) {
139
+ const shape = shapeOf(objectSchema);
140
+ const out = {};
141
+ for (const [key, value] of Object.entries(wire)) {
142
+ const name = snake(key);
143
+ // hasOwn on the READ, and defineProperty on the WRITE. Both are the same key: a
144
+ // wire name like "__proto__" or "constructor" resolves to an inherited member of
145
+ // Object.prototype, so a truthiness test hands the walk an object with no _def,
146
+ // and a plain assignment invokes the prototype SETTER instead of creating a
147
+ // member. An offer comes from a peer, so the key is attacker-chosen, and the
148
+ // second half was the dangerous one: the member vanished from the canonical form
149
+ // while the object silently inherited whatever the attacker put there. The signed
150
+ // bytes were unaffected, so an offer carrying an appended __proto__ still VERIFIED
151
+ // — the one member name that could be added to a signed offer for free — and the
152
+ // caller then read attacker-chosen values off a VerifiedOffer.
153
+ const field = Object.hasOwn(shape, name) ? shape[name] : undefined;
154
+ if (field === undefined) {
155
+ // Newer-than-pin field: keep it so verification fails CLOSED (the signature
156
+ // covered bytes this pin cannot reconstruct).
157
+ keep(out, name, value);
158
+ continue;
159
+ }
160
+ const canon = canonField(field, value);
161
+ if (canon !== OMIT)
162
+ keep(out, name, canon);
163
+ }
164
+ return out;
165
+ }
166
+ /**
167
+ * fromWireOffer reconstructs the canonical (signed) offer object from the WIRE
168
+ * offer object, mirroring sdk/python from_wire_offer. Feed its result to
169
+ * canonicalOfferPayload (or RFC 8785 JCS directly) to reproduce the exact bytes the
170
+ * offer signature covers.
171
+ */
172
+ export function fromWireOffer(offerWire) {
173
+ return canonMessage(OfferSchema, offerWire);
174
+ }
@@ -0,0 +1,38 @@
1
+ import type { Ed25519Verify } from "../core/verifier.ts";
2
+ /**
3
+ * A minimal structural view of the Hono context the middleware touches: the raw
4
+ * inbound Fetch Request and a Response slot the binding sets on deny. Declared
5
+ * structurally (not imported from Hono) so the core-adjacent binding stays free of
6
+ * a hard Hono runtime import while remaining drop-in for a real Hono `Context`.
7
+ */
8
+ export interface ForaVerifyContext {
9
+ req: {
10
+ raw: Request;
11
+ };
12
+ res: Response | undefined;
13
+ }
14
+ export type ForaVerifyNext = () => Promise<void> | void;
15
+ /** Options for the server-verify binding. The clock + verify primitive are
16
+ * injected exactly as the core Verifier's are; the binding owns no state. The
17
+ * GET-PoP path is self-verifying via the presented key, so no key resolver is
18
+ * accepted — a resolver here would be dead weight the middleware never reads. */
19
+ export interface ForaVerifyOptions {
20
+ now?: () => number;
21
+ verifyEd25519?: Ed25519Verify;
22
+ }
23
+ /**
24
+ * foraVerify builds the opt-in Hono server-verify middleware. On an inbound
25
+ * request it verifies the RFC 9421 GET PoP through the L1 verifyAgentBinding; on
26
+ * success it calls next() (the guarded handler runs); on failure it sets a 403 deny
27
+ * response and does NOT call next() (fail-closed).
28
+ *
29
+ * SCOPE LIMIT — presenter self-consistency only: the binding derives agentId
30
+ * from the PRESENTED key (thumbprint), so the 3-way check collapses to "the
31
+ * presenter signed with the key it presented". It does NOT anchor a URL-bound
32
+ * agent_id. A consumer enforcing URL-bound delivery (e.g. an edge serving
33
+ * signed URLs whose agent_id query param binds the fetcher) MUST call the L1
34
+ * verifyAgentBinding directly with the URL's agent_id as the anchor — building
35
+ * URL-bound verification on this middleware would silently accept any
36
+ * self-consistent presenter.
37
+ */
38
+ export declare function foraVerify(opts: ForaVerifyOptions): (ctx: ForaVerifyContext, next: ForaVerifyNext) => Promise<void>;
@@ -0,0 +1,64 @@
1
+ // sdk/ts/hono — the OPT-IN server/verify binding over sdk/ts/core, the natural
2
+ // adapter for Edge (src/edge/src/app.ts, a SERVER/VERIFIER). It is a thin Hono
3
+ // middleware that verifies an inbound RFC 9421 GET proof-of-possession at the HTTP
4
+ // seam (where the exact request exists) via the L1 verifyAgentBinding, and
5
+ // fail-closes: an unsigned/forged request never reaches the guarded handler and
6
+ // the binding sets a 403 deny response.
7
+ //
8
+ // This binding depends one-directionally on core + L1; core NEVER imports Hono.
9
+ // Hono is a PEER dependency of this binding, not of the core. The middleware is
10
+ // framework-shaped (a (ctx, next) pair) but does not import Hono's runtime — it
11
+ // operates over the standard Fetch Request the Hono context exposes as ctx.req.raw
12
+ // and the standard Response it assigns to ctx.res, so it is testable without a
13
+ // running Hono app and stays byte-neutral to the web standard.
14
+ import { decodeBase64Url } from "../src/base64url.js";
15
+ import { AGENT_KEY_HEADER, verifyAgentBinding } from "../src/pop.js";
16
+ import { thumbprint } from "../src/thumbprint.js";
17
+ const ED25519_PUBLIC_KEY_BYTES = 32;
18
+ /**
19
+ * foraVerify builds the opt-in Hono server-verify middleware. On an inbound
20
+ * request it verifies the RFC 9421 GET PoP through the L1 verifyAgentBinding; on
21
+ * success it calls next() (the guarded handler runs); on failure it sets a 403 deny
22
+ * response and does NOT call next() (fail-closed).
23
+ *
24
+ * SCOPE LIMIT — presenter self-consistency only: the binding derives agentId
25
+ * from the PRESENTED key (thumbprint), so the 3-way check collapses to "the
26
+ * presenter signed with the key it presented". It does NOT anchor a URL-bound
27
+ * agent_id. A consumer enforcing URL-bound delivery (e.g. an edge serving
28
+ * signed URLs whose agent_id query param binds the fetcher) MUST call the L1
29
+ * verifyAgentBinding directly with the URL's agent_id as the anchor — building
30
+ * URL-bound verification on this middleware would silently accept any
31
+ * self-consistent presenter.
32
+ */
33
+ export function foraVerify(opts) {
34
+ return async (ctx, next) => {
35
+ const req = ctx.req.raw;
36
+ // The presented key is the PoP authority; its thumbprint is the identity the
37
+ // 3-way check anchors on (keyid == thumbprint(presented) == agent_id). Derive
38
+ // agent_id from the presented key so a request that omits the URL param still
39
+ // verifies self-consistently.
40
+ const presentedRaw = req.headers.get(AGENT_KEY_HEADER);
41
+ const presented = presentedRaw ? decodeBase64Url(presentedRaw) : undefined;
42
+ if (!presented || presented.length !== ED25519_PUBLIC_KEY_BYTES) {
43
+ ctx.res = deny();
44
+ return;
45
+ }
46
+ const agentId = await thumbprint(presented);
47
+ const result = await verifyAgentBinding({
48
+ url: req.url,
49
+ method: req.method,
50
+ headers: req.headers,
51
+ agentId,
52
+ ...(opts.now ? { now: opts.now } : {}),
53
+ ...(opts.verifyEd25519 ? { verifyEd25519: opts.verifyEd25519 } : {}),
54
+ });
55
+ if (!result.ok) {
56
+ ctx.res = deny();
57
+ return;
58
+ }
59
+ await next();
60
+ };
61
+ }
62
+ function deny() {
63
+ return new Response("forbidden", { status: 403 });
64
+ }
@@ -0,0 +1,130 @@
1
+ /** Base of every resolver verdict. */
2
+ export declare class ResolverError extends Error {
3
+ }
4
+ /** No key is known for the requested keyid/thumbprint (fall-through miss) —
5
+ * the class face of Go `ErrUnknownKey` / Python `UnknownKeyError`. The SDK's
6
+ * own resolver faces signal this verdict with an `undefined` return; the class
7
+ * exists for consumers and fail-closed composite stacks that surface the miss
8
+ * as a catchable error, keeping the taxonomy identical across all three SDKs. */
9
+ export declare class UnknownKey extends ResolverError {
10
+ constructor(message: string, options?: {
11
+ cause?: unknown;
12
+ });
13
+ }
14
+ /** The WBA identity directory (or a well-known JWKS/manifest) could not be
15
+ * fetched, returned non-200, or failed to decode. DISTINCT from unknown-key so a
16
+ * fail-closed composite halts on an outage instead of falling through. */
17
+ export declare class DirectoryUnavailable extends ResolverError {
18
+ constructor(message: string, options?: {
19
+ cause?: unknown;
20
+ });
21
+ }
22
+ /** The thumbprint is present in the directory host's current revocation snapshot. */
23
+ export declare class KeyRevoked extends ResolverError {
24
+ constructor(message: string, options?: {
25
+ cause?: unknown;
26
+ });
27
+ }
28
+ /** The key exists but `now` falls outside its [not_before, not_after) window. */
29
+ export declare class KeyExpired extends ResolverError {
30
+ constructor(message: string, options?: {
31
+ cause?: unknown;
32
+ });
33
+ }
34
+ /** The key resolved, but its directory declares a revocation_url whose snapshot
35
+ * has never been fetched (unreachable or not host-anchored) — so revocation was
36
+ * NEVER EVALUATED, DISTINCT from KeyRevoked ("evaluated and revoked"). Only
37
+ * thrown when the resolver's `requireRevocation` option is set; the default
38
+ * keeps the prior best-effort behavior (a declared-but-unreachable revocation
39
+ * channel does not block resolution). It lets a caller that treats revocation as
40
+ * mandatory fail closed instead of trusting an unevaluated key. */
41
+ export declare class RevocationUnevaluated extends ResolverError {
42
+ constructor(message: string, options?: {
43
+ cause?: unknown;
44
+ });
45
+ }
46
+ /** A well-known manifest was fetched and decoded, but advertises no endpoint.
47
+ * DISTINCT from DirectoryUnavailable: the manifest is reachable, simply inert. */
48
+ export declare class NoEndpoint extends ResolverError {
49
+ constructor(message: string, options?: {
50
+ cause?: unknown;
51
+ });
52
+ }
53
+ /** A well-known manifest was read and advertises an endpoint this resolver will
54
+ * not hand back: one on a host or port unrelated to the domain that served the
55
+ * manifest, or one carrying userinfo.
56
+ *
57
+ * DISTINCT from NoEndpoint and from DirectoryUnavailable because it is a VERDICT
58
+ * — the Exchange answered, and the answer is not usable. A caller that classifies
59
+ * retryability reads this as final rather than as something to try again in a
60
+ * moment. */
61
+ export declare class EndpointRefused extends ResolverError {
62
+ constructor(message: string, options?: {
63
+ cause?: unknown;
64
+ });
65
+ }
66
+ /** The deployment's allow overlay excluded this Exchange domain, before anything
67
+ * was dialled — the class face of Go `ErrExchangeNotPermitted` / Python
68
+ * `ExchangeNotPermittedError`. It says nothing about whether the Exchange exists
69
+ * or answers, only that this deployment declined to ask, so the remedy is a
70
+ * configuration change rather than a retry. */
71
+ export declare class ExchangeNotPermitted extends ResolverError {
72
+ constructor(message: string, options?: {
73
+ cause?: unknown;
74
+ });
75
+ }
76
+ /** The document served at the domain's well-known path describes some other role
77
+ * — the class face of Go `ErrManifestNotExchange` / Python
78
+ * `ManifestNotExchangeError`. Registration requirements are an Exchange's to
79
+ * publish, so a manifest claiming to be an agent, a broker or a publisher is
80
+ * refused rather than read for members it has no business carrying. A manifest
81
+ * naming no role at all is refused the same way: the field is required by the
82
+ * contract, and reading silence as assent would make the check advisory. */
83
+ export declare class ManifestNotExchange extends ResolverError {
84
+ constructor(message: string, options?: {
85
+ cause?: unknown;
86
+ });
87
+ }
88
+ /** The document arrived and this reader cannot use it — the class face of Go
89
+ * `ErrManifestUnusable` / Python `ManifestUnusableError`. A VERDICT and not a
90
+ * failed read: the bytes were served, and the next attempt gets the same ones, so
91
+ * a caller told to retry retries forever.
92
+ *
93
+ * The SDK's own requirements reader throws it for exactly ONE thing: a document
94
+ * whose version it cannot classify. That is the contract's own first question about
95
+ * this document, asked before any other member is read, and a layout no reader can
96
+ * classify is not a disappointment about one member — it is the whole document being
97
+ * unreadable for what a registration owes.
98
+ *
99
+ * The other two ways a manifest can disappoint that reader stay deliberate
100
+ * non-errors: a member carrying a type the contract does not admit reads as ABSENT,
101
+ * because the projection is shared with the endpoint and key faces, and a document
102
+ * that does not parse at all is a transport failure.
103
+ *
104
+ * It is ALSO the word for a reader stricter than the SDK's own, which is why the
105
+ * seam admits it at all: that seam is injectable, and a reader validating the whole
106
+ * document — or applying a version rule narrower than this one — has to be able to
107
+ * say its refusal is final.
108
+ *
109
+ * Deliberately NOT `ManifestVersionRefused`, which belongs to the endpoint seam. The
110
+ * two vocabularies are disjoint: that one answers whether an endpoint may be dialled,
111
+ * this one whether a document can be read for what a registration owes. */
112
+ export declare class ManifestUnusable extends ResolverError {
113
+ constructor(message: string, options?: {
114
+ cause?: unknown;
115
+ });
116
+ }
117
+ /** A /.well-known/fora.json was fetched and parsed but carries a
118
+ * WellKnownManifest.ver this resolver does not accept: an unrecognised major
119
+ * version, a value that is not MAJOR.MINOR, or no version at all. The rule is
120
+ * manifestVersionRefusal in src/wire.ts.
121
+ *
122
+ * Like EndpointRefused it is a VERDICT — final, not a transport failure to
123
+ * retry — and it is never cached. The gate runs before any other member of the
124
+ * document is read, for the reason stated once on WellKnownManifest.ver in the
125
+ * proto. */
126
+ export declare class ManifestVersionRefused extends ResolverError {
127
+ constructor(message: string, options?: {
128
+ cause?: unknown;
129
+ });
130
+ }
@@ -0,0 +1,150 @@
1
+ // Typed error surface for the fetching resolver faces. The Go oracle uses
2
+ // errors.Is-DISTINCT sentinels (ErrKeyRevoked / ErrKeyExpired /
3
+ // ErrDirectoryUnavailable / ErrNoEndpoint / ErrUnknownKey) so a composite
4
+ // resolver can HALT on a fail-closed verdict rather than fall through as if the
5
+ // key were merely unknown. The TS port preserves that distinctness as distinct
6
+ // thrown classes. The SDK's own RequestKeyResolver faces still signal a plain
7
+ // miss with the `undefined` return (never a throw); UnknownKey below is the
8
+ // catchable class of that same verdict for consumers and composite stacks that
9
+ // need the error-shaped face Go and Python expose.
10
+ /** Base of every resolver verdict. */
11
+ export class ResolverError extends Error {
12
+ }
13
+ /** No key is known for the requested keyid/thumbprint (fall-through miss) —
14
+ * the class face of Go `ErrUnknownKey` / Python `UnknownKeyError`. The SDK's
15
+ * own resolver faces signal this verdict with an `undefined` return; the class
16
+ * exists for consumers and fail-closed composite stacks that surface the miss
17
+ * as a catchable error, keeping the taxonomy identical across all three SDKs. */
18
+ export class UnknownKey extends ResolverError {
19
+ constructor(message, options) {
20
+ super(message, options);
21
+ this.name = "UnknownKey";
22
+ }
23
+ }
24
+ /** The WBA identity directory (or a well-known JWKS/manifest) could not be
25
+ * fetched, returned non-200, or failed to decode. DISTINCT from unknown-key so a
26
+ * fail-closed composite halts on an outage instead of falling through. */
27
+ export class DirectoryUnavailable extends ResolverError {
28
+ constructor(message, options) {
29
+ super(message, options);
30
+ this.name = "DirectoryUnavailable";
31
+ }
32
+ }
33
+ /** The thumbprint is present in the directory host's current revocation snapshot. */
34
+ export class KeyRevoked extends ResolverError {
35
+ constructor(message, options) {
36
+ super(message, options);
37
+ this.name = "KeyRevoked";
38
+ }
39
+ }
40
+ /** The key exists but `now` falls outside its [not_before, not_after) window. */
41
+ export class KeyExpired extends ResolverError {
42
+ constructor(message, options) {
43
+ super(message, options);
44
+ this.name = "KeyExpired";
45
+ }
46
+ }
47
+ /** The key resolved, but its directory declares a revocation_url whose snapshot
48
+ * has never been fetched (unreachable or not host-anchored) — so revocation was
49
+ * NEVER EVALUATED, DISTINCT from KeyRevoked ("evaluated and revoked"). Only
50
+ * thrown when the resolver's `requireRevocation` option is set; the default
51
+ * keeps the prior best-effort behavior (a declared-but-unreachable revocation
52
+ * channel does not block resolution). It lets a caller that treats revocation as
53
+ * mandatory fail closed instead of trusting an unevaluated key. */
54
+ export class RevocationUnevaluated extends ResolverError {
55
+ constructor(message, options) {
56
+ super(message, options);
57
+ this.name = "RevocationUnevaluated";
58
+ }
59
+ }
60
+ /** A well-known manifest was fetched and decoded, but advertises no endpoint.
61
+ * DISTINCT from DirectoryUnavailable: the manifest is reachable, simply inert. */
62
+ export class NoEndpoint extends ResolverError {
63
+ constructor(message, options) {
64
+ super(message, options);
65
+ this.name = "NoEndpoint";
66
+ }
67
+ }
68
+ /** A well-known manifest was read and advertises an endpoint this resolver will
69
+ * not hand back: one on a host or port unrelated to the domain that served the
70
+ * manifest, or one carrying userinfo.
71
+ *
72
+ * DISTINCT from NoEndpoint and from DirectoryUnavailable because it is a VERDICT
73
+ * — the Exchange answered, and the answer is not usable. A caller that classifies
74
+ * retryability reads this as final rather than as something to try again in a
75
+ * moment. */
76
+ export class EndpointRefused extends ResolverError {
77
+ constructor(message, options) {
78
+ super(message, options);
79
+ this.name = "EndpointRefused";
80
+ }
81
+ }
82
+ /** The deployment's allow overlay excluded this Exchange domain, before anything
83
+ * was dialled — the class face of Go `ErrExchangeNotPermitted` / Python
84
+ * `ExchangeNotPermittedError`. It says nothing about whether the Exchange exists
85
+ * or answers, only that this deployment declined to ask, so the remedy is a
86
+ * configuration change rather than a retry. */
87
+ export class ExchangeNotPermitted extends ResolverError {
88
+ constructor(message, options) {
89
+ super(message, options);
90
+ this.name = "ExchangeNotPermitted";
91
+ }
92
+ }
93
+ /** The document served at the domain's well-known path describes some other role
94
+ * — the class face of Go `ErrManifestNotExchange` / Python
95
+ * `ManifestNotExchangeError`. Registration requirements are an Exchange's to
96
+ * publish, so a manifest claiming to be an agent, a broker or a publisher is
97
+ * refused rather than read for members it has no business carrying. A manifest
98
+ * naming no role at all is refused the same way: the field is required by the
99
+ * contract, and reading silence as assent would make the check advisory. */
100
+ export class ManifestNotExchange extends ResolverError {
101
+ constructor(message, options) {
102
+ super(message, options);
103
+ this.name = "ManifestNotExchange";
104
+ }
105
+ }
106
+ /** The document arrived and this reader cannot use it — the class face of Go
107
+ * `ErrManifestUnusable` / Python `ManifestUnusableError`. A VERDICT and not a
108
+ * failed read: the bytes were served, and the next attempt gets the same ones, so
109
+ * a caller told to retry retries forever.
110
+ *
111
+ * The SDK's own requirements reader throws it for exactly ONE thing: a document
112
+ * whose version it cannot classify. That is the contract's own first question about
113
+ * this document, asked before any other member is read, and a layout no reader can
114
+ * classify is not a disappointment about one member — it is the whole document being
115
+ * unreadable for what a registration owes.
116
+ *
117
+ * The other two ways a manifest can disappoint that reader stay deliberate
118
+ * non-errors: a member carrying a type the contract does not admit reads as ABSENT,
119
+ * because the projection is shared with the endpoint and key faces, and a document
120
+ * that does not parse at all is a transport failure.
121
+ *
122
+ * It is ALSO the word for a reader stricter than the SDK's own, which is why the
123
+ * seam admits it at all: that seam is injectable, and a reader validating the whole
124
+ * document — or applying a version rule narrower than this one — has to be able to
125
+ * say its refusal is final.
126
+ *
127
+ * Deliberately NOT `ManifestVersionRefused`, which belongs to the endpoint seam. The
128
+ * two vocabularies are disjoint: that one answers whether an endpoint may be dialled,
129
+ * this one whether a document can be read for what a registration owes. */
130
+ export class ManifestUnusable extends ResolverError {
131
+ constructor(message, options) {
132
+ super(message, options);
133
+ this.name = "ManifestUnusable";
134
+ }
135
+ }
136
+ /** A /.well-known/fora.json was fetched and parsed but carries a
137
+ * WellKnownManifest.ver this resolver does not accept: an unrecognised major
138
+ * version, a value that is not MAJOR.MINOR, or no version at all. The rule is
139
+ * manifestVersionRefusal in src/wire.ts.
140
+ *
141
+ * Like EndpointRefused it is a VERDICT — final, not a transport failure to
142
+ * retry — and it is never cached. The gate runs before any other member of the
143
+ * document is read, for the reason stated once on WellKnownManifest.ver in the
144
+ * proto. */
145
+ export class ManifestVersionRefused extends ResolverError {
146
+ constructor(message, options) {
147
+ super(message, options);
148
+ this.name = "ManifestVersionRefused";
149
+ }
150
+ }
@@ -0,0 +1,98 @@
1
+ import { buildConnector } from "undici";
2
+ /** The minimal response shape the resolvers read — a structural subset of the
3
+ * WHATWG `Response`, so the global `fetch` (and undici's) satisfies it. */
4
+ export interface FetchResponse {
5
+ status: number;
6
+ text(): Promise<string>;
7
+ }
8
+ /** An injected HTTP GET. Defaults to the SSRF-guarded transport (guardedFetch). */
9
+ export type FetchLike = (url: string) => Promise<FetchResponse>;
10
+ /** An SSRF error surfaced when the guarded transport refuses to dial a target.
11
+ * fetchStrict/fetchSoft see it as an ordinary transport failure (fail-closed
12
+ * DirectoryUnavailable / best-effort undefined), so the guard never resolves a
13
+ * blocked host as a valid — merely empty — directory. */
14
+ export declare class SsrfBlockedError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ /** An SSRF-guarded undici connector, injectable into any undici Dispatcher.
18
+ *
19
+ * DX: `new Agent({ connect: ssrfGuard() })`. The guard runs at the CONNECTION
20
+ * (dial) seam — the only place the DNS-REBINDING window is actually closed: it
21
+ * resolves the host, checks EVERY resolved address against blockedAddress, and
22
+ * pins the dial to a checked IP literal (undici does not re-resolve it), while
23
+ * the TLS `servername` keeps the original hostname so cert/SNI validation is
24
+ * unaffected. Because undici re-dials every followed redirect through the same
25
+ * connector, each redirect hop is re-vetted too (consistent with the Go and
26
+ * Python guards); a redirect into a non-http(s) scheme is a WHATWG-fetch network
27
+ * error, so the scheme allowlist is deny-by-default without a redirect handler. */
28
+ export declare function ssrfGuard(): buildConnector.connector;
29
+ /** The SSRF-guarded default transport. The directory host is derived from a
30
+ * caller-supplied Signature-Agent and the fetch runs BEFORE the ed25519 check,
31
+ * so an unguarded default would be a pre-auth SSRF lever. Runs on undici through
32
+ * the guarded connector (see ssrfGuard): the initial URL's scheme is vetted
33
+ * deny-by-default, every dial (initial + each redirect hop) is address-checked and
34
+ * pinned, the redirect chain is bounded to MAX_REDIRECTS, and undici owns
35
+ * status/redirect/1xx so a non-2xx is an ordinary response, never a crash. */
36
+ export declare const guardedFetch: FetchLike;
37
+ /** Whether the dial-time address guard is disabled (SKIP_SSRF). Default: off.
38
+ *
39
+ * Exported because the client tier builds its own dispatchers and has to honour the same
40
+ * deployment flag this tier does — Go reads it in NewGuardedTransport and Python in
41
+ * guarded_client, so a TypeScript client that ignored it would be the one place the
42
+ * documented opt-out did nothing. */
43
+ export declare function skipSSRF(): boolean;
44
+ /** The two-flag scheme decision: https always, http only under ALLOW_INSECURE,
45
+ * everything else denied (a scheme denylist is unwinnable — ftp, telnet, gopher,
46
+ * file, data, …). Case-insensitive. */
47
+ export declare function schemeGuardAllows(scheme: string): boolean;
48
+ /**
49
+ * requireScheme refuses a URL this SDK will not dial, BEFORE any dial happens.
50
+ *
51
+ * The connector returned by ssrfGuard is an ADDRESS pin — it decides what a hostname is
52
+ * allowed to resolve to, and it never sees the scheme, because by then the URL has already
53
+ * been reduced to a host and a port. So a dispatcher built from it alone will happily carry
54
+ * an RFC 9421 signature, or a proof of possession, over plaintext http. The scheme is a
55
+ * separate decision and needs a separate gate: Go states it in schemeGuardRoundTripper and
56
+ * Python in _SchemeGuardTransport, both wrapping the transport so it applies to whatever
57
+ * base a caller injected. This is the same gate for the callers that dial undici directly.
58
+ *
59
+ * Raising SsrfBlockedError rather than a typed client failure is deliberate: the client
60
+ * tier already classifies an unrecognised dial failure the way Go classifies this one,
61
+ * which reaches it through the RoundTripper for the same reason.
62
+ */
63
+ export declare function requireScheme(url: string): void;
64
+ /** The ONE public env-driven best-effort guarded fetch factory — every consumer's
65
+ * fetch for any third-party-influenceable request. Two orthogonal env flags drive
66
+ * it: SKIP_SSRF toggles the dial-time address guard (default: on), ALLOW_INSECURE
67
+ * toggles the scheme guard (default: https-only). There is no deployment-stack
68
+ * allow-list and no config error. Both paths use a per-factory undici Agent that
69
+ * ignores HTTP(S)_PROXY env, so a proxied CONNECT cannot tunnel a private target
70
+ * past the (guarded) dial guard, and both carry the same request timeout. Returns
71
+ * a FetchLike closing over one dispatcher. */
72
+ export declare function guardedFetchFromEnv(): FetchLike;
73
+ /** Default transport for a resolver whose URL is a FIXED, operator-chosen address
74
+ * (the well-known JWKS): a plain fetch, NOT SSRF-guarded. An on-prem JWKS may
75
+ * legitimately be private, and the operator rather than an attacker chose it.
76
+ *
77
+ * Which default a resolver takes follows its URL's PROVENANCE, and that is the
78
+ * whole rule — the Go oracle states it in the options struct these ports mirror.
79
+ * A fixed operator-chosen URL takes this transport. A REQUEST-DERIVED host — the
80
+ * WBA directory named by a Signature-Agent header, an Exchange domain read off an
81
+ * offer or a registration — takes `guardedFetchFromEnv`, because the party
82
+ * choosing the address is not the party running the process.
83
+ *
84
+ * The endpoint resolver in this package is request-derived and still defaults
85
+ * here, which is a known gap being closed separately; it is not the rule. Do not
86
+ * reach for this transport for a new resolver without first asking where its URL
87
+ * comes from. */
88
+ export declare const defaultFetch: FetchLike;
89
+ /** GET `url` and return the body text. A transport failure or a non-200 status
90
+ * throws DirectoryUnavailable (fail-closed halt) — the taxonomy a composite
91
+ * relies on to distinguish an outage from an unknown key. A blocked SSRF target
92
+ * is a transport failure and surfaces the same way (never a valid empty doc). */
93
+ export declare function fetchStrict(fetchFn: FetchLike, url: string): Promise<string>;
94
+ /** Best-effort GET: returns the body text on 200, or `undefined` on any
95
+ * transport/status failure. The revocation refresh uses this so a fetch blip
96
+ * leaves the prior snapshot in place (Go: best-effort refresh) rather than
97
+ * propagating — a stale-but-present snapshot is safer than dropping revocations. */
98
+ export declare function fetchSoft(fetchFn: FetchLike, url: string): Promise<string | undefined>;