@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,138 @@
1
+ /**
2
+ * bareDomainPattern is the wire shape of a domain-valued field: a bare domain
3
+ * with an optional ":port", never a URL. It carries the same bytes as the Go
4
+ * `helpers.BareDomainPattern` and as the protovalidate pattern on the contract's
5
+ * recipient-addressing fields — the `exchange` field on each addressed request,
6
+ * `Offer.exchange` and their neighbours, not every field in fora.proto that
7
+ * happens to hold a domain. One rule, so the check a client makes before sending
8
+ * and the check the wire makes on arrival cannot answer differently. The parity
9
+ * suite asserts these bytes against the shared vectors.
10
+ *
11
+ * The port is a real 1-65535 range rather than "one to five digits", which is why
12
+ * it is spelled out at this length: `:0`, `:65536` and `:99999` name no port at
13
+ * all, and `:0443` is not a spelling of 443 but a different string.
14
+ */
15
+ export declare const bareDomainPattern: string;
16
+ /**
17
+ * maxBareDomainLen is the length bound belonging to the same rule — the
18
+ * protovalidate `string.max_len` those fields carry, so this SDK cannot accept a
19
+ * pattern-valid but over-length value the server then rejects.
20
+ */
21
+ export declare const maxBareDomainLen = 260;
22
+ /**
23
+ * isBareDomain reports whether v is a bare domain of the shape the wire admits.
24
+ *
25
+ * The length is checked FIRST so the work stays bounded on hostile input. This is
26
+ * insurance rather than a fix for a known blowup: the pattern is unambiguous —
27
+ * every repetition is anchored by a literal dot no label class can consume — so it
28
+ * cannot backtrack catastrophically, and matching it costs time linear in the
29
+ * input. Bounding that is still worth one comparison on an engine that
30
+ * backtracks. The order costs nothing in agreement — a value whose length differs
31
+ * between UTF-16 units, code points and bytes contains something outside ASCII,
32
+ * and the pattern refuses it regardless.
33
+ */
34
+ export declare function isBareDomain(v: string): boolean;
35
+ /**
36
+ * The outcome of checking a request's claimed recipient against this Exchange's
37
+ * own identity. The tokens are the Go `AudienceVerdict.String()` vocabulary
38
+ * verbatim, which is what the shared vectors record.
39
+ *
40
+ * `no_verdict` means the check did not run because the configured identity is
41
+ * unusable. It is never RETURNED here — this port throws in that case, since a
42
+ * deployment fault is not something a caller should be able to read as a value
43
+ * — but it is in the vocabulary because the shared vectors carry it.
44
+ */
45
+ export type AudienceVerdict = "no_verdict" | "accepted" | "empty" | "malformed" | "mismatch";
46
+ /**
47
+ * checkAudience reports whether every claimed recipient names this Exchange.
48
+ *
49
+ * `self` is this Exchange's own bare domain — the domain it publishes as its
50
+ * IDENTITY, which is the value it stamps into the offers it issues. It is not the
51
+ * host the process happens to listen on, and the two are allowed to differ: an
52
+ * Exchange at `exchange.example` may serve its API from `api.exchange.example`, so
53
+ * an operator who configures this from the listening host would refuse every
54
+ * request that named them correctly.
55
+ *
56
+ * `claimed` holds the recipient
57
+ * values the request carries — ONE for a message with a single `exchange`
58
+ * field, MANY for a message whose audience lives per item (a TransactionRequest
59
+ * states it once per item, in each item's signed offer). Every value must name
60
+ * this Exchange; the first that does not decides the verdict, and a request
61
+ * carrying no values at all is refused rather than waved through.
62
+ *
63
+ * The comparison is EXACT: a subdomain of this Exchange is a different party and
64
+ * does not name it. That is narrower than the endpoint rule, which does let a
65
+ * manifest advertise its endpoint on a subdomain of the host that served it —
66
+ * there the question is which addresses one Exchange may be reached at, here it
67
+ * is who the Exchange IS.
68
+ *
69
+ * Two spellings of the same identity still match: case is folded, and a port of
70
+ * 443 written out is the same as leaving it off, since a schemeless domain is
71
+ * read as https throughout this SDK. Port 80 is not folded — it is not the
72
+ * default of the scheme a bare domain implies.
73
+ *
74
+ * Throws when `self` is not a bare domain. That is a fault in this deployment,
75
+ * never in the request, and the two are kept apart so a caller can map them onto
76
+ * different status codes without inspecting any message.
77
+ */
78
+ export declare function checkAudience(self: string, ...claimed: string[]): AudienceVerdict;
79
+ /**
80
+ * hostOf extracts the host (including any port) from a bare domain, a host:port
81
+ * pair, or a full URL. A ref with no scheme is read as though it carried https,
82
+ * since a bare domain is otherwise indistinguishable from a path.
83
+ *
84
+ * Throws when the reference cannot be read as a host at all.
85
+ */
86
+ export declare function hostOf(ref: string): string;
87
+ /**
88
+ * isBareHost reports whether ref is EXACTLY a host — nothing a URL could carry
89
+ * besides the authority. It answers false for a ref with a scheme, userinfo, a
90
+ * path, a query or a fragment, because hostOf had to strip something to reach the
91
+ * host. A port is NOT a strip: "exchange.example:8443" is a bare host, and the
92
+ * well-known resolver concatenates host-with-port unchanged.
93
+ *
94
+ * It exists for the callers that hand a network-supplied domain to code which
95
+ * builds a URL by concatenation. There, narrowing a rich reference to its host is
96
+ * the wrong repair: the value was never a domain, and accepting it silently means
97
+ * the far side chose the path that gets fetched, not just the host it is fetched
98
+ * from. Comparing against the extracted host is what makes the rejection
99
+ * structural rather than a blocklist of the separators anyone thought to name.
100
+ *
101
+ * This is NOT isBareDomain. A trailing root dot, a leading or trailing hyphen, an
102
+ * underscore, an empty label and a bracketed IPv6 literal are all usable hosts and
103
+ * none of them is a value the wire rule accepts. A caller vetting a value it is
104
+ * about to dial wants this one; a caller vetting a value that arrived in a message
105
+ * wants isBareDomain.
106
+ */
107
+ export declare function isBareHost(ref: string): boolean;
108
+ /**
109
+ * hostAnchored reports whether candidate is anchored to anchor — the same host and
110
+ * port, or a subdomain of that host on that port. Either side may be a bare
111
+ * domain, a host:port pair or a full URL; a reference that does not parse throws,
112
+ * which callers treat as "not anchored".
113
+ *
114
+ * The use is checking a value a remote document supplied against the host that
115
+ * served that document: it may point at itself or at one of its own subdomains,
116
+ * and nothing else. Without it, a host could redirect a signed request — or a
117
+ * revocation poll — to an unrelated third-party address that a dial-time address
118
+ * guard would happily allow, because the address is perfectly public.
119
+ *
120
+ * The PORT is part of the comparison. What is being anchored is a place a signed
121
+ * call is sent, and a different port is a different service — one the party that
122
+ * published the anchor need not control. A DEFAULT port and its omission are the
123
+ * same port, so https://x, https://x:443 and x all anchor to one another; refusing
124
+ * an operator who merely wrote :443 out in full would be a spelling check wearing
125
+ * a security check's clothes.
126
+ *
127
+ * The SCHEME is still not compared. Whether a leg may run in the clear is the
128
+ * guarded transport's decision, made in one place from one flag. Its only job here
129
+ * is choosing which port counts as the default — and a side that NAMED no scheme
130
+ * borrows the other's for that purpose, rather than being assumed to mean https.
131
+ * Both anchors in this SDK arrive schemeless: a WBA directory's authority and an
132
+ * Offer.exchange host are bare host[:port] values. Assuming https for them meant an
133
+ * anchor of "a.example:80" kept its port (80 is not https's default) while the
134
+ * candidate "http://a.example:80" folded it away — the same authority reaching two
135
+ * answers, which silently un-anchored every plaintext directory that spelled :80
136
+ * in full.
137
+ */
138
+ export declare function hostAnchored(anchor: string, candidate: string): boolean;
@@ -0,0 +1,214 @@
1
+ // Host predicates, the audience check and the bare-domain shape — TS port of the
2
+ // sdk/go oracle (helpers/hosts.go, helpers/audience.go).
3
+ //
4
+ // Two kinds of predicate live here and keeping them apart is the point. The
5
+ // ROUTING predicates — isBareHost and hostAnchored — precede a signed call to an
6
+ // address a network party named: a value that arrives inside an offer, or inside
7
+ // a manifest that offer pointed at, is about to be concatenated into a URL or
8
+ // dialled directly. The SHAPE predicate — isBareDomain — answers a different
9
+ // question: whether a value is the form the wire contract admits at all.
10
+ //
11
+ // Addressed requests carry the recipient's bare domain in a body field. The RFC
12
+ // 9421 signature does not already establish the recipient: it proves the sender
13
+ // signed THE URL IT DIALLED, not that the URL was the right one. That dial target
14
+ // is resolved from a fetched, cached /.well-known/fora.json, so a poisoned or
15
+ // stale resolution redirects the request while every signature still verifies.
16
+ // The field states whom the sender MEANT, independently of that resolution.
17
+ //
18
+ // The field is stamped by whoever authors each request — the agent on the requests
19
+ // it signs, a Broker on the legs it authors as sender. It is a statement BY that
20
+ // sender, not tamper-evidence against it. For transactions the binding audience
21
+ // statement is per item: Offer.exchange inside the Exchange-signed offer.
22
+ //
23
+ // Pure string work, no IO. Byte-parity-guarded against the Go oracle by the
24
+ // shared vectors at sdk/go/helpers/testdata/audience-vectors.json.
25
+ import { anchoredParsed, parseRef } from "./host-ref.js";
26
+ /**
27
+ * bareDomainPattern is the wire shape of a domain-valued field: a bare domain
28
+ * with an optional ":port", never a URL. It carries the same bytes as the Go
29
+ * `helpers.BareDomainPattern` and as the protovalidate pattern on the contract's
30
+ * recipient-addressing fields — the `exchange` field on each addressed request,
31
+ * `Offer.exchange` and their neighbours, not every field in fora.proto that
32
+ * happens to hold a domain. One rule, so the check a client makes before sending
33
+ * and the check the wire makes on arrival cannot answer differently. The parity
34
+ * suite asserts these bytes against the shared vectors.
35
+ *
36
+ * The port is a real 1-65535 range rather than "one to five digits", which is why
37
+ * it is spelled out at this length: `:0`, `:65536` and `:99999` name no port at
38
+ * all, and `:0443` is not a spelling of 443 but a different string.
39
+ */
40
+ export const bareDomainPattern = String.raw `^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$`;
41
+ /**
42
+ * maxBareDomainLen is the length bound belonging to the same rule — the
43
+ * protovalidate `string.max_len` those fields carry, so this SDK cannot accept a
44
+ * pattern-valid but over-length value the server then rejects.
45
+ */
46
+ export const maxBareDomainLen = 260;
47
+ // Compiled once. JavaScript's `$` (without the `m` flag) matches only at the end
48
+ // of input and — unlike Python's — does NOT match before a trailing newline, so
49
+ // `test` reproduces Go's RE2 anchoring here without further help. The shared
50
+ // vectors carry a trailing-newline case that fails any port which gets this
51
+ // wrong.
52
+ const bareDomainRe = new RegExp(bareDomainPattern);
53
+ /**
54
+ * isBareDomain reports whether v is a bare domain of the shape the wire admits.
55
+ *
56
+ * The length is checked FIRST so the work stays bounded on hostile input. This is
57
+ * insurance rather than a fix for a known blowup: the pattern is unambiguous —
58
+ * every repetition is anchored by a literal dot no label class can consume — so it
59
+ * cannot backtrack catastrophically, and matching it costs time linear in the
60
+ * input. Bounding that is still worth one comparison on an engine that
61
+ * backtracks. The order costs nothing in agreement — a value whose length differs
62
+ * between UTF-16 units, code points and bytes contains something outside ASCII,
63
+ * and the pattern refuses it regardless.
64
+ */
65
+ export function isBareDomain(v) {
66
+ return v.length <= maxBareDomainLen && bareDomainRe.test(v);
67
+ }
68
+ /**
69
+ * checkAudience reports whether every claimed recipient names this Exchange.
70
+ *
71
+ * `self` is this Exchange's own bare domain — the domain it publishes as its
72
+ * IDENTITY, which is the value it stamps into the offers it issues. It is not the
73
+ * host the process happens to listen on, and the two are allowed to differ: an
74
+ * Exchange at `exchange.example` may serve its API from `api.exchange.example`, so
75
+ * an operator who configures this from the listening host would refuse every
76
+ * request that named them correctly.
77
+ *
78
+ * `claimed` holds the recipient
79
+ * values the request carries — ONE for a message with a single `exchange`
80
+ * field, MANY for a message whose audience lives per item (a TransactionRequest
81
+ * states it once per item, in each item's signed offer). Every value must name
82
+ * this Exchange; the first that does not decides the verdict, and a request
83
+ * carrying no values at all is refused rather than waved through.
84
+ *
85
+ * The comparison is EXACT: a subdomain of this Exchange is a different party and
86
+ * does not name it. That is narrower than the endpoint rule, which does let a
87
+ * manifest advertise its endpoint on a subdomain of the host that served it —
88
+ * there the question is which addresses one Exchange may be reached at, here it
89
+ * is who the Exchange IS.
90
+ *
91
+ * Two spellings of the same identity still match: case is folded, and a port of
92
+ * 443 written out is the same as leaving it off, since a schemeless domain is
93
+ * read as https throughout this SDK. Port 80 is not folded — it is not the
94
+ * default of the scheme a bare domain implies.
95
+ *
96
+ * Throws when `self` is not a bare domain. That is a fault in this deployment,
97
+ * never in the request, and the two are kept apart so a caller can map them onto
98
+ * different status codes without inspecting any message.
99
+ */
100
+ export function checkAudience(self, ...claimed) {
101
+ if (!isBareDomain(self)) {
102
+ throw new Error(`hosts: configured Exchange identity is not a bare domain: ${JSON.stringify(self)}`);
103
+ }
104
+ if (claimed.length === 0) {
105
+ return "empty";
106
+ }
107
+ const want = normalizeDomain(self);
108
+ for (const c of claimed) {
109
+ if (c === "") {
110
+ return "empty";
111
+ }
112
+ if (!isBareDomain(c)) {
113
+ return "malformed";
114
+ }
115
+ if (normalizeDomain(c) !== want) {
116
+ return "mismatch";
117
+ }
118
+ }
119
+ return "accepted";
120
+ }
121
+ // normalizeDomain renders the two spellings of one identity as one string. It
122
+ // runs only on values isBareDomain has already accepted, so the input is ASCII
123
+ // and holds at most one colon followed by digits — which is what lets it split
124
+ // on that colon rather than parse a URL, and is why it reproduces the Go oracle
125
+ // exactly.
126
+ function normalizeDomain(v) {
127
+ const i = v.lastIndexOf(":");
128
+ const host = (i >= 0 ? v.slice(0, i) : v).toLowerCase();
129
+ const port = i >= 0 ? v.slice(i + 1) : "";
130
+ // A schemeless domain is read as https everywhere in this SDK, so 443 spelled
131
+ // out and 443 left implicit are the same port. Any other port is kept, 80
132
+ // included: folding it would be reading a scheme into a value that names none.
133
+ if (port === "" || port === "443") {
134
+ return host;
135
+ }
136
+ return `${host}:${port}`;
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // Routing predicates
140
+ // ---------------------------------------------------------------------------
141
+ /**
142
+ * hostOf extracts the host (including any port) from a bare domain, a host:port
143
+ * pair, or a full URL. A ref with no scheme is read as though it carried https,
144
+ * since a bare domain is otherwise indistinguishable from a path.
145
+ *
146
+ * Throws when the reference cannot be read as a host at all.
147
+ */
148
+ export function hostOf(ref) {
149
+ return parseRef(ref).host;
150
+ }
151
+ /**
152
+ * isBareHost reports whether ref is EXACTLY a host — nothing a URL could carry
153
+ * besides the authority. It answers false for a ref with a scheme, userinfo, a
154
+ * path, a query or a fragment, because hostOf had to strip something to reach the
155
+ * host. A port is NOT a strip: "exchange.example:8443" is a bare host, and the
156
+ * well-known resolver concatenates host-with-port unchanged.
157
+ *
158
+ * It exists for the callers that hand a network-supplied domain to code which
159
+ * builds a URL by concatenation. There, narrowing a rich reference to its host is
160
+ * the wrong repair: the value was never a domain, and accepting it silently means
161
+ * the far side chose the path that gets fetched, not just the host it is fetched
162
+ * from. Comparing against the extracted host is what makes the rejection
163
+ * structural rather than a blocklist of the separators anyone thought to name.
164
+ *
165
+ * This is NOT isBareDomain. A trailing root dot, a leading or trailing hyphen, an
166
+ * underscore, an empty label and a bracketed IPv6 literal are all usable hosts and
167
+ * none of them is a value the wire rule accepts. A caller vetting a value it is
168
+ * about to dial wants this one; a caller vetting a value that arrived in a message
169
+ * wants isBareDomain.
170
+ */
171
+ export function isBareHost(ref) {
172
+ const host = hostOf(ref);
173
+ // A trailing colon parses as a host with an empty port and would otherwise
174
+ // compare equal to itself. It is not a domain anyone meant to write, and the
175
+ // callers here concatenate the value into a URL, so it is refused rather than
176
+ // quietly normalized away.
177
+ if (host.endsWith(":")) {
178
+ return false;
179
+ }
180
+ return host === ref;
181
+ }
182
+ /**
183
+ * hostAnchored reports whether candidate is anchored to anchor — the same host and
184
+ * port, or a subdomain of that host on that port. Either side may be a bare
185
+ * domain, a host:port pair or a full URL; a reference that does not parse throws,
186
+ * which callers treat as "not anchored".
187
+ *
188
+ * The use is checking a value a remote document supplied against the host that
189
+ * served that document: it may point at itself or at one of its own subdomains,
190
+ * and nothing else. Without it, a host could redirect a signed request — or a
191
+ * revocation poll — to an unrelated third-party address that a dial-time address
192
+ * guard would happily allow, because the address is perfectly public.
193
+ *
194
+ * The PORT is part of the comparison. What is being anchored is a place a signed
195
+ * call is sent, and a different port is a different service — one the party that
196
+ * published the anchor need not control. A DEFAULT port and its omission are the
197
+ * same port, so https://x, https://x:443 and x all anchor to one another; refusing
198
+ * an operator who merely wrote :443 out in full would be a spelling check wearing
199
+ * a security check's clothes.
200
+ *
201
+ * The SCHEME is still not compared. Whether a leg may run in the clear is the
202
+ * guarded transport's decision, made in one place from one flag. Its only job here
203
+ * is choosing which port counts as the default — and a side that NAMED no scheme
204
+ * borrows the other's for that purpose, rather than being assumed to mean https.
205
+ * Both anchors in this SDK arrive schemeless: a WBA directory's authority and an
206
+ * Offer.exchange host are bare host[:port] values. Assuming https for them meant an
207
+ * anchor of "a.example:80" kept its port (80 is not https's default) while the
208
+ * candidate "http://a.example:80" folded it away — the same authority reaching two
209
+ * answers, which silently un-anchored every plaintext directory that spelled :80
210
+ * in full.
211
+ */
212
+ export function hostAnchored(anchor, candidate) {
213
+ return anchoredParsed(parseRef(anchor), parseRef(candidate));
214
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * generateIdempotencyKey returns a fresh cryptographically-random, URL-safe
3
+ * idempotency key (16 random bytes -> base64url, 22 chars, no padding). Use it
4
+ * once per logical operation; reuse a stored key only to deliberately replay.
5
+ */
6
+ export declare function generateIdempotencyKey(): string;
7
+ /**
8
+ * validateIdempotencyKey enforces the protocol's min_len=1 constraint, so the
9
+ * SDK rejects an empty key before the server does.
10
+ */
11
+ export declare function validateIdempotencyKey(key: string): void;
@@ -0,0 +1,27 @@
1
+ // Idempotency (ADR-019 §4) — TS port of the sdk/go oracle (helpers/idempotency.go).
2
+ // idempotency_key is a required, persisted, settlement-bound field on the
3
+ // mutating RPCs: the server dedupes on it so a replay returns the original
4
+ // result and cannot double-charge. The SDK mints a fresh key per call by
5
+ // default; to make a call a deliberate replay the application reuses a stored
6
+ // key. The SDK never tracks keys — the server owns the dedup store.
7
+ import { encodeBase64Url } from "./base64url.js";
8
+ // idempotencyKeyBytes is the entropy per key (128 bits -> 22 base64url chars).
9
+ const idempotencyKeyBytes = 16;
10
+ /**
11
+ * generateIdempotencyKey returns a fresh cryptographically-random, URL-safe
12
+ * idempotency key (16 random bytes -> base64url, 22 chars, no padding). Use it
13
+ * once per logical operation; reuse a stored key only to deliberately replay.
14
+ */
15
+ export function generateIdempotencyKey() {
16
+ const b = crypto.getRandomValues(new Uint8Array(idempotencyKeyBytes));
17
+ return encodeBase64Url(b);
18
+ }
19
+ /**
20
+ * validateIdempotencyKey enforces the protocol's min_len=1 constraint, so the
21
+ * SDK rejects an empty key before the server does.
22
+ */
23
+ export function validateIdempotencyKey(key) {
24
+ if (key === "") {
25
+ throw new Error("idempotency: idempotency_key must be non-empty");
26
+ }
27
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * How deep a document this SDK did not write may nest.
3
+ *
4
+ * The same 32 the error-detail reader uses and the protocol sets for a stranger's JSON in
5
+ * AccountRegistration.data_schema, so one number covers how deep any such document may be.
6
+ * The deepest instance in the whole conformance corpus is 5.
7
+ *
8
+ * It lives beside the scan rather than at either call site, because a bound stated twice is
9
+ * a bound two readers can disagree about.
10
+ */
11
+ export declare const MAX_BODY_DEPTH = 32;
12
+ /**
13
+ * rawNestingDepth returns the deepest JSON container nesting in `source`, counted
14
+ * lexically — no parse, no recursion, one pass.
15
+ *
16
+ * It is string-aware, so a brace inside a string literal is text rather than a container,
17
+ * and escape-aware so a literal quote does not end the string early. It does NOT check that
18
+ * the brackets balance: an unbalanced document is the parser's to reject, and this only has
19
+ * to produce an upper bound on how deep a parser would have to descend.
20
+ *
21
+ * It takes BYTES or TEXT and answers the same for both. Every delimiter it looks for is
22
+ * ASCII, and neither a UTF-8 continuation byte nor a UTF-16 code unit above the ASCII range
23
+ * can collide with one — so a caller that already holds one form never has to pay a
24
+ * conversion to the other. The registration-schema compiler measures the bytes as served;
25
+ * the client's readers already hold decoded text.
26
+ */
27
+ export declare function rawNestingDepth(source: Uint8Array | string): number;
@@ -0,0 +1,77 @@
1
+ // How deep a JSON document nests, counted without parsing it.
2
+ //
3
+ // Shared, because several readers need the same answer for the same reason and a second
4
+ // transcription of a security rule is how the three languages drifted apart elsewhere.
5
+ // TS mirror of sdk/python/fora_sdk/_jsondepth.py.
6
+ //
7
+ // Every JSON parser across the SDKs descends into a document, and what it does when the
8
+ // document is deeper than it can descend is a property of the runtime rather than a
9
+ // verdict: Python's raises RecursionError, which is neither what a malformed document
10
+ // raises nor a failure any of these packages says it raises. A depth check placed AFTER the
11
+ // parse is reached only by documents harmless enough to parse — precisely the ones that did
12
+ // not need it.
13
+ //
14
+ // So the scan is lexical and runs first. Counting needs no recursion.
15
+ //
16
+ // Three callers today: the registration-schema compiler, reading a schema out of a third
17
+ // party's manifest, and both of the client's readers of a peer's own bytes — the response
18
+ // reader and the delivery edge's refusal reader.
19
+ /**
20
+ * How deep a document this SDK did not write may nest.
21
+ *
22
+ * The same 32 the error-detail reader uses and the protocol sets for a stranger's JSON in
23
+ * AccountRegistration.data_schema, so one number covers how deep any such document may be.
24
+ * The deepest instance in the whole conformance corpus is 5.
25
+ *
26
+ * It lives beside the scan rather than at either call site, because a bound stated twice is
27
+ * a bound two readers can disagree about.
28
+ */
29
+ export const MAX_BODY_DEPTH = 32;
30
+ /**
31
+ * rawNestingDepth returns the deepest JSON container nesting in `source`, counted
32
+ * lexically — no parse, no recursion, one pass.
33
+ *
34
+ * It is string-aware, so a brace inside a string literal is text rather than a container,
35
+ * and escape-aware so a literal quote does not end the string early. It does NOT check that
36
+ * the brackets balance: an unbalanced document is the parser's to reject, and this only has
37
+ * to produce an upper bound on how deep a parser would have to descend.
38
+ *
39
+ * It takes BYTES or TEXT and answers the same for both. Every delimiter it looks for is
40
+ * ASCII, and neither a UTF-8 continuation byte nor a UTF-16 code unit above the ASCII range
41
+ * can collide with one — so a caller that already holds one form never has to pay a
42
+ * conversion to the other. The registration-schema compiler measures the bytes as served;
43
+ * the client's readers already hold decoded text.
44
+ */
45
+ export function rawNestingDepth(source) {
46
+ const text = typeof source === "string";
47
+ const length = source.length;
48
+ let depth = 0;
49
+ let deepest = 0;
50
+ let inString = false;
51
+ let escaped = false;
52
+ for (let i = 0; i < length; i++) {
53
+ const code = text
54
+ ? source.charCodeAt(i)
55
+ : source[i];
56
+ if (inString) {
57
+ if (escaped)
58
+ escaped = false;
59
+ else if (code === 0x5c)
60
+ escaped = true; // backslash
61
+ else if (code === 0x22)
62
+ inString = false; // quote
63
+ continue;
64
+ }
65
+ if (code === 0x22)
66
+ inString = true;
67
+ else if (code === 0x7b || code === 0x5b) {
68
+ // { [
69
+ depth++;
70
+ if (depth > deepest)
71
+ deepest = depth;
72
+ }
73
+ else if (code === 0x7d || code === 0x5d)
74
+ depth--; // } ]
75
+ }
76
+ return deepest;
77
+ }
@@ -0,0 +1,113 @@
1
+ /** Rejects a bare (non-namespaced) Pricing.unit that is not a registered metering token. */
2
+ export declare const RULE_PRICING_UNIT_REGISTERED = "pricing.unit.registered";
3
+ /** Rejects a bare Quota.metric that is not a registered quota token. */
4
+ export declare const RULE_QUOTA_METRIC_REGISTERED = "quota.metric.registered";
5
+ /**
6
+ * Rejects a restriction whose permitted and prohibited lists name the same token
7
+ * once both are canonicalised. The wire tier's rule compares the tokens AS
8
+ * WRITTEN, so two accepted spellings of one token — an alias beside its
9
+ * registered form, or two spellings differing only in ASCII case — pass it and
10
+ * collide only after the fold.
11
+ */
12
+ export declare const RULE_RESTRICTION_CANONICAL_DISJOINT = "restriction.canonical_disjoint";
13
+ /** Warns about a bare restriction token not registered on its axis; the term is accepted. */
14
+ export declare const RULE_RESTRICTION_TOKEN_REGISTERED = "restriction.token.registered";
15
+ /** Warns about an OBLIGATION_KIND_OTHER obligation carrying no detail. */
16
+ export declare const RULE_OBLIGATION_OTHER_REQUIRES_DETAIL = "obligation.other.requires_detail";
17
+ /**
18
+ * One reason an entry or a term would be refused. `rule` is the rule id (an
19
+ * ingest-tier id above, a cross-field CEL id, or `field.<zod issue code>` for a
20
+ * field-level refusal — the field-level ids are language-local); `path` is the
21
+ * snake_case proto-JSON field path relative to the checked message; `token` is
22
+ * the offending value when the rule is about one token, else "".
23
+ */
24
+ export interface RuleViolation {
25
+ rule: string;
26
+ path: string;
27
+ token: string;
28
+ message: string;
29
+ }
30
+ /** One non-fatal finding; `message` is the exact wire string of the warning. */
31
+ export interface RuleWarning {
32
+ rule: string;
33
+ path: string;
34
+ token: string;
35
+ message: string;
36
+ }
37
+ /**
38
+ * What validateLicenseTerm reports for one term. Every check but the disjointness
39
+ * one reads the term as already canonical; that one folds what it compares, so it
40
+ * is correct on a term as authored too.
41
+ */
42
+ export interface TermVerdict {
43
+ violation: RuleViolation | null;
44
+ warnings: RuleWarning[];
45
+ }
46
+ /** What validateResourceEntry reports: both tiers, wire tier first. */
47
+ export interface EntryVerdict {
48
+ ok: boolean;
49
+ violations: RuleViolation[];
50
+ warnings: RuleWarning[];
51
+ }
52
+ type Obj = Record<string, unknown>;
53
+ /**
54
+ * canonicalRestrictionToken returns the canonical form of a restriction token
55
+ * on an axis (`kind` is the RestrictionKind enum NAME): RFC 8259 whitespace
56
+ * trimmed, ASCII case folded (lower for FUNCTION and USER_TYPE, upper for
57
+ * GEOGRAPHY) and, where the axis authors aliases, the alias resolved to its
58
+ * registered token. OTHER and any unknown axis are returned unchanged.
59
+ * Applying it twice is a fixed point.
60
+ */
61
+ export declare function canonicalRestrictionToken(kind: string, token: string): string;
62
+ /**
63
+ * knownRestrictionToken reports whether an already-canonical token is
64
+ * registered on its axis. GEOGRAPHY admits the registered specials and any
65
+ * two-uppercase-letter ISO 3166-1 alpha-2 code; OTHER and unknown axes never.
66
+ */
67
+ export declare function knownRestrictionToken(kind: string, token: string): boolean;
68
+ /**
69
+ * normalizeLicenseTerm returns a deep copy of the term (proto-JSON, snake_case)
70
+ * with its restriction tokens rewritten to canonical form on every axis that
71
+ * carries a canonicalisation rule. Nothing else moves. The input is untouched;
72
+ * the Go oracle rewrites in place, and the corpus pins the output either way.
73
+ */
74
+ export declare function normalizeLicenseTerm(term: Obj): Obj;
75
+ /** normalizeResourceEntry returns a deep copy with every term normalised. */
76
+ export declare function normalizeResourceEntry(entry: Obj): Obj;
77
+ /**
78
+ * validateLicenseTerm runs the ingest-tier checks over one term, in a fixed
79
+ * order: a bare Pricing.unit that is not registered, then the first offending
80
+ * quota metric, then the first restriction whose permitted and prohibited lists
81
+ * name one token once canonicalised. An accepted term carries one warning per
82
+ * unregistered bare restriction token — restriction order, permitted before
83
+ * prohibited — then one per OBLIGATION_KIND_OTHER obligation without detail.
84
+ *
85
+ * Every check but the disjointness one reads the term as already canonical. The
86
+ * wire tier is not re-run here; disjointness is the one property both tiers
87
+ * assert, over different values, so a term the boundary clears can still fail
88
+ * here.
89
+ */
90
+ export declare function validateLicenseTerm(term: Obj): TermVerdict;
91
+ /**
92
+ * validateResourceEntry reports the verdict the Exchange reaches for one entry
93
+ * (proto-JSON, snake_case), both tiers in the Exchange's order: the wire tier —
94
+ * the generated field-level schema over the entry as given, plus every
95
+ * cross-field rule reachable from it — then the ingest tier over a normalised
96
+ * copy of the terms. The entry passed in is never modified. The Exchange stops
97
+ * at the first tier that fails; this face reports both so a publisher fixes
98
+ * everything in one round. Paths are relative to the entry.
99
+ *
100
+ * The wire tier runs UNDER THE WIRE POLICY, the same seam every other parse of a
101
+ * generated schema in this SDK goes through. A generated schema describes the
102
+ * message and cannot describe the two things that are true of the wire: a `null`
103
+ * is how proto-JSON spells "no value" for any field, and a lowerCamelCase
104
+ * json_name alias is out of contract. A bare safeParse would answer wrongly in
105
+ * both directions on bodies a publisher really produces — an Exchange serving
106
+ * EmitUnpopulated renders an unset message field as null, and stock
107
+ * protojson.Marshal emits camelCase, which the schemas STRIP, so an entry whose
108
+ * every multiword field was silently dropped would come back accepted. This
109
+ * face exists to predict the Exchange's verdict, so it applies the Exchange's
110
+ * reading of the bytes.
111
+ */
112
+ export declare function validateResourceEntry(entry: Obj): EntryVerdict;
113
+ export {};