@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,20 @@
1
+ /**
2
+ * normalizeScopes returns scopes with empty entries dropped, duplicates removed
3
+ * (first-seen), and a stable (lexicographic) order — so two callers supplying
4
+ * the same set produce identical bytes on the wire. No casing change, no
5
+ * trimming. The sdk/go oracle returns nil for empty/all-empty input (JSON
6
+ * null); the TS face returns `[]` (the parity comparator treats null == []).
7
+ */
8
+ export declare function normalizeScopes(scopes: string[]): string[];
9
+ /**
10
+ * scopesSubset reports whether every scope in `sub` is present in `sup`. It is
11
+ * the delegation-attenuation rule (Delegation.scopes MUST be a subset of the
12
+ * principal's granted scopes). An empty `sub` is always a subset.
13
+ */
14
+ export declare function scopesSubset(sub: string[], sup: string[]): boolean;
15
+ /**
16
+ * applyScopes is the functional analogue of the Go ApplyScopes (which mutates a
17
+ * proto Requester). The TS port has no generated Requester mutation face, so it
18
+ * returns the normalized scopes array the caller stamps onto its own request.
19
+ */
20
+ export declare function applyScopes(scopes: string[]): string[];
@@ -0,0 +1,45 @@
1
+ // Scopes / entitlements (ADR-020 §5) — TS port of the sdk/go oracle
2
+ // (helpers/scopes.go). The subscriptions/entitlements a requester holds are a
3
+ // SUPPLIED credential: the application hands the SDK what it holds and the SDK
4
+ // plumbs it into the request. NormalizeScopes/ScopesSubset are pure, byte-
5
+ // deterministic string ops pinned to the shared scopes-vectors.json.
6
+ /**
7
+ * normalizeScopes returns scopes with empty entries dropped, duplicates removed
8
+ * (first-seen), and a stable (lexicographic) order — so two callers supplying
9
+ * the same set produce identical bytes on the wire. No casing change, no
10
+ * trimming. The sdk/go oracle returns nil for empty/all-empty input (JSON
11
+ * null); the TS face returns `[]` (the parity comparator treats null == []).
12
+ */
13
+ export function normalizeScopes(scopes) {
14
+ const seen = new Set();
15
+ const out = [];
16
+ for (const s of scopes) {
17
+ if (s === "" || seen.has(s))
18
+ continue;
19
+ seen.add(s);
20
+ out.push(s);
21
+ }
22
+ out.sort();
23
+ return out;
24
+ }
25
+ /**
26
+ * scopesSubset reports whether every scope in `sub` is present in `sup`. It is
27
+ * the delegation-attenuation rule (Delegation.scopes MUST be a subset of the
28
+ * principal's granted scopes). An empty `sub` is always a subset.
29
+ */
30
+ export function scopesSubset(sub, sup) {
31
+ const set = new Set(sup);
32
+ for (const s of sub) {
33
+ if (!set.has(s))
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ /**
39
+ * applyScopes is the functional analogue of the Go ApplyScopes (which mutates a
40
+ * proto Requester). The TS port has no generated Requester mutation face, so it
41
+ * returns the normalized scopes array the caller stamps onto its own request.
42
+ */
43
+ export function applyScopes(scopes) {
44
+ return normalizeScopes(scopes);
45
+ }
@@ -0,0 +1,23 @@
1
+ /** Signed-URL sign inputs. An empty agentId yields a bearer (unbound) URL; an
2
+ * empty kid omits the kid param — mirror the Go signer's conditional sets. */
3
+ export interface SignUrlParams {
4
+ kid: string;
5
+ agentId: string;
6
+ expUnix: number;
7
+ }
8
+ /**
9
+ * signEd25519SignedUrl signs `source` with `privateKey`, embedding exp (and kid
10
+ * when set, agent_id when set) plus the base64url-no-pad signature over
11
+ * "GET\n<url>". The URL is signed as OPAQUE BYTES — scheme/host/path are
12
+ * preserved verbatim; only the query is deterministically re-encoded. The emitted
13
+ * string is byte-identical to the Go oracle (helpers.SignURLEd25519).
14
+ */
15
+ export declare function signEd25519SignedUrl(source: string, params: SignUrlParams, privateKey: CryptoKey): Promise<string>;
16
+ /**
17
+ * canonicalUrl builds the URL whose bytes the signature covers: the raw prefix
18
+ * (scheme+host+path, VERBATIM) plus the deterministically re-encoded query. It
19
+ * drops sig, applies `mutate`, sorts, and re-encodes. Shared by the sign face
20
+ * (here) and the verify face (verify.ts::canonicalMessage) so both agree on the
21
+ * byte contract by construction. Exported for that reuse only.
22
+ */
23
+ export declare function canonicalUrl(rawUrl: string, mutate?: (pairs: Array<[string, string]>) => void): string;
@@ -0,0 +1,107 @@
1
+ // sdk/ts Ed25519 signed delivery-URL SIGN face (ADR-013) — the Exchange-minting
2
+ // sibling of src/verify.ts. Mirror of the Go oracle helpers.SignURLEd25519.
3
+ //
4
+ // THE CONTRACT: the signature covers "GET\n<url>" as
5
+ // OPAQUE URL BYTES. Neither signer nor verifier re-normalizes scheme/host/path —
6
+ // a mixed-case host, an explicit default port, and a raw space or percent in the
7
+ // path are all preserved verbatim. The ONLY transform is deterministic query
8
+ // handling (add exp/kid/agent_id, remove sig, sort the query). This is why the
9
+ // canonical string is built by splitting the raw URL at its first "?" and keeping
10
+ // the prefix byte-for-byte, rather than round-tripping through `new URL()` (which
11
+ // WHATWG-lowercases the host, strips :443, and re-escapes the path).
12
+ //
13
+ // The query is re-encoded to match Go's url.Values.Encode() byte-for-byte
14
+ // (sort by key, space -> "+", unreserved A-Za-z0-9-._~ kept, everything else
15
+ // %XX uppercase) so the three SDKs produce identical bytes — see canonicalUrl.
16
+ import { encodeBase64Url, utf8Bytes } from "./base64url.js";
17
+ import { opaqueUrl } from "./opaque-url.js";
18
+ const SIG_PARAM = "sig";
19
+ const EXP_PARAM = "exp";
20
+ const KID_PARAM = "kid";
21
+ const AGENT_ID_PARAM = "agent_id";
22
+ /**
23
+ * signEd25519SignedUrl signs `source` with `privateKey`, embedding exp (and kid
24
+ * when set, agent_id when set) plus the base64url-no-pad signature over
25
+ * "GET\n<url>". The URL is signed as OPAQUE BYTES — scheme/host/path are
26
+ * preserved verbatim; only the query is deterministically re-encoded. The emitted
27
+ * string is byte-identical to the Go oracle (helpers.SignURLEd25519).
28
+ */
29
+ export async function signEd25519SignedUrl(source, params, privateKey) {
30
+ // Coerce a URL-like source (a Fastly Compute request URL object) to its opaque
31
+ // string form ONCE at the boundary; canonicalUrl needs string ops. No-op for
32
+ // string callers (verbatim bytes preserved).
33
+ const src = opaqueUrl(source);
34
+ const unsigned = canonicalUrl(src, (pairs) => {
35
+ setParam(pairs, EXP_PARAM, String(params.expUnix));
36
+ if (params.kid !== "")
37
+ setParam(pairs, KID_PARAM, params.kid);
38
+ if (params.agentId !== "")
39
+ setParam(pairs, AGENT_ID_PARAM, params.agentId);
40
+ });
41
+ const sig = new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, utf8Bytes(`GET\n${unsigned}`)));
42
+ const sigParam = encodeBase64Url(sig);
43
+ return canonicalUrl(unsigned, (pairs) => setParam(pairs, SIG_PARAM, sigParam));
44
+ }
45
+ /**
46
+ * canonicalUrl builds the URL whose bytes the signature covers: the raw prefix
47
+ * (scheme+host+path, VERBATIM) plus the deterministically re-encoded query. It
48
+ * drops sig, applies `mutate`, sorts, and re-encodes. Shared by the sign face
49
+ * (here) and the verify face (verify.ts::canonicalMessage) so both agree on the
50
+ * byte contract by construction. Exported for that reuse only.
51
+ */
52
+ export function canonicalUrl(rawUrl, mutate) {
53
+ const q = rawUrl.indexOf("?");
54
+ const prefix = q >= 0 ? rawUrl.slice(0, q) : rawUrl;
55
+ const rawQuery = q >= 0 ? rawUrl.slice(q + 1) : "";
56
+ const pairs = parseQueryPairs(rawQuery).filter(([k]) => k !== SIG_PARAM);
57
+ mutate?.(pairs);
58
+ const encoded = encodeQuery(pairs);
59
+ return encoded === "" ? prefix : `${prefix}?${encoded}`;
60
+ }
61
+ /** setParam overwrites an existing key in place (preserving order) or appends. */
62
+ function setParam(pairs, key, value) {
63
+ const existing = pairs.find(([k]) => k === key);
64
+ if (existing)
65
+ existing[1] = value;
66
+ else
67
+ pairs.push([key, value]);
68
+ }
69
+ /** parseQueryPairs splits a raw query into ordered [key, value] pairs, decoding
70
+ * each side exactly as Go's url.ParseQuery (a "+" decodes to a space). */
71
+ function parseQueryPairs(rawQuery) {
72
+ if (rawQuery === "")
73
+ return [];
74
+ const out = [];
75
+ for (const part of rawQuery.split("&")) {
76
+ if (part === "")
77
+ continue;
78
+ const eq = part.indexOf("=");
79
+ const key = eq >= 0 ? part.slice(0, eq) : part;
80
+ const value = eq >= 0 ? part.slice(eq + 1) : "";
81
+ out.push([queryUnescape(key), queryUnescape(value)]);
82
+ }
83
+ return out;
84
+ }
85
+ /** encodeQuery serializes pairs byte-identically to Go's url.Values.Encode():
86
+ * sort by key (stable per-key value order), escape each side, join "k=v&k=v". */
87
+ function encodeQuery(pairs) {
88
+ const sorted = [...pairs].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
89
+ return sorted.map(([k, v]) => `${queryEscape(k)}=${queryEscape(v)}`).join("&");
90
+ }
91
+ /** Go url.QueryUnescape: "+" -> space, "%XX" -> byte, then UTF-8 decode. */
92
+ function queryUnescape(s) {
93
+ return decodeURIComponent(s.replaceAll("+", " "));
94
+ }
95
+ /** Go url.QueryEscape: unreserved A-Za-z0-9-._~ kept, space -> "+", everything
96
+ * else percent-encoded uppercase (UTF-8). decodeURIComponent/encodeURIComponent
97
+ * keep a slightly wider unreserved set (!'()*), so post-process those to match. */
98
+ function queryEscape(s) {
99
+ const encoded = encodeURIComponent(s)
100
+ .replaceAll("%20", "+")
101
+ .replaceAll("!", "%21")
102
+ .replaceAll("'", "%27")
103
+ .replaceAll("(", "%28")
104
+ .replaceAll(")", "%29")
105
+ .replaceAll("*", "%2A");
106
+ return encoded;
107
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Compute the RFC 7638 JWK Thumbprint of a raw 32-byte Ed25519 public key,
3
+ * base64url-no-pad encoded (ADR-013 D4).
4
+ *
5
+ * The canonical JWK is fixed by RFC 7638 §3.2 for OKP keys —
6
+ * `{"crv":"Ed25519","kty":"OKP","x":"<base64url-nopad(pubkey)>"}`, members in
7
+ * lexicographic order, no whitespace — and the thumbprint is
8
+ * `base64url-nopad(SHA-256(canonical JWK))`.
9
+ *
10
+ * This MUST stay byte-identical to the sdk/go oracle (helpers.Thumbprint, which
11
+ * builds the same canonical JWK via go-jose). Both are pinned to the shared
12
+ * testdata/thumbprint-vectors.json.
13
+ */
14
+ export declare function thumbprint(pubkey: Uint8Array): Promise<string>;
@@ -0,0 +1,24 @@
1
+ import { encodeBase64Url } from "./base64url.js";
2
+ const ED25519_PUBLIC_KEY_BYTES = 32;
3
+ /**
4
+ * Compute the RFC 7638 JWK Thumbprint of a raw 32-byte Ed25519 public key,
5
+ * base64url-no-pad encoded (ADR-013 D4).
6
+ *
7
+ * The canonical JWK is fixed by RFC 7638 §3.2 for OKP keys —
8
+ * `{"crv":"Ed25519","kty":"OKP","x":"<base64url-nopad(pubkey)>"}`, members in
9
+ * lexicographic order, no whitespace — and the thumbprint is
10
+ * `base64url-nopad(SHA-256(canonical JWK))`.
11
+ *
12
+ * This MUST stay byte-identical to the sdk/go oracle (helpers.Thumbprint, which
13
+ * builds the same canonical JWK via go-jose). Both are pinned to the shared
14
+ * testdata/thumbprint-vectors.json.
15
+ */
16
+ export async function thumbprint(pubkey) {
17
+ if (pubkey.length !== ED25519_PUBLIC_KEY_BYTES) {
18
+ throw new Error(`thumbprint: public key must be ${ED25519_PUBLIC_KEY_BYTES} bytes`);
19
+ }
20
+ const x = encodeBase64Url(pubkey);
21
+ const canonical = `{"crv":"Ed25519","kty":"OKP","x":"${x}"}`;
22
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical));
23
+ return encodeBase64Url(new Uint8Array(digest));
24
+ }
@@ -0,0 +1,49 @@
1
+ export interface VerifyResult {
2
+ valid: boolean;
3
+ expired: boolean;
4
+ kid?: string;
5
+ /** RFC 7638 thumbprint the URL is bound to (base64url string, the agent_id
6
+ * param verbatim) — the same name/representation as Go VerifiedURL.AgentID
7
+ * and Python SignedUrlResult.agent_id. Absent on a bearer (unbound) URL. */
8
+ agentId?: string;
9
+ reason?: VerifyFailure;
10
+ }
11
+ export type VerifyFailure = "missing_sig" | "missing_exp" | "bad_sig_encoding" | "bad_exp_encoding" | "expired" | "bad_agent_encoding" | "signature_mismatch";
12
+ /**
13
+ * The Ed25519 primitive the verify envelope calls once a key is resolved. It
14
+ * receives the resolved key (whatever type `resolveKey` yields — a WebCrypto
15
+ * `CryptoKey` by default, but a raw 32-byte public key on a runtime whose
16
+ * SubtleCrypto lacks Ed25519), the canonical message bytes, and the signature
17
+ * bytes, and returns whether the signature verifies. INJECTABLE so a consumer
18
+ * on a runtime without native Ed25519 (Fastly Compute) swaps in @noble/ed25519
19
+ * and reuses this whole envelope (param parse, expiry, reason mapping) instead
20
+ * of re-hand-rolling it — mirroring how the sign/verify faces already inject
21
+ * key resolution. `K` is inferred from `resolveKey`.
22
+ */
23
+ export type Ed25519Verifier<K = CryptoKey> = (key: K, message: Uint8Array<ArrayBuffer>, sig: Uint8Array<ArrayBuffer>) => Promise<boolean> | boolean;
24
+ export interface VerifyDeps<K = CryptoKey> {
25
+ now?: () => number;
26
+ resolveKey: (kid: string | undefined) => Promise<K | undefined>;
27
+ /** Ed25519 primitive; defaults to WebCrypto native (`crypto.subtle.verify`).
28
+ * A runtime lacking native Ed25519 injects its own (e.g. @noble/ed25519) and
29
+ * MUST when `resolveKey` yields a non-`CryptoKey` (e.g. raw public-key bytes). */
30
+ verify?: Ed25519Verifier<K>;
31
+ }
32
+ export declare function verifyEd25519SignedUrl<K = CryptoKey>(rawUrl: string, deps: VerifyDeps<K>): Promise<VerifyResult>;
33
+ /**
34
+ * The canonical signed message: "GET\n<url>" over the URL as OPAQUE BYTES. Only
35
+ * the `sig` param is stripped and the query is deterministically re-encoded;
36
+ * scheme/host/path are preserved verbatim (no `new URL()` host-lowercasing,
37
+ * default-port stripping, or path-escaping). Reuses the SAME builder the sign
38
+ * face uses (signurl.ts::canonicalUrl) so signer and verifier agree by
39
+ * construction — see the verbatim-canonicalization contract there.
40
+ *
41
+ * rawUrl is coerced via opaqueUrl defensively: some edge runtimes (Fastly Compute)
42
+ * hand the request URL as a URL-like object rather than a primitive string, and
43
+ * canonicalUrl needs string operations. For a primitive string this is a no-op,
44
+ * so the verbatim bytes are preserved for the server-to-server callers that own
45
+ * the raw URL string. This coercion is retained even though verifyEd25519SignedUrl
46
+ * now coerces at its boundary: canonicalMessage is a public export with its own
47
+ * external callers, so it owns its input contract independently.
48
+ */
49
+ export declare function canonicalMessage(rawUrl: string): Uint8Array<ArrayBuffer>;
@@ -0,0 +1,109 @@
1
+ import { z } from "zod";
2
+ import { decodeBase64Url, utf8Bytes } from "./base64url.js";
3
+ import { opaqueUrl } from "./opaque-url.js";
4
+ import { canonicalUrl } from "./signurl.js";
5
+ const ParamsSchema = z.object({
6
+ sig: z.string().min(1),
7
+ exp: z.string().regex(/^\d+$/),
8
+ kid: z.string().min(1).optional(),
9
+ agentId: z.string().min(1).optional(),
10
+ });
11
+ // The default primitive: WebCrypto native Ed25519. Byte-identical to the prior
12
+ // inline crypto.subtle.verify — the argument order is adapted (WebCrypto takes
13
+ // (algo, key, sig, data); the injectable contract is (key, message, sig)).
14
+ const defaultVerify = (key, message, sig) => crypto.subtle.verify("Ed25519", key, sig, message);
15
+ export async function verifyEd25519SignedUrl(rawUrl, deps) {
16
+ // Coerce a URL-like input (a Fastly Compute request URL object) to its opaque
17
+ // string form ONCE at the boundary, so param parsing and the canonical message
18
+ // both operate on the same verbatim bytes. No-op for string callers.
19
+ const raw = opaqueUrl(rawUrl);
20
+ const url = new URL(raw);
21
+ const params = parseParams(url);
22
+ if (!params.ok) {
23
+ return { valid: false, expired: false, reason: params.reason };
24
+ }
25
+ const { sig, exp, kid, agentId } = params.value;
26
+ const nowSec = Math.floor((deps.now?.() ?? Date.now()) / 1000);
27
+ if (nowSec >= Number(exp)) {
28
+ return { valid: false, expired: true, reason: "expired" };
29
+ }
30
+ const sigBytes = decodeBase64Url(sig);
31
+ if (!sigBytes) {
32
+ return { valid: false, expired: false, reason: "bad_sig_encoding" };
33
+ }
34
+ if (agentId !== undefined) {
35
+ // Decode purely as VALIDATION: a malformed agent_id is rejected before any
36
+ // signature work; the public surface carries the base64url STRING verbatim.
37
+ if (!decodeBase64Url(agentId)) {
38
+ return { valid: false, expired: false, reason: "bad_agent_encoding" };
39
+ }
40
+ }
41
+ const key = await deps.resolveKey(kid);
42
+ if (!key) {
43
+ return buildResult({ valid: false, expired: false, kid, reason: "signature_mismatch" });
44
+ }
45
+ const message = canonicalMessage(raw);
46
+ // The default cast is dead when K !== CryptoKey: such a consumer always
47
+ // supplies its own verifier (the `??` short-circuits), so defaultVerify — which
48
+ // only type-checks for CryptoKey — is never reached with a foreign key type.
49
+ const verify = deps.verify ?? defaultVerify;
50
+ const okSig = await verify(key, message, sigBytes);
51
+ if (!okSig) {
52
+ return buildResult({ valid: false, expired: false, kid, reason: "signature_mismatch" });
53
+ }
54
+ return buildResult({ valid: true, expired: false, kid, ...(agentId !== undefined && { agentId }) });
55
+ }
56
+ function buildResult(r) {
57
+ const out = { valid: r.valid, expired: r.expired };
58
+ if (r.kid !== undefined)
59
+ out.kid = r.kid;
60
+ if (r.agentId !== undefined)
61
+ out.agentId = r.agentId;
62
+ if (r.reason !== undefined)
63
+ out.reason = r.reason;
64
+ return out;
65
+ }
66
+ function parseParams(url) {
67
+ const sig = url.searchParams.get("sig");
68
+ if (!sig)
69
+ return { ok: false, reason: "missing_sig" };
70
+ const exp = url.searchParams.get("exp");
71
+ if (!exp)
72
+ return { ok: false, reason: "missing_exp" };
73
+ const kidRaw = url.searchParams.get("kid");
74
+ const agentRaw = url.searchParams.get("agent_id");
75
+ const candidate = { sig, exp };
76
+ if (kidRaw !== null)
77
+ candidate.kid = kidRaw;
78
+ if (agentRaw !== null)
79
+ candidate.agentId = agentRaw;
80
+ const parsed = ParamsSchema.safeParse(candidate);
81
+ if (!parsed.success) {
82
+ return { ok: false, reason: "bad_exp_encoding" };
83
+ }
84
+ const value = { sig: parsed.data.sig, exp: parsed.data.exp };
85
+ if (parsed.data.kid !== undefined)
86
+ value.kid = parsed.data.kid;
87
+ if (parsed.data.agentId !== undefined)
88
+ value.agentId = parsed.data.agentId;
89
+ return { ok: true, value };
90
+ }
91
+ /**
92
+ * The canonical signed message: "GET\n<url>" over the URL as OPAQUE BYTES. Only
93
+ * the `sig` param is stripped and the query is deterministically re-encoded;
94
+ * scheme/host/path are preserved verbatim (no `new URL()` host-lowercasing,
95
+ * default-port stripping, or path-escaping). Reuses the SAME builder the sign
96
+ * face uses (signurl.ts::canonicalUrl) so signer and verifier agree by
97
+ * construction — see the verbatim-canonicalization contract there.
98
+ *
99
+ * rawUrl is coerced via opaqueUrl defensively: some edge runtimes (Fastly Compute)
100
+ * hand the request URL as a URL-like object rather than a primitive string, and
101
+ * canonicalUrl needs string operations. For a primitive string this is a no-op,
102
+ * so the verbatim bytes are preserved for the server-to-server callers that own
103
+ * the raw URL string. This coercion is retained even though verifyEd25519SignedUrl
104
+ * now coerces at its boundary: canonicalMessage is a public export with its own
105
+ * external callers, so it owns its input contract independently.
106
+ */
107
+ export function canonicalMessage(rawUrl) {
108
+ return utf8Bytes(`GET\n${canonicalUrl(opaqueUrl(rawUrl))}`);
109
+ }
@@ -0,0 +1 @@
1
+ export { snakeFromJsonName } from "../../../gen/ts/wire/names.ts";
@@ -0,0 +1,16 @@
1
+ // Recovering a proto field name from protojson's lowerCamelCase spelling of it.
2
+ //
3
+ // Re-export, not a second copy. The rule lives in gen/ts/wire/names.ts, beside the
4
+ // generated schemas, because the schema seam itself depends on it — wire/base.ts refuses
5
+ // an answer spelled in the json_name alias, and a rule the seam needs cannot live in a
6
+ // tier above the seam. It is re-exported here so the SDK's own reader of the alias — the
7
+ // Connect error-detail `debug` projection, which IS lowerCamelCase and cannot be made
8
+ // otherwise — reaches the same implementation rather than transcribing it.
9
+ //
10
+ // One rule, two callers, opposite verdicts, which is the reason it is worth sharing:
11
+ // errordetail NORMALIZES the alias (Connect emits it there and no server codec replaces
12
+ // it), while the schema seam REFUSES it (a whole response body in the alias means the peer
13
+ // is not speaking the contract).
14
+ //
15
+ // See gen/ts/wire/names.ts for the rule itself and why the boundary test is ASCII-only.
16
+ export { snakeFromJsonName } from "../../../gen/ts/wire/names.js";
@@ -0,0 +1,68 @@
1
+ /** ContentTypeProto is the Content-Type for binary protobuf bodies. */
2
+ export declare const ContentTypeProto = "application/proto";
3
+ /** ContentTypeJSON is the Content-Type for canonical proto-JSON bodies. */
4
+ export declare const ContentTypeJSON = "application/json";
5
+ /** ConnectProtocolVersionHeader carries the Connect unary protocol version. */
6
+ export declare const ConnectProtocolVersionHeader = "Connect-Protocol-Version";
7
+ /** ConnectProtocolVersion is the only Connect protocol version FORA speaks. */
8
+ export declare const ConnectProtocolVersion = "1";
9
+ /**
10
+ * ProtocolVersion is the FORA protocol version stamped on the `ver` field of
11
+ * every FORA message — NOT the Connect transport version above. Senders stamp
12
+ * it from here so a protocol bump is a single edit; receivers treat `ver` as
13
+ * advisory. The /.well-known/fora.json document carries its own version in a
14
+ * separate namespace, which this constant does NOT supply — that is
15
+ * WellKnownManifestVersion.
16
+ */
17
+ export declare const ProtocolVersion = "1.0";
18
+ /**
19
+ * WellKnownManifestVersion is the version of the /.well-known/fora.json DOCUMENT
20
+ * layout, stamped on WellKnownManifest.ver by every party that serves one. A
21
+ * namespace separate from ProtocolVersion and never derived from it: a change
22
+ * to the manifest layout bumps both numbers, a protocol change that leaves the
23
+ * manifest untouched bumps only ProtocolVersion. Both read "1.0" today because
24
+ * neither has moved yet; neither is derived from the other. The receive-side
25
+ * check a manifest reader applies is manifestVersionRefusal.
26
+ */
27
+ export declare const WellKnownManifestVersion = "1.0";
28
+ /** RequestIDHeader correlates a request across services and the edge. */
29
+ export declare const RequestIDHeader = "X-Request-ID";
30
+ /** SignatureAgentHeader carries the signer's Web Bot Auth key-directory URL. */
31
+ export declare const SignatureAgentHeader = "Signature-Agent";
32
+ /**
33
+ * Header carrying the fetcher's raw Ed25519 public key on a PoP GET. Canonical
34
+ * (Go) casing; HTTP field names are case-insensitive, so lookups lowercase it —
35
+ * see `AGENT_KEY_HEADER` in `pop.ts`, which derives from this rather than
36
+ * restating the string.
37
+ */
38
+ export declare const AgentKeyHeader = "X-FORA-Agent-Key";
39
+ /**
40
+ * Path of the discovery document on every Exchange host. The one bootstrap
41
+ * coordinate: a client holding only a hostname fetches
42
+ * `{scheme}://{host}${WellKnownPath}` to learn the endpoint and the keys, so the
43
+ * three SDKs agreeing on it is the precondition for interop, not a tidiness
44
+ * concern.
45
+ * `WellKnownManifestVersion` versions the document's content; `WellKnownPath`
46
+ * specifies where it is served.
47
+ */
48
+ export declare const WellKnownPath = "/.well-known/fora.json";
49
+ /**
50
+ * The receive-side rule for WellKnownManifest.ver, as a pure verdict.
51
+ *
52
+ * Returns undefined when the document is accepted — its MAJOR equals the major
53
+ * of WellKnownManifestVersion, whatever the MINOR. Otherwise returns the reason:
54
+ * an unrecognised major, a value that is not MAJOR.MINOR, or an absent member
55
+ * (undefined, null, or any non-string is how a missing `ver` arrives from
56
+ * JSON.parse). Absent is refused because a document with no version is one whose
57
+ * layout the reader cannot classify. Why a minor is accepted, and why the gate
58
+ * runs before any other member is read and fails closed, is stated once on
59
+ * WellKnownManifest.ver in the proto.
60
+ *
61
+ * The message names the value found so an operator can tell a version mismatch
62
+ * from a network failure, clipped to MAX_ECHOED_VER characters: the body is read
63
+ * up to 1 MiB and a refusal is never cached, so an unclipped echo would let a
64
+ * hostile origin size every error. The three SDK languages pin this verdict to a
65
+ * shared corpus. The error a resolver throws for a refusal is
66
+ * ManifestVersionRefused.
67
+ */
68
+ export declare function manifestVersionRefusal(ver: unknown): string | undefined;
@@ -0,0 +1,114 @@
1
+ // Wire constants shared across the SDK — TS port of the sdk/go oracle
2
+ // (helpers/constants.go + core/requestid.go). Encoding is negotiated per hop via
3
+ // Content-Type (ADR-020): application/proto for binary, application/json for
4
+ // canonical proto-JSON. The Go layer splits RequestIDHeader across
5
+ // helpers/constants.go and core/requestid.go; the single TS module exposes all
6
+ // ten values once. Pinned to wire-constants-vectors.json.
7
+ //
8
+ // Also home to the pure receive-side rule for WellKnownManifest.ver, so the
9
+ // constant and the check that reads it sit together.
10
+ /** ContentTypeProto is the Content-Type for binary protobuf bodies. */
11
+ export const ContentTypeProto = "application/proto";
12
+ /** ContentTypeJSON is the Content-Type for canonical proto-JSON bodies. */
13
+ export const ContentTypeJSON = "application/json";
14
+ /** ConnectProtocolVersionHeader carries the Connect unary protocol version. */
15
+ export const ConnectProtocolVersionHeader = "Connect-Protocol-Version";
16
+ /** ConnectProtocolVersion is the only Connect protocol version FORA speaks. */
17
+ export const ConnectProtocolVersion = "1";
18
+ /**
19
+ * ProtocolVersion is the FORA protocol version stamped on the `ver` field of
20
+ * every FORA message — NOT the Connect transport version above. Senders stamp
21
+ * it from here so a protocol bump is a single edit; receivers treat `ver` as
22
+ * advisory. The /.well-known/fora.json document carries its own version in a
23
+ * separate namespace, which this constant does NOT supply — that is
24
+ * WellKnownManifestVersion.
25
+ */
26
+ export const ProtocolVersion = "1.0";
27
+ /**
28
+ * WellKnownManifestVersion is the version of the /.well-known/fora.json DOCUMENT
29
+ * layout, stamped on WellKnownManifest.ver by every party that serves one. A
30
+ * namespace separate from ProtocolVersion and never derived from it: a change
31
+ * to the manifest layout bumps both numbers, a protocol change that leaves the
32
+ * manifest untouched bumps only ProtocolVersion. Both read "1.0" today because
33
+ * neither has moved yet; neither is derived from the other. The receive-side
34
+ * check a manifest reader applies is manifestVersionRefusal.
35
+ */
36
+ export const WellKnownManifestVersion = "1.0";
37
+ /** RequestIDHeader correlates a request across services and the edge. */
38
+ export const RequestIDHeader = "X-Request-ID";
39
+ /** SignatureAgentHeader carries the signer's Web Bot Auth key-directory URL. */
40
+ export const SignatureAgentHeader = "Signature-Agent";
41
+ /**
42
+ * Header carrying the fetcher's raw Ed25519 public key on a PoP GET. Canonical
43
+ * (Go) casing; HTTP field names are case-insensitive, so lookups lowercase it —
44
+ * see `AGENT_KEY_HEADER` in `pop.ts`, which derives from this rather than
45
+ * restating the string.
46
+ */
47
+ export const AgentKeyHeader = "X-FORA-Agent-Key";
48
+ /**
49
+ * Path of the discovery document on every Exchange host. The one bootstrap
50
+ * coordinate: a client holding only a hostname fetches
51
+ * `{scheme}://{host}${WellKnownPath}` to learn the endpoint and the keys, so the
52
+ * three SDKs agreeing on it is the precondition for interop, not a tidiness
53
+ * concern.
54
+ * `WellKnownManifestVersion` versions the document's content; `WellKnownPath`
55
+ * specifies where it is served.
56
+ */
57
+ export const WellKnownPath = "/.well-known/fora.json";
58
+ const ASCII_DIGITS = /^[0-9]+$/;
59
+ /** The MAJOR run of a MAJOR.MINOR string, or undefined when `ver` is not one.
60
+ * Both runs must be non-empty ASCII digits joined by exactly one dot; a missing
61
+ * minor, a patch component, a leading "v", surrounding whitespace or a non-digit
62
+ * is not a version this rule recognises. */
63
+ function parseMajor(ver) {
64
+ const dot = ver.indexOf(".");
65
+ if (dot < 0)
66
+ return undefined;
67
+ const major = ver.slice(0, dot);
68
+ const minor = ver.slice(dot + 1);
69
+ if (!ASCII_DIGITS.test(major) || !ASCII_DIGITS.test(minor))
70
+ return undefined;
71
+ return major;
72
+ }
73
+ /**
74
+ * The receive-side rule for WellKnownManifest.ver, as a pure verdict.
75
+ *
76
+ * Returns undefined when the document is accepted — its MAJOR equals the major
77
+ * of WellKnownManifestVersion, whatever the MINOR. Otherwise returns the reason:
78
+ * an unrecognised major, a value that is not MAJOR.MINOR, or an absent member
79
+ * (undefined, null, or any non-string is how a missing `ver` arrives from
80
+ * JSON.parse). Absent is refused because a document with no version is one whose
81
+ * layout the reader cannot classify. Why a minor is accepted, and why the gate
82
+ * runs before any other member is read and fails closed, is stated once on
83
+ * WellKnownManifest.ver in the proto.
84
+ *
85
+ * The message names the value found so an operator can tell a version mismatch
86
+ * from a network failure, clipped to MAX_ECHOED_VER characters: the body is read
87
+ * up to 1 MiB and a refusal is never cached, so an unclipped echo would let a
88
+ * hostile origin size every error. The three SDK languages pin this verdict to a
89
+ * shared corpus. The error a resolver throws for a refusal is
90
+ * ManifestVersionRefused.
91
+ */
92
+ export function manifestVersionRefusal(ver) {
93
+ const acceptMajor = parseMajor(WellKnownManifestVersion);
94
+ if (acceptMajor === undefined) {
95
+ throw new Error(`WellKnownManifestVersion is not MAJOR.MINOR: ${WellKnownManifestVersion}`);
96
+ }
97
+ if (typeof ver !== "string" || ver === "") {
98
+ return `ver is absent, accept major ${acceptMajor}`;
99
+ }
100
+ const major = parseMajor(ver);
101
+ if (major === undefined) {
102
+ return `ver ${JSON.stringify(echoVer(ver))} is not MAJOR.MINOR, accept major ${acceptMajor}`;
103
+ }
104
+ if (major !== acceptMajor) {
105
+ return `ver ${JSON.stringify(echoVer(ver))} has major ${major}, accept major ${acceptMajor}`;
106
+ }
107
+ return undefined;
108
+ }
109
+ /** How much of a refused `ver` an error message repeats. Go and Python clip at
110
+ * the same length, so an operator reading three SDKs' logs sees the same prefix. */
111
+ const MAX_ECHOED_VER = 64;
112
+ function echoVer(ver) {
113
+ return ver.length <= MAX_ECHOED_VER ? ver : `${ver.slice(0, MAX_ECHOED_VER)}...`;
114
+ }