@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,433 @@
1
+ // License-term canonicalisation and the ingest-tier checks — TS port of the
2
+ // sdk/go oracle (helpers/licenseterm.go), pinned to the shared
3
+ // licenseterm-vectors.json.
4
+ //
5
+ // A pushed entry passes two tiers at the Exchange. The wire tier is the
6
+ // generated field-level schema plus the cross-field (message-CEL) rules,
7
+ // applied to the entry as received. The ingest tier runs over the CANONICALISED
8
+ // terms: restriction tokens are folded and alias-resolved to their registered
9
+ // form, then a bare Pricing.unit or Quota.metric that is not a registered token
10
+ // is rejected, as is a restriction whose permitted and prohibited lists name one
11
+ // token once folded, while an unregistered restriction token and an
12
+ // OBLIGATION_KIND_OTHER obligation without detail are accepted with a warning
13
+ // that reaches PushResourcesResponse.warnings. The Exchange's own run is the
14
+ // deciding one; a client-side verdict is advice about what that run will say.
15
+ //
16
+ // Folding is ASCII-only and only RFC 8259 whitespace is trimmed — a platform
17
+ // Unicode fold turns U+212A KELVIN SIGN into "k" and a homograph into a
18
+ // registered token. Messages are the exact strings the Go oracle emits, which
19
+ // are the strings an Exchange puts on the wire.
20
+ import { JSON_NAME_ALIAS_ERROR, WireNamingError, underWirePolicy, } from "../../../gen/ts/wire/base.js";
21
+ import { ResourceEntrySchema } from "../../../gen/ts/wire/schemas.js";
22
+ import * as functiontokens from "../../../gen/ts/vocab/functiontokens.js";
23
+ import * as geographytokens from "../../../gen/ts/vocab/geographytokens.js";
24
+ import * as pricingunits from "../../../gen/ts/vocab/pricingunits.js";
25
+ import * as quotametrics from "../../../gen/ts/vocab/quotametrics.js";
26
+ import * as usertypes from "../../../gen/ts/vocab/usertypes.js";
27
+ import { crossFieldRuleIds } from "./crossfield.js";
28
+ /** Rejects a bare (non-namespaced) Pricing.unit that is not a registered metering token. */
29
+ export const RULE_PRICING_UNIT_REGISTERED = "pricing.unit.registered";
30
+ /** Rejects a bare Quota.metric that is not a registered quota token. */
31
+ export const RULE_QUOTA_METRIC_REGISTERED = "quota.metric.registered";
32
+ /**
33
+ * Rejects a restriction whose permitted and prohibited lists name the same token
34
+ * once both are canonicalised. The wire tier's rule compares the tokens AS
35
+ * WRITTEN, so two accepted spellings of one token — an alias beside its
36
+ * registered form, or two spellings differing only in ASCII case — pass it and
37
+ * collide only after the fold.
38
+ */
39
+ export const RULE_RESTRICTION_CANONICAL_DISJOINT = "restriction.canonical_disjoint";
40
+ /** Warns about a bare restriction token not registered on its axis; the term is accepted. */
41
+ export const RULE_RESTRICTION_TOKEN_REGISTERED = "restriction.token.registered";
42
+ /** Warns about an OBLIGATION_KIND_OTHER obligation carrying no detail. */
43
+ export const RULE_OBLIGATION_OTHER_REQUIRES_DETAIL = "obligation.other.requires_detail";
44
+ const KIND_FUNCTION = "RESTRICTION_KIND_FUNCTION";
45
+ const KIND_GEOGRAPHY = "RESTRICTION_KIND_GEOGRAPHY";
46
+ const KIND_USER_TYPE = "RESTRICTION_KIND_USER_TYPE";
47
+ // The enum's zero, as the oracle spells it when a restriction carries no kind.
48
+ const KIND_UNSPECIFIED = "RESTRICTION_KIND_UNSPECIFIED";
49
+ const OBLIGATION_KIND_OTHER = "OBLIGATION_KIND_OTHER";
50
+ function asObj(v) {
51
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined;
52
+ }
53
+ function asArr(v) {
54
+ return Array.isArray(v) ? v : [];
55
+ }
56
+ function str(v) {
57
+ return typeof v === "string" ? v : "";
58
+ }
59
+ function isJSONWhitespace(c) {
60
+ return c === " " || c === "\t" || c === "\n" || c === "\r";
61
+ }
62
+ // The four RFC 8259 whitespace characters and nothing else, so a token padded
63
+ // with a non-breaking space stays padded, in every language.
64
+ function trimJSONWhitespace(s) {
65
+ let start = 0;
66
+ let end = s.length;
67
+ while (start < end && isJSONWhitespace(s.charAt(start)))
68
+ start++;
69
+ while (end > start && isJSONWhitespace(s.charAt(end - 1)))
70
+ end--;
71
+ return s.slice(start, end);
72
+ }
73
+ // ASCII letters only — no flag on the class, so no Unicode case folding.
74
+ function asciiLower(s) {
75
+ return s.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
76
+ }
77
+ function asciiUpper(s) {
78
+ return s.replace(/[a-z]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 32));
79
+ }
80
+ function hasCanonicalRule(kind) {
81
+ return kind === KIND_FUNCTION || kind === KIND_USER_TYPE || kind === KIND_GEOGRAPHY;
82
+ }
83
+ function isNamespacedToken(token) {
84
+ return token.includes(":");
85
+ }
86
+ // Two uppercase ASCII letters — the structural shape of an ISO 3166-1 alpha-2
87
+ // code, which the geography registry deliberately does not enumerate.
88
+ function isISOAlpha2(token) {
89
+ if (token.length !== 2)
90
+ return false;
91
+ const a = token.charCodeAt(0);
92
+ const b = token.charCodeAt(1);
93
+ return a >= 65 && a <= 90 && b >= 65 && b <= 90;
94
+ }
95
+ /**
96
+ * canonicalRestrictionToken returns the canonical form of a restriction token
97
+ * on an axis (`kind` is the RestrictionKind enum NAME): RFC 8259 whitespace
98
+ * trimmed, ASCII case folded (lower for FUNCTION and USER_TYPE, upper for
99
+ * GEOGRAPHY) and, where the axis authors aliases, the alias resolved to its
100
+ * registered token. OTHER and any unknown axis are returned unchanged.
101
+ * Applying it twice is a fixed point.
102
+ */
103
+ export function canonicalRestrictionToken(kind, token) {
104
+ switch (kind) {
105
+ case KIND_FUNCTION:
106
+ return functiontokens.canonical(asciiLower(trimJSONWhitespace(token)));
107
+ case KIND_USER_TYPE:
108
+ return usertypes.canonical(asciiLower(trimJSONWhitespace(token)));
109
+ case KIND_GEOGRAPHY:
110
+ return geographytokens.canonical(asciiUpper(trimJSONWhitespace(token)));
111
+ default:
112
+ return token;
113
+ }
114
+ }
115
+ /**
116
+ * knownRestrictionToken reports whether an already-canonical token is
117
+ * registered on its axis. GEOGRAPHY admits the registered specials and any
118
+ * two-uppercase-letter ISO 3166-1 alpha-2 code; OTHER and unknown axes never.
119
+ */
120
+ export function knownRestrictionToken(kind, token) {
121
+ switch (kind) {
122
+ case KIND_FUNCTION:
123
+ return functiontokens.isRegistered(token);
124
+ case KIND_USER_TYPE:
125
+ return usertypes.isRegistered(token);
126
+ case KIND_GEOGRAPHY:
127
+ return geographytokens.isRegistered(token) || isISOAlpha2(token);
128
+ default:
129
+ return false;
130
+ }
131
+ }
132
+ function canonicalizeTokens(kind, tokens) {
133
+ if (!Array.isArray(tokens))
134
+ return tokens;
135
+ return tokens.map((t) => (typeof t === "string" ? canonicalRestrictionToken(kind, t) : t));
136
+ }
137
+ /**
138
+ * normalizeLicenseTerm returns a deep copy of the term (proto-JSON, snake_case)
139
+ * with its restriction tokens rewritten to canonical form on every axis that
140
+ * carries a canonicalisation rule. Nothing else moves. The input is untouched;
141
+ * the Go oracle rewrites in place, and the corpus pins the output either way.
142
+ */
143
+ export function normalizeLicenseTerm(term) {
144
+ const out = structuredClone(term);
145
+ for (const r of asArr(out["restrictions"])) {
146
+ const restriction = asObj(r);
147
+ if (!restriction)
148
+ continue;
149
+ const kind = str(restriction["kind"]);
150
+ if (!hasCanonicalRule(kind))
151
+ continue;
152
+ if (restriction["permitted"] !== undefined) {
153
+ restriction["permitted"] = canonicalizeTokens(kind, restriction["permitted"]);
154
+ }
155
+ if (restriction["prohibited"] !== undefined) {
156
+ restriction["prohibited"] = canonicalizeTokens(kind, restriction["prohibited"]);
157
+ }
158
+ }
159
+ return out;
160
+ }
161
+ /** normalizeResourceEntry returns a deep copy with every term normalised. */
162
+ export function normalizeResourceEntry(entry) {
163
+ const out = structuredClone(entry);
164
+ const terms = asArr(out["terms"]);
165
+ if (terms.length) {
166
+ out["terms"] = terms.map((t) => {
167
+ const term = asObj(t);
168
+ return term ? normalizeLicenseTerm(term) : t;
169
+ });
170
+ }
171
+ return out;
172
+ }
173
+ function bareUnregistered(token, registered) {
174
+ if (token === "" || isNamespacedToken(token))
175
+ return false;
176
+ return !registered(token);
177
+ }
178
+ function restrictionTokenWarning(kind, tok, path) {
179
+ if (tok === "" || isNamespacedToken(tok) || knownRestrictionToken(kind, tok))
180
+ return undefined;
181
+ return {
182
+ rule: RULE_RESTRICTION_TOKEN_REGISTERED,
183
+ path,
184
+ token: tok,
185
+ // An absent kind is the enum's zero, and the oracle names it: Go formats the
186
+ // enum, which spells the unset value rather than leaving a gap. These strings
187
+ // are the exact bytes an Exchange puts in warnings[], so a doubled space here
188
+ // is a real difference in what a publisher is told, not a rendering detail.
189
+ message: `unregistered ${kind || KIND_UNSPECIFIED} restriction token "${tok}" (term accepted)`,
190
+ };
191
+ }
192
+ /**
193
+ * canonicalDisjointViolation returns the violation for the first permitted token
194
+ * of r whose canonical form is also the canonical form of one of its prohibited
195
+ * tokens, or undefined when the two lists name no token in common. It
196
+ * canonicalises what it compares rather than trusting the term to have been
197
+ * normalised, and it runs on every axis — where the fold is a no-op the check is
198
+ * plain equality, which is what a server that never asked for the wire tier
199
+ * needs. The finding names the canonical token, not the spellings that produced
200
+ * it, so the message does not depend on whether the caller folded first.
201
+ *
202
+ * An element that is not a string is SKIPPED rather than coerced. Coercing it to ""
203
+ * would report two ill-typed elements as a collision on the empty token, which names
204
+ * the wrong fault; the wire tier already refuses a non-string where the schema says
205
+ * string, the same division this file applies to an empty or namespaced token. Two
206
+ * genuinely empty strings still collide, which is what the Go oracle answers.
207
+ */
208
+ function canonicalDisjointViolation(i, r) {
209
+ const permitted = asArr(r["permitted"]);
210
+ const prohibited = asArr(r["prohibited"]);
211
+ if (permitted.length === 0 || prohibited.length === 0)
212
+ return undefined;
213
+ const kind = str(r["kind"]);
214
+ const banned = new Set(prohibited.filter((t) => typeof t === "string").map((t) => canonicalRestrictionToken(kind, t)));
215
+ for (let j = 0; j < permitted.length; j++) {
216
+ const tok = permitted[j];
217
+ if (typeof tok !== "string")
218
+ continue;
219
+ const canon = canonicalRestrictionToken(kind, tok);
220
+ if (!banned.has(canon))
221
+ continue;
222
+ return {
223
+ rule: RULE_RESTRICTION_CANONICAL_DISJOINT,
224
+ path: `restrictions[${i}].permitted[${j}]`,
225
+ token: canon,
226
+ message: `restriction token "${canon}" is both permitted and prohibited after canonicalisation`,
227
+ };
228
+ }
229
+ return undefined;
230
+ }
231
+ /**
232
+ * validateLicenseTerm runs the ingest-tier checks over one term, in a fixed
233
+ * order: a bare Pricing.unit that is not registered, then the first offending
234
+ * quota metric, then the first restriction whose permitted and prohibited lists
235
+ * name one token once canonicalised. An accepted term carries one warning per
236
+ * unregistered bare restriction token — restriction order, permitted before
237
+ * prohibited — then one per OBLIGATION_KIND_OTHER obligation without detail.
238
+ *
239
+ * Every check but the disjointness one reads the term as already canonical. The
240
+ * wire tier is not re-run here; disjointness is the one property both tiers
241
+ * assert, over different values, so a term the boundary clears can still fail
242
+ * here.
243
+ */
244
+ export function validateLicenseTerm(term) {
245
+ const unit = str(asObj(term["pricing"])?.["unit"]);
246
+ if (bareUnregistered(unit, pricingunits.isRegistered)) {
247
+ return {
248
+ violation: {
249
+ rule: RULE_PRICING_UNIT_REGISTERED,
250
+ path: "pricing.unit",
251
+ token: unit,
252
+ message: `pricing unit "${unit}" is not a registered metering token`,
253
+ },
254
+ warnings: [],
255
+ };
256
+ }
257
+ const quotas = asArr(term["quotas"]);
258
+ for (let i = 0; i < quotas.length; i++) {
259
+ const metric = str(asObj(quotas[i])?.["metric"]);
260
+ if (bareUnregistered(metric, quotametrics.isRegistered)) {
261
+ return {
262
+ violation: {
263
+ rule: RULE_QUOTA_METRIC_REGISTERED,
264
+ path: `quotas[${i}].metric`,
265
+ token: metric,
266
+ message: `quota metric "${metric}" is not a registered quota token`,
267
+ },
268
+ warnings: [],
269
+ };
270
+ }
271
+ }
272
+ const restrictions = asArr(term["restrictions"]);
273
+ for (let i = 0; i < restrictions.length; i++) {
274
+ const r = asObj(restrictions[i]);
275
+ if (!r)
276
+ continue;
277
+ const violation = canonicalDisjointViolation(i, r);
278
+ if (violation)
279
+ return { violation, warnings: [] };
280
+ }
281
+ const warnings = [];
282
+ for (let i = 0; i < restrictions.length; i++) {
283
+ const r = asObj(restrictions[i]);
284
+ if (!r)
285
+ continue;
286
+ const kind = str(r["kind"]);
287
+ const permitted = asArr(r["permitted"]);
288
+ for (let j = 0; j < permitted.length; j++) {
289
+ const w = restrictionTokenWarning(kind, str(permitted[j]), `restrictions[${i}].permitted[${j}]`);
290
+ if (w)
291
+ warnings.push(w);
292
+ }
293
+ const prohibited = asArr(r["prohibited"]);
294
+ for (let j = 0; j < prohibited.length; j++) {
295
+ const w = restrictionTokenWarning(kind, str(prohibited[j]), `restrictions[${i}].prohibited[${j}]`);
296
+ if (w)
297
+ warnings.push(w);
298
+ }
299
+ }
300
+ const obligations = asArr(term["obligations"]);
301
+ for (let i = 0; i < obligations.length; i++) {
302
+ const o = asObj(obligations[i]);
303
+ if (!o)
304
+ continue;
305
+ if (str(o["kind"]) === OBLIGATION_KIND_OTHER && str(o["detail"]) === "") {
306
+ warnings.push({
307
+ rule: RULE_OBLIGATION_OTHER_REQUIRES_DETAIL,
308
+ path: `obligations[${i}].detail`,
309
+ token: "",
310
+ message: "obligation of kind OTHER has no detail (term accepted)",
311
+ });
312
+ }
313
+ }
314
+ return { violation: null, warnings };
315
+ }
316
+ // Zod 3 types an issue path as (string | number)[]; Zod 4 widens it to PropertyKey[].
317
+ // A symbol segment cannot occur on a JSON document, so it is rendered by name.
318
+ function zodPath(path) {
319
+ let out = "";
320
+ for (const seg of path) {
321
+ if (typeof seg === "number")
322
+ out += `[${seg}]`;
323
+ else
324
+ out = out === "" ? String(seg) : `${out}.${String(seg)}`;
325
+ }
326
+ return out;
327
+ }
328
+ // Every message instance reachable from an entry that carries a cross-field
329
+ // rule, with its entry-relative path. The generated schema is field-level and
330
+ // the composed cross-field schemas attach per message, so the walk is explicit.
331
+ function crossFieldSites(entry) {
332
+ const sites = [];
333
+ const terms = asArr(entry["terms"]);
334
+ for (let i = 0; i < terms.length; i++) {
335
+ const term = asObj(terms[i]);
336
+ if (!term)
337
+ continue;
338
+ const base = `terms[${i}]`;
339
+ sites.push({ path: base, message: "LicenseTerm", value: term });
340
+ if (term["license"] !== undefined)
341
+ sites.push({ path: `${base}.license`, message: "License", value: term["license"] });
342
+ if (term["pricing"] !== undefined)
343
+ sites.push({ path: `${base}.pricing`, message: "Pricing", value: term["pricing"] });
344
+ const restrictions = asArr(term["restrictions"]);
345
+ for (let j = 0; j < restrictions.length; j++) {
346
+ sites.push({ path: `${base}.restrictions[${j}]`, message: "Restriction", value: restrictions[j] });
347
+ }
348
+ const obligations = asArr(term["obligations"]);
349
+ for (let k = 0; k < obligations.length; k++) {
350
+ const o = asObj(obligations[k]);
351
+ sites.push({ path: `${base}.obligations[${k}]`, message: "Obligation", value: obligations[k] });
352
+ if (o?.["scope_license"] !== undefined) {
353
+ sites.push({ path: `${base}.obligations[${k}].scope_license`, message: "License", value: o["scope_license"] });
354
+ }
355
+ }
356
+ }
357
+ return sites;
358
+ }
359
+ /**
360
+ * validateResourceEntry reports the verdict the Exchange reaches for one entry
361
+ * (proto-JSON, snake_case), both tiers in the Exchange's order: the wire tier —
362
+ * the generated field-level schema over the entry as given, plus every
363
+ * cross-field rule reachable from it — then the ingest tier over a normalised
364
+ * copy of the terms. The entry passed in is never modified. The Exchange stops
365
+ * at the first tier that fails; this face reports both so a publisher fixes
366
+ * everything in one round. Paths are relative to the entry.
367
+ *
368
+ * The wire tier runs UNDER THE WIRE POLICY, the same seam every other parse of a
369
+ * generated schema in this SDK goes through. A generated schema describes the
370
+ * message and cannot describe the two things that are true of the wire: a `null`
371
+ * is how proto-JSON spells "no value" for any field, and a lowerCamelCase
372
+ * json_name alias is out of contract. A bare safeParse would answer wrongly in
373
+ * both directions on bodies a publisher really produces — an Exchange serving
374
+ * EmitUnpopulated renders an unset message field as null, and stock
375
+ * protojson.Marshal emits camelCase, which the schemas STRIP, so an entry whose
376
+ * every multiword field was silently dropped would come back accepted. This
377
+ * face exists to predict the Exchange's verdict, so it applies the Exchange's
378
+ * reading of the bytes.
379
+ */
380
+ export function validateResourceEntry(entry) {
381
+ const violations = [];
382
+ try {
383
+ const parsed = ResourceEntrySchema.safeParse(underWirePolicy(ResourceEntrySchema, entry, ""));
384
+ if (!parsed.success) {
385
+ for (const issue of parsed.error.issues) {
386
+ violations.push({ rule: `field.${issue.code}`, path: zodPath(issue.path), token: "", message: issue.message });
387
+ }
388
+ }
389
+ }
390
+ catch (cause) {
391
+ // The naming refusal is the one policy failure that throws rather than
392
+ // returning an issue, so it is mapped to a violation here. Same id as the
393
+ // Python port reports it under, so a consumer branches on one string.
394
+ if (!(cause instanceof WireNamingError))
395
+ throw cause;
396
+ violations.push({
397
+ rule: `field.${JSON_NAME_ALIAS_ERROR}`,
398
+ path: cause.path,
399
+ token: "",
400
+ message: cause.message,
401
+ });
402
+ }
403
+ // Everything below reads members off the entry, so a value that is not an object
404
+ // has nothing to walk. The parse above has already said so — a non-object yields
405
+ // the schema's own type violation — and continuing would throw instead of
406
+ // returning that verdict. A publisher feeding this a parsed JSONL line reaches
407
+ // it with whatever the line held, so a malformed row must get a verdict like any
408
+ // other refusal rather than an exception the Python port does not raise either.
409
+ if (!asObj(entry))
410
+ return { ok: violations.length === 0, violations, warnings: [] };
411
+ for (const site of crossFieldSites(entry)) {
412
+ for (const id of crossFieldRuleIds(site.message, site.value)) {
413
+ violations.push({ rule: id, path: site.path, token: "", message: `cross-field rule violated: ${id}` });
414
+ }
415
+ }
416
+ const warnings = [];
417
+ const normalized = normalizeResourceEntry(entry);
418
+ const terms = asArr(normalized["terms"]);
419
+ for (let i = 0; i < terms.length; i++) {
420
+ const term = asObj(terms[i]);
421
+ if (!term)
422
+ continue;
423
+ const prefix = `terms[${i}].`;
424
+ const verdict = validateLicenseTerm(term);
425
+ if (verdict.violation) {
426
+ violations.push({ ...verdict.violation, path: prefix + verdict.violation.path });
427
+ continue;
428
+ }
429
+ for (const w of verdict.warnings)
430
+ warnings.push({ ...w, path: prefix + w.path });
431
+ }
432
+ return { ok: violations.length === 0, violations, warnings };
433
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * parseMoney validates a canonical wire decimal string, rejecting the empty
3
+ * (unset) string and any value the wire pattern forbids — signs, exponents, a
4
+ * leading dot — so a value that would fail the server's protovalidate never
5
+ * silently parses here. Returns the validated string (the TS decimal surface).
6
+ */
7
+ export declare function parseMoney(s: string): string;
8
+ /**
9
+ * formatMoney renders a validated money string as the canonical wire form: no
10
+ * sign, no exponent, insignificant LEADING integer zeros dropped ("007" -> "7",
11
+ * "00.5" -> "0.5", "000" -> "0") and insignificant trailing fractional zeros +
12
+ * a bare trailing dot stripped ("0.050" -> "0.05", "1.00" -> "1"). A negative
13
+ * value is rejected — FORA money is non-negative.
14
+ */
15
+ export declare function formatMoney(s: string): string;
16
+ /**
17
+ * canonicalizeMoney normalizes a wire decimal string to its canonical form
18
+ * (parse then format) — the convenience used when echoing a money value back
19
+ * onto the wire without doing arithmetic.
20
+ */
21
+ export declare function canonicalizeMoney(s: string): string;
@@ -0,0 +1,60 @@
1
+ // Money (ADR-020) — TS port of the sdk/go oracle (helpers/money.go). FORA money
2
+ // fields (Pricing.rate, Cost.amount, *.unit_cost) are exact decimal strings —
3
+ // never floats — constrained by protovalidate to the wire pattern below:
4
+ // non-negative, no sign, no exponent, optional fractional part, empty string
5
+ // for "unset". The Go oracle round-trips shopspring decimal (NewFromString then
6
+ // String); TS has no stdlib decimal, so the surface is a validated canonical
7
+ // STRING. canonicalizeMoney reproduces the Go bytes: strip insignificant
8
+ // LEADING integer zeros AND trailing fractional zeros + a bare trailing dot.
9
+ // moneyWire mirrors the protovalidate constraint `^([0-9]+([.][0-9]+)?)?$`
10
+ // exactly (kept in lockstep with fora.proto Pricing.rate). The empty string
11
+ // matches the pattern but is rejected by parseMoney as "unset".
12
+ const moneyWire = /^([0-9]+([.][0-9]+)?)?$/;
13
+ /**
14
+ * parseMoney validates a canonical wire decimal string, rejecting the empty
15
+ * (unset) string and any value the wire pattern forbids — signs, exponents, a
16
+ * leading dot — so a value that would fail the server's protovalidate never
17
+ * silently parses here. Returns the validated string (the TS decimal surface).
18
+ */
19
+ export function parseMoney(s) {
20
+ if (s === "") {
21
+ throw new Error("money: empty money string (field is unset)");
22
+ }
23
+ // Mirror protovalidate string.max_len = 32 (fora.proto Pricing.rate) so a
24
+ // pattern-valid but over-length value is rejected here, not only server-side.
25
+ if (s.length > 32) {
26
+ throw new Error(`money: string length ${s.length} exceeds max 32`);
27
+ }
28
+ if (!moneyWire.test(s)) {
29
+ throw new Error(`money: ${JSON.stringify(s)} is not a canonical money string`);
30
+ }
31
+ return s;
32
+ }
33
+ /**
34
+ * formatMoney renders a validated money string as the canonical wire form: no
35
+ * sign, no exponent, insignificant LEADING integer zeros dropped ("007" -> "7",
36
+ * "00.5" -> "0.5", "000" -> "0") and insignificant trailing fractional zeros +
37
+ * a bare trailing dot stripped ("0.050" -> "0.05", "1.00" -> "1"). A negative
38
+ * value is rejected — FORA money is non-negative.
39
+ */
40
+ export function formatMoney(s) {
41
+ if (s.startsWith("-")) {
42
+ throw new Error(`money: negative money ${JSON.stringify(s)} is not representable on the wire`);
43
+ }
44
+ const dot = s.indexOf(".");
45
+ let intPart = dot === -1 ? s : s.slice(0, dot);
46
+ let fracPart = dot === -1 ? "" : s.slice(dot + 1);
47
+ intPart = intPart.replace(/^0+/, "");
48
+ if (intPart === "")
49
+ intPart = "0";
50
+ fracPart = fracPart.replace(/0+$/, "");
51
+ return fracPart === "" ? intPart : `${intPart}.${fracPart}`;
52
+ }
53
+ /**
54
+ * canonicalizeMoney normalizes a wire decimal string to its canonical form
55
+ * (parse then format) — the convenience used when echoing a money value back
56
+ * onto the wire without doing arithmetic.
57
+ */
58
+ export function canonicalizeMoney(s) {
59
+ return formatMoney(parseMoney(s));
60
+ }
@@ -0,0 +1,11 @@
1
+ import { OFFER_SIGNATURE_ALGORITHM } from "../core/verifier.ts";
2
+ export { OFFER_SIGNATURE_ALGORITHM };
3
+ /**
4
+ * signOffer signs the canonical offer payload (JCS over the offer with its
5
+ * signature/signature_algorithm cleared) with the exchange's Ed25519 private key
6
+ * and returns the hex signature for Offer.signature. The offer must already be
7
+ * canonical proto-JSON (snake_case, enums-as-names, omit-unpopulated), as the
8
+ * verify face requires — canonicalOfferPayload clears the two signature keys and
9
+ * re-JCS-es.
10
+ */
11
+ export declare function signOffer(offer: Record<string, unknown>, privateKey: CryptoKey): Promise<string>;
@@ -0,0 +1,31 @@
1
+ // sdk/ts offer SIGN face — the Exchange-minting sibling of the core Verifier's
2
+ // offer-verification path. Mirror of the Go oracle helpers.SignOffer and Python
3
+ // core.sign_offer_jcs.
4
+ //
5
+ // The signed bytes are JCS(protojson(offer with signature+signature_algorithm
6
+ // cleared)) — the SAME canonical builder the verify face reuses
7
+ // (core/verifier.ts canonicalOfferPayload), never a re-derived canonicalization.
8
+ // Ed25519 is deterministic (RFC 8032), so the hex signature is byte-identical to
9
+ // the Go oracle. This half is UNAFFECTED by the signed-URL opaque-bytes decision:
10
+ // JCS over proto-JSON is already deterministic and cross-language byte-agreed.
11
+ //
12
+ // WebCrypto is the crypto primitive (the same one the acceptance/verify faces
13
+ // use); the caller supplies the imported CryptoKey, exactly as acceptance.ts does.
14
+ import { canonicalOfferPayload, OFFER_SIGNATURE_ALGORITHM } from "../core/verifier.js";
15
+ export { OFFER_SIGNATURE_ALGORITHM };
16
+ /**
17
+ * signOffer signs the canonical offer payload (JCS over the offer with its
18
+ * signature/signature_algorithm cleared) with the exchange's Ed25519 private key
19
+ * and returns the hex signature for Offer.signature. The offer must already be
20
+ * canonical proto-JSON (snake_case, enums-as-names, omit-unpopulated), as the
21
+ * verify face requires — canonicalOfferPayload clears the two signature keys and
22
+ * re-JCS-es.
23
+ */
24
+ export async function signOffer(offer, privateKey) {
25
+ const payload = canonicalOfferPayload(offer);
26
+ const sig = new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, payload));
27
+ let hex = "";
28
+ for (const b of sig)
29
+ hex += b.toString(16).padStart(2, "0");
30
+ return hex;
31
+ }
@@ -0,0 +1,3 @@
1
+ export declare function opaqueUrl(url: string | {
2
+ toString(): string;
3
+ }): string;
@@ -0,0 +1,20 @@
1
+ // opaqueUrl coerces a URL-like input to the primitive string the OPAQUE-URL-BYTES
2
+ // contract (signurl.ts) operates on, ONCE at each public SDK boundary.
3
+ //
4
+ // The URL-consuming faces (Ed25519 signed-URL verify, RFC 9421 GET-PoP verify)
5
+ // type their input as `string`, but some edge runtimes — Fastly Compute — hand
6
+ // the request URL as a URL-LIKE OBJECT. Left uncoerced, that object either throws
7
+ // (canonicalUrl's indexOf/slice string ops) or silently WHATWG-normalizes (a
8
+ // `@target-uri` template literal calls toString(): lowercased host, stripped
9
+ // :443, re-escaped path) — producing a signature base that diverges from the
10
+ // verbatim bytes the signer covered.
11
+ //
12
+ // Coercing here is a NO-OP for the common server-to-server string caller
13
+ // (String(s) === s, so the verbatim bytes are preserved) and gives a URL-like
14
+ // object a single, explicit string form instead of an implicit toString() at
15
+ // every downstream parse/interpolation site. It is deliberately NOT
16
+ // `new URL(x).toString()`: that re-normalizes and would break byte-parity with
17
+ // the Go signer's verbatim @target-uri (sigbase.go reconstructTargetURI).
18
+ export function opaqueUrl(url) {
19
+ return String(url);
20
+ }
@@ -0,0 +1,51 @@
1
+ export declare const AGENT_KEY_HEADER: string;
2
+ export type PopFailure = "missing_agent_key" | "bad_agent_key" | "missing_sig" | "malformed_sig_input" | "unsupported_alg" | "bad_covered_components" | "keyid_mismatch" | "thumbprint_mismatch" | "pop_missing_created" | "pop_future_created" | "pop_missing_exp" | "pop_expired" | "pop_sig_invalid";
3
+ export interface PopResult {
4
+ ok: boolean;
5
+ reason?: PopFailure;
6
+ }
7
+ /**
8
+ * Ed25519 verify primitive: (publicKey, signature, message) -> valid?. Injected
9
+ * so non-WebCrypto runtimes can supply their own without changing the byte
10
+ * contract. Defaults to WebCrypto. Byte params are `Uint8Array<ArrayBuffer>`
11
+ * (never SharedArrayBuffer-backed) so the default can hand them to WebCrypto's
12
+ * BufferSource without casts.
13
+ */
14
+ export type Ed25519Verify = (publicKey: Uint8Array<ArrayBuffer>, signature: Uint8Array<ArrayBuffer>, message: Uint8Array<ArrayBuffer>) => Promise<boolean>;
15
+ export interface PopInput {
16
+ /** Full request URL — the value the agent signed as `@target-uri`. */
17
+ url: string;
18
+ method: string;
19
+ headers: Headers;
20
+ /** The bound thumbprint carried in the URL's `agent_id` param. */
21
+ agentId: string;
22
+ now?: () => number;
23
+ /** Optional Ed25519 verify primitive; defaults to WebCrypto crypto.subtle. */
24
+ verifyEd25519?: Ed25519Verify;
25
+ }
26
+ interface SignatureInput {
27
+ covered: string[];
28
+ /** Verbatim params string after `label=`, used to rebuild @signature-params. */
29
+ rawParams: string;
30
+ keyid: string;
31
+ alg: string;
32
+ created?: number;
33
+ expires?: number;
34
+ }
35
+ /**
36
+ * Verify the agent's proof of possession of the key bound to `agentId`.
37
+ * Returns `{ ok: true }` only when the presented key, the RFC 9421 signature,
38
+ * and the 3-way identity all check out.
39
+ */
40
+ export declare function verifyAgentBinding(input: PopInput): Promise<PopResult>;
41
+ /**
42
+ * Rebuild the RFC 9421 signature base for the covered GET components. Order
43
+ * mirrors the inner list (`@method`, `@target-uri`), then `@signature-params`.
44
+ * MUST stay byte-identical to the sdk/go signer (sigbase.go buildSignatureBase).
45
+ */
46
+ export declare function signatureBase(method: string, url: string, rawParams: string): string;
47
+ /** Parse `label=("@method" "@target-uri");keyid="..";alg="..";created=..;expires=..`. */
48
+ export declare function parseSignatureInput(raw: string | null): SignatureInput | undefined;
49
+ /** Parse the RFC 9421 `Signature` header value `label=:<base64>:`. */
50
+ export declare function parseSignature(raw: string | null): Uint8Array<ArrayBuffer> | undefined;
51
+ export {};