@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.
- package/LICENSE +201 -0
- package/README.md +37 -0
- package/gen/ts/vocab/functiontokens.js +78 -0
- package/gen/ts/vocab/functiontokens.ts +84 -0
- package/gen/ts/vocab/geographytokens.js +30 -0
- package/gen/ts/vocab/geographytokens.ts +37 -0
- package/gen/ts/vocab/pricingunits.js +56 -0
- package/gen/ts/vocab/pricingunits.ts +63 -0
- package/gen/ts/vocab/quotametrics.js +40 -0
- package/gen/ts/vocab/quotametrics.ts +47 -0
- package/gen/ts/vocab/usertypes.js +40 -0
- package/gen/ts/vocab/usertypes.ts +46 -0
- package/gen/ts/wire/base.js +245 -0
- package/gen/ts/wire/base.ts +255 -0
- package/gen/ts/wire/names.js +41 -0
- package/gen/ts/wire/names.ts +42 -0
- package/gen/ts/wire/schemas.js +111 -0
- package/gen/ts/wire/schemas.ts +216 -0
- package/package.json +164 -0
- package/sdk/ts/client/content.d.ts +74 -0
- package/sdk/ts/client/content.js +466 -0
- package/sdk/ts/client/errors.d.ts +79 -0
- package/sdk/ts/client/errors.js +148 -0
- package/sdk/ts/client/index.d.ts +241 -0
- package/sdk/ts/client/index.js +740 -0
- package/sdk/ts/client/route.d.ts +41 -0
- package/sdk/ts/client/route.js +107 -0
- package/sdk/ts/client/send.d.ts +64 -0
- package/sdk/ts/client/send.js +142 -0
- package/sdk/ts/client/transport.d.ts +187 -0
- package/sdk/ts/client/transport.js +381 -0
- package/sdk/ts/core/multisig-parse.d.ts +47 -0
- package/sdk/ts/core/multisig-parse.js +234 -0
- package/sdk/ts/core/sign-request.d.ts +83 -0
- package/sdk/ts/core/sign-request.js +146 -0
- package/sdk/ts/core/sign.d.ts +31 -0
- package/sdk/ts/core/sign.js +82 -0
- package/sdk/ts/core/signing-transport.d.ts +75 -0
- package/sdk/ts/core/signing-transport.js +172 -0
- package/sdk/ts/core/verifier.d.ts +177 -0
- package/sdk/ts/core/verifier.js +269 -0
- package/sdk/ts/core/verify-multisig-request.d.ts +72 -0
- package/sdk/ts/core/verify-multisig-request.js +112 -0
- package/sdk/ts/core/verify-request.d.ts +181 -0
- package/sdk/ts/core/verify-request.js +284 -0
- package/sdk/ts/core/window.d.ts +22 -0
- package/sdk/ts/core/window.js +43 -0
- package/sdk/ts/core/wire-canon.d.ts +20 -0
- package/sdk/ts/core/wire-canon.js +174 -0
- package/sdk/ts/hono/middleware.d.ts +38 -0
- package/sdk/ts/hono/middleware.js +64 -0
- package/sdk/ts/resolvers/errors.d.ts +130 -0
- package/sdk/ts/resolvers/errors.js +150 -0
- package/sdk/ts/resolvers/http.d.ts +98 -0
- package/sdk/ts/resolvers/http.js +274 -0
- package/sdk/ts/resolvers/index.d.ts +9 -0
- package/sdk/ts/resolvers/index.js +15 -0
- package/sdk/ts/resolvers/jwks.d.ts +2 -0
- package/sdk/ts/resolvers/jwks.js +43 -0
- package/sdk/ts/resolvers/offer-key-cache.d.ts +68 -0
- package/sdk/ts/resolvers/offer-key-cache.js +152 -0
- package/sdk/ts/resolvers/registration-requirements.d.ts +47 -0
- package/sdk/ts/resolvers/registration-requirements.js +220 -0
- package/sdk/ts/resolvers/ssrf.d.ts +28 -0
- package/sdk/ts/resolvers/ssrf.js +235 -0
- package/sdk/ts/resolvers/static.d.ts +7 -0
- package/sdk/ts/resolvers/static.js +16 -0
- package/sdk/ts/resolvers/wba.d.ts +134 -0
- package/sdk/ts/resolvers/wba.js +506 -0
- package/sdk/ts/resolvers/wellknown.d.ts +28 -0
- package/sdk/ts/resolvers/wellknown.js +169 -0
- package/sdk/ts/src/acceptance.d.ts +46 -0
- package/sdk/ts/src/acceptance.js +162 -0
- package/sdk/ts/src/base64url.d.ts +19 -0
- package/sdk/ts/src/base64url.js +55 -0
- package/sdk/ts/src/crossfield.d.ts +28 -0
- package/sdk/ts/src/crossfield.js +215 -0
- package/sdk/ts/src/endpoint-rule.d.ts +30 -0
- package/sdk/ts/src/endpoint-rule.js +63 -0
- package/sdk/ts/src/errordetail.d.ts +115 -0
- package/sdk/ts/src/errordetail.js +256 -0
- package/sdk/ts/src/hashurl.d.ts +5 -0
- package/sdk/ts/src/hashurl.js +15 -0
- package/sdk/ts/src/host-ref.d.ts +70 -0
- package/sdk/ts/src/host-ref.js +320 -0
- package/sdk/ts/src/hosts.d.ts +138 -0
- package/sdk/ts/src/hosts.js +214 -0
- package/sdk/ts/src/idempotency.d.ts +11 -0
- package/sdk/ts/src/idempotency.js +27 -0
- package/sdk/ts/src/jsondepth.d.ts +27 -0
- package/sdk/ts/src/jsondepth.js +77 -0
- package/sdk/ts/src/licenseterm.d.ts +113 -0
- package/sdk/ts/src/licenseterm.js +433 -0
- package/sdk/ts/src/money.d.ts +21 -0
- package/sdk/ts/src/money.js +60 -0
- package/sdk/ts/src/offer-sign.d.ts +11 -0
- package/sdk/ts/src/offer-sign.js +31 -0
- package/sdk/ts/src/opaque-url.d.ts +3 -0
- package/sdk/ts/src/opaque-url.js +20 -0
- package/sdk/ts/src/pop.d.ts +51 -0
- package/sdk/ts/src/pop.js +166 -0
- package/sdk/ts/src/regschema.d.ts +229 -0
- package/sdk/ts/src/regschema.js +1315 -0
- package/sdk/ts/src/scopes.d.ts +20 -0
- package/sdk/ts/src/scopes.js +45 -0
- package/sdk/ts/src/signurl.d.ts +23 -0
- package/sdk/ts/src/signurl.js +107 -0
- package/sdk/ts/src/thumbprint.d.ts +14 -0
- package/sdk/ts/src/thumbprint.js +24 -0
- package/sdk/ts/src/verify.d.ts +49 -0
- package/sdk/ts/src/verify.js +109 -0
- package/sdk/ts/src/wire-names.d.ts +1 -0
- package/sdk/ts/src/wire-names.js +16 -0
- package/sdk/ts/src/wire.d.ts +68 -0
- 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 {};
|