@anis-ly/partners 0.0.0-stage → 1.0.0
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +96 -2
- package/SECURITY.md +13 -0
- package/dist/index.cjs +3002 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1053 -0
- package/dist/index.d.ts +1053 -0
- package/dist/index.js +2946 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,2946 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/internal/uuid.ts
|
|
8
|
+
var uuid_exports = {};
|
|
9
|
+
__export(uuid_exports, {
|
|
10
|
+
canonicalUuid: () => canonicalUuid
|
|
11
|
+
});
|
|
12
|
+
function canonicalUuid(value, argumentName = "UUID") {
|
|
13
|
+
if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value)) {
|
|
14
|
+
throw new TypeError(`${argumentName} must be a canonical UUID.`);
|
|
15
|
+
}
|
|
16
|
+
return value.toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/signing/keyed-signer.ts
|
|
20
|
+
var KeyedSigner = class {
|
|
21
|
+
/** Creates a binding; canonical identity avoids a valid signature that names an unknown credential. */
|
|
22
|
+
constructor(signer, keyId) {
|
|
23
|
+
this.signer = signer;
|
|
24
|
+
this.keyId = canonicalUuid(keyId, "keyId");
|
|
25
|
+
}
|
|
26
|
+
signer;
|
|
27
|
+
/** The enrolled credential identifier, canonicalized so Anis can resolve it. */
|
|
28
|
+
keyId;
|
|
29
|
+
/** Signs exact bytes through partner-owned key custody without exposing key material. */
|
|
30
|
+
sign(data) {
|
|
31
|
+
return this.signer.sign(data);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/signing/pem-p256-signer.ts
|
|
36
|
+
import { readFile } from "fs/promises";
|
|
37
|
+
|
|
38
|
+
// src/internal/bytes.ts
|
|
39
|
+
import { timingSafeEqual } from "crypto";
|
|
40
|
+
function fixedTimeEqual(left, right) {
|
|
41
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
42
|
+
}
|
|
43
|
+
function arrayBufferOf(bytes) {
|
|
44
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
45
|
+
copy.set(bytes);
|
|
46
|
+
return copy.buffer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/errors/anis-partners-error.ts
|
|
50
|
+
import { inspect } from "util";
|
|
51
|
+
var errorBrandPrefix = "@anis-ly/partners.error:";
|
|
52
|
+
var AnisPartnersError = class extends Error {
|
|
53
|
+
constructor(message, options) {
|
|
54
|
+
super(message, options);
|
|
55
|
+
let prototype = new.target.prototype;
|
|
56
|
+
while (prototype !== Error.prototype && prototype !== Object.prototype) {
|
|
57
|
+
const constructorName = prototype.constructor.name ?? "AnisPartnersError";
|
|
58
|
+
Object.defineProperty(this, /* @__PURE__ */ Symbol.for(`${errorBrandPrefix}${constructorName}`), { value: true });
|
|
59
|
+
const parent = Object.getPrototypeOf(prototype);
|
|
60
|
+
if (parent === null) break;
|
|
61
|
+
prototype = parent;
|
|
62
|
+
}
|
|
63
|
+
this.name = new.target.name;
|
|
64
|
+
}
|
|
65
|
+
static [Symbol.hasInstance](value) {
|
|
66
|
+
if (typeof value !== "object" || value === null) return false;
|
|
67
|
+
return value[/* @__PURE__ */ Symbol.for(`${errorBrandPrefix}${this.name}`)] === true;
|
|
68
|
+
}
|
|
69
|
+
[inspect.custom]() {
|
|
70
|
+
return `${this.name}: ${this.message}`;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/errors/private-key-error.ts
|
|
75
|
+
var PrivateKeyError = class extends AnisPartnersError {
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/signing/pem-p256-signer.ts
|
|
79
|
+
var PemP256Signer = class _PemP256Signer {
|
|
80
|
+
constructor(publicKey, privateKey) {
|
|
81
|
+
this.publicKey = publicKey;
|
|
82
|
+
this.#privateKey = privateKey;
|
|
83
|
+
}
|
|
84
|
+
publicKey;
|
|
85
|
+
#privateKey;
|
|
86
|
+
/** Imports a PKCS#8 PEM key and refuses other curves at load time, before a partner receives an avoidable refusal. */
|
|
87
|
+
static async fromPem(pem) {
|
|
88
|
+
if (typeof pem !== "string") throw new PrivateKeyError("The private key must be supplied as PEM text.");
|
|
89
|
+
const normalized = pem.replace(/^\uFEFF/, "").trim();
|
|
90
|
+
if (normalized.includes("-----BEGIN ENCRYPTED PRIVATE KEY-----"))
|
|
91
|
+
throw new PrivateKeyError("Encrypted PEM keys are not supported; decrypt the key before loading it.");
|
|
92
|
+
if (normalized.includes("-----BEGIN EC PRIVATE KEY-----"))
|
|
93
|
+
throw new PrivateKeyError("SEC1 PEM keys are not supported; convert to PKCS#8 before loading the key.");
|
|
94
|
+
const match = /-----BEGIN PRIVATE KEY-----\s*([A-Za-z0-9+/=\r\n]+)\s*-----END PRIVATE KEY-----/.exec(normalized);
|
|
95
|
+
if (!match?.[1]) throw new PrivateKeyError("Expected a PKCS#8 PEM block labeled PRIVATE KEY.");
|
|
96
|
+
const pkcs8 = Buffer.from(match[1].replace(/\s/g, ""), "base64");
|
|
97
|
+
try {
|
|
98
|
+
const extractableKey = await globalThis.crypto.subtle.importKey(
|
|
99
|
+
"pkcs8",
|
|
100
|
+
pkcs8,
|
|
101
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
102
|
+
true,
|
|
103
|
+
["sign"]
|
|
104
|
+
);
|
|
105
|
+
const jwk = await globalThis.crypto.subtle.exportKey("jwk", extractableKey);
|
|
106
|
+
if (!jwk.x || !jwk.y) throw new PrivateKeyError("The private key did not export public coordinates.");
|
|
107
|
+
const publicKey = await globalThis.crypto.subtle.importKey(
|
|
108
|
+
"jwk",
|
|
109
|
+
{ kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y, ext: true },
|
|
110
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
111
|
+
true,
|
|
112
|
+
["verify"]
|
|
113
|
+
);
|
|
114
|
+
const privateKey = await globalThis.crypto.subtle.importKey(
|
|
115
|
+
"pkcs8",
|
|
116
|
+
pkcs8,
|
|
117
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
118
|
+
false,
|
|
119
|
+
["sign"]
|
|
120
|
+
);
|
|
121
|
+
return new _PemP256Signer(publicKey, privateKey);
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
throw new PrivateKeyError("The PEM key must be a valid ECDSA P-256 PKCS#8 private key.", { cause });
|
|
124
|
+
} finally {
|
|
125
|
+
pkcs8.fill(0);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Reads and imports a protected PKCS#8 PEM file so the private key can remain in host-controlled storage. */
|
|
129
|
+
static async fromPemFile(path) {
|
|
130
|
+
try {
|
|
131
|
+
return await this.fromPem(await readFile(path, "utf8"));
|
|
132
|
+
} catch (cause) {
|
|
133
|
+
if (cause instanceof PrivateKeyError) throw cause;
|
|
134
|
+
throw new PrivateKeyError("The PEM key file could not be read.", { cause });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Signs bytes in WebCrypto's 64-byte P1363 form; no DER conversion can silently change the wire signature. */
|
|
138
|
+
async sign(data) {
|
|
139
|
+
try {
|
|
140
|
+
return new Uint8Array(
|
|
141
|
+
await globalThis.crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, this.#privateKey, arrayBufferOf(data))
|
|
142
|
+
);
|
|
143
|
+
} catch (cause) {
|
|
144
|
+
throw new PrivateKeyError("The in-memory P-256 private key could not sign the request.", { cause });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Exports only public coordinates for enrollment and verification; the private half is never part of the JWK. */
|
|
148
|
+
async publicJwk() {
|
|
149
|
+
try {
|
|
150
|
+
const jwk = await globalThis.crypto.subtle.exportKey("jwk", this.publicKey);
|
|
151
|
+
return {
|
|
152
|
+
kty: "EC",
|
|
153
|
+
crv: "P-256",
|
|
154
|
+
...jwk.x === void 0 ? {} : { x: jwk.x },
|
|
155
|
+
...jwk.y === void 0 ? {} : { y: jwk.y }
|
|
156
|
+
};
|
|
157
|
+
} catch (cause) {
|
|
158
|
+
throw new PrivateKeyError("The P-256 public key could not be exported.", { cause });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Binds this key to the issued credential UUID used by Anis to resolve request signatures. */
|
|
162
|
+
forKey(keyId) {
|
|
163
|
+
return new KeyedSigner(this, keyId);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/signing/request-signing-error.ts
|
|
168
|
+
var RequestSigningError = class extends AnisPartnersError {
|
|
169
|
+
/** Preserves the original signer failure as `cause` while keeping sensitive signature material out of the message. */
|
|
170
|
+
constructor(cause) {
|
|
171
|
+
super("The request could not be signed and was not sent.", { cause });
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/enrollment/key-thumbprint.ts
|
|
176
|
+
import { createHash } from "crypto";
|
|
177
|
+
|
|
178
|
+
// src/internal/base64url.ts
|
|
179
|
+
function encodeBase64Url(bytes) {
|
|
180
|
+
return Buffer.from(bytes).toString("base64url");
|
|
181
|
+
}
|
|
182
|
+
function decodeBase64Url(value) {
|
|
183
|
+
if (!value || value.length % 4 === 1 || !/^[A-Za-z0-9_-]+$/.test(value)) return void 0;
|
|
184
|
+
try {
|
|
185
|
+
const decoded = Buffer.from(value, "base64url");
|
|
186
|
+
return decoded.toString("base64url") === value ? new Uint8Array(decoded) : void 0;
|
|
187
|
+
} catch {
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/enrollment/key-thumbprint.ts
|
|
193
|
+
function keyThumbprint(publicJwk) {
|
|
194
|
+
if (publicJwk.kty !== "EC" || publicJwk.crv !== "P-256")
|
|
195
|
+
throw new TypeError("The key must be a public EC P-256 JWK.");
|
|
196
|
+
const x = publicJwk.x;
|
|
197
|
+
const y = publicJwk.y;
|
|
198
|
+
if (!x || !y) throw new TypeError("A P-256 public key must include both coordinates.");
|
|
199
|
+
for (const [name, value] of [
|
|
200
|
+
["x", x],
|
|
201
|
+
["y", y]
|
|
202
|
+
])
|
|
203
|
+
if (decodeBase64Url(value)?.length !== 32)
|
|
204
|
+
throw new TypeError(`The JWK member "${name}" must be a 32-byte base64url coordinate.`);
|
|
205
|
+
const canonical = `{"crv":"P-256","kty":"EC","x":"${x}","y":"${y}"}`;
|
|
206
|
+
return encodeBase64Url(createHash("sha256").update(canonical, "utf8").digest());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/enrollment/safety-code.ts
|
|
210
|
+
var alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
211
|
+
function safetyCodeFromThumbprint(thumbprint) {
|
|
212
|
+
const bytes = decodeBase64Url(thumbprint);
|
|
213
|
+
if (bytes?.length !== 32) throw new TypeError("A thumbprint must encode 32 bytes.");
|
|
214
|
+
let buffer = 0;
|
|
215
|
+
let bits = 0;
|
|
216
|
+
let code = "";
|
|
217
|
+
for (const value of bytes.subarray(0, 10)) {
|
|
218
|
+
buffer = buffer << 8 | value;
|
|
219
|
+
bits += 8;
|
|
220
|
+
while (bits >= 5) {
|
|
221
|
+
bits -= 5;
|
|
222
|
+
code += alphabet.charAt(buffer >> bits & 31);
|
|
223
|
+
}
|
|
224
|
+
buffer &= (1 << bits) - 1;
|
|
225
|
+
}
|
|
226
|
+
return `${code.slice(0, 4)}-${code.slice(4, 8)}-${code.slice(8, 12)}-${code.slice(12)}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/enrollment/enrollment-proof.ts
|
|
230
|
+
import { createHash as createHash2 } from "crypto";
|
|
231
|
+
var DOMAIN_SEPARATOR = "anis.partners.v2.credential-proof";
|
|
232
|
+
function proofMessage(keyId, challengeGeneration, challenge, thumbprint) {
|
|
233
|
+
if (challenge.length === 0) throw new TypeError("An enrollment challenge is required for a possession proof.");
|
|
234
|
+
if (thumbprint.length === 0) throw new TypeError("A key thumbprint is required for a possession proof.");
|
|
235
|
+
const hash = createHash2("sha256").update(challenge, "utf8").digest("hex");
|
|
236
|
+
return new TextEncoder().encode(
|
|
237
|
+
[DOMAIN_SEPARATOR, canonicalUuid(keyId, "keyId"), String(challengeGeneration), hash, thumbprint].join("\n")
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
async function proofSignature(message, signer) {
|
|
241
|
+
let signature;
|
|
242
|
+
try {
|
|
243
|
+
signature = await signer.sign(message);
|
|
244
|
+
} catch (cause) {
|
|
245
|
+
throw new RequestSigningError(cause);
|
|
246
|
+
}
|
|
247
|
+
if (!(signature instanceof Uint8Array) || signature.length !== 64)
|
|
248
|
+
throw new RequestSigningError(new Error("An enrollment proof signer must return 64-byte P-256 P1363 bytes."));
|
|
249
|
+
return encodeBase64Url(signature);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/models/model-parsing.ts
|
|
253
|
+
function asObject(value, label) {
|
|
254
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
255
|
+
throw new TypeError(`${label} must be a JSON object.`);
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
function requiredString(object, field, label) {
|
|
260
|
+
const value = object[field];
|
|
261
|
+
if (typeof value !== "string") {
|
|
262
|
+
throw new TypeError(`${label}.${field} must be a string.`);
|
|
263
|
+
}
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
function optionalString(object, field, label) {
|
|
267
|
+
const value = object[field];
|
|
268
|
+
if (value === void 0 || value === null) {
|
|
269
|
+
return void 0;
|
|
270
|
+
}
|
|
271
|
+
if (typeof value !== "string") {
|
|
272
|
+
throw new TypeError(`${label}.${field} must be a string when present.`);
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
function requiredUuid(object, field, label) {
|
|
277
|
+
const { canonicalUuid: canonicalUuid2 } = uuid_exports;
|
|
278
|
+
return canonicalUuid2(requiredString(object, field, label), `${label}.${field}`);
|
|
279
|
+
}
|
|
280
|
+
function optionalUuid(object, field, label) {
|
|
281
|
+
const value = optionalString(object, field, label);
|
|
282
|
+
return value === void 0 ? void 0 : canonicalUuid(value, `${label}.${field}`);
|
|
283
|
+
}
|
|
284
|
+
function optionalTimestamp(object, field, label) {
|
|
285
|
+
const value = optionalString(object, field, label);
|
|
286
|
+
if (value === void 0) {
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
const result = new Date(value);
|
|
290
|
+
if (!isStrictIsoTimestamp(value) || Number.isNaN(result.getTime())) {
|
|
291
|
+
throw new TypeError(`${label}.${field} must be a valid timestamp.`);
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
function isStrictIsoTimestamp(value) {
|
|
296
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.exec(
|
|
297
|
+
value
|
|
298
|
+
);
|
|
299
|
+
if (match === null) return false;
|
|
300
|
+
const year = Number(match[1]);
|
|
301
|
+
const month = Number(match[2]);
|
|
302
|
+
const day = Number(match[3]);
|
|
303
|
+
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
304
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth;
|
|
305
|
+
}
|
|
306
|
+
function optionalInteger(object, field, label) {
|
|
307
|
+
const value = object[field];
|
|
308
|
+
if (value === void 0 || value === null) {
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
312
|
+
throw new TypeError(`${label}.${field} must be an integer when present.`);
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
function optionalBoolean(object, field, label) {
|
|
317
|
+
const value = object[field];
|
|
318
|
+
if (value === void 0 || value === null) {
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
if (typeof value !== "boolean") {
|
|
322
|
+
throw new TypeError(`${label}.${field} must be a boolean when present.`);
|
|
323
|
+
}
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/models/enrollment.ts
|
|
328
|
+
function parseEnrollmentState(json) {
|
|
329
|
+
const object = asObject(json, "EnrollmentState");
|
|
330
|
+
const invitationId = optionalUuid(object, "invitationId", "EnrollmentState");
|
|
331
|
+
const applicationId = optionalUuid(object, "applicationId", "EnrollmentState");
|
|
332
|
+
const state = optionalString(object, "state", "EnrollmentState");
|
|
333
|
+
const expiresAt = optionalTimestamp(object, "expiresAt", "EnrollmentState");
|
|
334
|
+
return {
|
|
335
|
+
...invitationId === void 0 ? {} : { invitationId },
|
|
336
|
+
...applicationId === void 0 ? {} : { applicationId },
|
|
337
|
+
...state === void 0 ? {} : { state },
|
|
338
|
+
...expiresAt === void 0 ? {} : { expiresAt }
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function parseEnrollmentKeyResult(json) {
|
|
342
|
+
const object = asObject(json, "EnrollmentKeyResult");
|
|
343
|
+
const keyId = requiredUuid(object, "keyId", "EnrollmentKeyResult");
|
|
344
|
+
const thumbprint = optionalString(object, "thumbprint", "EnrollmentKeyResult");
|
|
345
|
+
const safetyCode = optionalString(object, "safetyCode", "EnrollmentKeyResult");
|
|
346
|
+
const challenge = optionalString(object, "challenge", "EnrollmentKeyResult");
|
|
347
|
+
const challengeGeneration = optionalInteger(object, "challengeGeneration", "EnrollmentKeyResult");
|
|
348
|
+
return {
|
|
349
|
+
keyId,
|
|
350
|
+
...thumbprint === void 0 ? {} : { thumbprint },
|
|
351
|
+
...safetyCode === void 0 ? {} : { safetyCode },
|
|
352
|
+
...challenge === void 0 ? {} : { challenge },
|
|
353
|
+
...challengeGeneration === void 0 ? {} : { challengeGeneration }
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function parseEnrollmentStatus(json) {
|
|
357
|
+
const object = asObject(json, "EnrollmentStatus");
|
|
358
|
+
const keyId = optionalUuid(object, "keyId", "EnrollmentStatus");
|
|
359
|
+
const challengeGeneration = optionalInteger(object, "challengeGeneration", "EnrollmentStatus");
|
|
360
|
+
const proofState = optionalString(object, "proofState", "EnrollmentStatus");
|
|
361
|
+
const approvalState = optionalString(object, "approvalState", "EnrollmentStatus");
|
|
362
|
+
const state = optionalString(object, "state", "EnrollmentStatus");
|
|
363
|
+
const expiresAt = optionalTimestamp(object, "expiresAt", "EnrollmentStatus");
|
|
364
|
+
const keyExpiresAt = optionalTimestamp(object, "keyExpiresAt", "EnrollmentStatus");
|
|
365
|
+
return {
|
|
366
|
+
...keyId === void 0 ? {} : { keyId },
|
|
367
|
+
...challengeGeneration === void 0 ? {} : { challengeGeneration },
|
|
368
|
+
...proofState === void 0 ? {} : { proofState },
|
|
369
|
+
...approvalState === void 0 ? {} : { approvalState },
|
|
370
|
+
...state === void 0 ? {} : { state },
|
|
371
|
+
...expiresAt === void 0 ? {} : { expiresAt },
|
|
372
|
+
...keyExpiresAt === void 0 ? {} : { keyExpiresAt }
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function parseSignatureDiagnostic(json) {
|
|
376
|
+
const object = asObject(json, "SignatureDiagnostic");
|
|
377
|
+
const routeId = optionalString(object, "routeId", "SignatureDiagnostic");
|
|
378
|
+
const method = optionalString(object, "method", "SignatureDiagnostic");
|
|
379
|
+
const authority = optionalString(object, "authority", "SignatureDiagnostic");
|
|
380
|
+
const path = optionalString(object, "path", "SignatureDiagnostic");
|
|
381
|
+
const canonicalQuery = optionalString(object, "canonicalQuery", "SignatureDiagnostic");
|
|
382
|
+
const requestKind = optionalString(object, "requestKind", "SignatureDiagnostic");
|
|
383
|
+
const requiredScope = optionalString(object, "requiredScope", "SignatureDiagnostic");
|
|
384
|
+
const coveredComponents = stringArray(object.coveredComponents, "SignatureDiagnostic.coveredComponents");
|
|
385
|
+
const keyId = optionalUuid(object, "keyId", "SignatureDiagnostic");
|
|
386
|
+
const partnerId = optionalUuid(object, "partnerId", "SignatureDiagnostic");
|
|
387
|
+
const applicationId = optionalUuid(object, "applicationId", "SignatureDiagnostic");
|
|
388
|
+
const policyVersion = optionalInteger(object, "policyVersion", "SignatureDiagnostic");
|
|
389
|
+
const effectiveScopes = stringArray(object.effectiveScopes, "SignatureDiagnostic.effectiveScopes");
|
|
390
|
+
const receivedAt = optionalTimestamp(object, "receivedAt", "SignatureDiagnostic");
|
|
391
|
+
return {
|
|
392
|
+
coveredComponents,
|
|
393
|
+
effectiveScopes,
|
|
394
|
+
...routeId === void 0 ? {} : { routeId },
|
|
395
|
+
...method === void 0 ? {} : { method },
|
|
396
|
+
...authority === void 0 ? {} : { authority },
|
|
397
|
+
...path === void 0 ? {} : { path },
|
|
398
|
+
...canonicalQuery === void 0 ? {} : { canonicalQuery },
|
|
399
|
+
...requestKind === void 0 ? {} : { requestKind },
|
|
400
|
+
...requiredScope === void 0 ? {} : { requiredScope },
|
|
401
|
+
...keyId === void 0 ? {} : { keyId },
|
|
402
|
+
...partnerId === void 0 ? {} : { partnerId },
|
|
403
|
+
...applicationId === void 0 ? {} : { applicationId },
|
|
404
|
+
...policyVersion === void 0 ? {} : { policyVersion },
|
|
405
|
+
...receivedAt === void 0 ? {} : { receivedAt }
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function stringArray(value, label) {
|
|
409
|
+
const items = value ?? [];
|
|
410
|
+
if (!Array.isArray(items) || !items.every((item) => typeof item === "string")) {
|
|
411
|
+
throw new TypeError(`${label} must be an array of strings.`);
|
|
412
|
+
}
|
|
413
|
+
return items;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/operations/partner-transport.ts
|
|
417
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
418
|
+
|
|
419
|
+
// src/errors/error-codes.generated.ts
|
|
420
|
+
var ERROR_CODES = [
|
|
421
|
+
"account_inactive",
|
|
422
|
+
"allowed_debt_consent_required",
|
|
423
|
+
"binding_not_authorized",
|
|
424
|
+
"business_subscription_required",
|
|
425
|
+
"card_not_found",
|
|
426
|
+
"card_unavailable",
|
|
427
|
+
"challenge_expired",
|
|
428
|
+
"currency_not_supported",
|
|
429
|
+
"daily_limit_exceeded",
|
|
430
|
+
"dependency_unavailable",
|
|
431
|
+
"idempotency_conflict",
|
|
432
|
+
"insufficient_balance",
|
|
433
|
+
"insufficient_scope",
|
|
434
|
+
"internal_error",
|
|
435
|
+
"invalid_content_digest",
|
|
436
|
+
"invalid_credentials",
|
|
437
|
+
"invitation_invalid",
|
|
438
|
+
"invoice_reveal_limit_exceeded",
|
|
439
|
+
"key_duplicate",
|
|
440
|
+
"key_proof_invalid",
|
|
441
|
+
"malformed_signed_request",
|
|
442
|
+
"operation_processing",
|
|
443
|
+
"owner_limit_exceeded",
|
|
444
|
+
"price_changed",
|
|
445
|
+
"purchase_not_allowed",
|
|
446
|
+
"quantity_unavailable",
|
|
447
|
+
"rate_limited",
|
|
448
|
+
"replay_detected",
|
|
449
|
+
"request_timeout",
|
|
450
|
+
"resource_not_found",
|
|
451
|
+
"reveal_not_allowed",
|
|
452
|
+
"signature_expired",
|
|
453
|
+
"source_ip_not_allowed",
|
|
454
|
+
"validation_failed",
|
|
455
|
+
"wallet_disabled",
|
|
456
|
+
"wallet_expired",
|
|
457
|
+
"wallet_not_granted"
|
|
458
|
+
];
|
|
459
|
+
var RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
460
|
+
"dependency_unavailable",
|
|
461
|
+
"internal_error",
|
|
462
|
+
"operation_processing",
|
|
463
|
+
"rate_limited",
|
|
464
|
+
"replay_detected",
|
|
465
|
+
"request_timeout",
|
|
466
|
+
"signature_expired"
|
|
467
|
+
]);
|
|
468
|
+
function parseErrorCode(code) {
|
|
469
|
+
if (code === null || code === void 0) {
|
|
470
|
+
return "unknown";
|
|
471
|
+
}
|
|
472
|
+
return ERROR_CODES.find((known) => known === code) ?? "unknown";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/models/problem.ts
|
|
476
|
+
function parseProblem(json) {
|
|
477
|
+
const object = asObject(json, "Problem");
|
|
478
|
+
const type = optionalString(object, "type", "Problem");
|
|
479
|
+
const title = optionalString(object, "title", "Problem");
|
|
480
|
+
const detail = optionalString(object, "detail", "Problem");
|
|
481
|
+
const code = optionalString(object, "code", "Problem");
|
|
482
|
+
const requestId = optionalString(object, "requestId", "Problem");
|
|
483
|
+
const status = optionalInteger(object, "status", "Problem") ?? 0;
|
|
484
|
+
const extensions = object.extensions;
|
|
485
|
+
return {
|
|
486
|
+
status,
|
|
487
|
+
...type === void 0 ? {} : { type },
|
|
488
|
+
...title === void 0 ? {} : { title },
|
|
489
|
+
...detail === void 0 ? {} : { detail },
|
|
490
|
+
...code === void 0 ? {} : { code },
|
|
491
|
+
...requestId === void 0 ? {} : { requestId },
|
|
492
|
+
...extensions === void 0 || extensions === null ? {} : { extensions }
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/internal/retry-after.ts
|
|
497
|
+
function retryAfterSeconds(value) {
|
|
498
|
+
const first = value?.split(",")[0]?.trim();
|
|
499
|
+
if (first === void 0 || !/^\d+$/.test(first)) return void 0;
|
|
500
|
+
const seconds = Number(first);
|
|
501
|
+
return Number.isSafeInteger(seconds) && seconds <= 2147483647 ? seconds : void 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/errors/anis-api-error.ts
|
|
505
|
+
var AnisApiError = class extends AnisPartnersError {
|
|
506
|
+
/** Complete parsed problem returned by Anis. */
|
|
507
|
+
problem;
|
|
508
|
+
/** Known machine code, or unknown when this SDK has not seen the value. */
|
|
509
|
+
code;
|
|
510
|
+
/** Exact wire code, including an unknown future value. */
|
|
511
|
+
rawCode;
|
|
512
|
+
/** HTTP status of the verified response. */
|
|
513
|
+
status;
|
|
514
|
+
/** Correlation id to quote when asking Anis about the call. */
|
|
515
|
+
requestId;
|
|
516
|
+
/** Permanent documentation link for this code. */
|
|
517
|
+
typeUri;
|
|
518
|
+
/** Signed Retry-After value in seconds, when supplied. */
|
|
519
|
+
retryAfter;
|
|
520
|
+
/** True when Anis returned the recorded refusal for this operation id. */
|
|
521
|
+
isReplayed;
|
|
522
|
+
/** Whether the catalogue marks this code retryable; this does not decide order recovery. */
|
|
523
|
+
isRetryable;
|
|
524
|
+
/** Whether the refusal leaves an order open for resume. */
|
|
525
|
+
orderOutcome;
|
|
526
|
+
/**
|
|
527
|
+
* Creates a structured API error from a verified problem.
|
|
528
|
+
*
|
|
529
|
+
* @remarks The code is the stable branching contract. Titles and details change with language and copy updates, so
|
|
530
|
+
* callers that branch on the message can break without any API behavior changing.
|
|
531
|
+
*/
|
|
532
|
+
constructor(problem, status, retryAfter, isReplayed = false) {
|
|
533
|
+
super(
|
|
534
|
+
`Anis returned ${String(status)} ${problem.code ?? "unknown"}${isReplayed ? " (recorded answer for this operation)" : ""}${problem.requestId === void 0 ? "" : ` (request ${problem.requestId})`}. Branch on the code, not on this message.`
|
|
535
|
+
);
|
|
536
|
+
this.problem = problem;
|
|
537
|
+
Object.defineProperty(this, "problem", { enumerable: false });
|
|
538
|
+
this.status = status;
|
|
539
|
+
this.code = parseErrorCode(problem.code);
|
|
540
|
+
this.isReplayed = isReplayed;
|
|
541
|
+
this.isRetryable = RETRYABLE_ERROR_CODES.has(this.code);
|
|
542
|
+
this.orderOutcome = isReplayed ? "notPlaced" : outcomeOf(this.code);
|
|
543
|
+
if (problem.code !== void 0) {
|
|
544
|
+
this.rawCode = problem.code;
|
|
545
|
+
}
|
|
546
|
+
if (problem.requestId !== void 0) {
|
|
547
|
+
this.requestId = problem.requestId;
|
|
548
|
+
}
|
|
549
|
+
if (problem.type !== void 0) {
|
|
550
|
+
this.typeUri = problem.type;
|
|
551
|
+
}
|
|
552
|
+
if (retryAfter !== void 0) {
|
|
553
|
+
this.retryAfter = retryAfter;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
var InsufficientBalanceError = class extends AnisApiError {
|
|
558
|
+
};
|
|
559
|
+
var PriceChangedError = class extends AnisApiError {
|
|
560
|
+
};
|
|
561
|
+
var OutOfStockError = class extends AnisApiError {
|
|
562
|
+
};
|
|
563
|
+
var IdempotencyConflictError = class extends AnisApiError {
|
|
564
|
+
};
|
|
565
|
+
var RateLimitedError = class extends AnisApiError {
|
|
566
|
+
};
|
|
567
|
+
var LimitExceededError = class extends AnisApiError {
|
|
568
|
+
};
|
|
569
|
+
var InvalidCredentialsError = class extends AnisApiError {
|
|
570
|
+
};
|
|
571
|
+
var ReplayDetectedError = class extends AnisApiError {
|
|
572
|
+
};
|
|
573
|
+
var AuthorizationError = class extends AnisApiError {
|
|
574
|
+
};
|
|
575
|
+
var ResourceNotFoundError = class extends AnisApiError {
|
|
576
|
+
};
|
|
577
|
+
var ValidationFailedError = class extends AnisApiError {
|
|
578
|
+
};
|
|
579
|
+
var DependencyUnavailableError = class extends AnisApiError {
|
|
580
|
+
};
|
|
581
|
+
var EnrollmentRefusedError = class extends AnisApiError {
|
|
582
|
+
};
|
|
583
|
+
function outcomeOf(code) {
|
|
584
|
+
return code === "dependency_unavailable" || code === "request_timeout" || code === "internal_error" || code === "operation_processing" || code === "replay_detected" || code === "rate_limited" || code === "unknown" || refusedAtTheDoor(code) ? "unknown" : "notPlaced";
|
|
585
|
+
}
|
|
586
|
+
function refusedAtTheDoor(code) {
|
|
587
|
+
return code === "invalid_credentials" || code === "signature_expired" || code === "insufficient_scope" || code === "wallet_not_granted" || code === "malformed_signed_request";
|
|
588
|
+
}
|
|
589
|
+
function createAnisApiError(body, status, headers) {
|
|
590
|
+
const problem = parseProblemOrFallback(body, status);
|
|
591
|
+
const retryAfter = retryAfterSeconds(headers?.get("retry-after"));
|
|
592
|
+
const isReplayed = headerValues(headers?.get("idempotency-replayed")).some((value) => value.toLowerCase() === "true");
|
|
593
|
+
const ErrorType = errorTypeFor(problem.code);
|
|
594
|
+
return new ErrorType(problem, status, retryAfter, isReplayed);
|
|
595
|
+
}
|
|
596
|
+
function parseProblemOrFallback(body, status) {
|
|
597
|
+
try {
|
|
598
|
+
const text = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
599
|
+
return parseProblem(JSON.parse(text));
|
|
600
|
+
} catch {
|
|
601
|
+
return { type: "about:blank", title: "Unreadable problem", status, code: "internal_error" };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function headerValues(value) {
|
|
605
|
+
return value === void 0 || value === null ? [] : value.split(",").map((part) => part.trim());
|
|
606
|
+
}
|
|
607
|
+
function errorTypeFor(code) {
|
|
608
|
+
switch (parseErrorCode(code)) {
|
|
609
|
+
case "insufficient_balance":
|
|
610
|
+
return InsufficientBalanceError;
|
|
611
|
+
case "price_changed":
|
|
612
|
+
return PriceChangedError;
|
|
613
|
+
case "quantity_unavailable":
|
|
614
|
+
case "card_unavailable":
|
|
615
|
+
return OutOfStockError;
|
|
616
|
+
case "idempotency_conflict":
|
|
617
|
+
return IdempotencyConflictError;
|
|
618
|
+
case "rate_limited":
|
|
619
|
+
return RateLimitedError;
|
|
620
|
+
case "owner_limit_exceeded":
|
|
621
|
+
case "daily_limit_exceeded":
|
|
622
|
+
return LimitExceededError;
|
|
623
|
+
case "invalid_credentials":
|
|
624
|
+
case "signature_expired":
|
|
625
|
+
return InvalidCredentialsError;
|
|
626
|
+
case "replay_detected":
|
|
627
|
+
return ReplayDetectedError;
|
|
628
|
+
case "insufficient_scope":
|
|
629
|
+
case "source_ip_not_allowed":
|
|
630
|
+
case "binding_not_authorized":
|
|
631
|
+
case "account_inactive":
|
|
632
|
+
case "business_subscription_required":
|
|
633
|
+
case "wallet_disabled":
|
|
634
|
+
case "wallet_expired":
|
|
635
|
+
case "purchase_not_allowed":
|
|
636
|
+
case "reveal_not_allowed":
|
|
637
|
+
return AuthorizationError;
|
|
638
|
+
case "resource_not_found":
|
|
639
|
+
case "card_not_found":
|
|
640
|
+
case "wallet_not_granted":
|
|
641
|
+
return ResourceNotFoundError;
|
|
642
|
+
case "validation_failed":
|
|
643
|
+
case "currency_not_supported":
|
|
644
|
+
return ValidationFailedError;
|
|
645
|
+
case "dependency_unavailable":
|
|
646
|
+
case "request_timeout":
|
|
647
|
+
case "internal_error":
|
|
648
|
+
return DependencyUnavailableError;
|
|
649
|
+
case "invitation_invalid":
|
|
650
|
+
case "challenge_expired":
|
|
651
|
+
case "key_proof_invalid":
|
|
652
|
+
case "key_duplicate":
|
|
653
|
+
return EnrollmentRefusedError;
|
|
654
|
+
default:
|
|
655
|
+
return AnisApiError;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/signing/content-digest.ts
|
|
660
|
+
import { createHash as createHash3 } from "crypto";
|
|
661
|
+
var EMPTY_OBJECT_BODY = new TextEncoder().encode("{}");
|
|
662
|
+
function contentDigestOf(bytes) {
|
|
663
|
+
return `sha-256=:${createHash3("sha256").update(bytes).digest("base64")}:`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/signing/partner-request-signer.ts
|
|
667
|
+
import { inspect as inspect2 } from "util";
|
|
668
|
+
|
|
669
|
+
// src/signing/signature-profile.ts
|
|
670
|
+
function componentsOf(profile) {
|
|
671
|
+
switch (profile) {
|
|
672
|
+
case "SafeRead":
|
|
673
|
+
return ["@method", "@authority", "@path", "@query", "x-anis-date"];
|
|
674
|
+
case "BodylessNonceMutation":
|
|
675
|
+
return ["@method", "@authority", "@path", "@query", "content-digest", "nonce", "x-anis-date"];
|
|
676
|
+
case "OrderMutation":
|
|
677
|
+
return ["@method", "@authority", "@path", "@query", "content-digest", "nonce", "idempotency-key", "x-anis-date"];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/signing/signature-inputs.ts
|
|
682
|
+
function canonicalizeSignatureInputs(inputs) {
|
|
683
|
+
if (inputs.path.includes("%")) throw new TypeError("Paths containing percent escapes cannot be signed.");
|
|
684
|
+
return {
|
|
685
|
+
method: inputs.method.toUpperCase(),
|
|
686
|
+
authority: inputs.authority.toLowerCase(),
|
|
687
|
+
path: inputs.path,
|
|
688
|
+
canonicalQuery: inputs.canonicalQuery,
|
|
689
|
+
anisDate: inputs.anisDate,
|
|
690
|
+
...inputs.contentDigest === void 0 ? {} : { contentDigest: inputs.contentDigest },
|
|
691
|
+
...inputs.nonce === void 0 ? {} : { nonce: inputs.nonce },
|
|
692
|
+
...inputs.idempotencyKey === void 0 ? {} : { idempotencyKey: canonicalUuid(inputs.idempotencyKey, "idempotencyKey") }
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function valueOf(inputs, component) {
|
|
696
|
+
const values = {
|
|
697
|
+
"@method": inputs.method,
|
|
698
|
+
"@authority": inputs.authority,
|
|
699
|
+
"@path": inputs.path,
|
|
700
|
+
"@query": `?${inputs.canonicalQuery}`,
|
|
701
|
+
"content-digest": inputs.contentDigest,
|
|
702
|
+
nonce: inputs.nonce,
|
|
703
|
+
"idempotency-key": inputs.idempotencyKey,
|
|
704
|
+
"x-anis-date": inputs.anisDate
|
|
705
|
+
};
|
|
706
|
+
const value = values[component];
|
|
707
|
+
if (value === void 0) throw new TypeError(`The covered component '${component}' has no value.`);
|
|
708
|
+
return value;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/signing/partner-request-signature-base.ts
|
|
712
|
+
var PartnerRequestSignatureBase = {
|
|
713
|
+
/** The sole signature label accepted by Anis; an alternate label is rejected before signature verification. */
|
|
714
|
+
label: "sig1",
|
|
715
|
+
/** The only algorithm negotiated by neither side, preventing a fallback with different signature semantics. */
|
|
716
|
+
algorithm: "ecdsa-p256-sha256",
|
|
717
|
+
/** The gateway's independent upper bound; longer windows are refused before a request can be accepted. */
|
|
718
|
+
maxSignatureLifetimeSeconds: 300,
|
|
719
|
+
/** Builds exact UTF-8 bytes; the final line has no newline because even one extra byte invalidates the signature. */
|
|
720
|
+
build(profile, inputs, created, expires, keyId) {
|
|
721
|
+
const nonce = inputs.nonce;
|
|
722
|
+
const params = this.parameters(profile, created, expires, keyId, nonce);
|
|
723
|
+
const lines = componentsOf(profile).map((component) => `"${component}": ${valueOf(inputs, component)}
|
|
724
|
+
`);
|
|
725
|
+
lines.push(`"@signature-params": ${params}`);
|
|
726
|
+
return new TextEncoder().encode(lines.join(""));
|
|
727
|
+
},
|
|
728
|
+
/** Renders contract-ordered parameters, including the nonce only when the route profile requires it. */
|
|
729
|
+
parameters(profile, created, expires, keyId, nonce) {
|
|
730
|
+
if (expires - created > this.maxSignatureLifetimeSeconds)
|
|
731
|
+
throw new RangeError("A Partner signature may live at most 300 seconds.");
|
|
732
|
+
let invalidNonce = nonce === void 0 || nonce.length === 0;
|
|
733
|
+
if (nonce !== void 0) {
|
|
734
|
+
for (const character of nonce)
|
|
735
|
+
if (character === '"' || character === "\\" || character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)
|
|
736
|
+
invalidNonce = true;
|
|
737
|
+
}
|
|
738
|
+
if (profile === "SafeRead" ? nonce !== void 0 : invalidNonce) {
|
|
739
|
+
throw new TypeError("The nonce does not match the selected signature profile.");
|
|
740
|
+
}
|
|
741
|
+
const ids = componentsOf(profile).map((part) => `"${part}"`).join(" ");
|
|
742
|
+
return `(${ids});created=${String(created)};expires=${String(expires)};keyid="${canonicalUuid(keyId, "keyId")}";alg="${PartnerRequestSignatureBase.algorithm}"${nonce === void 0 ? "" : `;nonce="${nonce}"`}`;
|
|
743
|
+
},
|
|
744
|
+
/** Renders the sole supported label with the same parameters used to build the signed bytes. */
|
|
745
|
+
signatureInputHeader(parameters) {
|
|
746
|
+
return `${this.label}=${parameters}`;
|
|
747
|
+
},
|
|
748
|
+
/** Renders the exact P1363 bytes; converting to DER would make Anis refuse this otherwise valid signature. */
|
|
749
|
+
signatureHeader(signature) {
|
|
750
|
+
return `${this.label}=:${Buffer.from(signature).toString("base64")}:`;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// src/signing/partner-request-signer.ts
|
|
755
|
+
var PartnerRequestSigner = class {
|
|
756
|
+
/** Creates a signer backed by partner-owned key custody; the private key never enters this API. */
|
|
757
|
+
constructor(signer) {
|
|
758
|
+
this.signer = signer;
|
|
759
|
+
}
|
|
760
|
+
signer;
|
|
761
|
+
/** Builds and signs one attempt; errors retain the signer cause without exposing the base or signature in the message. */
|
|
762
|
+
async sign(profile, source, created, expires) {
|
|
763
|
+
const inputs = canonicalizeSignatureInputs(source);
|
|
764
|
+
if (profile === "SafeRead" !== (inputs.nonce === void 0))
|
|
765
|
+
throw new TypeError("The nonce does not match the selected signature profile.");
|
|
766
|
+
for (const name of componentsOf(profile)) {
|
|
767
|
+
if (name === "content-digest" && inputs.contentDigest === void 0)
|
|
768
|
+
throw new TypeError("The selected profile requires Content-Digest.");
|
|
769
|
+
if (name === "idempotency-key" && inputs.idempotencyKey === void 0)
|
|
770
|
+
throw new TypeError("The order profile requires a caller-owned idempotency key.");
|
|
771
|
+
}
|
|
772
|
+
const base = PartnerRequestSignatureBase.build(profile, inputs, created, expires, this.signer.keyId);
|
|
773
|
+
let signature;
|
|
774
|
+
try {
|
|
775
|
+
signature = await this.signer.sign(base);
|
|
776
|
+
} catch (cause) {
|
|
777
|
+
throw new RequestSigningError(cause);
|
|
778
|
+
}
|
|
779
|
+
if (!(signature instanceof Uint8Array) || signature.length !== 64)
|
|
780
|
+
throw new RequestSigningError(
|
|
781
|
+
new Error(
|
|
782
|
+
`The signer must return a 64-byte Uint8Array for P-256 P1363. A 70\u201372 byte result is almost certainly DER.`
|
|
783
|
+
)
|
|
784
|
+
);
|
|
785
|
+
const params = PartnerRequestSignatureBase.parameters(profile, created, expires, this.signer.keyId, inputs.nonce);
|
|
786
|
+
return {
|
|
787
|
+
signatureInput: PartnerRequestSignatureBase.signatureInputHeader(params),
|
|
788
|
+
signature: PartnerRequestSignatureBase.signatureHeader(signature),
|
|
789
|
+
anisDate: inputs.anisDate,
|
|
790
|
+
...inputs.contentDigest === void 0 ? {} : { contentDigest: inputs.contentDigest },
|
|
791
|
+
...inputs.nonce === void 0 ? {} : { nonce: inputs.nonce },
|
|
792
|
+
...inputs.idempotencyKey === void 0 ? {} : { idempotencyKey: inputs.idempotencyKey },
|
|
793
|
+
signatureBase: base,
|
|
794
|
+
[inspect2.custom]() {
|
|
795
|
+
return "SignedRequestHeaders { signatureInput: <redacted>, signature: <redacted>, nonce: <redacted>, signatureBase: <redacted> }";
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// src/verification/partner-response-verifier.ts
|
|
802
|
+
import { createHash as createHash4 } from "crypto";
|
|
803
|
+
|
|
804
|
+
// src/internal/clock.ts
|
|
805
|
+
var systemClock = { now: () => /* @__PURE__ */ new Date() };
|
|
806
|
+
|
|
807
|
+
// src/verification/signature-input-parser.ts
|
|
808
|
+
function parseSignatureInput(value) {
|
|
809
|
+
if (!value) return void 0;
|
|
810
|
+
const equal = value.indexOf("=");
|
|
811
|
+
if (equal <= 0 || value[equal + 1] !== "(") return void 0;
|
|
812
|
+
const label = value.slice(0, equal);
|
|
813
|
+
let index = equal + 2;
|
|
814
|
+
const identifiers = [];
|
|
815
|
+
while (index < value.length && value[index] !== ")") {
|
|
816
|
+
if (value[index] === " ") {
|
|
817
|
+
while (value[index] === " ") index++;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (value[index] !== '"') return void 0;
|
|
821
|
+
const end = value.indexOf('"', index + 1);
|
|
822
|
+
if (end < 0) return void 0;
|
|
823
|
+
const name = value.slice(index + 1, end);
|
|
824
|
+
index = end + 1;
|
|
825
|
+
let requestBound = false;
|
|
826
|
+
if (value[index] === ";") {
|
|
827
|
+
if (!value.startsWith(";req", index)) return void 0;
|
|
828
|
+
index += 4;
|
|
829
|
+
requestBound = true;
|
|
830
|
+
}
|
|
831
|
+
identifiers.push(requestBound ? `${name};req` : name);
|
|
832
|
+
}
|
|
833
|
+
if (value[index] !== ")") return void 0;
|
|
834
|
+
index++;
|
|
835
|
+
let created;
|
|
836
|
+
let keyId;
|
|
837
|
+
let algorithm;
|
|
838
|
+
while (index < value.length) {
|
|
839
|
+
if (value[index] !== ";") return void 0;
|
|
840
|
+
const start = index + 1;
|
|
841
|
+
const eq = value.indexOf("=", start);
|
|
842
|
+
if (eq < 0) return void 0;
|
|
843
|
+
const name = value.slice(start, eq);
|
|
844
|
+
index = eq + 1;
|
|
845
|
+
if (value[index] === '"') {
|
|
846
|
+
const end = value.indexOf('"', index + 1);
|
|
847
|
+
if (end < 0) return void 0;
|
|
848
|
+
const text = value.slice(index + 1, end);
|
|
849
|
+
index = end + 1;
|
|
850
|
+
if (name === "keyid") keyId = text;
|
|
851
|
+
else if (name === "alg") algorithm = text;
|
|
852
|
+
} else {
|
|
853
|
+
let end = index;
|
|
854
|
+
while (end < value.length && value[end] !== ";") end++;
|
|
855
|
+
const text = value.slice(index, end);
|
|
856
|
+
if (!/^\d+$/.test(text)) return void 0;
|
|
857
|
+
if (name === "created") {
|
|
858
|
+
const integer = BigInt(text);
|
|
859
|
+
if (integer > 9223372036854775807n) return void 0;
|
|
860
|
+
created = Number(integer);
|
|
861
|
+
}
|
|
862
|
+
index = end;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (created === void 0 || keyId === void 0) return void 0;
|
|
866
|
+
return { label, identifiers, created, keyId, ...algorithm === void 0 ? {} : { algorithm } };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/verification/partner-response-signature-base.ts
|
|
870
|
+
var PartnerResponseSignatureBase = {
|
|
871
|
+
/** Response signatures are accepted within this whole-second window to match the gateway's freshness calculation. */
|
|
872
|
+
MAX_AGE_SECONDS: 60,
|
|
873
|
+
/** The sole response label; disagreement between headers is refused. */
|
|
874
|
+
label: "sig1",
|
|
875
|
+
/** The only supported algorithm; there is no weaker fallback for responses. */
|
|
876
|
+
algorithm: "ecdsa-p256-sha256",
|
|
877
|
+
/** Derives covered fields from what actually arrived so absent optional values are never signed as empty fields. */
|
|
878
|
+
components(status, digest, requestId, requestSignatureInput, location, retryAfter, replayed, cacheControl) {
|
|
879
|
+
const result = [
|
|
880
|
+
{ name: "@status", value: String(status), requestBound: false },
|
|
881
|
+
{ name: "content-digest", value: digest, requestBound: false },
|
|
882
|
+
{ name: "x-request-id", value: requestId, requestBound: false }
|
|
883
|
+
];
|
|
884
|
+
if (requestSignatureInput)
|
|
885
|
+
result.push({ name: "signature-input", value: requestSignatureInput, requestBound: true });
|
|
886
|
+
for (const [name, value] of [
|
|
887
|
+
["location", location],
|
|
888
|
+
["retry-after", retryAfter],
|
|
889
|
+
["idempotency-replayed", replayed],
|
|
890
|
+
["cache-control", cacheControl]
|
|
891
|
+
])
|
|
892
|
+
if (value) result.push({ name, value, requestBound: false });
|
|
893
|
+
return result;
|
|
894
|
+
},
|
|
895
|
+
/** Builds exact UTF-8 bytes; request binding and its `;req` identifier must agree in both places. */
|
|
896
|
+
build(components, created, keyId) {
|
|
897
|
+
const params = `(${components.map((part) => `"${part.name}"${part.requestBound ? ";req" : ""}`).join(" ")});created=${String(created)};keyid="${keyId}";alg="${PartnerResponseSignatureBase.algorithm}"`;
|
|
898
|
+
const lines = components.map((part) => `"${part.name}"${part.requestBound ? ";req" : ""}: ${part.value}
|
|
899
|
+
`);
|
|
900
|
+
lines.push(`"@signature-params": ${params}`);
|
|
901
|
+
return new TextEncoder().encode(lines.join(""));
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// src/verification/unverifiable-response-error.ts
|
|
906
|
+
var UnverifiableResponseError = class extends AnisPartnersError {
|
|
907
|
+
/** The contract reason for refusing the response, without requiring callers to parse message text. */
|
|
908
|
+
failure;
|
|
909
|
+
/** Creates a response refusal while keeping its body, signature, base, and key material out of the message. */
|
|
910
|
+
constructor(failure, detail) {
|
|
911
|
+
super(`The Anis response could not be verified (${failure}): ${detail} Its content has been discarded.`);
|
|
912
|
+
this.failure = failure;
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
// src/internal/safe-telemetry.ts
|
|
917
|
+
import {
|
|
918
|
+
metrics,
|
|
919
|
+
SpanKind,
|
|
920
|
+
trace
|
|
921
|
+
} from "@opentelemetry/api";
|
|
922
|
+
|
|
923
|
+
// src/observability/telemetry-names.ts
|
|
924
|
+
var AnisPartnersTelemetry = {
|
|
925
|
+
/** Instrumentation scope used for every SDK span and metric. */
|
|
926
|
+
scope: "@anis-ly/partners",
|
|
927
|
+
/** Stable low-cardinality attribute names. */
|
|
928
|
+
attributes: {
|
|
929
|
+
client: "anis.client",
|
|
930
|
+
route: "anis.route",
|
|
931
|
+
method: "http.request.method",
|
|
932
|
+
statusCode: "http.response.status_code",
|
|
933
|
+
errorType: "error.type",
|
|
934
|
+
errorCode: "anis.error.code",
|
|
935
|
+
operationId: "anis.operation_id",
|
|
936
|
+
requestId: "anis.request_id"
|
|
937
|
+
},
|
|
938
|
+
/** Metric instrument names emitted by the SDK. */
|
|
939
|
+
instruments: {
|
|
940
|
+
requestDuration: "anis.partners.request.duration",
|
|
941
|
+
signatureDuration: "anis.partners.signature.duration",
|
|
942
|
+
verificationFailures: "anis.partners.response.verification.failures",
|
|
943
|
+
orderOutcomes: "anis.partners.order.outcomes",
|
|
944
|
+
signingKeyFetches: "anis.partners.signing_keys.fetches"
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
// src/internal/safe-telemetry.ts
|
|
949
|
+
var packageVersion = "1.0.0";
|
|
950
|
+
var histograms = /* @__PURE__ */ new WeakMap();
|
|
951
|
+
var counters = /* @__PURE__ */ new WeakMap();
|
|
952
|
+
async function withSafeSpan(name, action) {
|
|
953
|
+
let callbackResult;
|
|
954
|
+
try {
|
|
955
|
+
const tracer = trace.getTracer(AnisPartnersTelemetry.scope, packageVersion);
|
|
956
|
+
const result = tracer.startActiveSpan(name, { kind: SpanKind.CLIENT }, (span) => {
|
|
957
|
+
callbackResult = Promise.resolve().then(() => action(span));
|
|
958
|
+
return callbackResult;
|
|
959
|
+
});
|
|
960
|
+
void Promise.resolve(result).catch(() => void 0);
|
|
961
|
+
if (callbackResult !== void 0) return await callbackResult;
|
|
962
|
+
await result;
|
|
963
|
+
} catch {
|
|
964
|
+
if (callbackResult !== void 0) return callbackResult;
|
|
965
|
+
}
|
|
966
|
+
return action(void 0);
|
|
967
|
+
}
|
|
968
|
+
function safeSpan(span, action) {
|
|
969
|
+
if (span === void 0) return;
|
|
970
|
+
try {
|
|
971
|
+
action(span);
|
|
972
|
+
} catch {
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function safeLog(logger, level, message, fields) {
|
|
976
|
+
if (logger === void 0) return;
|
|
977
|
+
try {
|
|
978
|
+
const result = logger[level]?.(message, fields);
|
|
979
|
+
if (result !== void 0 && typeof result.then === "function") {
|
|
980
|
+
void Promise.resolve(result).catch(() => void 0);
|
|
981
|
+
}
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function safeHistogram(name, value, attributes, options) {
|
|
986
|
+
try {
|
|
987
|
+
const meter = metrics.getMeter(AnisPartnersTelemetry.scope, packageVersion);
|
|
988
|
+
const instruments = instrumentsFor(histograms, meter);
|
|
989
|
+
let instrument = instruments.get(name);
|
|
990
|
+
if (instrument === void 0) {
|
|
991
|
+
instrument = meter.createHistogram(name, { ...options, description: `${name} for signed Partner API calls` });
|
|
992
|
+
instruments.set(name, instrument);
|
|
993
|
+
}
|
|
994
|
+
instrument.record(value, attributes);
|
|
995
|
+
} catch {
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function safeCounter(name, attributes) {
|
|
999
|
+
try {
|
|
1000
|
+
const meter = metrics.getMeter(AnisPartnersTelemetry.scope, packageVersion);
|
|
1001
|
+
const instruments = instrumentsFor(counters, meter);
|
|
1002
|
+
let instrument = instruments.get(name);
|
|
1003
|
+
if (instrument === void 0) {
|
|
1004
|
+
instrument = meter.createCounter(name, { description: `${name} for signed Partner API calls` });
|
|
1005
|
+
instruments.set(name, instrument);
|
|
1006
|
+
}
|
|
1007
|
+
instrument.add(1, attributes);
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function instrumentsFor(cache, meter) {
|
|
1012
|
+
let instruments = cache.get(meter);
|
|
1013
|
+
if (instruments === void 0) {
|
|
1014
|
+
instruments = /* @__PURE__ */ new Map();
|
|
1015
|
+
cache.set(meter, instruments);
|
|
1016
|
+
}
|
|
1017
|
+
return instruments;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/verification/partner-response-verifier.ts
|
|
1021
|
+
var PartnerResponseVerifier = class {
|
|
1022
|
+
/** Creates a verifier with key rotation and time supplied separately so neither weakens the wire checks. */
|
|
1023
|
+
constructor(keys, clock = systemClock, logger) {
|
|
1024
|
+
this.keys = keys;
|
|
1025
|
+
this.clock = clock;
|
|
1026
|
+
this.logger = logger;
|
|
1027
|
+
}
|
|
1028
|
+
keys;
|
|
1029
|
+
clock;
|
|
1030
|
+
logger;
|
|
1031
|
+
/** Resolves only for a verifiable response; every refusal throws so untrusted content cannot reach a caller. */
|
|
1032
|
+
async verify(response, options) {
|
|
1033
|
+
const signatureInput = header(response, "Signature-Input");
|
|
1034
|
+
const signature = header(response, "Signature");
|
|
1035
|
+
if (!signatureInput || !signature) fail("signature_missing", "The response carried no signature headers.");
|
|
1036
|
+
const parsed = parseSignatureInput(signatureInput);
|
|
1037
|
+
if (!parsed) fail("signature_malformed", "Signature-Input could not be parsed.");
|
|
1038
|
+
if (parsed.label !== "sig1" || !signature.startsWith("sig1="))
|
|
1039
|
+
fail("label_unexpected", "The label is frozen at sig1 on both headers.");
|
|
1040
|
+
if (parsed.algorithm !== void 0 && parsed.algorithm !== "ecdsa-p256-sha256")
|
|
1041
|
+
fail("algorithm_not_supported", "There is no alternate algorithm to negotiate.");
|
|
1042
|
+
const digest = header(response, "Content-Digest");
|
|
1043
|
+
if (!digest || !fixedTimeEqual(
|
|
1044
|
+
new TextEncoder().encode(digest),
|
|
1045
|
+
new TextEncoder().encode(`sha-256=:${createHash4("sha256").update(response.body).digest("base64")}:`)
|
|
1046
|
+
))
|
|
1047
|
+
fail("content_digest_mismatch", "The Content-Digest does not describe the body.");
|
|
1048
|
+
const requestId = header(response, "X-Request-Id");
|
|
1049
|
+
if (!requestId) fail("covered_components_mismatch", "The response carried no X-Request-Id.");
|
|
1050
|
+
const components = PartnerResponseSignatureBase.components(
|
|
1051
|
+
response.status,
|
|
1052
|
+
digest,
|
|
1053
|
+
requestId,
|
|
1054
|
+
response.requestSignatureInput,
|
|
1055
|
+
header(response, "Location"),
|
|
1056
|
+
header(response, "Retry-After"),
|
|
1057
|
+
header(response, "Idempotency-Replayed"),
|
|
1058
|
+
header(response, "Cache-Control")
|
|
1059
|
+
);
|
|
1060
|
+
const expected = components.map((part) => `${part.name}${part.requestBound ? ";req" : ""}`);
|
|
1061
|
+
if (expected.length !== parsed.identifiers.length || expected.some((part, i) => part !== parsed.identifiers[i]))
|
|
1062
|
+
fail("covered_components_mismatch", "The advertised components differ from the frozen response profile.");
|
|
1063
|
+
if (parsed.identifiers.some((id) => id.endsWith(";req")) && !response.requestSignatureInput)
|
|
1064
|
+
fail("covered_components_mismatch", "The request binding cannot be rebuilt.");
|
|
1065
|
+
const age = Math.floor(this.clock.now().getTime() / 1e3) - parsed.created;
|
|
1066
|
+
if (!Number.isFinite(age) || Math.abs(age) > PartnerResponseSignatureBase.MAX_AGE_SECONDS)
|
|
1067
|
+
fail("created_out_of_window", `created is ${String(age)}s from this clock.`);
|
|
1068
|
+
let document = await this.keys.get(options);
|
|
1069
|
+
let key = resolve(document, parsed.keyId);
|
|
1070
|
+
if (!key) {
|
|
1071
|
+
safeLog(this.logger, "warn", "Anis response used an unpublished signing key", {
|
|
1072
|
+
keyId: parsed.keyId,
|
|
1073
|
+
eventId: 1006
|
|
1074
|
+
});
|
|
1075
|
+
document = await this.keys.refresh(options);
|
|
1076
|
+
key = resolve(document, parsed.keyId);
|
|
1077
|
+
}
|
|
1078
|
+
if (document.keys.some((candidate) => candidate.d !== void 0 && candidate.d !== ""))
|
|
1079
|
+
fail("key_rejected", "The published key document carries a private member.");
|
|
1080
|
+
if (!key) fail("unknown_key", `keyid ${parsed.keyId} is not published.`);
|
|
1081
|
+
const signatureBytes = byteSequence(signature);
|
|
1082
|
+
if (signatureBytes?.length !== 64) fail("signature_malformed", "P-256 P1363 signatures must be exactly 64 bytes.");
|
|
1083
|
+
const x = decodeBase64Url(key.x);
|
|
1084
|
+
const y = decodeBase64Url(key.y);
|
|
1085
|
+
if (!x || !y || x.length !== 32 || y.length !== 32)
|
|
1086
|
+
fail("key_rejected", "The published key must have two 32-byte P-256 coordinates.");
|
|
1087
|
+
let publicKey;
|
|
1088
|
+
const keyX = key.x;
|
|
1089
|
+
const keyY = key.y;
|
|
1090
|
+
if (!keyX || !keyY) fail("key_rejected", "The published key has missing coordinates.");
|
|
1091
|
+
try {
|
|
1092
|
+
publicKey = await globalThis.crypto.subtle.importKey(
|
|
1093
|
+
"jwk",
|
|
1094
|
+
{ kty: "EC", crv: "P-256", x: keyX, y: keyY, ext: true },
|
|
1095
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1096
|
+
false,
|
|
1097
|
+
["verify"]
|
|
1098
|
+
);
|
|
1099
|
+
} catch {
|
|
1100
|
+
fail("key_rejected", "The published coordinates are not a valid P-256 point.");
|
|
1101
|
+
}
|
|
1102
|
+
const base = PartnerResponseSignatureBase.build(components, parsed.created, parsed.keyId);
|
|
1103
|
+
try {
|
|
1104
|
+
if (!await globalThis.crypto.subtle.verify(
|
|
1105
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
1106
|
+
publicKey,
|
|
1107
|
+
arrayBufferOf(signatureBytes),
|
|
1108
|
+
arrayBufferOf(base)
|
|
1109
|
+
))
|
|
1110
|
+
fail("signature_invalid", "The signature does not verify over the rebuilt base.");
|
|
1111
|
+
} catch (cause) {
|
|
1112
|
+
if (cause instanceof UnverifiableResponseError) throw cause;
|
|
1113
|
+
fail("signature_invalid", "The signature could not be verified over the rebuilt base.");
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
function header(response, name) {
|
|
1118
|
+
for (const key of Object.keys(response.headers))
|
|
1119
|
+
if (key.toLowerCase() === name.toLowerCase()) return response.headers[key];
|
|
1120
|
+
return void 0;
|
|
1121
|
+
}
|
|
1122
|
+
function resolve(document, keyId) {
|
|
1123
|
+
return document.keys.find((key) => key.kid === keyId && key.x && key.y);
|
|
1124
|
+
}
|
|
1125
|
+
function byteSequence(value) {
|
|
1126
|
+
const match = /^sig1=:([A-Za-z0-9+/]+={0,2}):$/.exec(value);
|
|
1127
|
+
if (!match) return void 0;
|
|
1128
|
+
const encoded = match[1];
|
|
1129
|
+
if (encoded === void 0) return void 0;
|
|
1130
|
+
try {
|
|
1131
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
1132
|
+
return bytes.toString("base64") === encoded ? new Uint8Array(bytes) : void 0;
|
|
1133
|
+
} catch {
|
|
1134
|
+
return void 0;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function fail(reason, detail) {
|
|
1138
|
+
throw new UnverifiableResponseError(reason, detail);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/operations/partner-routes.ts
|
|
1142
|
+
var PARTNER_ROUTES = [
|
|
1143
|
+
{ method: "GET", template: "/v1/profile", profile: "SafeRead" },
|
|
1144
|
+
{ method: "GET", template: "/v1/wallets", profile: "SafeRead" },
|
|
1145
|
+
{ method: "GET", template: "/v1/wallets/{walletId}", profile: "SafeRead" },
|
|
1146
|
+
{ method: "GET", template: "/v1/wallets/{walletId}/catalog/categories", profile: "SafeRead" },
|
|
1147
|
+
{
|
|
1148
|
+
method: "GET",
|
|
1149
|
+
template: "/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories",
|
|
1150
|
+
profile: "SafeRead"
|
|
1151
|
+
},
|
|
1152
|
+
{ method: "GET", template: "/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}", profile: "SafeRead" },
|
|
1153
|
+
{
|
|
1154
|
+
method: "GET",
|
|
1155
|
+
template: "/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards",
|
|
1156
|
+
profile: "SafeRead"
|
|
1157
|
+
},
|
|
1158
|
+
{ method: "POST", template: "/v1/wallets/{walletId}/orders", profile: "OrderMutation" },
|
|
1159
|
+
{ method: "GET", template: "/v1/orders/{operationId}", profile: "SafeRead" },
|
|
1160
|
+
{ method: "GET", template: "/v1/wallets/{walletId}/cards", profile: "SafeRead" },
|
|
1161
|
+
{ method: "GET", template: "/v1/wallets/{walletId}/cards/{soldCardId}", profile: "SafeRead" },
|
|
1162
|
+
{ method: "POST", template: "/v1/wallets/{walletId}/cards/{soldCardId}/reveal", profile: "BodylessNonceMutation" },
|
|
1163
|
+
{
|
|
1164
|
+
method: "POST",
|
|
1165
|
+
template: "/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal",
|
|
1166
|
+
profile: "BodylessNonceMutation"
|
|
1167
|
+
},
|
|
1168
|
+
{ method: "POST", template: "/v1/diagnostics/signature", profile: "BodylessNonceMutation" },
|
|
1169
|
+
{ method: "GET", template: "/v1/enrollments/{invitationId}" },
|
|
1170
|
+
{ method: "POST", template: "/v1/enrollments/{invitationId}/keys" },
|
|
1171
|
+
{ method: "POST", template: "/v1/enrollments/{invitationId}/proof" },
|
|
1172
|
+
{ method: "GET", template: "/v1/enrollments/{invitationId}/status" },
|
|
1173
|
+
{ method: "GET", template: "/.well-known/partner-signing-keys.json" }
|
|
1174
|
+
];
|
|
1175
|
+
|
|
1176
|
+
// src/signing/nonce-factory.ts
|
|
1177
|
+
import { randomBytes } from "crypto";
|
|
1178
|
+
var RandomNonceFactory = class {
|
|
1179
|
+
/** Generates a fresh 16-byte nonce so retries do not reuse a replayable request value. */
|
|
1180
|
+
create() {
|
|
1181
|
+
return encodeBase64Url(randomBytes(16));
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
// src/errors/malformed-response-error.ts
|
|
1186
|
+
var MalformedResponseError = class extends AnisPartnersError {
|
|
1187
|
+
model;
|
|
1188
|
+
constructor(model) {
|
|
1189
|
+
super(`The verified Anis response did not match the ${model} contract.`);
|
|
1190
|
+
this.model = model;
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
// src/errors/signing-key-document-unavailable-error.ts
|
|
1195
|
+
var SigningKeyDocumentUnavailableError = class extends AnisPartnersError {
|
|
1196
|
+
constructor() {
|
|
1197
|
+
super("The Anis signing-key document could not be loaded.");
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
// src/operations/partner-transport.ts
|
|
1202
|
+
var PartnerTransport = class {
|
|
1203
|
+
/** Keeps request signing and response verification together so callers cannot return an unverified answer. */
|
|
1204
|
+
constructor(settings) {
|
|
1205
|
+
this.settings = settings;
|
|
1206
|
+
if (settings.signer !== void 0) this.requestSigner = new PartnerRequestSigner(settings.signer);
|
|
1207
|
+
this.verifier = new PartnerResponseVerifier(settings.keySource, void 0, settings.logger);
|
|
1208
|
+
this.nonceFactory = settings.nonceFactory ?? new RandomNonceFactory();
|
|
1209
|
+
}
|
|
1210
|
+
settings;
|
|
1211
|
+
/** Configured bounded telemetry name for this client instance. */
|
|
1212
|
+
get clientName() {
|
|
1213
|
+
return this.settings.name;
|
|
1214
|
+
}
|
|
1215
|
+
requestSigner;
|
|
1216
|
+
verifier;
|
|
1217
|
+
nonceFactory;
|
|
1218
|
+
/** Sends one API request; response data is not parsed until its signature and digest pass. */
|
|
1219
|
+
async send(request) {
|
|
1220
|
+
return withSafeSpan(`anis.partners ${request.route}`, async (span) => {
|
|
1221
|
+
const started = performance.now();
|
|
1222
|
+
const timed = operationSignal(request.signal, this.settings.timeoutMs);
|
|
1223
|
+
const attributes = {
|
|
1224
|
+
"anis.client": this.settings.name,
|
|
1225
|
+
"anis.route": request.route,
|
|
1226
|
+
"http.request.method": request.method
|
|
1227
|
+
};
|
|
1228
|
+
safeSpan(span, (active) => active.setAttributes(attributes));
|
|
1229
|
+
const operationId = request.operationId;
|
|
1230
|
+
if (operationId !== void 0) safeSpan(span, (active) => active.setAttribute("anis.operation_id", operationId));
|
|
1231
|
+
try {
|
|
1232
|
+
const route = PARTNER_ROUTES.find(
|
|
1233
|
+
(entry) => entry.template === request.route && entry.method === request.method
|
|
1234
|
+
);
|
|
1235
|
+
if (route === void 0) {
|
|
1236
|
+
safeSpan(span, (active) => active.setStatus({ code: SpanStatusCode.ERROR, message: "unknown route" }));
|
|
1237
|
+
throw new RequestSigningError(new Error("The route is not in the SDK route table."));
|
|
1238
|
+
}
|
|
1239
|
+
const routeProfile = "profile" in route ? route.profile : void 0;
|
|
1240
|
+
if (request.profile !== void 0 && request.profile !== routeProfile) {
|
|
1241
|
+
safeSpan(
|
|
1242
|
+
span,
|
|
1243
|
+
(active) => active.setStatus({ code: SpanStatusCode.ERROR, message: "route profile mismatch" })
|
|
1244
|
+
);
|
|
1245
|
+
throw new RequestSigningError(new Error("The signing profile must match the published route table."));
|
|
1246
|
+
}
|
|
1247
|
+
const profile = routeProfile;
|
|
1248
|
+
let url;
|
|
1249
|
+
try {
|
|
1250
|
+
url = new URL(request.path, this.settings.authority);
|
|
1251
|
+
} catch (cause) {
|
|
1252
|
+
throw new RequestSigningError(cause);
|
|
1253
|
+
}
|
|
1254
|
+
const body = request.body ?? new Uint8Array();
|
|
1255
|
+
const headers = new Headers();
|
|
1256
|
+
headers.set("Accept-Encoding", "identity");
|
|
1257
|
+
if (this.settings.acceptLanguage) headers.set("Accept-Language", this.settings.acceptLanguage);
|
|
1258
|
+
if (request.enrollmentToken !== void 0)
|
|
1259
|
+
headers.set("Authorization", `Enrollment ${request.enrollmentToken}`);
|
|
1260
|
+
if (body.length > 0) headers.set("Content-Type", "application/json");
|
|
1261
|
+
let signedIdempotencyKey;
|
|
1262
|
+
if (profile !== void 0) {
|
|
1263
|
+
if (this.requestSigner === void 0 || this.settings.signer === void 0)
|
|
1264
|
+
throw new RequestSigningError(new Error("A request signer is required for this route."));
|
|
1265
|
+
const now = /* @__PURE__ */ new Date();
|
|
1266
|
+
const nonceValue = profile === "SafeRead" ? void 0 : this.nonceFactory.create();
|
|
1267
|
+
const digest = profile === "SafeRead" ? void 0 : contentDigestOf(body);
|
|
1268
|
+
const sigStarted = performance.now();
|
|
1269
|
+
let signed;
|
|
1270
|
+
try {
|
|
1271
|
+
signed = await this.requestSigner.sign(
|
|
1272
|
+
profile,
|
|
1273
|
+
{
|
|
1274
|
+
method: request.method,
|
|
1275
|
+
authority: url.host,
|
|
1276
|
+
path: url.pathname,
|
|
1277
|
+
canonicalQuery: url.search.slice(1),
|
|
1278
|
+
anisDate: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
1279
|
+
...digest === void 0 ? {} : { contentDigest: digest },
|
|
1280
|
+
...nonceValue === void 0 ? {} : { nonce: nonceValue },
|
|
1281
|
+
...request.operationId === void 0 ? {} : { idempotencyKey: request.operationId }
|
|
1282
|
+
},
|
|
1283
|
+
Math.floor(now.getTime() / 1e3),
|
|
1284
|
+
Math.floor(now.getTime() / 1e3) + this.settings.signatureLifetimeSeconds
|
|
1285
|
+
);
|
|
1286
|
+
signedIdempotencyKey = signed.idempotencyKey;
|
|
1287
|
+
} catch (cause) {
|
|
1288
|
+
const error = cause instanceof RequestSigningError ? cause : new RequestSigningError(cause);
|
|
1289
|
+
safeLog(
|
|
1290
|
+
this.settings.logger,
|
|
1291
|
+
"warn",
|
|
1292
|
+
`Request signing failed: method=${request.method}, route=${request.route}.`,
|
|
1293
|
+
{
|
|
1294
|
+
eventId: 1007,
|
|
1295
|
+
method: request.method,
|
|
1296
|
+
route: request.route,
|
|
1297
|
+
reason: "signing"
|
|
1298
|
+
}
|
|
1299
|
+
);
|
|
1300
|
+
safeHistogram(
|
|
1301
|
+
"anis.partners.request.duration",
|
|
1302
|
+
performance.now() - started,
|
|
1303
|
+
{
|
|
1304
|
+
...attributes,
|
|
1305
|
+
"error.type": "signing"
|
|
1306
|
+
},
|
|
1307
|
+
{ unit: "ms" }
|
|
1308
|
+
);
|
|
1309
|
+
safeSpan(span, (active) => active.setStatus({ code: SpanStatusCode.ERROR, message: "signing" }));
|
|
1310
|
+
safeSpan(span, (active) => active.setAttribute("error.type", "signing"));
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
1313
|
+
safeHistogram(
|
|
1314
|
+
"anis.partners.signature.duration",
|
|
1315
|
+
performance.now() - sigStarted,
|
|
1316
|
+
{
|
|
1317
|
+
"anis.signature.profile": profile
|
|
1318
|
+
},
|
|
1319
|
+
{ unit: "ms" }
|
|
1320
|
+
);
|
|
1321
|
+
safeLog(
|
|
1322
|
+
this.settings.logger,
|
|
1323
|
+
"debug",
|
|
1324
|
+
`Request signed: method=${request.method}, route=${request.route}, profile=${profile}.`,
|
|
1325
|
+
{
|
|
1326
|
+
eventId: 1e3,
|
|
1327
|
+
method: request.method,
|
|
1328
|
+
route: request.route,
|
|
1329
|
+
profile,
|
|
1330
|
+
keyId: this.settings.signer.keyId
|
|
1331
|
+
}
|
|
1332
|
+
);
|
|
1333
|
+
headers.set("X-Anis-Date", signed.anisDate);
|
|
1334
|
+
if (signed.contentDigest !== void 0) headers.set("Content-Digest", signed.contentDigest);
|
|
1335
|
+
if (signed.nonce !== void 0) headers.set("Nonce", signed.nonce);
|
|
1336
|
+
headers.set("Signature-Input", signed.signatureInput);
|
|
1337
|
+
headers.set("Signature", signed.signature);
|
|
1338
|
+
}
|
|
1339
|
+
const signal = timed.signal;
|
|
1340
|
+
if (signedIdempotencyKey !== void 0) headers.set("Idempotency-Key", signedIdempotencyKey);
|
|
1341
|
+
let response;
|
|
1342
|
+
try {
|
|
1343
|
+
const receivedResponse = await this.settings.fetcher(url, {
|
|
1344
|
+
method: request.method,
|
|
1345
|
+
headers,
|
|
1346
|
+
...body.length === 0 ? {} : { body: Buffer.from(body) },
|
|
1347
|
+
signal,
|
|
1348
|
+
redirect: "manual"
|
|
1349
|
+
});
|
|
1350
|
+
response = receivedResponse;
|
|
1351
|
+
const contentEncoding = receivedResponse.headers.get("content-encoding");
|
|
1352
|
+
if (contentEncoding !== null && contentEncoding.toLowerCase() !== "identity") {
|
|
1353
|
+
await cancelBody(receivedResponse);
|
|
1354
|
+
safeCounter("anis.partners.response.verification.failures", {
|
|
1355
|
+
"anis.verification.failure": "content_digest_mismatch"
|
|
1356
|
+
});
|
|
1357
|
+
safeLog(this.settings.logger, "error", "Response discarded: failure=content_digest_mismatch.", {
|
|
1358
|
+
eventId: 1003,
|
|
1359
|
+
failure: "content_digest_mismatch"
|
|
1360
|
+
});
|
|
1361
|
+
throw new UnverifiableResponseError(
|
|
1362
|
+
"content_digest_mismatch",
|
|
1363
|
+
"The response was content-encoded, so its received bytes cannot match the signed digest."
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
const responseBody = new Uint8Array(await receivedResponse.arrayBuffer());
|
|
1367
|
+
const mergedHeaders = {};
|
|
1368
|
+
receivedResponse.headers.forEach((value, name) => {
|
|
1369
|
+
mergedHeaders[name] = value;
|
|
1370
|
+
});
|
|
1371
|
+
try {
|
|
1372
|
+
const requestSignatureInput = headers.get("Signature-Input");
|
|
1373
|
+
await this.verifier.verify(
|
|
1374
|
+
{
|
|
1375
|
+
status: receivedResponse.status,
|
|
1376
|
+
headers: mergedHeaders,
|
|
1377
|
+
body: responseBody,
|
|
1378
|
+
...requestSignatureInput === null ? {} : { requestSignatureInput }
|
|
1379
|
+
},
|
|
1380
|
+
{ signal }
|
|
1381
|
+
);
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
if (error instanceof UnverifiableResponseError)
|
|
1384
|
+
safeCounter("anis.partners.response.verification.failures", {
|
|
1385
|
+
"anis.verification.failure": error.failure
|
|
1386
|
+
});
|
|
1387
|
+
safeSpan(span, (active) => active.setStatus({ code: SpanStatusCode.ERROR, message: "unverifiable" }));
|
|
1388
|
+
if (error instanceof UnverifiableResponseError)
|
|
1389
|
+
safeLog(this.settings.logger, "error", `Response discarded: failure=${error.failure}.`, {
|
|
1390
|
+
eventId: 1003,
|
|
1391
|
+
failure: error.failure
|
|
1392
|
+
});
|
|
1393
|
+
throw error;
|
|
1394
|
+
}
|
|
1395
|
+
safeSpan(span, (active) => active.setAttribute("http.response.status_code", receivedResponse.status));
|
|
1396
|
+
const requestId = receivedResponse.headers.get("x-request-id");
|
|
1397
|
+
if (requestId !== null) safeSpan(span, (active) => active.setAttribute("anis.request_id", requestId));
|
|
1398
|
+
const elapsedMs = performance.now() - started;
|
|
1399
|
+
safeLog(
|
|
1400
|
+
this.settings.logger,
|
|
1401
|
+
"debug",
|
|
1402
|
+
`Request completed: method=${request.method}, route=${request.route}, status=${String(receivedResponse.status)}, requestId=${requestId ?? "none"}, elapsedMs=${String(elapsedMs)}.`,
|
|
1403
|
+
{
|
|
1404
|
+
eventId: 1001,
|
|
1405
|
+
method: request.method,
|
|
1406
|
+
route: request.route,
|
|
1407
|
+
statusCode: receivedResponse.status,
|
|
1408
|
+
elapsedMs,
|
|
1409
|
+
requestId
|
|
1410
|
+
}
|
|
1411
|
+
);
|
|
1412
|
+
if (!receivedResponse.ok) {
|
|
1413
|
+
const refusal = createAnisApiError(responseBody, receivedResponse.status, receivedResponse.headers);
|
|
1414
|
+
safeSpan(span, (active) => active.setAttribute("anis.error.code", refusal.rawCode ?? refusal.code));
|
|
1415
|
+
safeHistogram(
|
|
1416
|
+
"anis.partners.request.duration",
|
|
1417
|
+
performance.now() - started,
|
|
1418
|
+
{
|
|
1419
|
+
...attributes,
|
|
1420
|
+
"http.response.status_code": receivedResponse.status,
|
|
1421
|
+
"anis.error.code": refusal.rawCode ?? refusal.code
|
|
1422
|
+
},
|
|
1423
|
+
{ unit: "ms" }
|
|
1424
|
+
);
|
|
1425
|
+
safeLog(
|
|
1426
|
+
this.settings.logger,
|
|
1427
|
+
"warn",
|
|
1428
|
+
`Request refused: method=${request.method}, route=${request.route}, status=${String(receivedResponse.status)}, code=${refusal.rawCode ?? refusal.code}, requestId=${refusal.requestId ?? "none"}, retryable=${String(refusal.isRetryable)}, replayed=${String(refusal.isReplayed)}.`,
|
|
1429
|
+
{
|
|
1430
|
+
eventId: 1002,
|
|
1431
|
+
method: request.method,
|
|
1432
|
+
route: request.route,
|
|
1433
|
+
code: refusal.rawCode ?? refusal.code,
|
|
1434
|
+
statusCode: receivedResponse.status,
|
|
1435
|
+
requestId: refusal.requestId,
|
|
1436
|
+
retryable: refusal.isRetryable,
|
|
1437
|
+
replayed: refusal.isReplayed
|
|
1438
|
+
}
|
|
1439
|
+
);
|
|
1440
|
+
throw refusal;
|
|
1441
|
+
}
|
|
1442
|
+
safeHistogram(
|
|
1443
|
+
"anis.partners.request.duration",
|
|
1444
|
+
performance.now() - started,
|
|
1445
|
+
{
|
|
1446
|
+
...attributes,
|
|
1447
|
+
"http.response.status_code": receivedResponse.status
|
|
1448
|
+
},
|
|
1449
|
+
{ unit: "ms" }
|
|
1450
|
+
);
|
|
1451
|
+
return { response: receivedResponse, body: responseBody };
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
if (response !== void 0) await cancelBody(response);
|
|
1454
|
+
if (error instanceof AnisApiError) {
|
|
1455
|
+
safeSpan(span, (active) => active.setAttribute("anis.error.code", error.rawCode ?? error.code));
|
|
1456
|
+
safeSpan(
|
|
1457
|
+
span,
|
|
1458
|
+
(active) => active.setStatus({ code: SpanStatusCode.ERROR, message: error.rawCode ?? error.code })
|
|
1459
|
+
);
|
|
1460
|
+
throw error;
|
|
1461
|
+
}
|
|
1462
|
+
const reason = error instanceof UnverifiableResponseError ? "unverifiable" : error instanceof SigningKeyDocumentUnavailableError ? "connection" : request.signal?.aborted ? "canceled" : signal.aborted ? "timeout" : error instanceof TypeError ? "connection" : "other";
|
|
1463
|
+
safeSpan(span, (active) => active.setStatus({ code: SpanStatusCode.ERROR, message: reason }));
|
|
1464
|
+
safeSpan(span, (active) => active.setAttribute("error.type", reason));
|
|
1465
|
+
safeHistogram(
|
|
1466
|
+
"anis.partners.request.duration",
|
|
1467
|
+
performance.now() - started,
|
|
1468
|
+
{
|
|
1469
|
+
...attributes,
|
|
1470
|
+
"error.type": reason
|
|
1471
|
+
},
|
|
1472
|
+
{ unit: "ms" }
|
|
1473
|
+
);
|
|
1474
|
+
safeLog(
|
|
1475
|
+
this.settings.logger,
|
|
1476
|
+
"warn",
|
|
1477
|
+
`No usable answer: method=${request.method}, route=${request.route}, reason=${reason}.`,
|
|
1478
|
+
{
|
|
1479
|
+
eventId: 1007,
|
|
1480
|
+
method: request.method,
|
|
1481
|
+
route: request.route,
|
|
1482
|
+
elapsedMs: performance.now() - started,
|
|
1483
|
+
reason
|
|
1484
|
+
}
|
|
1485
|
+
);
|
|
1486
|
+
throw error;
|
|
1487
|
+
}
|
|
1488
|
+
} finally {
|
|
1489
|
+
timed.dispose();
|
|
1490
|
+
safeSpan(span, (active) => {
|
|
1491
|
+
active.end();
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
/** Sends and parses a verified non-empty JSON success. */
|
|
1497
|
+
async json(request, parse) {
|
|
1498
|
+
const result = await this.send(request);
|
|
1499
|
+
if (result.body.length === 0)
|
|
1500
|
+
throw new AnisApiError(
|
|
1501
|
+
{ status: result.response.status, code: "internal_error", title: "Empty body" },
|
|
1502
|
+
result.response.status
|
|
1503
|
+
);
|
|
1504
|
+
try {
|
|
1505
|
+
const decoded = JSON.parse(new TextDecoder().decode(result.body));
|
|
1506
|
+
if (decoded === null) throw new MalformedResponseError("JSON object");
|
|
1507
|
+
return parse(decoded);
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
if (error instanceof MalformedResponseError) throw error;
|
|
1510
|
+
if (error instanceof AnisApiError) throw error;
|
|
1511
|
+
throw new MalformedResponseError("response model");
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/** Records the closed order outcome without attaching credential data to logs. */
|
|
1515
|
+
reportOrderOutcome(operationId, outcome, reason) {
|
|
1516
|
+
const message = outcome === "unknown" ? `Order outcome unknown: operationId=${operationId}, reason=${reason ?? "unspecified"}; resume the same id.` : `Order outcome ${outcome}: operationId=${operationId}.`;
|
|
1517
|
+
safeLog(this.settings.logger, outcome === "unknown" ? "warn" : "info", message, {
|
|
1518
|
+
eventId: outcome === "unknown" ? 1008 : 1004,
|
|
1519
|
+
operationId,
|
|
1520
|
+
outcome,
|
|
1521
|
+
...reason === void 0 ? {} : { reason }
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
function operationSignal(caller, timeoutMs) {
|
|
1526
|
+
const controller = new AbortController();
|
|
1527
|
+
const abortFromCaller = () => {
|
|
1528
|
+
controller.abort(caller?.reason);
|
|
1529
|
+
};
|
|
1530
|
+
caller?.addEventListener("abort", abortFromCaller, { once: true });
|
|
1531
|
+
if (caller?.aborted) abortFromCaller();
|
|
1532
|
+
const timer = setTimeout(() => {
|
|
1533
|
+
controller.abort(new DOMException("The operation timed out.", "TimeoutError"));
|
|
1534
|
+
}, timeoutMs);
|
|
1535
|
+
return {
|
|
1536
|
+
signal: controller.signal,
|
|
1537
|
+
dispose() {
|
|
1538
|
+
clearTimeout(timer);
|
|
1539
|
+
caller?.removeEventListener("abort", abortFromCaller);
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
async function cancelBody(response) {
|
|
1544
|
+
try {
|
|
1545
|
+
await response.body?.cancel();
|
|
1546
|
+
} catch {
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/client-options.ts
|
|
1551
|
+
function validateClientOptions(options) {
|
|
1552
|
+
const authority = new URL(options.authority);
|
|
1553
|
+
const hostname = authority.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1554
|
+
const loopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
1555
|
+
if (authority.protocol !== "https:" && !(authority.protocol === "http:" && loopback))
|
|
1556
|
+
throw new TypeError(
|
|
1557
|
+
"Authority must use HTTPS; plain HTTP lets an on-path attacker replace the unsigned key document and read card codes. HTTP is allowed only for loopback testing."
|
|
1558
|
+
);
|
|
1559
|
+
if (authority.pathname !== "/" || authority.search || authority.hash)
|
|
1560
|
+
throw new TypeError("Authority must not include a path, query, or fragment.");
|
|
1561
|
+
const signatureLifetimeSeconds = options.signatureLifetimeSeconds ?? 60;
|
|
1562
|
+
const signingKeyCacheSeconds = options.signingKeyCacheSeconds ?? 600;
|
|
1563
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
1564
|
+
if (!Number.isInteger(signatureLifetimeSeconds) || signatureLifetimeSeconds < 1 || signatureLifetimeSeconds > 60)
|
|
1565
|
+
throw new RangeError(
|
|
1566
|
+
"signatureLifetimeSeconds must be from 1 through 60 so signed order answers remain fresh enough to verify."
|
|
1567
|
+
);
|
|
1568
|
+
if (!Number.isFinite(signingKeyCacheSeconds) || signingKeyCacheSeconds <= 0)
|
|
1569
|
+
throw new RangeError("signingKeyCacheSeconds must be a positive number.");
|
|
1570
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)
|
|
1571
|
+
throw new RangeError("timeoutMs must be an integer from 1 through 2147483647.");
|
|
1572
|
+
const acceptLanguage = options.acceptLanguage;
|
|
1573
|
+
if (acceptLanguage !== void 0 && acceptLanguage !== "ar" && acceptLanguage !== "en")
|
|
1574
|
+
throw new TypeError("acceptLanguage must be either 'ar' or 'en'.");
|
|
1575
|
+
return {
|
|
1576
|
+
authority,
|
|
1577
|
+
signatureLifetimeSeconds,
|
|
1578
|
+
signingKeyCacheSeconds,
|
|
1579
|
+
timeoutMs,
|
|
1580
|
+
name: options.name ?? "default",
|
|
1581
|
+
...acceptLanguage === void 0 ? {} : { acceptLanguage }
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// src/verification/http-signing-key-source.ts
|
|
1586
|
+
import { createHash as createHash5 } from "crypto";
|
|
1587
|
+
|
|
1588
|
+
// src/verification/partner-jwk.ts
|
|
1589
|
+
function parseSigningKeySet(json) {
|
|
1590
|
+
const value = JSON.parse(json);
|
|
1591
|
+
if (typeof value !== "object" || value === null || !("keys" in value) || !Array.isArray(value.keys))
|
|
1592
|
+
throw new TypeError("The signing-key document must contain a keys array.");
|
|
1593
|
+
return { keys: value.keys.map(parsePartnerJwk) };
|
|
1594
|
+
}
|
|
1595
|
+
var jwkStringMembers = ["kty", "crv", "x", "y", "kid", "use", "alg", "d"];
|
|
1596
|
+
function parsePartnerJwk(value) {
|
|
1597
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1598
|
+
throw new TypeError("Every signing key must be a JSON object.");
|
|
1599
|
+
const source = value;
|
|
1600
|
+
const result = {};
|
|
1601
|
+
for (const member of jwkStringMembers) {
|
|
1602
|
+
const entry = source[member];
|
|
1603
|
+
if (member === "d" && entry === null) continue;
|
|
1604
|
+
if (entry === void 0) continue;
|
|
1605
|
+
if (typeof entry !== "string") throw new TypeError(`The signing-key member "${member}" must be a string.`);
|
|
1606
|
+
result[member] = entry;
|
|
1607
|
+
}
|
|
1608
|
+
return result;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/verification/http-signing-key-source.ts
|
|
1612
|
+
var HttpSigningKeySource = class {
|
|
1613
|
+
authority;
|
|
1614
|
+
cacheSeconds;
|
|
1615
|
+
fetcher;
|
|
1616
|
+
cache;
|
|
1617
|
+
logger;
|
|
1618
|
+
cached;
|
|
1619
|
+
inFlight;
|
|
1620
|
+
/** Creates an authority-scoped source; caching avoids a key-document request for every Partner response. */
|
|
1621
|
+
constructor(options) {
|
|
1622
|
+
this.authority = new URL(options.authority);
|
|
1623
|
+
this.cacheSeconds = options.cacheSeconds ?? 600;
|
|
1624
|
+
if (!Number.isFinite(this.cacheSeconds) || this.cacheSeconds < 0)
|
|
1625
|
+
throw new RangeError("Cache seconds must be non-negative.");
|
|
1626
|
+
this.fetcher = options.fetch ?? globalThis.fetch;
|
|
1627
|
+
if (options.documentCache !== void 0) this.cache = options.documentCache;
|
|
1628
|
+
if (options.logger !== void 0) this.logger = options.logger;
|
|
1629
|
+
}
|
|
1630
|
+
/** Returns an unexpired document and shares one first-use fetch among concurrent callers. */
|
|
1631
|
+
async get(options) {
|
|
1632
|
+
throwIfAborted(options?.signal);
|
|
1633
|
+
if (this.cached && this.cached.until > performance.now()) return this.cached.document;
|
|
1634
|
+
if (this.inFlight) return this.waitForFlight(this.inFlight, options?.signal);
|
|
1635
|
+
const reason = this.cached ? "expired" : "first-use";
|
|
1636
|
+
return this.fetchAndCache(reason, options?.signal);
|
|
1637
|
+
}
|
|
1638
|
+
/** Forces a fetch after rotation or an explicit caller refresh, even when the cache is still fresh. */
|
|
1639
|
+
async refresh(options) {
|
|
1640
|
+
throwIfAborted(options?.signal);
|
|
1641
|
+
return this.fetchAndCache("refresh", options?.signal);
|
|
1642
|
+
}
|
|
1643
|
+
fetchAndCache(reason, signal) {
|
|
1644
|
+
if (this.inFlight) return this.waitForFlight(this.inFlight, signal);
|
|
1645
|
+
const controller = new AbortController();
|
|
1646
|
+
const promise = this.fetchDocument(reason, controller.signal).finally(() => {
|
|
1647
|
+
if (this.inFlight?.controller === controller) this.inFlight = void 0;
|
|
1648
|
+
});
|
|
1649
|
+
const flight = { controller, waiters: 0, promise };
|
|
1650
|
+
this.inFlight = flight;
|
|
1651
|
+
return this.waitForFlight(flight, signal);
|
|
1652
|
+
}
|
|
1653
|
+
waitForFlight(flight, signal) {
|
|
1654
|
+
flight.waiters += 1;
|
|
1655
|
+
return new Promise((resolve2, reject) => {
|
|
1656
|
+
let finished = false;
|
|
1657
|
+
const release = () => {
|
|
1658
|
+
if (finished) return;
|
|
1659
|
+
finished = true;
|
|
1660
|
+
signal?.removeEventListener("abort", abort);
|
|
1661
|
+
flight.waiters -= 1;
|
|
1662
|
+
};
|
|
1663
|
+
const abort = () => {
|
|
1664
|
+
release();
|
|
1665
|
+
if (flight.waiters === 0) {
|
|
1666
|
+
if (this.inFlight?.controller === flight.controller) this.inFlight = void 0;
|
|
1667
|
+
flight.controller.abort(signal?.reason);
|
|
1668
|
+
}
|
|
1669
|
+
reject(abortReason(signal));
|
|
1670
|
+
};
|
|
1671
|
+
if (signal?.aborted) {
|
|
1672
|
+
abort();
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1676
|
+
flight.promise.then(
|
|
1677
|
+
(document) => {
|
|
1678
|
+
release();
|
|
1679
|
+
resolve2(document);
|
|
1680
|
+
},
|
|
1681
|
+
(error) => {
|
|
1682
|
+
release();
|
|
1683
|
+
reject(error instanceof Error ? error : new Error("The signing-key request failed.", { cause: error }));
|
|
1684
|
+
}
|
|
1685
|
+
);
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
async fetchDocument(reason, signal) {
|
|
1689
|
+
const normalizedAuthority = this.authority.toString().toLowerCase().replace(/\/+$/, "");
|
|
1690
|
+
const cacheKey = `anis_partners_keys_${createHash5("sha256").update(normalizedAuthority).digest("hex").slice(0, 32)}`;
|
|
1691
|
+
if (reason === "first-use" && this.cache) {
|
|
1692
|
+
const cached = await settleCache(() => this.cache?.get(cacheKey) ?? Promise.resolve(void 0), signal);
|
|
1693
|
+
throwIfAborted(signal);
|
|
1694
|
+
const stored = cached.ok ? cached.value : void 0;
|
|
1695
|
+
if (stored !== void 0) {
|
|
1696
|
+
try {
|
|
1697
|
+
const envelope = JSON.parse(stored);
|
|
1698
|
+
if (isCacheEnvelope(envelope)) {
|
|
1699
|
+
const nowSeconds = Date.now() / 1e3;
|
|
1700
|
+
const remainingMs = (envelope.fetchedAt + this.cacheSeconds - nowSeconds) * 1e3;
|
|
1701
|
+
if (remainingMs > 0 && envelope.fetchedAt <= nowSeconds + 60) {
|
|
1702
|
+
const document2 = parseSigningKeySet(envelope.document);
|
|
1703
|
+
this.cached = { document: document2, until: performance.now() + remainingMs };
|
|
1704
|
+
safeLog(this.logger, "debug", "signing keys read from the shared cache", {});
|
|
1705
|
+
return document2;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
} catch {
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
const url = new URL("/.well-known/partner-signing-keys.json", this.authority);
|
|
1713
|
+
let response;
|
|
1714
|
+
try {
|
|
1715
|
+
response = await this.fetcher(url, {
|
|
1716
|
+
headers: { "Accept-Encoding": "identity" },
|
|
1717
|
+
redirect: "manual",
|
|
1718
|
+
signal
|
|
1719
|
+
});
|
|
1720
|
+
} catch {
|
|
1721
|
+
if (signal.aborted) throw abortReason(signal);
|
|
1722
|
+
throw new SigningKeyDocumentUnavailableError();
|
|
1723
|
+
}
|
|
1724
|
+
if (!response.ok) {
|
|
1725
|
+
await cancelBody2(response);
|
|
1726
|
+
throw new SigningKeyDocumentUnavailableError();
|
|
1727
|
+
}
|
|
1728
|
+
const contentEncoding = response.headers.get("content-encoding");
|
|
1729
|
+
if (contentEncoding !== null && contentEncoding.toLowerCase() !== "identity") {
|
|
1730
|
+
await cancelBody2(response);
|
|
1731
|
+
throw new SigningKeyDocumentUnavailableError();
|
|
1732
|
+
}
|
|
1733
|
+
let text;
|
|
1734
|
+
let document;
|
|
1735
|
+
try {
|
|
1736
|
+
text = await response.text();
|
|
1737
|
+
document = parseSigningKeySet(text);
|
|
1738
|
+
} catch {
|
|
1739
|
+
if (signal.aborted) throw abortReason(signal);
|
|
1740
|
+
throw new SigningKeyDocumentUnavailableError();
|
|
1741
|
+
}
|
|
1742
|
+
const fetchedAt = Math.floor(Date.now() / 1e3);
|
|
1743
|
+
this.cached = { document, until: performance.now() + this.cacheSeconds * 1e3 };
|
|
1744
|
+
if (this.cache !== void 0) {
|
|
1745
|
+
const value = JSON.stringify({ fetchedAt, document: text });
|
|
1746
|
+
const written = await settleCache(
|
|
1747
|
+
() => this.cache?.set(cacheKey, value, this.cacheSeconds) ?? Promise.resolve(),
|
|
1748
|
+
signal
|
|
1749
|
+
);
|
|
1750
|
+
if (!written.ok) safeLog(this.logger, "warn", "shared signing-key cache write failed", {});
|
|
1751
|
+
}
|
|
1752
|
+
safeLog(this.logger, "info", `Signing keys fetched: reason=${reason}, keyCount=${String(document.keys.length)}.`, {
|
|
1753
|
+
reason,
|
|
1754
|
+
keyCount: document.keys.length
|
|
1755
|
+
});
|
|
1756
|
+
safeCounter("anis.partners.signing_keys.fetches", { "anis.fetch.reason": reason });
|
|
1757
|
+
return document;
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
function isCacheEnvelope(value) {
|
|
1761
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1762
|
+
const record = value;
|
|
1763
|
+
return typeof record.fetchedAt === "number" && Number.isSafeInteger(record.fetchedAt) && typeof record.document === "string";
|
|
1764
|
+
}
|
|
1765
|
+
function settleCache(operation, signal) {
|
|
1766
|
+
return new Promise((resolve2) => {
|
|
1767
|
+
let settled = false;
|
|
1768
|
+
const finish = (result) => {
|
|
1769
|
+
if (settled) return;
|
|
1770
|
+
settled = true;
|
|
1771
|
+
clearTimeout(timer);
|
|
1772
|
+
signal.removeEventListener("abort", onAbort);
|
|
1773
|
+
resolve2(result);
|
|
1774
|
+
};
|
|
1775
|
+
const onAbort = () => {
|
|
1776
|
+
finish({ ok: false });
|
|
1777
|
+
};
|
|
1778
|
+
const timer = setTimeout(() => {
|
|
1779
|
+
finish({ ok: false });
|
|
1780
|
+
}, 2e3);
|
|
1781
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1782
|
+
Promise.resolve().then(operation).then(
|
|
1783
|
+
(value) => {
|
|
1784
|
+
finish({ ok: true, value });
|
|
1785
|
+
},
|
|
1786
|
+
() => {
|
|
1787
|
+
finish({ ok: false });
|
|
1788
|
+
}
|
|
1789
|
+
);
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
function throwIfAborted(signal) {
|
|
1793
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1794
|
+
}
|
|
1795
|
+
async function cancelBody2(response) {
|
|
1796
|
+
try {
|
|
1797
|
+
await response.body?.cancel();
|
|
1798
|
+
} catch {
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function abortReason(signal) {
|
|
1802
|
+
return signal?.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// src/enrollment/enrollment-client.ts
|
|
1806
|
+
var EnrollmentKeyMismatchError = class extends AnisPartnersError {
|
|
1807
|
+
/** Thumbprint calculated from the public key submitted by this caller. */
|
|
1808
|
+
localThumbprint;
|
|
1809
|
+
/** Thumbprint returned by Anis, when present. */
|
|
1810
|
+
serverThumbprint;
|
|
1811
|
+
/** Stops a proof from being made with a challenge bound to another key. */
|
|
1812
|
+
constructor(localThumbprint, serverThumbprint) {
|
|
1813
|
+
super(
|
|
1814
|
+
"The key Anis holds differs from the one submitted. Do not prove possession; ask Anis to restart enrollment."
|
|
1815
|
+
);
|
|
1816
|
+
this.localThumbprint = localThumbprint;
|
|
1817
|
+
if (serverThumbprint !== void 0) this.serverThumbprint = serverThumbprint;
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
var AnisEnrollmentClient = class _AnisEnrollmentClient {
|
|
1821
|
+
transport;
|
|
1822
|
+
invitation;
|
|
1823
|
+
#token;
|
|
1824
|
+
constructor(transport, invitationId, token) {
|
|
1825
|
+
this.transport = transport;
|
|
1826
|
+
this.invitation = canonicalUuid(invitationId, "invitationId");
|
|
1827
|
+
this.#token = token;
|
|
1828
|
+
}
|
|
1829
|
+
/** Creates an enrollment client over the same mandatory response-verification pipeline. */
|
|
1830
|
+
static create(input) {
|
|
1831
|
+
if (input.enrollmentToken.trim().length === 0 || hasControlCharacters(input.enrollmentToken))
|
|
1832
|
+
throw new TypeError("The enrollment token must be non-blank and contain no control characters.");
|
|
1833
|
+
const options = validateClientOptions({
|
|
1834
|
+
authority: input.authority,
|
|
1835
|
+
...input.signingKeyCacheSeconds === void 0 ? {} : { signingKeyCacheSeconds: input.signingKeyCacheSeconds },
|
|
1836
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs }
|
|
1837
|
+
});
|
|
1838
|
+
const fetcher = input.fetch ?? globalThis.fetch;
|
|
1839
|
+
const keySource = new HttpSigningKeySource({
|
|
1840
|
+
authority: options.authority,
|
|
1841
|
+
cacheSeconds: options.signingKeyCacheSeconds,
|
|
1842
|
+
fetch: fetcher,
|
|
1843
|
+
...input.keyCache === void 0 ? {} : { documentCache: input.keyCache },
|
|
1844
|
+
...input.logger === void 0 ? {} : {
|
|
1845
|
+
logger: {
|
|
1846
|
+
debug(message, fields) {
|
|
1847
|
+
safeLog(input.logger, "debug", message, fields);
|
|
1848
|
+
},
|
|
1849
|
+
info(message, fields) {
|
|
1850
|
+
safeLog(input.logger, "info", message, { ...fields, eventId: 1005 });
|
|
1851
|
+
},
|
|
1852
|
+
warn(message, fields) {
|
|
1853
|
+
safeLog(input.logger, "warn", message, fields);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
const transportOptions = {
|
|
1859
|
+
...options,
|
|
1860
|
+
fetcher,
|
|
1861
|
+
keySource,
|
|
1862
|
+
...input.logger === void 0 ? {} : { logger: input.logger }
|
|
1863
|
+
};
|
|
1864
|
+
return new _AnisEnrollmentClient(new PartnerTransport(transportOptions), input.invitationId, input.enrollmentToken);
|
|
1865
|
+
}
|
|
1866
|
+
/** Reads invitation state. */
|
|
1867
|
+
async get(options) {
|
|
1868
|
+
return this.transport.json(
|
|
1869
|
+
{
|
|
1870
|
+
method: "GET",
|
|
1871
|
+
route: "/v1/enrollments/{invitationId}",
|
|
1872
|
+
path: `/v1/enrollments/${this.invitation}`,
|
|
1873
|
+
enrollmentToken: this.#token,
|
|
1874
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1875
|
+
},
|
|
1876
|
+
parseEnrollmentState
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
/** Reads approval and key-expiry state. */
|
|
1880
|
+
async getStatus(options) {
|
|
1881
|
+
return this.transport.json(
|
|
1882
|
+
{
|
|
1883
|
+
method: "GET",
|
|
1884
|
+
route: "/v1/enrollments/{invitationId}/status",
|
|
1885
|
+
path: `/v1/enrollments/${this.invitation}/status`,
|
|
1886
|
+
enrollmentToken: this.#token,
|
|
1887
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1888
|
+
},
|
|
1889
|
+
parseEnrollmentStatus
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
/** Submits a public key, then checks the verified answer against its local RFC 7638 thumbprint. */
|
|
1893
|
+
async submitKey(request, options) {
|
|
1894
|
+
rejectPrivateMembers(request.publicJwk);
|
|
1895
|
+
const local = keyThumbprint(request.publicJwk);
|
|
1896
|
+
const bytes = new TextEncoder().encode(
|
|
1897
|
+
JSON.stringify({
|
|
1898
|
+
publicJwk: {
|
|
1899
|
+
kty: request.publicJwk.kty,
|
|
1900
|
+
crv: request.publicJwk.crv,
|
|
1901
|
+
x: request.publicJwk.x,
|
|
1902
|
+
y: request.publicJwk.y
|
|
1903
|
+
},
|
|
1904
|
+
notBefore: request.notBefore,
|
|
1905
|
+
expiresAt: request.expiresAt
|
|
1906
|
+
})
|
|
1907
|
+
);
|
|
1908
|
+
const answer = await this.transport.json(
|
|
1909
|
+
{
|
|
1910
|
+
method: "POST",
|
|
1911
|
+
route: "/v1/enrollments/{invitationId}/keys",
|
|
1912
|
+
path: `/v1/enrollments/${this.invitation}/keys`,
|
|
1913
|
+
body: bytes,
|
|
1914
|
+
enrollmentToken: this.#token,
|
|
1915
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1916
|
+
},
|
|
1917
|
+
parseEnrollmentKeyResult
|
|
1918
|
+
);
|
|
1919
|
+
if (answer.thumbprint === void 0 || !fixedTimeEqual(new TextEncoder().encode(local), new TextEncoder().encode(answer.thumbprint)))
|
|
1920
|
+
throw new EnrollmentKeyMismatchError(local, answer.thumbprint);
|
|
1921
|
+
return { ...answer, safetyCode: safetyCodeFromThumbprint(local) };
|
|
1922
|
+
}
|
|
1923
|
+
/** Builds the possession message, obtains its P-256 signature, and submits the proof. */
|
|
1924
|
+
async prove(submitted, signer, options) {
|
|
1925
|
+
const message = proofMessage(
|
|
1926
|
+
submitted.keyId,
|
|
1927
|
+
submitted.challengeGeneration ?? missing("challengeGeneration"),
|
|
1928
|
+
submitted.challenge ?? missing("challenge"),
|
|
1929
|
+
submitted.thumbprint ?? missing("thumbprint")
|
|
1930
|
+
);
|
|
1931
|
+
const signature = await proofSignature(message, signer);
|
|
1932
|
+
return this.submitProof(
|
|
1933
|
+
{
|
|
1934
|
+
keyId: submitted.keyId,
|
|
1935
|
+
challengeGeneration: submitted.challengeGeneration ?? missing("challengeGeneration"),
|
|
1936
|
+
signature
|
|
1937
|
+
},
|
|
1938
|
+
options
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
/** Submits a previously prepared proof for the current challenge generation. */
|
|
1942
|
+
async submitProof(request, options) {
|
|
1943
|
+
return this.transport.json(
|
|
1944
|
+
{
|
|
1945
|
+
method: "POST",
|
|
1946
|
+
route: "/v1/enrollments/{invitationId}/proof",
|
|
1947
|
+
path: `/v1/enrollments/${this.invitation}/proof`,
|
|
1948
|
+
body: new TextEncoder().encode(
|
|
1949
|
+
JSON.stringify({
|
|
1950
|
+
keyId: request.keyId,
|
|
1951
|
+
challengeGeneration: request.challengeGeneration,
|
|
1952
|
+
signature: request.signature
|
|
1953
|
+
})
|
|
1954
|
+
),
|
|
1955
|
+
enrollmentToken: this.#token,
|
|
1956
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1957
|
+
},
|
|
1958
|
+
parseEnrollmentStatus
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
var privateJwkMembers = ["d", "p", "q", "dp", "dq", "qi", "oth", "k"];
|
|
1963
|
+
function rejectPrivateMembers(jwk) {
|
|
1964
|
+
for (const member of privateJwkMembers) {
|
|
1965
|
+
if (Object.hasOwn(jwk, member)) throw new TypeError("Enrollment accepts a public JWK only.");
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function missing(member) {
|
|
1969
|
+
throw new TypeError(`Enrollment key answer has no ${member}.`);
|
|
1970
|
+
}
|
|
1971
|
+
function hasControlCharacters(value) {
|
|
1972
|
+
for (const character of value) {
|
|
1973
|
+
const code = character.charCodeAt(0);
|
|
1974
|
+
if (code <= 31 || code === 127) return true;
|
|
1975
|
+
}
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// src/models/money.ts
|
|
1980
|
+
import { inspect as inspect3 } from "util";
|
|
1981
|
+
var moneyBrand = /* @__PURE__ */ Symbol.for("@anis-ly/partners.Money");
|
|
1982
|
+
var Money = class _Money {
|
|
1983
|
+
constructor(thousandths, currency, asOf) {
|
|
1984
|
+
this.currency = currency;
|
|
1985
|
+
this.asOf = asOf;
|
|
1986
|
+
this.#thousandths = thousandths;
|
|
1987
|
+
Object.defineProperty(this, moneyBrand, { value: true });
|
|
1988
|
+
}
|
|
1989
|
+
currency;
|
|
1990
|
+
asOf;
|
|
1991
|
+
#thousandths;
|
|
1992
|
+
static [Symbol.hasInstance](value) {
|
|
1993
|
+
return typeof value === "object" && value !== null && value[moneyBrand] === true;
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Builds an amount from its decimal wire spelling.
|
|
1997
|
+
*
|
|
1998
|
+
* @remarks A number has already passed through floating-point arithmetic, so accepting one could make an order
|
|
1999
|
+
* total differ from the exact unit price the API checks.
|
|
2000
|
+
*/
|
|
2001
|
+
static of(amount, currency, asOf) {
|
|
2002
|
+
if (typeof amount !== "string") {
|
|
2003
|
+
throw new TypeError("Money amounts must be decimal strings.");
|
|
2004
|
+
}
|
|
2005
|
+
if (typeof currency !== "string" || currency.length === 0) {
|
|
2006
|
+
throw new TypeError("A currency code is required.");
|
|
2007
|
+
}
|
|
2008
|
+
const match = /^(-?)(0|[1-9]\d*)(?:\.(\d+))?$/.exec(amount);
|
|
2009
|
+
if (match === null) {
|
|
2010
|
+
throw new TypeError("A money amount must be a decimal string.");
|
|
2011
|
+
}
|
|
2012
|
+
const sign = match[1] === "-" ? -1n : 1n;
|
|
2013
|
+
const whole = BigInt(match[2] ?? "0");
|
|
2014
|
+
const fraction = match[3] ?? "";
|
|
2015
|
+
if (fraction.length > 3) {
|
|
2016
|
+
throw new TypeError("Anis amounts have at most three decimal places");
|
|
2017
|
+
}
|
|
2018
|
+
const thousandths = whole * 1000n + BigInt((fraction + "000").slice(0, 3));
|
|
2019
|
+
return new _Money(sign * thousandths, currency, asOf);
|
|
2020
|
+
}
|
|
2021
|
+
/** The amount rendered with exactly three decimal places so Anis receives the contract's decimal-string form. */
|
|
2022
|
+
get amount() {
|
|
2023
|
+
const negative = this.#thousandths < 0n;
|
|
2024
|
+
const absolute = negative ? -this.#thousandths : this.#thousandths;
|
|
2025
|
+
const whole = absolute / 1000n;
|
|
2026
|
+
const fraction = String(absolute % 1000n).padStart(3, "0");
|
|
2027
|
+
return `${negative ? "-" : ""}${String(whole)}.${fraction}`;
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Multiplies by a whole quantity without losing fractional currency units.
|
|
2031
|
+
*
|
|
2032
|
+
* @remarks The source instant describes the unit quote, not the computed order total, so it is cleared on the result.
|
|
2033
|
+
*/
|
|
2034
|
+
multiply(quantity) {
|
|
2035
|
+
if (!Number.isSafeInteger(quantity)) {
|
|
2036
|
+
throw new TypeError("Money can only be multiplied by a safe whole number.");
|
|
2037
|
+
}
|
|
2038
|
+
return new _Money(this.#thousandths * BigInt(quantity), this.currency);
|
|
2039
|
+
}
|
|
2040
|
+
/** Uses the familiar decimal and currency spelling when a balance is interpolated into application text. */
|
|
2041
|
+
toString() {
|
|
2042
|
+
return `${this.amount} ${this.currency}`;
|
|
2043
|
+
}
|
|
2044
|
+
[inspect3.custom]() {
|
|
2045
|
+
return this.toString();
|
|
2046
|
+
}
|
|
2047
|
+
/** Converts to the contract object while retaining its decimal-string amount. */
|
|
2048
|
+
toJSON() {
|
|
2049
|
+
return {
|
|
2050
|
+
amount: this.amount,
|
|
2051
|
+
currency: this.currency,
|
|
2052
|
+
...this.asOf === void 0 ? {} : { asOf: timestampToWire(this.asOf) }
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
function parseMoney(json) {
|
|
2057
|
+
const object = asObject(json, "Money");
|
|
2058
|
+
const amount = requiredString(object, "amount", "Money");
|
|
2059
|
+
const currency = requiredString(object, "currency", "Money");
|
|
2060
|
+
const asOf = optionalTimestamp(object, "asOf", "Money");
|
|
2061
|
+
return Money.of(amount, currency, asOf);
|
|
2062
|
+
}
|
|
2063
|
+
function timestampToWire(value) {
|
|
2064
|
+
if (Number.isNaN(value.getTime())) {
|
|
2065
|
+
throw new TypeError("Money.asOf must be a valid date.");
|
|
2066
|
+
}
|
|
2067
|
+
return value.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/models/catalogue.ts
|
|
2071
|
+
function parseLocalizedText(json) {
|
|
2072
|
+
const object = asObject(json, "LocalizedText");
|
|
2073
|
+
const ar = optionalString(object, "ar", "LocalizedText");
|
|
2074
|
+
const en = optionalString(object, "en", "LocalizedText");
|
|
2075
|
+
return { ...ar === void 0 ? {} : { ar }, ...en === void 0 ? {} : { en } };
|
|
2076
|
+
}
|
|
2077
|
+
function parseCatalogueCategoryType(value) {
|
|
2078
|
+
if (typeof value !== "string") {
|
|
2079
|
+
return "unknown";
|
|
2080
|
+
}
|
|
2081
|
+
switch (value.toLowerCase()) {
|
|
2082
|
+
case "local":
|
|
2083
|
+
return "local";
|
|
2084
|
+
case "international":
|
|
2085
|
+
return "international";
|
|
2086
|
+
default:
|
|
2087
|
+
return "unknown";
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
function parseCatalogueCategory(json) {
|
|
2091
|
+
const object = asObject(json, "CatalogueCategory");
|
|
2092
|
+
const name = parseOptionalLocalized(object, "name", "CatalogueCategory");
|
|
2093
|
+
const description = parseOptionalLocalized(object, "description", "CatalogueCategory");
|
|
2094
|
+
const logo = optionalString(object, "logo", "CatalogueCategory");
|
|
2095
|
+
return {
|
|
2096
|
+
id: requiredUuid(object, "id", "CatalogueCategory"),
|
|
2097
|
+
type: parseCatalogueCategoryType(object.type),
|
|
2098
|
+
inStock: optionalBoolean(object, "inStock", "CatalogueCategory") ?? false,
|
|
2099
|
+
displayOrder: optionalInteger(object, "displayOrder", "CatalogueCategory") ?? 0,
|
|
2100
|
+
...name === void 0 ? {} : { name },
|
|
2101
|
+
...description === void 0 ? {} : { description },
|
|
2102
|
+
...logo === void 0 ? {} : { logo }
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
function parseCatalogueSubcategory(json) {
|
|
2106
|
+
const object = asObject(json, "CatalogueSubcategory");
|
|
2107
|
+
const name = parseOptionalLocalized(object, "name", "CatalogueSubcategory");
|
|
2108
|
+
const description = parseOptionalLocalized(object, "description", "CatalogueSubcategory");
|
|
2109
|
+
const logo = optionalString(object, "logo", "CatalogueSubcategory");
|
|
2110
|
+
const disclaimer = parseOptionalLocalized(object, "disclaimer", "CatalogueSubcategory");
|
|
2111
|
+
return {
|
|
2112
|
+
id: requiredUuid(object, "id", "CatalogueSubcategory"),
|
|
2113
|
+
categoryId: optionalUuid(object, "categoryId", "CatalogueSubcategory") ?? "00000000-0000-0000-0000-000000000000",
|
|
2114
|
+
isBestSelling: optionalBoolean(object, "isBestSelling", "CatalogueSubcategory") ?? false,
|
|
2115
|
+
displayOrder: optionalInteger(object, "displayOrder", "CatalogueSubcategory") ?? 0,
|
|
2116
|
+
available: optionalBoolean(object, "available", "CatalogueSubcategory") ?? false,
|
|
2117
|
+
...name === void 0 ? {} : { name },
|
|
2118
|
+
...description === void 0 ? {} : { description },
|
|
2119
|
+
...logo === void 0 ? {} : { logo },
|
|
2120
|
+
...disclaimer === void 0 ? {} : { disclaimer }
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
function parseCatalogueCard(json) {
|
|
2124
|
+
const object = asObject(json, "CatalogueCard");
|
|
2125
|
+
const name = parseOptionalLocalized(object, "name", "CatalogueCard");
|
|
2126
|
+
const faceValue = optionalString(object, "faceValue", "CatalogueCard");
|
|
2127
|
+
const unitPrice = parseOptionalMoney(object, "unitPrice");
|
|
2128
|
+
const businessPrice = parseOptionalMoney(object, "businessPrice");
|
|
2129
|
+
const personalPrice = parseOptionalMoney(object, "personalPrice");
|
|
2130
|
+
const specialOfferPrice = parseOptionalMoney(object, "specialOfferPrice");
|
|
2131
|
+
const minimumQuantity = optionalInteger(object, "minimumQuantity", "CatalogueCard");
|
|
2132
|
+
const maximumQuantity = optionalInteger(object, "maximumQuantity", "CatalogueCard");
|
|
2133
|
+
return {
|
|
2134
|
+
id: requiredUuid(object, "id", "CatalogueCard"),
|
|
2135
|
+
subcategoryId: optionalUuid(object, "subcategoryId", "CatalogueCard") ?? "00000000-0000-0000-0000-000000000000",
|
|
2136
|
+
hasSpecialOffer: optionalBoolean(object, "hasSpecialOffer", "CatalogueCard") ?? false,
|
|
2137
|
+
available: optionalBoolean(object, "available", "CatalogueCard") ?? false,
|
|
2138
|
+
...name === void 0 ? {} : { name },
|
|
2139
|
+
...faceValue === void 0 ? {} : { faceValue },
|
|
2140
|
+
...unitPrice === void 0 ? {} : { unitPrice },
|
|
2141
|
+
...businessPrice === void 0 ? {} : { businessPrice },
|
|
2142
|
+
...personalPrice === void 0 ? {} : { personalPrice },
|
|
2143
|
+
...specialOfferPrice === void 0 ? {} : { specialOfferPrice },
|
|
2144
|
+
...minimumQuantity === void 0 ? {} : { minimumQuantity },
|
|
2145
|
+
...maximumQuantity === void 0 ? {} : { maximumQuantity }
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function parsePage(json, parseItem) {
|
|
2149
|
+
const object = asObject(json, "Page");
|
|
2150
|
+
const items = object.items ?? [];
|
|
2151
|
+
if (!Array.isArray(items)) {
|
|
2152
|
+
throw new TypeError("Page.items must be an array.");
|
|
2153
|
+
}
|
|
2154
|
+
const nextCursor = optionalString(object, "nextCursor", "Page");
|
|
2155
|
+
return {
|
|
2156
|
+
items: items.map(parseItem),
|
|
2157
|
+
...nextCursor === void 0 ? {} : { nextCursor }
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function parseOptionalLocalized(object, field, label) {
|
|
2161
|
+
const value = object[field];
|
|
2162
|
+
if (value === void 0 || value === null) {
|
|
2163
|
+
return void 0;
|
|
2164
|
+
}
|
|
2165
|
+
try {
|
|
2166
|
+
return parseLocalizedText(value);
|
|
2167
|
+
} catch (cause) {
|
|
2168
|
+
throw new TypeError(`${label}.${field} must contain Arabic or English text.`, { cause });
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
function parseOptionalMoney(object, field) {
|
|
2172
|
+
const value = object[field];
|
|
2173
|
+
return value === void 0 || value === null ? void 0 : parseMoney(value);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// src/models/cards.ts
|
|
2177
|
+
import { inspect as inspect4 } from "util";
|
|
2178
|
+
function parseMaskedCard(json) {
|
|
2179
|
+
const object = asObject(json, "MaskedCard");
|
|
2180
|
+
const orderOperationId = optionalUuid(object, "orderOperationId", "MaskedCard");
|
|
2181
|
+
const invoiceId = optionalUuid(object, "invoiceId", "MaskedCard");
|
|
2182
|
+
const card = parseOptionalProduct(object, "card");
|
|
2183
|
+
const serialNumberMasked = optionalString(object, "serialNumberMasked", "MaskedCard");
|
|
2184
|
+
const purchasedAt = optionalTimestamp(object, "purchasedAt", "MaskedCard");
|
|
2185
|
+
const unitPrice = object.unitPrice === void 0 || object.unitPrice === null ? void 0 : parseMoney(object.unitPrice);
|
|
2186
|
+
const expiryDate = optionalDateOnly(object, "expiryDate", "MaskedCard");
|
|
2187
|
+
const invoiceNumber = optionalInteger(object, "invoiceNumber", "MaskedCard");
|
|
2188
|
+
const faceValue = optionalString(object, "faceValue", "MaskedCard");
|
|
2189
|
+
const subcategory = parseOptionalSubcategory(object);
|
|
2190
|
+
return {
|
|
2191
|
+
id: requiredUuid(object, "id", "MaskedCard"),
|
|
2192
|
+
credentialAvailable: optionalBoolean(object, "credentialAvailable", "MaskedCard") ?? false,
|
|
2193
|
+
...orderOperationId === void 0 ? {} : { orderOperationId },
|
|
2194
|
+
...invoiceId === void 0 ? {} : { invoiceId },
|
|
2195
|
+
...card === void 0 ? {} : { card },
|
|
2196
|
+
...serialNumberMasked === void 0 ? {} : { serialNumberMasked },
|
|
2197
|
+
...purchasedAt === void 0 ? {} : { purchasedAt },
|
|
2198
|
+
...unitPrice === void 0 ? {} : { unitPrice },
|
|
2199
|
+
...expiryDate === void 0 ? {} : { expiryDate },
|
|
2200
|
+
...invoiceNumber === void 0 ? {} : { invoiceNumber },
|
|
2201
|
+
...faceValue === void 0 ? {} : { faceValue },
|
|
2202
|
+
...subcategory === void 0 ? {} : { subcategory }
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function parseRevealedCredential(json) {
|
|
2206
|
+
const object = asObject(json, "RevealedCredential");
|
|
2207
|
+
const serialNumber = optionalString(object, "serialNumber", "RevealedCredential");
|
|
2208
|
+
const voucher = optionalString(object, "voucher", "RevealedCredential");
|
|
2209
|
+
const revealedAt = optionalTimestamp(object, "revealedAt", "RevealedCredential");
|
|
2210
|
+
const expiryDate = optionalDateOnly(object, "expiryDate", "RevealedCredential");
|
|
2211
|
+
const invoiceId = optionalUuid(object, "invoiceId", "RevealedCredential");
|
|
2212
|
+
const card = parseOptionalProduct(object, "card");
|
|
2213
|
+
const purchasedAt = optionalTimestamp(object, "purchasedAt", "RevealedCredential");
|
|
2214
|
+
const credential = {
|
|
2215
|
+
soldCardId: optionalUuid(object, "soldCardId", "RevealedCredential") ?? "00000000-0000-0000-0000-000000000000",
|
|
2216
|
+
...serialNumber === void 0 ? {} : { serialNumber },
|
|
2217
|
+
...voucher === void 0 ? {} : { voucher },
|
|
2218
|
+
...revealedAt === void 0 ? {} : { revealedAt },
|
|
2219
|
+
...expiryDate === void 0 ? {} : { expiryDate },
|
|
2220
|
+
...invoiceId === void 0 ? {} : { invoiceId },
|
|
2221
|
+
...card === void 0 ? {} : { card },
|
|
2222
|
+
...purchasedAt === void 0 ? {} : { purchasedAt },
|
|
2223
|
+
toString() {
|
|
2224
|
+
return `RevealedCredential { SoldCardId = ${this.soldCardId}, Secret = <redacted> }`;
|
|
2225
|
+
},
|
|
2226
|
+
[inspect4.custom]() {
|
|
2227
|
+
return `RevealedCredential { SoldCardId = ${this.soldCardId}, Secret = <redacted> }`;
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
Object.defineProperty(credential, "toString", { enumerable: false });
|
|
2231
|
+
Object.defineProperty(credential, inspect4.custom, { enumerable: false });
|
|
2232
|
+
return credential;
|
|
2233
|
+
}
|
|
2234
|
+
function parseRevealedCredentialCollection(json) {
|
|
2235
|
+
const object = asObject(json, "RevealedCredentialCollection");
|
|
2236
|
+
const items = object.items ?? [];
|
|
2237
|
+
if (!Array.isArray(items)) {
|
|
2238
|
+
throw new TypeError("RevealedCredentialCollection.items must be an array.");
|
|
2239
|
+
}
|
|
2240
|
+
return { items: items.map(parseRevealedCredential) };
|
|
2241
|
+
}
|
|
2242
|
+
function parseOptionalProduct(object, field) {
|
|
2243
|
+
const value = object[field];
|
|
2244
|
+
if (value === void 0 || value === null) {
|
|
2245
|
+
return void 0;
|
|
2246
|
+
}
|
|
2247
|
+
const nested = asObject(value, `MaskedCard.${field}`);
|
|
2248
|
+
const name = nested.name === void 0 || nested.name === null ? void 0 : parseLocalizedText(nested.name);
|
|
2249
|
+
return {
|
|
2250
|
+
id: requiredUuid(nested, "id", `MaskedCard.${field}`),
|
|
2251
|
+
...name === void 0 ? {} : { name }
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
function parseOptionalSubcategory(object) {
|
|
2255
|
+
const value = object.subcategory;
|
|
2256
|
+
if (value === void 0 || value === null) {
|
|
2257
|
+
return void 0;
|
|
2258
|
+
}
|
|
2259
|
+
const nested = asObject(value, "MaskedCard.subcategory");
|
|
2260
|
+
const name = nested.name === void 0 || nested.name === null ? void 0 : parseLocalizedText(nested.name);
|
|
2261
|
+
return {
|
|
2262
|
+
id: requiredUuid(nested, "id", "MaskedCard.subcategory"),
|
|
2263
|
+
...name === void 0 ? {} : { name }
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
function optionalDateOnly(object, field, label) {
|
|
2267
|
+
const value = optionalString(object, field, label);
|
|
2268
|
+
if (value === void 0) {
|
|
2269
|
+
return void 0;
|
|
2270
|
+
}
|
|
2271
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
2272
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) {
|
|
2273
|
+
throw new TypeError(`${label}.${field} must be a calendar date.`);
|
|
2274
|
+
}
|
|
2275
|
+
return value;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// src/models/orders.ts
|
|
2279
|
+
function parseOrder(json) {
|
|
2280
|
+
const object = asObject(json, "Order");
|
|
2281
|
+
const invoiceId = optionalUuid(object, "invoiceId", "Order");
|
|
2282
|
+
const walletId = optionalUuid(object, "walletId", "Order");
|
|
2283
|
+
const cardId = optionalUuid(object, "cardId", "Order");
|
|
2284
|
+
const quantity = optionalInteger(object, "quantity", "Order");
|
|
2285
|
+
const total = object.total === void 0 || object.total === null ? void 0 : parseMoney(object.total);
|
|
2286
|
+
const soldCards = parseOptionalCredentials(object.soldCards);
|
|
2287
|
+
const completedAt = optionalTimestamp(object, "completedAt", "Order");
|
|
2288
|
+
const externalReference = optionalString(object, "externalReference", "Order");
|
|
2289
|
+
const failureCode = optionalString(object, "failureCode", "Order");
|
|
2290
|
+
const codesWithheld = optionalBoolean(object, "codesWithheld", "Order");
|
|
2291
|
+
return {
|
|
2292
|
+
operationId: requiredUuid(object, "operationId", "Order"),
|
|
2293
|
+
status: parseOrderStatus(object.status),
|
|
2294
|
+
...invoiceId === void 0 ? {} : { invoiceId },
|
|
2295
|
+
...walletId === void 0 ? {} : { walletId },
|
|
2296
|
+
...cardId === void 0 ? {} : { cardId },
|
|
2297
|
+
...quantity === void 0 ? {} : { quantity },
|
|
2298
|
+
...total === void 0 ? {} : { total },
|
|
2299
|
+
...soldCards === void 0 ? {} : { soldCards },
|
|
2300
|
+
...completedAt === void 0 ? {} : { completedAt },
|
|
2301
|
+
...externalReference === void 0 ? {} : { externalReference },
|
|
2302
|
+
...failureCode === void 0 ? {} : { failureCode },
|
|
2303
|
+
...codesWithheld === void 0 ? {} : { codesWithheld }
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
function parseOrderStatus(value) {
|
|
2307
|
+
if (typeof value !== "string") {
|
|
2308
|
+
return "unknown";
|
|
2309
|
+
}
|
|
2310
|
+
switch (value.toLowerCase()) {
|
|
2311
|
+
case "processing":
|
|
2312
|
+
return "processing";
|
|
2313
|
+
case "recoveryexhausted":
|
|
2314
|
+
return "recoveryExhausted";
|
|
2315
|
+
case "completed":
|
|
2316
|
+
return "completed";
|
|
2317
|
+
case "failed":
|
|
2318
|
+
return "failed";
|
|
2319
|
+
default:
|
|
2320
|
+
return "unknown";
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
function parseOptionalCredentials(value) {
|
|
2324
|
+
if (value === void 0 || value === null) {
|
|
2325
|
+
return void 0;
|
|
2326
|
+
}
|
|
2327
|
+
if (!Array.isArray(value)) {
|
|
2328
|
+
throw new TypeError("Order.soldCards must be an array when present.");
|
|
2329
|
+
}
|
|
2330
|
+
return value.map(parseRevealedCredential);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/models/wallets.ts
|
|
2334
|
+
function parseWallet(json) {
|
|
2335
|
+
const object = asObject(json, "Wallet");
|
|
2336
|
+
const name = optionalString(object, "name", "Wallet");
|
|
2337
|
+
const currency = optionalString(object, "currency", "Wallet");
|
|
2338
|
+
return {
|
|
2339
|
+
id: requiredUuid(object, "id", "Wallet"),
|
|
2340
|
+
balance: parseMoney(object.balance),
|
|
2341
|
+
...name === void 0 ? {} : { name },
|
|
2342
|
+
...currency === void 0 ? {} : { currency }
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
function parsePartnerProfile(json) {
|
|
2346
|
+
const object = asObject(json, "PartnerProfile");
|
|
2347
|
+
const partner = object.partner === void 0 || object.partner === null ? void 0 : parsePartnerIdentity(object.partner);
|
|
2348
|
+
const application = object.application === void 0 || object.application === null ? void 0 : parseApplicationIdentity(object.application);
|
|
2349
|
+
const ownerAccount = object.ownerAccount === void 0 || object.ownerAccount === null ? void 0 : parseOwnerAccount(object.ownerAccount);
|
|
2350
|
+
const documentationVersion = optionalString(object, "documentationVersion", "PartnerProfile");
|
|
2351
|
+
return {
|
|
2352
|
+
...partner === void 0 ? {} : { partner },
|
|
2353
|
+
...application === void 0 ? {} : { application },
|
|
2354
|
+
...ownerAccount === void 0 ? {} : { ownerAccount },
|
|
2355
|
+
...documentationVersion === void 0 ? {} : { documentationVersion }
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function parsePartnerIdentity(json) {
|
|
2359
|
+
const object = asObject(json, "PartnerIdentity");
|
|
2360
|
+
return { id: requiredUuid(object, "id", "PartnerIdentity") };
|
|
2361
|
+
}
|
|
2362
|
+
function parseApplicationIdentity(json) {
|
|
2363
|
+
const object = asObject(json, "ApplicationIdentity");
|
|
2364
|
+
const scopes = object.scopes ?? [];
|
|
2365
|
+
if (!Array.isArray(scopes) || !scopes.every((scope) => typeof scope === "string")) {
|
|
2366
|
+
throw new TypeError("ApplicationIdentity.scopes must be an array of strings.");
|
|
2367
|
+
}
|
|
2368
|
+
return { id: requiredUuid(object, "id", "ApplicationIdentity"), scopes };
|
|
2369
|
+
}
|
|
2370
|
+
function parseOwnerAccount(json) {
|
|
2371
|
+
const object = asObject(json, "OwnerAccount");
|
|
2372
|
+
const displayName = optionalString(object, "displayName", "OwnerAccount");
|
|
2373
|
+
return {
|
|
2374
|
+
id: requiredUuid(object, "id", "OwnerAccount"),
|
|
2375
|
+
...displayName === void 0 ? {} : { displayName }
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// src/operations/operations.ts
|
|
2380
|
+
var ProfileOperations = class {
|
|
2381
|
+
/** Shares the verified transport so profile reads follow the client's authority and signing policy. */
|
|
2382
|
+
constructor(transport) {
|
|
2383
|
+
this.transport = transport;
|
|
2384
|
+
}
|
|
2385
|
+
transport;
|
|
2386
|
+
/** Reads identity and effective scopes from the live policy. */
|
|
2387
|
+
async get(options) {
|
|
2388
|
+
return this.transport.json(
|
|
2389
|
+
{
|
|
2390
|
+
method: "GET",
|
|
2391
|
+
route: "/v1/profile",
|
|
2392
|
+
path: "/v1/profile",
|
|
2393
|
+
profile: "SafeRead",
|
|
2394
|
+
signal: options?.signal
|
|
2395
|
+
},
|
|
2396
|
+
parsePartnerProfile
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
var WalletOperations = class {
|
|
2401
|
+
/** Shares the verified transport so wallet reads follow the client's authority and signing policy. */
|
|
2402
|
+
constructor(transport) {
|
|
2403
|
+
this.transport = transport;
|
|
2404
|
+
}
|
|
2405
|
+
transport;
|
|
2406
|
+
/** Walks wallet pages so callers need not manage continuation cursors. */
|
|
2407
|
+
async *list(options) {
|
|
2408
|
+
yield* pages((cursor) => this.listPage(pageOptions(cursor, options)));
|
|
2409
|
+
}
|
|
2410
|
+
/** Reads one page when the caller manages the cursor. */
|
|
2411
|
+
async listPage(options) {
|
|
2412
|
+
return this.transport.json(
|
|
2413
|
+
{
|
|
2414
|
+
method: "GET",
|
|
2415
|
+
route: "/v1/wallets",
|
|
2416
|
+
path: pagePath("/v1/wallets", options?.cursor),
|
|
2417
|
+
profile: "SafeRead",
|
|
2418
|
+
signal: options?.signal
|
|
2419
|
+
},
|
|
2420
|
+
(json) => parsePage(json, parseWallet)
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
/** Reads one granted wallet by canonical UUID. */
|
|
2424
|
+
async get(walletId, options) {
|
|
2425
|
+
const id = canonicalUuid(walletId, "walletId");
|
|
2426
|
+
return this.transport.json(
|
|
2427
|
+
{
|
|
2428
|
+
method: "GET",
|
|
2429
|
+
route: "/v1/wallets/{walletId}",
|
|
2430
|
+
path: `/v1/wallets/${id}`,
|
|
2431
|
+
profile: "SafeRead",
|
|
2432
|
+
signal: options?.signal
|
|
2433
|
+
},
|
|
2434
|
+
parseWallet
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
var CatalogueOperations = class {
|
|
2439
|
+
/** Shares the verified transport so catalogue reads use the client's price and signing policy. */
|
|
2440
|
+
constructor(transport) {
|
|
2441
|
+
this.transport = transport;
|
|
2442
|
+
}
|
|
2443
|
+
transport;
|
|
2444
|
+
/** Walks category pages for a wallet. */
|
|
2445
|
+
async *listCategories(walletId, options) {
|
|
2446
|
+
yield* pages((cursor) => this.listCategoriesPage(walletId, pageOptions(cursor, options)));
|
|
2447
|
+
}
|
|
2448
|
+
/** Reads one category page. */
|
|
2449
|
+
async listCategoriesPage(walletId, options) {
|
|
2450
|
+
const id = canonicalUuid(walletId, "walletId");
|
|
2451
|
+
return this.transport.json(
|
|
2452
|
+
{
|
|
2453
|
+
method: "GET",
|
|
2454
|
+
route: "/v1/wallets/{walletId}/catalog/categories",
|
|
2455
|
+
path: pagePath(`/v1/wallets/${id}/catalog/categories`, options?.cursor),
|
|
2456
|
+
profile: "SafeRead",
|
|
2457
|
+
signal: options?.signal
|
|
2458
|
+
},
|
|
2459
|
+
(json) => parsePage(json, parseCatalogueCategory)
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
/** Walks subcategory pages. */
|
|
2463
|
+
async *listSubcategories(walletId, categoryId, options) {
|
|
2464
|
+
yield* pages((cursor) => this.listSubcategoriesPage(walletId, categoryId, pageOptions(cursor, options)));
|
|
2465
|
+
}
|
|
2466
|
+
/** Reads one subcategory page. */
|
|
2467
|
+
async listSubcategoriesPage(walletId, categoryId, options) {
|
|
2468
|
+
const wallet = canonicalUuid(walletId, "walletId");
|
|
2469
|
+
const category = canonicalUuid(categoryId, "categoryId");
|
|
2470
|
+
const route = "/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories";
|
|
2471
|
+
return this.transport.json(
|
|
2472
|
+
{
|
|
2473
|
+
method: "GET",
|
|
2474
|
+
route,
|
|
2475
|
+
path: pagePath(`/v1/wallets/${wallet}/catalog/categories/${category}/subcategories`, options?.cursor),
|
|
2476
|
+
profile: "SafeRead",
|
|
2477
|
+
signal: options?.signal
|
|
2478
|
+
},
|
|
2479
|
+
(json) => parsePage(json, parseCatalogueSubcategory)
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
/** Reads one subcategory. */
|
|
2483
|
+
async getSubcategory(walletId, subcategoryId, options) {
|
|
2484
|
+
const wallet = canonicalUuid(walletId, "walletId");
|
|
2485
|
+
const subcategory = canonicalUuid(subcategoryId, "subcategoryId");
|
|
2486
|
+
return this.transport.json(
|
|
2487
|
+
{
|
|
2488
|
+
method: "GET",
|
|
2489
|
+
route: "/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}",
|
|
2490
|
+
path: `/v1/wallets/${wallet}/catalog/subcategories/${subcategory}`,
|
|
2491
|
+
profile: "SafeRead",
|
|
2492
|
+
signal: options?.signal
|
|
2493
|
+
},
|
|
2494
|
+
parseCatalogueSubcategory
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
/** Walks card pages for the given wallet and subcategory. */
|
|
2498
|
+
async *listCards(walletId, subcategoryId, options) {
|
|
2499
|
+
yield* pages((cursor) => this.listCardsPage(walletId, subcategoryId, pageOptions(cursor, options)));
|
|
2500
|
+
}
|
|
2501
|
+
/** Reads one catalogue card page. */
|
|
2502
|
+
async listCardsPage(walletId, subcategoryId, options) {
|
|
2503
|
+
const wallet = canonicalUuid(walletId, "walletId");
|
|
2504
|
+
const subcategory = canonicalUuid(subcategoryId, "subcategoryId");
|
|
2505
|
+
const route = "/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards";
|
|
2506
|
+
return this.transport.json(
|
|
2507
|
+
{
|
|
2508
|
+
method: "GET",
|
|
2509
|
+
route,
|
|
2510
|
+
path: pagePath(`/v1/wallets/${wallet}/catalog/subcategories/${subcategory}/cards`, options?.cursor),
|
|
2511
|
+
profile: "SafeRead",
|
|
2512
|
+
signal: options?.signal
|
|
2513
|
+
},
|
|
2514
|
+
(json) => parsePage(json, parseCatalogueCard)
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
var OrderOperations = class {
|
|
2519
|
+
/** Shares the verified transport so order recovery uses the same signer and idempotency rules. */
|
|
2520
|
+
constructor(transport) {
|
|
2521
|
+
this.transport = transport;
|
|
2522
|
+
}
|
|
2523
|
+
transport;
|
|
2524
|
+
/** Creates an order under an id the caller has persisted, preventing a dropped answer from causing duplicate purchases. */
|
|
2525
|
+
create(walletId, operationId, order, options) {
|
|
2526
|
+
return this.send(walletId, operationId, order, false, options);
|
|
2527
|
+
}
|
|
2528
|
+
/** Repeats the same request and operation id to recover an unknown result. */
|
|
2529
|
+
resume(walletId, operationId, order, options) {
|
|
2530
|
+
return this.send(walletId, operationId, order, true, options);
|
|
2531
|
+
}
|
|
2532
|
+
/** Reads order state without dispatching a new order or returning credentials. */
|
|
2533
|
+
async get(operationId, options) {
|
|
2534
|
+
const id = canonicalUuid(operationId, "operationId");
|
|
2535
|
+
return this.transport.json(
|
|
2536
|
+
{
|
|
2537
|
+
method: "GET",
|
|
2538
|
+
route: "/v1/orders/{operationId}",
|
|
2539
|
+
path: `/v1/orders/${id}`,
|
|
2540
|
+
profile: "SafeRead",
|
|
2541
|
+
signal: options?.signal
|
|
2542
|
+
},
|
|
2543
|
+
parseOrder
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
async send(walletId, operationId, order, resuming, options) {
|
|
2547
|
+
guardOrder(order);
|
|
2548
|
+
const wallet = canonicalUuid(walletId, "walletId");
|
|
2549
|
+
const operation = canonicalUuid(operationId, "operationId");
|
|
2550
|
+
const card = canonicalUuid(order.cardId, "cardId");
|
|
2551
|
+
let bytes;
|
|
2552
|
+
try {
|
|
2553
|
+
bytes = new TextEncoder().encode(
|
|
2554
|
+
JSON.stringify({
|
|
2555
|
+
cardId: card,
|
|
2556
|
+
quantity: order.quantity,
|
|
2557
|
+
expectedUnitPrice: order.expectedUnitPrice.toJSON(),
|
|
2558
|
+
expectedTotal: order.expectedTotal.toJSON(),
|
|
2559
|
+
...order.externalReference === void 0 ? {} : { externalReference: order.externalReference },
|
|
2560
|
+
useAllowedDebt: order.useAllowedDebt ?? false
|
|
2561
|
+
})
|
|
2562
|
+
);
|
|
2563
|
+
} catch (cause) {
|
|
2564
|
+
throw new RequestSigningError(cause);
|
|
2565
|
+
}
|
|
2566
|
+
let result;
|
|
2567
|
+
let report = true;
|
|
2568
|
+
let rethrow = false;
|
|
2569
|
+
let cancellation;
|
|
2570
|
+
let outcomeReason;
|
|
2571
|
+
try {
|
|
2572
|
+
const { response, body } = await this.transport.send({
|
|
2573
|
+
method: "POST",
|
|
2574
|
+
route: "/v1/wallets/{walletId}/orders",
|
|
2575
|
+
path: `/v1/wallets/${wallet}/orders`,
|
|
2576
|
+
profile: "OrderMutation",
|
|
2577
|
+
operationId: operation,
|
|
2578
|
+
body: bytes,
|
|
2579
|
+
signal: options?.signal
|
|
2580
|
+
});
|
|
2581
|
+
if (body.length === 0) {
|
|
2582
|
+
outcomeReason = "empty_body";
|
|
2583
|
+
result = {
|
|
2584
|
+
kind: "unknown",
|
|
2585
|
+
operationId: operation,
|
|
2586
|
+
suggestedDelayMs: 5e3,
|
|
2587
|
+
cause: emptyBodyError(response.status)
|
|
2588
|
+
};
|
|
2589
|
+
} else {
|
|
2590
|
+
let decoded;
|
|
2591
|
+
try {
|
|
2592
|
+
decoded = parseBody(body);
|
|
2593
|
+
} catch {
|
|
2594
|
+
throw new MalformedResponseError("order response");
|
|
2595
|
+
}
|
|
2596
|
+
if (decoded === null) {
|
|
2597
|
+
outcomeReason = "empty_body";
|
|
2598
|
+
result = {
|
|
2599
|
+
kind: "unknown",
|
|
2600
|
+
operationId: operation,
|
|
2601
|
+
suggestedDelayMs: 5e3,
|
|
2602
|
+
cause: emptyBodyError(response.status)
|
|
2603
|
+
};
|
|
2604
|
+
} else if (response.status === 202) {
|
|
2605
|
+
const orderValue = parseOrderSafely(decoded);
|
|
2606
|
+
result = {
|
|
2607
|
+
kind: "processing",
|
|
2608
|
+
operationId: operation,
|
|
2609
|
+
order: orderValue,
|
|
2610
|
+
suggestedDelayMs: retryAfterSeconds(response.headers.get("retry-after")) === void 0 ? 5e3 : (retryAfterSeconds(response.headers.get("retry-after")) ?? 0) * 1e3,
|
|
2611
|
+
...response.headers.get("location") === null ? {} : { location: response.headers.get("location") ?? "" }
|
|
2612
|
+
};
|
|
2613
|
+
} else {
|
|
2614
|
+
const orderValue = parseOrderSafely(decoded);
|
|
2615
|
+
const credentials = orderValue.soldCards ?? [];
|
|
2616
|
+
if (credentials.length > 0) {
|
|
2617
|
+
result = {
|
|
2618
|
+
kind: "completed",
|
|
2619
|
+
operationId: operation,
|
|
2620
|
+
order: orderValue,
|
|
2621
|
+
credentials,
|
|
2622
|
+
codesWithheld: orderValue.codesWithheld === true || credentials.length === 0
|
|
2623
|
+
};
|
|
2624
|
+
} else if (headerValues2(response.headers.get("idempotency-replayed")).some((value) => value.toLowerCase() === "true")) {
|
|
2625
|
+
result = { kind: "replayed", operationId: operation, order: orderValue };
|
|
2626
|
+
} else {
|
|
2627
|
+
result = {
|
|
2628
|
+
kind: "completed",
|
|
2629
|
+
operationId: operation,
|
|
2630
|
+
order: orderValue,
|
|
2631
|
+
credentials,
|
|
2632
|
+
codesWithheld: orderValue.codesWithheld === true || credentials.length === 0
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
if (error instanceof AnisApiError) {
|
|
2639
|
+
if (error.orderOutcome === "notPlaced" && (error.isReplayed || !resuming)) {
|
|
2640
|
+
result = { kind: "notPlaced", operationId: operation, refusal: error };
|
|
2641
|
+
report = false;
|
|
2642
|
+
} else {
|
|
2643
|
+
outcomeReason = error.rawCode ?? "refused";
|
|
2644
|
+
const delay = error.retryAfter === void 0 ? refusedAtTheDoor(error.code) ? 6e4 : 5e3 : error.retryAfter * 1e3;
|
|
2645
|
+
result = { kind: "unknown", operationId: operation, suggestedDelayMs: delay, cause: error };
|
|
2646
|
+
}
|
|
2647
|
+
} else if (error instanceof RequestSigningError) {
|
|
2648
|
+
throw error;
|
|
2649
|
+
} else {
|
|
2650
|
+
outcomeReason = options?.signal?.aborted ? "canceled" : errorType(error);
|
|
2651
|
+
result = { kind: "unknown", operationId: operation, suggestedDelayMs: 5e3, cause: error };
|
|
2652
|
+
rethrow = options?.signal?.aborted ?? false;
|
|
2653
|
+
cancellation = error;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
if (report) {
|
|
2657
|
+
if (result.kind === "unknown") {
|
|
2658
|
+
result = reportUnknown(
|
|
2659
|
+
this.transport,
|
|
2660
|
+
operation,
|
|
2661
|
+
outcomeReason ?? errorType(result.cause),
|
|
2662
|
+
result.cause,
|
|
2663
|
+
result.suggestedDelayMs
|
|
2664
|
+
);
|
|
2665
|
+
} else {
|
|
2666
|
+
result = reportOutcome(this.transport, operation, result);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
if (rethrow) throw cancellation;
|
|
2670
|
+
return result;
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
var OwnedCardOperations = class {
|
|
2674
|
+
/** Shares the verified transport so card reads and reveals keep the client's response checks. */
|
|
2675
|
+
constructor(transport) {
|
|
2676
|
+
this.transport = transport;
|
|
2677
|
+
}
|
|
2678
|
+
transport;
|
|
2679
|
+
/** Walks owned-card pages. */
|
|
2680
|
+
async *list(walletId, options) {
|
|
2681
|
+
yield* pages((cursor) => this.listPage(walletId, pageOptions(cursor, options)));
|
|
2682
|
+
}
|
|
2683
|
+
/** Reads one owned-card page. */
|
|
2684
|
+
async listPage(walletId, options) {
|
|
2685
|
+
const wallet = canonicalUuid(walletId, "walletId");
|
|
2686
|
+
return this.transport.json(
|
|
2687
|
+
{
|
|
2688
|
+
method: "GET",
|
|
2689
|
+
route: "/v1/wallets/{walletId}/cards",
|
|
2690
|
+
path: pagePath(`/v1/wallets/${wallet}/cards`, options?.cursor),
|
|
2691
|
+
profile: "SafeRead",
|
|
2692
|
+
signal: options?.signal
|
|
2693
|
+
},
|
|
2694
|
+
(json) => parsePage(json, parseMaskedCard)
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
/** Reads a masked owned card. */
|
|
2698
|
+
async get(walletId, soldCardId, options) {
|
|
2699
|
+
const wallet = canonicalUuid(walletId, "walletId");
|
|
2700
|
+
const card = canonicalUuid(soldCardId, "soldCardId");
|
|
2701
|
+
return this.transport.json(
|
|
2702
|
+
{
|
|
2703
|
+
method: "GET",
|
|
2704
|
+
route: "/v1/wallets/{walletId}/cards/{soldCardId}",
|
|
2705
|
+
path: `/v1/wallets/${wallet}/cards/${card}`,
|
|
2706
|
+
profile: "SafeRead",
|
|
2707
|
+
signal: options?.signal
|
|
2708
|
+
},
|
|
2709
|
+
parseMaskedCard
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
/** Reveals one credential using a signed mutation with no body bytes. */
|
|
2713
|
+
async reveal(walletId, soldCardId, options) {
|
|
2714
|
+
const wallet = canonicalUuid(walletId, "walletId");
|
|
2715
|
+
const card = canonicalUuid(soldCardId, "soldCardId");
|
|
2716
|
+
return this.transport.json(
|
|
2717
|
+
{
|
|
2718
|
+
method: "POST",
|
|
2719
|
+
route: "/v1/wallets/{walletId}/cards/{soldCardId}/reveal",
|
|
2720
|
+
path: `/v1/wallets/${wallet}/cards/${card}/reveal`,
|
|
2721
|
+
profile: "BodylessNonceMutation",
|
|
2722
|
+
signal: options?.signal
|
|
2723
|
+
},
|
|
2724
|
+
parseRevealedCredential
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
/** Reveals every credential on an invoice atomically. */
|
|
2728
|
+
async revealInvoice(walletId, invoiceId, options) {
|
|
2729
|
+
const wallet = canonicalUuid(walletId, "walletId");
|
|
2730
|
+
const invoice = canonicalUuid(invoiceId, "invoiceId");
|
|
2731
|
+
const route = "/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal";
|
|
2732
|
+
return this.transport.json(
|
|
2733
|
+
{
|
|
2734
|
+
method: "POST",
|
|
2735
|
+
route,
|
|
2736
|
+
path: `/v1/wallets/${wallet}/invoices/${invoice}/cards/reveal`,
|
|
2737
|
+
profile: "BodylessNonceMutation",
|
|
2738
|
+
signal: options?.signal
|
|
2739
|
+
},
|
|
2740
|
+
parseRevealedCredentialCollection
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
var DiagnosticsOperations = class {
|
|
2745
|
+
/** Shares the verified transport so the self-check uses the same request as the partner API. */
|
|
2746
|
+
constructor(transport) {
|
|
2747
|
+
this.transport = transport;
|
|
2748
|
+
}
|
|
2749
|
+
transport;
|
|
2750
|
+
/** Sends the exact empty JSON object expected by the self-check route. */
|
|
2751
|
+
async checkSignature(options) {
|
|
2752
|
+
return this.transport.json(
|
|
2753
|
+
{
|
|
2754
|
+
method: "POST",
|
|
2755
|
+
route: "/v1/diagnostics/signature",
|
|
2756
|
+
path: "/v1/diagnostics/signature",
|
|
2757
|
+
profile: "BodylessNonceMutation",
|
|
2758
|
+
body: EMPTY_OBJECT_BODY,
|
|
2759
|
+
signal: options?.signal
|
|
2760
|
+
},
|
|
2761
|
+
parseSignatureDiagnostic
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
async function* pages(load) {
|
|
2766
|
+
let cursor;
|
|
2767
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
2768
|
+
do {
|
|
2769
|
+
const page = await load(cursor);
|
|
2770
|
+
for (const item of page.items) yield item;
|
|
2771
|
+
cursor = page.nextCursor;
|
|
2772
|
+
if (cursor !== void 0 && cursor.length > 0) {
|
|
2773
|
+
if (seenCursors.has(cursor)) throw new MalformedResponseError("page continuation cursor");
|
|
2774
|
+
seenCursors.add(cursor);
|
|
2775
|
+
}
|
|
2776
|
+
} while (cursor !== void 0 && cursor.length > 0);
|
|
2777
|
+
}
|
|
2778
|
+
function pagePath(path, cursor) {
|
|
2779
|
+
return cursor === void 0 || cursor.length === 0 ? path : `${path}?cursor=${encodeURIComponent(cursor)}`;
|
|
2780
|
+
}
|
|
2781
|
+
function pageOptions(cursor, options) {
|
|
2782
|
+
return {
|
|
2783
|
+
...cursor === void 0 ? {} : { cursor },
|
|
2784
|
+
signal: options?.signal
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
function parseBody(body) {
|
|
2788
|
+
return JSON.parse(new TextDecoder().decode(body));
|
|
2789
|
+
}
|
|
2790
|
+
function parseOrderSafely(value) {
|
|
2791
|
+
try {
|
|
2792
|
+
return parseOrder(value);
|
|
2793
|
+
} catch {
|
|
2794
|
+
throw new MalformedResponseError("order response");
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
function headerValues2(value) {
|
|
2798
|
+
return value === null ? [] : value.split(",").map((part) => part.trim());
|
|
2799
|
+
}
|
|
2800
|
+
function guardOrder(order) {
|
|
2801
|
+
if (!Number.isInteger(order.quantity) || order.quantity < 1)
|
|
2802
|
+
throw new RangeError("An order must be for at least one card.");
|
|
2803
|
+
canonicalUuid(order.cardId, "cardId");
|
|
2804
|
+
if (!(order.expectedUnitPrice instanceof Money) || !(order.expectedTotal instanceof Money))
|
|
2805
|
+
throw new TypeError("Order prices must be Money values.");
|
|
2806
|
+
if (BigInt(order.expectedUnitPrice.amount.replace(".", "")) <= 0n)
|
|
2807
|
+
throw new RangeError("ExpectedUnitPrice must be greater than zero.");
|
|
2808
|
+
if (order.expectedUnitPrice.multiply(order.quantity).amount !== order.expectedTotal.amount)
|
|
2809
|
+
throw new TypeError("ExpectedTotal must equal unit price multiplied by quantity.");
|
|
2810
|
+
if (order.expectedUnitPrice.currency !== order.expectedTotal.currency)
|
|
2811
|
+
throw new TypeError("Order prices must use the same currency.");
|
|
2812
|
+
}
|
|
2813
|
+
function reportOutcome(transport, operationId, result) {
|
|
2814
|
+
safeCounter("anis.partners.order.outcomes", {
|
|
2815
|
+
"anis.client": transport.clientName,
|
|
2816
|
+
"anis.order.outcome": result.kind
|
|
2817
|
+
});
|
|
2818
|
+
transport.reportOrderOutcome(operationId, result.kind);
|
|
2819
|
+
return result;
|
|
2820
|
+
}
|
|
2821
|
+
function reportUnknown(transport, operationId, reason, cause, suggestedDelayMs = 5e3) {
|
|
2822
|
+
safeCounter("anis.partners.order.outcomes", {
|
|
2823
|
+
"anis.client": transport.clientName,
|
|
2824
|
+
"anis.order.outcome": "unknown",
|
|
2825
|
+
"error.type": reason
|
|
2826
|
+
});
|
|
2827
|
+
transport.reportOrderOutcome(operationId, "unknown", reason);
|
|
2828
|
+
return { kind: "unknown", operationId, suggestedDelayMs, cause };
|
|
2829
|
+
}
|
|
2830
|
+
function errorType(error) {
|
|
2831
|
+
if (error instanceof MalformedResponseError) return "other";
|
|
2832
|
+
if (error instanceof SigningKeyDocumentUnavailableError) return "connection";
|
|
2833
|
+
if (error instanceof UnverifiableResponseError) return "unverifiable";
|
|
2834
|
+
if (error instanceof DOMException && error.name === "TimeoutError") return "timeout";
|
|
2835
|
+
if (error instanceof TypeError) return "connection";
|
|
2836
|
+
if (error instanceof SyntaxError) return "invalid_json";
|
|
2837
|
+
return error instanceof Error ? error.name : "other";
|
|
2838
|
+
}
|
|
2839
|
+
function emptyBodyError(status) {
|
|
2840
|
+
return new AnisApiError({ status, code: "internal_error", title: "Empty body" }, status);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// src/client.ts
|
|
2844
|
+
var AnisPartnersClient = class _AnisPartnersClient {
|
|
2845
|
+
/** Reads identity and live scopes. */
|
|
2846
|
+
profile;
|
|
2847
|
+
/** Lists wallets available to this application. */
|
|
2848
|
+
wallets;
|
|
2849
|
+
/** Reads wallet-priced catalogue entries. */
|
|
2850
|
+
catalogue;
|
|
2851
|
+
/** Creates and resumes caller-keyed orders. */
|
|
2852
|
+
orders;
|
|
2853
|
+
/** Reads owned cards and reveals credentials. */
|
|
2854
|
+
ownedCards;
|
|
2855
|
+
/** Runs the side-effect-free signature check. */
|
|
2856
|
+
diagnostics;
|
|
2857
|
+
constructor(transport) {
|
|
2858
|
+
this.profile = new ProfileOperations(transport);
|
|
2859
|
+
this.wallets = new WalletOperations(transport);
|
|
2860
|
+
this.catalogue = new CatalogueOperations(transport);
|
|
2861
|
+
this.orders = new OrderOperations(transport);
|
|
2862
|
+
this.ownedCards = new OwnedCardOperations(transport);
|
|
2863
|
+
this.diagnostics = new DiagnosticsOperations(transport);
|
|
2864
|
+
}
|
|
2865
|
+
/** Builds the grouped API client and its unsigned public key-document source. */
|
|
2866
|
+
static create(input) {
|
|
2867
|
+
const options = validateClientOptions(input.options);
|
|
2868
|
+
const signer = input.signer;
|
|
2869
|
+
if (typeof signer !== "object" || signer === null || !("sign" in signer) || typeof signer.sign !== "function")
|
|
2870
|
+
throw new TypeError("A request signer with a sign method is required.");
|
|
2871
|
+
if (!("keyId" in signer) || typeof signer.keyId !== "string")
|
|
2872
|
+
throw new TypeError("The request signer needs a key id.");
|
|
2873
|
+
canonicalUuid(signer.keyId, "signer.keyId");
|
|
2874
|
+
const fetcher = input.fetch ?? globalThis.fetch;
|
|
2875
|
+
const logger = input.logger;
|
|
2876
|
+
const keyLogger = logger === void 0 ? void 0 : {
|
|
2877
|
+
debug(message, fields) {
|
|
2878
|
+
safeLog(logger, "debug", message, fields);
|
|
2879
|
+
},
|
|
2880
|
+
info(message, fields) {
|
|
2881
|
+
safeLog(logger, "info", message, { ...fields, eventId: 1005 });
|
|
2882
|
+
},
|
|
2883
|
+
warn(message, fields) {
|
|
2884
|
+
safeLog(logger, "warn", message, fields);
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
const keySource = new HttpSigningKeySource({
|
|
2888
|
+
authority: options.authority,
|
|
2889
|
+
cacheSeconds: options.signingKeyCacheSeconds,
|
|
2890
|
+
fetch: fetcher,
|
|
2891
|
+
...input.keyCache === void 0 ? {} : { documentCache: input.keyCache },
|
|
2892
|
+
...keyLogger === void 0 ? {} : { logger: keyLogger }
|
|
2893
|
+
});
|
|
2894
|
+
const transport = new PartnerTransport({
|
|
2895
|
+
...options,
|
|
2896
|
+
signer: input.signer,
|
|
2897
|
+
fetcher,
|
|
2898
|
+
keySource,
|
|
2899
|
+
...logger === void 0 ? {} : { logger }
|
|
2900
|
+
});
|
|
2901
|
+
return new _AnisPartnersClient(transport);
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
export {
|
|
2905
|
+
AnisApiError,
|
|
2906
|
+
AnisEnrollmentClient,
|
|
2907
|
+
AnisPartnersClient,
|
|
2908
|
+
AnisPartnersError,
|
|
2909
|
+
AnisPartnersTelemetry,
|
|
2910
|
+
AuthorizationError,
|
|
2911
|
+
CatalogueOperations,
|
|
2912
|
+
DOMAIN_SEPARATOR,
|
|
2913
|
+
DependencyUnavailableError,
|
|
2914
|
+
DiagnosticsOperations,
|
|
2915
|
+
ERROR_CODES,
|
|
2916
|
+
EnrollmentKeyMismatchError,
|
|
2917
|
+
EnrollmentRefusedError,
|
|
2918
|
+
IdempotencyConflictError,
|
|
2919
|
+
InsufficientBalanceError,
|
|
2920
|
+
InvalidCredentialsError,
|
|
2921
|
+
KeyedSigner,
|
|
2922
|
+
LimitExceededError,
|
|
2923
|
+
MalformedResponseError,
|
|
2924
|
+
Money,
|
|
2925
|
+
OrderOperations,
|
|
2926
|
+
OutOfStockError,
|
|
2927
|
+
OwnedCardOperations,
|
|
2928
|
+
PemP256Signer,
|
|
2929
|
+
PriceChangedError,
|
|
2930
|
+
PrivateKeyError,
|
|
2931
|
+
ProfileOperations,
|
|
2932
|
+
RateLimitedError,
|
|
2933
|
+
ReplayDetectedError,
|
|
2934
|
+
RequestSigningError,
|
|
2935
|
+
ResourceNotFoundError,
|
|
2936
|
+
SigningKeyDocumentUnavailableError,
|
|
2937
|
+
UnverifiableResponseError,
|
|
2938
|
+
ValidationFailedError,
|
|
2939
|
+
WalletOperations,
|
|
2940
|
+
keyThumbprint,
|
|
2941
|
+
proofMessage,
|
|
2942
|
+
proofSignature,
|
|
2943
|
+
safetyCodeFromThumbprint,
|
|
2944
|
+
validateClientOptions
|
|
2945
|
+
};
|
|
2946
|
+
//# sourceMappingURL=index.js.map
|