@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,169 @@
|
|
|
1
|
+
// The two well-known fetching faces: WellKnownKeyResolver (one fixed JWKS URL,
|
|
2
|
+
// kid-keyed) and WellKnownEndpointResolver (host-keyed fora.json → endpoint).
|
|
3
|
+
// Both port the Go oracle's lazy-fetch + TTL-cache + single-flight coalescing and
|
|
4
|
+
// its fail-closed taxonomy: a fetch/decode failure throws DirectoryUnavailable;
|
|
5
|
+
// an unknown kid is `undefined`; a manifest whose `ver` is not a recognised major
|
|
6
|
+
// (or is absent) throws ManifestVersionRefused before anything else is read — on
|
|
7
|
+
// the endpoint face only, the key face reads a JWKS document that carries no
|
|
8
|
+
// manifest version; a manifest with no endpoint throws NoEndpoint.
|
|
9
|
+
import { endpointRefusal } from "../src/endpoint-rule.js";
|
|
10
|
+
import { invalidHost } from "../src/host-ref.js";
|
|
11
|
+
import { isBareHost } from "../src/hosts.js";
|
|
12
|
+
import { manifestVersionRefusal, WellKnownPath } from "../src/wire.js";
|
|
13
|
+
import { DirectoryUnavailable, EndpointRefused, ManifestVersionRefused, NoEndpoint } from "./errors.js";
|
|
14
|
+
import { defaultFetch, fetchStrict } from "./http.js";
|
|
15
|
+
import { ed25519KeysFromJwks } from "./jwks.js";
|
|
16
|
+
const DEFAULT_TTL_MS = 300_000; // 5 minutes
|
|
17
|
+
/** Lazily fetch the JWKS at `url`, cache resolved keys with a TTL. */
|
|
18
|
+
export function createWellKnownKeyResolver(url, opts = {}) {
|
|
19
|
+
return new KeyResolverImpl(url, opts);
|
|
20
|
+
}
|
|
21
|
+
/** Host-keyed resolver of an Exchange domain → its self-advertised endpoint. */
|
|
22
|
+
export function createWellKnownEndpointResolver(opts = {}) {
|
|
23
|
+
return new EndpointResolverImpl(opts);
|
|
24
|
+
}
|
|
25
|
+
class KeyResolverImpl {
|
|
26
|
+
url;
|
|
27
|
+
fetchFn;
|
|
28
|
+
ttlMs;
|
|
29
|
+
now;
|
|
30
|
+
allow;
|
|
31
|
+
cache = new Map();
|
|
32
|
+
cacheExp = 0;
|
|
33
|
+
inflight;
|
|
34
|
+
constructor(url, opts) {
|
|
35
|
+
this.url = url;
|
|
36
|
+
this.fetchFn = opts.fetch ?? defaultFetch;
|
|
37
|
+
this.ttlMs = opts.ttlMs && opts.ttlMs > 0 ? opts.ttlMs : DEFAULT_TTL_MS;
|
|
38
|
+
this.now = opts.now ?? Date.now;
|
|
39
|
+
this.allow = opts.allow;
|
|
40
|
+
}
|
|
41
|
+
async resolve(keyid) {
|
|
42
|
+
if (this.allow && !this.allow(keyid))
|
|
43
|
+
return undefined;
|
|
44
|
+
const hit = this.cachedKey(keyid);
|
|
45
|
+
if (hit)
|
|
46
|
+
return hit;
|
|
47
|
+
await this.refresh();
|
|
48
|
+
return this.cachedKey(keyid);
|
|
49
|
+
}
|
|
50
|
+
cachedKey(keyid) {
|
|
51
|
+
if (this.now() > this.cacheExp)
|
|
52
|
+
return undefined;
|
|
53
|
+
return this.cache.get(keyid);
|
|
54
|
+
}
|
|
55
|
+
async refresh() {
|
|
56
|
+
if (this.now() <= this.cacheExp)
|
|
57
|
+
return; // another refresh won the race
|
|
58
|
+
if (this.inflight)
|
|
59
|
+
return this.inflight;
|
|
60
|
+
this.inflight = this.doRefresh();
|
|
61
|
+
try {
|
|
62
|
+
await this.inflight;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
this.inflight = undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async doRefresh() {
|
|
69
|
+
const body = await fetchStrict(this.fetchFn, this.url);
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(body);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
throw new DirectoryUnavailable(`jwks decode ${this.url}`, { cause: err });
|
|
76
|
+
}
|
|
77
|
+
this.cache = ed25519KeysFromJwks(parsed);
|
|
78
|
+
this.cacheExp = this.now() + this.ttlMs;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
class EndpointResolverImpl {
|
|
82
|
+
fetchFn;
|
|
83
|
+
ttlMs;
|
|
84
|
+
now;
|
|
85
|
+
scheme;
|
|
86
|
+
allow;
|
|
87
|
+
cache = new Map();
|
|
88
|
+
flight = new Map();
|
|
89
|
+
constructor(opts) {
|
|
90
|
+
this.fetchFn = opts.fetch ?? defaultFetch;
|
|
91
|
+
this.ttlMs = opts.ttlMs && opts.ttlMs > 0 ? opts.ttlMs : DEFAULT_TTL_MS;
|
|
92
|
+
this.now = opts.now ?? Date.now;
|
|
93
|
+
this.scheme = opts.scheme && opts.scheme !== "" ? opts.scheme : "https";
|
|
94
|
+
this.allow = opts.allow;
|
|
95
|
+
}
|
|
96
|
+
async resolveEndpoint(host) {
|
|
97
|
+
// Checked BEFORE the allow overlay and before the cache. The fetch URL is
|
|
98
|
+
// built by concatenation, so a value carrying a path or a query would choose
|
|
99
|
+
// WHAT gets fetched rather than merely where from — and the raw string is the
|
|
100
|
+
// cache key, so admitting one would also put it in a shared map.
|
|
101
|
+
// A fault in the CALLER's value, not a verdict on the Exchange's answer, so
|
|
102
|
+
// it is the same invalid-host error isBareHost itself throws for a reference
|
|
103
|
+
// it cannot read at all — the oracle uses one sentinel across both branches
|
|
104
|
+
// here for the same reason. EndpointRefused stays what it says: the manifest
|
|
105
|
+
// was read and its answer is unusable.
|
|
106
|
+
//
|
|
107
|
+
// Built by the shared constructor rather than by hand. A value that PARSES and
|
|
108
|
+
// carries a credential — user:pass@exchange.example — reaches this branch
|
|
109
|
+
// rather than the throwing one, so a message written out here is the one path
|
|
110
|
+
// into these refusals that would name the credential.
|
|
111
|
+
if (!isBareHost(host)) {
|
|
112
|
+
throw invalidHost(host, "not a bare host");
|
|
113
|
+
}
|
|
114
|
+
if (this.allow && !this.allow(host))
|
|
115
|
+
throw new NoEndpoint(`host ${host} not allowed`);
|
|
116
|
+
const hit = this.cached(host);
|
|
117
|
+
if (hit !== undefined)
|
|
118
|
+
return hit;
|
|
119
|
+
let flight = this.flight.get(host);
|
|
120
|
+
if (!flight) {
|
|
121
|
+
flight = this.fetchEndpoint(host);
|
|
122
|
+
this.flight.set(host, flight);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return await flight;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
this.flight.delete(host);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
cached(host) {
|
|
132
|
+
const entry = this.cache.get(host);
|
|
133
|
+
if (!entry || this.now() > entry.exp)
|
|
134
|
+
return undefined;
|
|
135
|
+
return entry.endpoint;
|
|
136
|
+
}
|
|
137
|
+
async fetchEndpoint(host) {
|
|
138
|
+
const hit = this.cached(host);
|
|
139
|
+
if (hit !== undefined)
|
|
140
|
+
return hit; // filled while we queued behind the flight lock
|
|
141
|
+
const url = `${this.scheme}://${host}${WellKnownPath}`;
|
|
142
|
+
const body = await fetchStrict(this.fetchFn, url);
|
|
143
|
+
let doc;
|
|
144
|
+
try {
|
|
145
|
+
doc = JSON.parse(body);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
throw new DirectoryUnavailable(`manifest decode ${url}`, { cause: err });
|
|
149
|
+
}
|
|
150
|
+
// The document version gate runs before the endpoint is so much as looked
|
|
151
|
+
// at, and never caches: the next resolve fetches again.
|
|
152
|
+
const versionRefusal = manifestVersionRefusal(doc?.ver);
|
|
153
|
+
if (versionRefusal !== undefined) {
|
|
154
|
+
throw new ManifestVersionRefused(versionRefusal);
|
|
155
|
+
}
|
|
156
|
+
const endpoint = doc.endpoint;
|
|
157
|
+
if (typeof endpoint !== "string" || endpoint === "") {
|
|
158
|
+
throw new NoEndpoint(`host=${host}`);
|
|
159
|
+
}
|
|
160
|
+
// Vetted BEFORE it is cached, so a refused endpoint is not held for the TTL
|
|
161
|
+
// and then served to every later caller out of memory.
|
|
162
|
+
const refusal = endpointRefusal(host, endpoint);
|
|
163
|
+
if (refusal !== undefined) {
|
|
164
|
+
throw new EndpointRefused(refusal);
|
|
165
|
+
}
|
|
166
|
+
this.cache.set(host, { endpoint, exp: this.now() + this.ttlMs });
|
|
167
|
+
return endpoint;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** The JOSE/JWA algorithm identifier advertised on AgentAcceptance.signature. Always EdDSA for Ed25519
|
|
2
|
+
* (mirror helpers.AcceptanceSignatureAlgorithm). */
|
|
3
|
+
export declare const ACCEPTANCE_SIGNATURE_ALGORITHM = "EdDSA";
|
|
4
|
+
/** The acceptance binding fields. EVERY empty field is omitted from the canonical
|
|
5
|
+
* payload, not only requesterDomain (proto omit-unpopulated) — so an empty value and
|
|
6
|
+
* an absent one sign the same bytes, and neither signs the bytes of a populated one.
|
|
7
|
+
* An empty offerSig is rejected fail-closed — an empty anchor would let the
|
|
8
|
+
* acceptance float free of any concrete offer. */
|
|
9
|
+
export interface AcceptanceInput {
|
|
10
|
+
offerSig: string;
|
|
11
|
+
requesterId: string;
|
|
12
|
+
requesterDomain: string;
|
|
13
|
+
idempotencyKey: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* acceptancePayload reproduces the canonical signed bytes:
|
|
17
|
+
* JCS(protojson(AgentAcceptancePayload)) with EVERY empty string field omitted.
|
|
18
|
+
* Throws on an empty offer signature (fail-closed, mirror Go
|
|
19
|
+
* CanonicalAcceptanceBytes / Python jcs_acceptance_payload).
|
|
20
|
+
*/
|
|
21
|
+
export declare function acceptancePayload(input: AcceptanceInput): Uint8Array<ArrayBuffer>;
|
|
22
|
+
/**
|
|
23
|
+
* signOfferAcceptance signs the canonical acceptance payload with the agent's
|
|
24
|
+
* Ed25519 private key and returns the hex signature for AgentAcceptance.signature.
|
|
25
|
+
*/
|
|
26
|
+
export declare function signOfferAcceptance(input: AcceptanceInput, privateKey: CryptoKey): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* verifyOfferAcceptance verifies a hex acceptance signature over the canonical
|
|
29
|
+
* payload with the agent's Ed25519 public key. Returns false (never throws) on a
|
|
30
|
+
* bad binding, a bad key, or an unsigned offer.
|
|
31
|
+
*/
|
|
32
|
+
export declare function verifyOfferAcceptance(input: AcceptanceInput, signatureHex: string, publicKey: CryptoKey): Promise<boolean>;
|
|
33
|
+
export interface RequestAcceptanceItemInput {
|
|
34
|
+
offerSig: string;
|
|
35
|
+
exchange: string;
|
|
36
|
+
}
|
|
37
|
+
export interface RequestAcceptanceInput {
|
|
38
|
+
items: RequestAcceptanceItemInput[];
|
|
39
|
+
requesterId: string;
|
|
40
|
+
requesterDomain: string;
|
|
41
|
+
idempotencyKey: string;
|
|
42
|
+
}
|
|
43
|
+
/** Canonical JCS(protojson(...)) bytes for the complete ordered execute set. */
|
|
44
|
+
export declare function requestAcceptancePayload(input: RequestAcceptanceInput): Uint8Array<ArrayBuffer>;
|
|
45
|
+
export declare function signRequestAcceptance(input: RequestAcceptanceInput, privateKey: CryptoKey): Promise<string>;
|
|
46
|
+
export declare function verifyRequestAcceptance(input: RequestAcceptanceInput, signatureHex: string, publicKey: CryptoKey): Promise<boolean>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// sdk/ts offer-acceptance sign/verify face — the AGENT-role acceptance surface,
|
|
2
|
+
// mirror of Go helpers.SignOfferAcceptance / VerifyOfferAcceptance and Python
|
|
3
|
+
// core.sign_offer_acceptance_jcs / verify_offer_acceptance_jcs.
|
|
4
|
+
//
|
|
5
|
+
// An agent signs a DETACHED, content-bound acceptance over an accepted Offer;
|
|
6
|
+
// the Exchange verifies it to bind the agent to the transaction. Unlike the
|
|
7
|
+
// transport RFC 9421 request signature, the acceptance is topology-independent:
|
|
8
|
+
// it stays valid no matter how many brokers relay the request, because it covers
|
|
9
|
+
// the offer signature + requester + idempotency, not the HTTP envelope.
|
|
10
|
+
//
|
|
11
|
+
// The signed bytes are JCS(protojson(AgentAcceptancePayload)) — snake_case,
|
|
12
|
+
// omit-unpopulated — the SAME primitive the offer signature uses, so every
|
|
13
|
+
// language reproduces the exact bytes. Ed25519 is deterministic (RFC 8032), so a
|
|
14
|
+
// given (payload, key) always yields the same signature; the parity suite pins
|
|
15
|
+
// the TS output byte-identical to the Go oracle.
|
|
16
|
+
//
|
|
17
|
+
// WebCrypto is the crypto primitive (the same one the L1 pop/verify faces use);
|
|
18
|
+
// the caller supplies the imported CryptoKey, exactly as core/sign.ts does.
|
|
19
|
+
import canonicalize from "canonicalize";
|
|
20
|
+
import { utf8Bytes } from "./base64url.js";
|
|
21
|
+
/** The JOSE/JWA algorithm identifier advertised on AgentAcceptance.signature. Always EdDSA for Ed25519
|
|
22
|
+
* (mirror helpers.AcceptanceSignatureAlgorithm). */
|
|
23
|
+
export const ACCEPTANCE_SIGNATURE_ALGORITHM = "EdDSA";
|
|
24
|
+
// Strict about what a hex digit is, and about the length. Number.parseInt accepts a sign,
|
|
25
|
+
// leading whitespace and a trailing tail — "-1", "+f" and " a" all produce a number — so a
|
|
26
|
+
// signature carrying any of them would decode to bytes rather than being refused, where
|
|
27
|
+
// Go's hex.DecodeString and Python's bytes.fromhex refuse it. An odd length used to
|
|
28
|
+
// truncate silently. The value comes from a peer, so the three languages have to agree on
|
|
29
|
+
// what is a signature and what is garbage.
|
|
30
|
+
const HEX_PAIR = /^[0-9a-fA-F]{2}$/;
|
|
31
|
+
function hexToBytes(hex) {
|
|
32
|
+
if (hex.length % 2 !== 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
const out = new Uint8Array(hex.length / 2);
|
|
35
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
36
|
+
const pair = hex.slice(i * 2, i * 2 + 2);
|
|
37
|
+
if (!HEX_PAIR.test(pair))
|
|
38
|
+
return undefined;
|
|
39
|
+
out[i] = Number.parseInt(pair, 16);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function bytesToHex(bytes) {
|
|
44
|
+
let hex = "";
|
|
45
|
+
for (const b of bytes)
|
|
46
|
+
hex += b.toString(16).padStart(2, "0");
|
|
47
|
+
return hex;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* acceptancePayload reproduces the canonical signed bytes:
|
|
51
|
+
* JCS(protojson(AgentAcceptancePayload)) with EVERY empty string field omitted.
|
|
52
|
+
* Throws on an empty offer signature (fail-closed, mirror Go
|
|
53
|
+
* CanonicalAcceptanceBytes / Python jcs_acceptance_payload).
|
|
54
|
+
*/
|
|
55
|
+
export function acceptancePayload(input) {
|
|
56
|
+
if (input.offerSig === "") {
|
|
57
|
+
throw new Error("fora/acceptance: cannot accept an unsigned offer (empty offer signature)");
|
|
58
|
+
}
|
|
59
|
+
// proto omit-unpopulated: every empty string field is absent before JCS. The Go
|
|
60
|
+
// oracle gets that structurally from EmitUnpopulated=false; this record is
|
|
61
|
+
// hand-built, so the omission is applied once over the whole record rather than
|
|
62
|
+
// per key. A per-key guard is how the rule went missing for requester_id --
|
|
63
|
+
// wire-valid, since Requester.id carries no min_len -- which signed bytes Go never
|
|
64
|
+
// produces. The filter tests for the empty STRING, which covers every member
|
|
65
|
+
// AgentAcceptancePayload has (the field-set guard in the Go suite pins that list),
|
|
66
|
+
// so a string field added to the message cannot arrive without its omission. A
|
|
67
|
+
// non-string field would need its own zero-value test.
|
|
68
|
+
const payload = {
|
|
69
|
+
offer_sig: input.offerSig,
|
|
70
|
+
requester_id: input.requesterId,
|
|
71
|
+
requester_domain: input.requesterDomain,
|
|
72
|
+
idempotency_key: input.idempotencyKey,
|
|
73
|
+
};
|
|
74
|
+
const obj = Object.fromEntries(Object.entries(payload).filter(([, v]) => v !== ""));
|
|
75
|
+
const jcs = canonicalize(obj);
|
|
76
|
+
if (jcs === undefined) {
|
|
77
|
+
throw new Error("fora/acceptance: payload is not JSON-serializable");
|
|
78
|
+
}
|
|
79
|
+
return utf8Bytes(jcs);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* signOfferAcceptance signs the canonical acceptance payload with the agent's
|
|
83
|
+
* Ed25519 private key and returns the hex signature for AgentAcceptance.signature.
|
|
84
|
+
*/
|
|
85
|
+
export async function signOfferAcceptance(input, privateKey) {
|
|
86
|
+
const payload = acceptancePayload(input);
|
|
87
|
+
const sig = new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, payload));
|
|
88
|
+
return bytesToHex(sig);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* verifyOfferAcceptance verifies a hex acceptance signature over the canonical
|
|
92
|
+
* payload with the agent's Ed25519 public key. Returns false (never throws) on a
|
|
93
|
+
* bad binding, a bad key, or an unsigned offer.
|
|
94
|
+
*/
|
|
95
|
+
export async function verifyOfferAcceptance(input, signatureHex, publicKey) {
|
|
96
|
+
let payload;
|
|
97
|
+
try {
|
|
98
|
+
payload = acceptancePayload(input);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const signature = hexToBytes(signatureHex);
|
|
104
|
+
if (signature === undefined)
|
|
105
|
+
return false;
|
|
106
|
+
try {
|
|
107
|
+
return await crypto.subtle.verify("Ed25519", publicKey, signature, payload);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Canonical JCS(protojson(...)) bytes for the complete ordered execute set. */
|
|
114
|
+
export function requestAcceptancePayload(input) {
|
|
115
|
+
if (input.items.length === 0) {
|
|
116
|
+
throw new Error("fora/acceptance: request acceptance requires at least one item");
|
|
117
|
+
}
|
|
118
|
+
const items = input.items.map((item, index) => {
|
|
119
|
+
if (item.offerSig === "") {
|
|
120
|
+
throw new Error(`fora/acceptance: request item ${index} has an empty offer signature`);
|
|
121
|
+
}
|
|
122
|
+
if (item.exchange === "") {
|
|
123
|
+
throw new Error(`fora/acceptance: request item ${index} has an empty exchange`);
|
|
124
|
+
}
|
|
125
|
+
return { offer_sig: item.offerSig, exchange: item.exchange };
|
|
126
|
+
});
|
|
127
|
+
const payload = {
|
|
128
|
+
items,
|
|
129
|
+
requester_id: input.requesterId,
|
|
130
|
+
requester_domain: input.requesterDomain,
|
|
131
|
+
idempotency_key: input.idempotencyKey,
|
|
132
|
+
};
|
|
133
|
+
const obj = Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== ""));
|
|
134
|
+
const jcs = canonicalize(obj);
|
|
135
|
+
if (jcs === undefined) {
|
|
136
|
+
throw new Error("fora/acceptance: request payload is not JSON-serializable");
|
|
137
|
+
}
|
|
138
|
+
return utf8Bytes(jcs);
|
|
139
|
+
}
|
|
140
|
+
export async function signRequestAcceptance(input, privateKey) {
|
|
141
|
+
const payload = requestAcceptancePayload(input);
|
|
142
|
+
const sig = new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, payload));
|
|
143
|
+
return bytesToHex(sig);
|
|
144
|
+
}
|
|
145
|
+
export async function verifyRequestAcceptance(input, signatureHex, publicKey) {
|
|
146
|
+
let payload;
|
|
147
|
+
try {
|
|
148
|
+
payload = requestAcceptancePayload(input);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const signature = hexToBytes(signatureHex);
|
|
154
|
+
if (signature === undefined)
|
|
155
|
+
return false;
|
|
156
|
+
try {
|
|
157
|
+
return await crypto.subtle.verify("Ed25519", publicKey, signature, payload);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Decode a base64url (or std-base64) string to bytes, or `undefined` on error.
|
|
2
|
+
* Typed `Uint8Array<ArrayBuffer>` (never SharedArrayBuffer-backed) so decoded
|
|
3
|
+
* bytes satisfy WebCrypto's BufferSource without casts. */
|
|
4
|
+
export declare function decodeBase64Url(input: string): Uint8Array<ArrayBuffer> | undefined;
|
|
5
|
+
/** Strict base64url decode for JWK OKP `x` key material (RFC 8037: key material
|
|
6
|
+
* MUST be unpadded base64url). Rejects `=` padding and the standard (`+`/`/`)
|
|
7
|
+
* alphabet that {@link decodeBase64Url} silently tolerates, mirroring Go's
|
|
8
|
+
* `base64.RawURLEncoding` byte-for-byte so the tri-language WBA active-key
|
|
9
|
+
* selector / thumbprint resolver picks the SAME key on a directory holding a
|
|
10
|
+
* standard-alphabet or padded `x`. Returns `undefined` on any non-urlsafe
|
|
11
|
+
* character, `=` padding, or an invalid unpadded length (`len % 4 === 1`). */
|
|
12
|
+
export declare function decodeBase64UrlStrict(input: string): Uint8Array<ArrayBuffer> | undefined;
|
|
13
|
+
/** UTF-8 encode, pinned ArrayBuffer-backed. TextEncoder.encode always allocates
|
|
14
|
+
* a fresh ArrayBuffer, but some lib typings widen the return to ArrayBufferLike;
|
|
15
|
+
* pin it once here so consumers on any TS/lib version feed WebCrypto without
|
|
16
|
+
* casts of their own. */
|
|
17
|
+
export declare function utf8Bytes(s: string): Uint8Array<ArrayBuffer>;
|
|
18
|
+
/** Encode bytes as base64url with no padding. */
|
|
19
|
+
export declare function encodeBase64Url(bytes: Uint8Array): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// base64url codec — the shared byte primitive under the thumbprint, signed-URL,
|
|
2
|
+
// and PoP helpers. Relocated verbatim from the app edge (src/edge/src/verify.ts)
|
|
3
|
+
// so the SDK's canonical encodings stay byte-identical to the wire contract the
|
|
4
|
+
// sdk/go oracle emits.
|
|
5
|
+
//
|
|
6
|
+
// The decoder accepts both url- and std-alphabet input (it remaps `-_` -> `+/`
|
|
7
|
+
// and pads), so an RFC 9421 Signature header — which carries STANDARD base64 —
|
|
8
|
+
// decodes correctly through the same path (the remap is a no-op on std input).
|
|
9
|
+
/** Decode a base64url (or std-base64) string to bytes, or `undefined` on error.
|
|
10
|
+
* Typed `Uint8Array<ArrayBuffer>` (never SharedArrayBuffer-backed) so decoded
|
|
11
|
+
* bytes satisfy WebCrypto's BufferSource without casts. */
|
|
12
|
+
export function decodeBase64Url(input) {
|
|
13
|
+
const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - (input.length % 4));
|
|
14
|
+
const b64 = input.replaceAll("-", "+").replaceAll("_", "/") + pad;
|
|
15
|
+
try {
|
|
16
|
+
const bin = atob(b64);
|
|
17
|
+
const out = new Uint8Array(bin.length);
|
|
18
|
+
for (let i = 0; i < bin.length; i += 1)
|
|
19
|
+
out[i] = bin.charCodeAt(i);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Strict base64url decode for JWK OKP `x` key material (RFC 8037: key material
|
|
27
|
+
* MUST be unpadded base64url). Rejects `=` padding and the standard (`+`/`/`)
|
|
28
|
+
* alphabet that {@link decodeBase64Url} silently tolerates, mirroring Go's
|
|
29
|
+
* `base64.RawURLEncoding` byte-for-byte so the tri-language WBA active-key
|
|
30
|
+
* selector / thumbprint resolver picks the SAME key on a directory holding a
|
|
31
|
+
* standard-alphabet or padded `x`. Returns `undefined` on any non-urlsafe
|
|
32
|
+
* character, `=` padding, or an invalid unpadded length (`len % 4 === 1`). */
|
|
33
|
+
export function decodeBase64UrlStrict(input) {
|
|
34
|
+
if (!/^[A-Za-z0-9_-]*$/.test(input))
|
|
35
|
+
return undefined;
|
|
36
|
+
if (input.length % 4 === 1)
|
|
37
|
+
return undefined;
|
|
38
|
+
// Input is now guaranteed unpadded url-alphabet, so the lenient path decodes it
|
|
39
|
+
// exactly (its `-_`->`+/` remap + re-pad is the identity transform here).
|
|
40
|
+
return decodeBase64Url(input);
|
|
41
|
+
}
|
|
42
|
+
/** UTF-8 encode, pinned ArrayBuffer-backed. TextEncoder.encode always allocates
|
|
43
|
+
* a fresh ArrayBuffer, but some lib typings widen the return to ArrayBufferLike;
|
|
44
|
+
* pin it once here so consumers on any TS/lib version feed WebCrypto without
|
|
45
|
+
* casts of their own. */
|
|
46
|
+
export function utf8Bytes(s) {
|
|
47
|
+
return new TextEncoder().encode(s);
|
|
48
|
+
}
|
|
49
|
+
/** Encode bytes as base64url with no padding. */
|
|
50
|
+
export function encodeBase64Url(bytes) {
|
|
51
|
+
let bin = "";
|
|
52
|
+
for (let i = 0; i < bytes.length; i += 1)
|
|
53
|
+
bin += String.fromCharCode(bytes[i]);
|
|
54
|
+
return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { GetAccountStatusResponseSchema, LicenseSchema, LicenseTermSchema, ObligationSchema, PricingSchema, RegistrationFailureSchema, RestrictionSchema, WellKnownManifestSchema } from "../../../gen/ts/wire/schemas.ts";
|
|
3
|
+
/** Stable rule-id carried on each cross-field Zod issue via params.ruleId. */
|
|
4
|
+
export interface CrossFieldIssueParams {
|
|
5
|
+
ruleId: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* crossFieldRuleIds returns the cross-field (message-CEL) rule-ids the instance
|
|
9
|
+
* violates, empty when it passes cross-field validation. Direct analogue of the
|
|
10
|
+
* Go oracle's ValidationRuleIDs(err) over the crossfield corpus.
|
|
11
|
+
*/
|
|
12
|
+
export declare function crossFieldRuleIds(message: string, json: unknown): string[];
|
|
13
|
+
/**
|
|
14
|
+
* The cross-field composition of a generated schema: exactly what `superRefine`
|
|
15
|
+
* returns on it. Spelled as the method's own return type, not as a Zod class,
|
|
16
|
+
* so the published declaration holds under both supported majors — Zod 3 returns
|
|
17
|
+
* a ZodEffects wrapper, Zod 4 returns the schema's own class — while keeping the
|
|
18
|
+
* schema's output and input inference.
|
|
19
|
+
*/
|
|
20
|
+
export type CrossField<T extends z.ZodTypeAny> = ReturnType<T["superRefine"]>;
|
|
21
|
+
export declare const GetAccountStatusResponseCrossFieldSchema: CrossField<typeof GetAccountStatusResponseSchema>;
|
|
22
|
+
export declare const LicenseCrossFieldSchema: CrossField<typeof LicenseSchema>;
|
|
23
|
+
export declare const LicenseTermCrossFieldSchema: CrossField<typeof LicenseTermSchema>;
|
|
24
|
+
export declare const ObligationCrossFieldSchema: CrossField<typeof ObligationSchema>;
|
|
25
|
+
export declare const PricingCrossFieldSchema: CrossField<typeof PricingSchema>;
|
|
26
|
+
export declare const RestrictionCrossFieldSchema: CrossField<typeof RestrictionSchema>;
|
|
27
|
+
export declare const RegistrationFailureCrossFieldSchema: CrossField<typeof RegistrationFailureSchema>;
|
|
28
|
+
export declare const WellKnownManifestCrossFieldSchema: CrossField<typeof WellKnownManifestSchema>;
|