@stripe/link-sdk 0.5.0 → 0.7.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/dist/client.d.ts +5 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1220 -113
- package/dist/index.js.map +1 -1
- package/dist/resources/__tests__/attestations-crypto.test.d.ts +2 -0
- package/dist/resources/__tests__/attestations-crypto.test.d.ts.map +1 -0
- package/dist/resources/__tests__/attestations.test.d.ts +2 -0
- package/dist/resources/__tests__/attestations.test.d.ts.map +1 -0
- package/dist/resources/__tests__/credentials.test.d.ts +2 -0
- package/dist/resources/__tests__/credentials.test.d.ts.map +1 -0
- package/dist/resources/__tests__/holder-jwk.test.d.ts +2 -0
- package/dist/resources/__tests__/holder-jwk.test.d.ts.map +1 -0
- package/dist/resources/__tests__/summaries.test.d.ts +2 -0
- package/dist/resources/__tests__/summaries.test.d.ts.map +1 -0
- package/dist/resources/__tests__/ucp.test.d.ts +2 -0
- package/dist/resources/__tests__/ucp.test.d.ts.map +1 -0
- package/dist/resources/attestations-crypto.d.ts +35 -0
- package/dist/resources/attestations-crypto.d.ts.map +1 -0
- package/dist/resources/attestations.d.ts +14 -0
- package/dist/resources/attestations.d.ts.map +1 -0
- package/dist/resources/balances.d.ts.map +1 -1
- package/dist/resources/base.d.ts +1 -0
- package/dist/resources/base.d.ts.map +1 -1
- package/dist/resources/holder-jwk.d.ts +11 -0
- package/dist/resources/holder-jwk.d.ts.map +1 -0
- package/dist/resources/identity-credentials.d.ts +9 -0
- package/dist/resources/identity-credentials.d.ts.map +1 -0
- package/dist/resources/interfaces.d.ts +86 -1
- package/dist/resources/interfaces.d.ts.map +1 -1
- package/dist/resources/summaries.d.ts +10 -0
- package/dist/resources/summaries.d.ts.map +1 -0
- package/dist/resources/ucp.d.ts +25 -0
- package/dist/resources/ucp.d.ts.map +1 -0
- package/dist/resources/user-info.d.ts.map +1 -1
- package/dist/types/index.d.ts +151 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// src/resources/
|
|
1
|
+
// src/resources/attestations.ts
|
|
2
|
+
import { createHash as createHash2 } from "crypto";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
|
|
4
5
|
// src/errors.ts
|
|
@@ -44,6 +45,364 @@ var LinkApiError = class extends LinkSdkError {
|
|
|
44
45
|
}
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
// src/resources/attestations-crypto.ts
|
|
49
|
+
import {
|
|
50
|
+
createHash,
|
|
51
|
+
createPublicKey,
|
|
52
|
+
randomBytes,
|
|
53
|
+
timingSafeEqual
|
|
54
|
+
} from "crypto";
|
|
55
|
+
var TOKEN_TYPE = 2;
|
|
56
|
+
var NONCE_SIZE = 32;
|
|
57
|
+
var CHALLENGE_DIGEST_SIZE = 32;
|
|
58
|
+
var TOKEN_KEY_ID_SIZE = 32;
|
|
59
|
+
function base64urlEncode(buf) {
|
|
60
|
+
return Buffer.from(buf).toString("base64url");
|
|
61
|
+
}
|
|
62
|
+
function base64urlDecode(str) {
|
|
63
|
+
return new Uint8Array(Buffer.from(str, "base64url"));
|
|
64
|
+
}
|
|
65
|
+
function bytesToBigInt(bytes) {
|
|
66
|
+
let hex = "";
|
|
67
|
+
for (const b of bytes) {
|
|
68
|
+
hex += b.toString(16).padStart(2, "0");
|
|
69
|
+
}
|
|
70
|
+
return hex.length === 0 ? 0n : BigInt(`0x${hex}`);
|
|
71
|
+
}
|
|
72
|
+
function bigIntToBytes(n, length) {
|
|
73
|
+
if (!Number.isInteger(length) || length < 0) {
|
|
74
|
+
throw new Error("Integer encoding length must be a non-negative integer");
|
|
75
|
+
}
|
|
76
|
+
if (n < 0n || n >= 1n << BigInt(length * 8)) {
|
|
77
|
+
throw new Error(`Integer does not fit in ${length} bytes`);
|
|
78
|
+
}
|
|
79
|
+
const hex = n.toString(16).padStart(length * 2, "0");
|
|
80
|
+
const bytes = new Uint8Array(length);
|
|
81
|
+
for (let i = 0; i < length; i++) {
|
|
82
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
83
|
+
}
|
|
84
|
+
return bytes;
|
|
85
|
+
}
|
|
86
|
+
function bitLength(value) {
|
|
87
|
+
if (value <= 0n) throw new Error("Bit length requires a positive integer");
|
|
88
|
+
return value.toString(2).length;
|
|
89
|
+
}
|
|
90
|
+
function modPow(base, exp, mod) {
|
|
91
|
+
let result = 1n;
|
|
92
|
+
let b = (base % mod + mod) % mod;
|
|
93
|
+
let e = exp;
|
|
94
|
+
while (e > 0n) {
|
|
95
|
+
if (e & 1n) {
|
|
96
|
+
result = result * b % mod;
|
|
97
|
+
}
|
|
98
|
+
e >>= 1n;
|
|
99
|
+
b = b * b % mod;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function modInverse(a, m) {
|
|
104
|
+
let [oldR, r] = [a, m];
|
|
105
|
+
let [oldS, s] = [1n, 0n];
|
|
106
|
+
while (r !== 0n) {
|
|
107
|
+
const q = oldR / r;
|
|
108
|
+
[oldR, r] = [r, oldR - q * r];
|
|
109
|
+
[oldS, s] = [s, oldS - q * s];
|
|
110
|
+
}
|
|
111
|
+
if (oldR !== 1n) {
|
|
112
|
+
throw new Error("Value has no modular inverse");
|
|
113
|
+
}
|
|
114
|
+
return (oldS % m + m) % m;
|
|
115
|
+
}
|
|
116
|
+
function greatestCommonDivisor(a, b) {
|
|
117
|
+
let x = a < 0n ? -a : a;
|
|
118
|
+
let y = b < 0n ? -b : b;
|
|
119
|
+
while (y !== 0n) {
|
|
120
|
+
[x, y] = [y, x % y];
|
|
121
|
+
}
|
|
122
|
+
return x;
|
|
123
|
+
}
|
|
124
|
+
function parseSpkiPublicKey(spkiDer) {
|
|
125
|
+
let keyObject;
|
|
126
|
+
try {
|
|
127
|
+
keyObject = createPublicKey({
|
|
128
|
+
key: Buffer.from(spkiDer),
|
|
129
|
+
format: "der",
|
|
130
|
+
type: "spki"
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new Error("Invalid RSA issuer public key", { cause: error });
|
|
134
|
+
}
|
|
135
|
+
const details = keyObject.asymmetricKeyDetails;
|
|
136
|
+
if (keyObject.asymmetricKeyType !== "rsa-pss" || details?.modulusLength !== 2048 || details.hashAlgorithm !== "sha384" || details.mgf1HashAlgorithm !== "sha384" || details.saltLength !== 48) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"Issuer key must use 2048-bit RSA-PSS with SHA-384, MGF1-SHA-384, and a 48-byte salt"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const spki = readSequence(spkiDer, 0);
|
|
142
|
+
const algorithm = readTlv(spkiDer, spki.contentStart);
|
|
143
|
+
const bitString = readTlv(spkiDer, algorithm.end);
|
|
144
|
+
if (bitString.tag !== 3) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Expected BIT STRING in SPKI, got 0x${bitString.tag.toString(16)}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (bitString.contentStart >= bitString.end || spkiDer[bitString.contentStart] !== 0) {
|
|
150
|
+
throw new Error("RSA SPKI BIT STRING must have zero unused bits");
|
|
151
|
+
}
|
|
152
|
+
const rsaPublicKeyDer = spkiDer.slice(
|
|
153
|
+
bitString.contentStart + 1,
|
|
154
|
+
bitString.end
|
|
155
|
+
);
|
|
156
|
+
const rsaPublicKey = readSequence(rsaPublicKeyDer, 0);
|
|
157
|
+
const modulus = readInteger(rsaPublicKeyDer, rsaPublicKey.contentStart);
|
|
158
|
+
const exponent = readInteger(rsaPublicKeyDer, modulus.end);
|
|
159
|
+
const n = bytesToBigInt(modulus.value);
|
|
160
|
+
const e = bytesToBigInt(exponent.value);
|
|
161
|
+
if (modulus.value.length !== 256 || bitLength(n) !== 2048) {
|
|
162
|
+
throw new Error("Token type 0x0002 requires a 2048-bit RSA modulus");
|
|
163
|
+
}
|
|
164
|
+
if (e < 3n || e % 2n === 0n) {
|
|
165
|
+
throw new Error("RSA public exponent must be an odd integer of at least 3");
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
n,
|
|
169
|
+
e,
|
|
170
|
+
nLen: modulus.value.length
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function readTlv(data, offset) {
|
|
174
|
+
if (offset >= data.length) {
|
|
175
|
+
throw new Error(`Unexpected end of DER at offset ${offset}`);
|
|
176
|
+
}
|
|
177
|
+
const tag = data[offset];
|
|
178
|
+
if (tag === void 0) {
|
|
179
|
+
throw new Error(`Unexpected end of DER at offset ${offset}`);
|
|
180
|
+
}
|
|
181
|
+
const { value: length, bytesRead } = parseDerLength(data, offset + 1);
|
|
182
|
+
const contentStart = offset + 1 + bytesRead;
|
|
183
|
+
const end = contentStart + length;
|
|
184
|
+
if (end > data.length) {
|
|
185
|
+
throw new Error(`DER element at offset ${offset} overruns the buffer`);
|
|
186
|
+
}
|
|
187
|
+
return { tag, contentStart, end };
|
|
188
|
+
}
|
|
189
|
+
function readSequence(data, offset) {
|
|
190
|
+
const tlv = readTlv(data, offset);
|
|
191
|
+
if (tlv.tag !== 48) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Expected SEQUENCE at offset ${offset}, got 0x${tlv.tag.toString(16)}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return tlv;
|
|
197
|
+
}
|
|
198
|
+
function readInteger(data, offset) {
|
|
199
|
+
const tlv = readTlv(data, offset);
|
|
200
|
+
if (tlv.tag !== 2) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Expected INTEGER at offset ${offset}, got 0x${tlv.tag.toString(16)}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
let value = data.slice(tlv.contentStart, tlv.end);
|
|
206
|
+
if (value.length > 1 && value[0] === 0) {
|
|
207
|
+
value = value.slice(1);
|
|
208
|
+
}
|
|
209
|
+
return { value, end: tlv.end };
|
|
210
|
+
}
|
|
211
|
+
function parseDerLength(data, offset) {
|
|
212
|
+
const first = data[offset];
|
|
213
|
+
if (first === void 0) {
|
|
214
|
+
throw new Error(`Unexpected end of DER at offset ${offset}`);
|
|
215
|
+
}
|
|
216
|
+
if (first < 128) {
|
|
217
|
+
return { value: first, bytesRead: 1 };
|
|
218
|
+
}
|
|
219
|
+
const numBytes = first & 127;
|
|
220
|
+
if (numBytes === 0 || offset + numBytes >= data.length) {
|
|
221
|
+
throw new Error(`Invalid DER length at offset ${offset}`);
|
|
222
|
+
}
|
|
223
|
+
let value = 0;
|
|
224
|
+
for (let i = 0; i < numBytes; i++) {
|
|
225
|
+
const byte = data[offset + 1 + i];
|
|
226
|
+
if (byte === void 0) {
|
|
227
|
+
throw new Error(`Unexpected end of DER at offset ${offset + 1 + i}`);
|
|
228
|
+
}
|
|
229
|
+
value = value * 256 + byte;
|
|
230
|
+
}
|
|
231
|
+
return { value, bytesRead: 1 + numBytes };
|
|
232
|
+
}
|
|
233
|
+
function emsaPssEncode(message, emBits) {
|
|
234
|
+
const hashAlg = "sha384";
|
|
235
|
+
const hLen = 48;
|
|
236
|
+
const sLen = 48;
|
|
237
|
+
const emLen = Math.ceil(emBits / 8);
|
|
238
|
+
const mHash = createHash(hashAlg).update(message).digest();
|
|
239
|
+
if (emLen < hLen + sLen + 2) {
|
|
240
|
+
throw new Error("Encoding error: emLen too small");
|
|
241
|
+
}
|
|
242
|
+
const salt = randomBytes(sLen);
|
|
243
|
+
const mPrime = Buffer.concat([Buffer.alloc(8), mHash, salt]);
|
|
244
|
+
const h = createHash(hashAlg).update(mPrime).digest();
|
|
245
|
+
const ps = Buffer.alloc(emLen - sLen - hLen - 2);
|
|
246
|
+
const db = Buffer.concat([ps, Buffer.from([1]), salt]);
|
|
247
|
+
const dbMask = mgf1(h, db.length, hashAlg);
|
|
248
|
+
const maskedDb = Buffer.alloc(db.length);
|
|
249
|
+
for (let i = 0; i < db.length; i++) {
|
|
250
|
+
maskedDb.writeUInt8(db.readUInt8(i) ^ dbMask.readUInt8(i), i);
|
|
251
|
+
}
|
|
252
|
+
const topBits = 8 * emLen - emBits;
|
|
253
|
+
maskedDb.writeUInt8(maskedDb.readUInt8(0) & 255 >> topBits, 0);
|
|
254
|
+
return new Uint8Array(Buffer.concat([maskedDb, h, Buffer.from([188])]));
|
|
255
|
+
}
|
|
256
|
+
function mgf1(seed, length, hashAlg) {
|
|
257
|
+
const hLen = hashAlg === "sha384" ? 48 : 32;
|
|
258
|
+
const result = Buffer.alloc(length);
|
|
259
|
+
let offset = 0;
|
|
260
|
+
let counter = 0;
|
|
261
|
+
while (offset < length) {
|
|
262
|
+
const c = Buffer.alloc(4);
|
|
263
|
+
c.writeUInt32BE(counter);
|
|
264
|
+
const hash = createHash(hashAlg).update(seed).update(c).digest();
|
|
265
|
+
const toCopy = Math.min(hLen, length - offset);
|
|
266
|
+
hash.copy(result, offset, 0, toCopy);
|
|
267
|
+
offset += toCopy;
|
|
268
|
+
counter++;
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
function generateBlindingFactor(n, nLen) {
|
|
273
|
+
while (true) {
|
|
274
|
+
const rBytes = randomBytes(nLen);
|
|
275
|
+
const r = bytesToBigInt(new Uint8Array(rBytes));
|
|
276
|
+
if (r < 1n || r >= n) continue;
|
|
277
|
+
try {
|
|
278
|
+
const rInv = modInverse(r, n);
|
|
279
|
+
return { r, rInv };
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function generateBlindedMessages(spkiDer, count, challengeDigest) {
|
|
285
|
+
if (!Number.isInteger(count) || count < 1 || count > 100) {
|
|
286
|
+
throw new Error("Token count must be an integer from 1 to 100");
|
|
287
|
+
}
|
|
288
|
+
if (challengeDigest.length !== CHALLENGE_DIGEST_SIZE) {
|
|
289
|
+
throw new Error("Challenge digest must be exactly 32 bytes");
|
|
290
|
+
}
|
|
291
|
+
const publicKey = parseSpkiPublicKey(spkiDer);
|
|
292
|
+
const tokenKeyId = new Uint8Array(
|
|
293
|
+
createHash("sha256").update(spkiDer).digest()
|
|
294
|
+
);
|
|
295
|
+
const emBits = bitLength(publicKey.n) - 1;
|
|
296
|
+
const tokens = [];
|
|
297
|
+
for (let i = 0; i < count; i++) {
|
|
298
|
+
const nonce = new Uint8Array(randomBytes(NONCE_SIZE));
|
|
299
|
+
const tokenInput = new Uint8Array(
|
|
300
|
+
2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE + TOKEN_KEY_ID_SIZE
|
|
301
|
+
);
|
|
302
|
+
tokenInput[0] = TOKEN_TYPE >> 8 & 255;
|
|
303
|
+
tokenInput[1] = TOKEN_TYPE & 255;
|
|
304
|
+
tokenInput.set(nonce, 2);
|
|
305
|
+
tokenInput.set(challengeDigest, 2 + NONCE_SIZE);
|
|
306
|
+
tokenInput.set(tokenKeyId, 2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE);
|
|
307
|
+
const encoded = emsaPssEncode(tokenInput, emBits);
|
|
308
|
+
const message = bytesToBigInt(encoded);
|
|
309
|
+
if (greatestCommonDivisor(message, publicKey.n) !== 1n) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
"PSS-encoded token input is not coprime to the RSA modulus"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const { r, rInv } = generateBlindingFactor(publicKey.n, publicKey.nLen);
|
|
315
|
+
const blindedMessage = message * modPow(r, publicKey.e, publicKey.n) % publicKey.n;
|
|
316
|
+
tokens.push({
|
|
317
|
+
nonce,
|
|
318
|
+
blindedMsg: bigIntToBytes(blindedMessage, publicKey.nLen),
|
|
319
|
+
blindInverse: rInv,
|
|
320
|
+
encodedMessage: encoded
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return { tokens, publicKey, challengeDigest, tokenKeyId };
|
|
324
|
+
}
|
|
325
|
+
function unblindSignatures(state, blindSigs) {
|
|
326
|
+
const { tokens, publicKey, tokenKeyId } = state;
|
|
327
|
+
if (blindSigs.length !== tokens.length) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Expected ${tokens.length} blind signatures, got ${blindSigs.length}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const results = [];
|
|
333
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
334
|
+
const token = tokens[i];
|
|
335
|
+
const blindSignature = blindSigs[i];
|
|
336
|
+
if (!token || blindSignature === void 0) {
|
|
337
|
+
throw new Error(`Missing blind signature state at index ${i}`);
|
|
338
|
+
}
|
|
339
|
+
const blindSigBytes = base64urlDecode(blindSignature);
|
|
340
|
+
if (blindSigBytes.length !== publicKey.nLen) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Blind signature ${i} must be exactly ${publicKey.nLen} bytes`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const blindSigInt = bytesToBigInt(blindSigBytes);
|
|
346
|
+
const sigInt = blindSigInt * token.blindInverse % publicKey.n;
|
|
347
|
+
const authenticator = bigIntToBytes(sigInt, publicKey.nLen);
|
|
348
|
+
const recoveredMessage = bigIntToBytes(
|
|
349
|
+
modPow(sigInt, publicKey.e, publicKey.n),
|
|
350
|
+
publicKey.nLen
|
|
351
|
+
);
|
|
352
|
+
if (!timingSafeEqual(
|
|
353
|
+
Buffer.from(recoveredMessage),
|
|
354
|
+
Buffer.from(token.encodedMessage)
|
|
355
|
+
)) {
|
|
356
|
+
throw new Error(`Blind signature ${i} failed verification`);
|
|
357
|
+
}
|
|
358
|
+
const raw = new Uint8Array(
|
|
359
|
+
2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE + TOKEN_KEY_ID_SIZE + publicKey.nLen
|
|
360
|
+
);
|
|
361
|
+
raw[0] = TOKEN_TYPE >> 8 & 255;
|
|
362
|
+
raw[1] = TOKEN_TYPE & 255;
|
|
363
|
+
raw.set(token.nonce, 2);
|
|
364
|
+
raw.set(state.challengeDigest, 2 + NONCE_SIZE);
|
|
365
|
+
raw.set(tokenKeyId, 2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE);
|
|
366
|
+
raw.set(
|
|
367
|
+
authenticator,
|
|
368
|
+
2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE + TOKEN_KEY_ID_SIZE
|
|
369
|
+
);
|
|
370
|
+
results.push({
|
|
371
|
+
raw,
|
|
372
|
+
base64url: base64urlEncode(raw)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return results;
|
|
376
|
+
}
|
|
377
|
+
function computeChallengeDigest(tokenType, issuerName) {
|
|
378
|
+
const issuerBytes = Buffer.from(issuerName, "utf-8");
|
|
379
|
+
const originBytes = Buffer.alloc(0);
|
|
380
|
+
if (!Number.isInteger(tokenType) || tokenType < 0 || tokenType > 65535) {
|
|
381
|
+
throw new Error("Token type must fit in an unsigned 16-bit integer");
|
|
382
|
+
}
|
|
383
|
+
if (issuerBytes.length > 65535) {
|
|
384
|
+
throw new Error("Issuer name must fit in an unsigned 16-bit length");
|
|
385
|
+
}
|
|
386
|
+
const challenge = Buffer.alloc(
|
|
387
|
+
2 + 2 + issuerBytes.length + 1 + 2 + originBytes.length
|
|
388
|
+
);
|
|
389
|
+
let offset = 0;
|
|
390
|
+
challenge.writeUInt16BE(tokenType, offset);
|
|
391
|
+
offset += 2;
|
|
392
|
+
challenge.writeUInt16BE(issuerBytes.length, offset);
|
|
393
|
+
offset += 2;
|
|
394
|
+
issuerBytes.copy(challenge, offset);
|
|
395
|
+
offset += issuerBytes.length;
|
|
396
|
+
challenge.writeUInt8(0, offset);
|
|
397
|
+
offset += 1;
|
|
398
|
+
challenge.writeUInt16BE(originBytes.length, offset);
|
|
399
|
+
offset += 2;
|
|
400
|
+
if (originBytes.length > 0) {
|
|
401
|
+
originBytes.copy(challenge, offset);
|
|
402
|
+
}
|
|
403
|
+
return new Uint8Array(createHash("sha256").update(challenge).digest());
|
|
404
|
+
}
|
|
405
|
+
|
|
47
406
|
// src/config.ts
|
|
48
407
|
var DEFAULT_API_BASE_URL = "https://api.link.com";
|
|
49
408
|
function createDefaultHeadersFetch(baseFetch, defaultHeaders) {
|
|
@@ -162,7 +521,8 @@ var BaseResource = class {
|
|
|
162
521
|
method: opts.method,
|
|
163
522
|
...opts.headers !== void 0 && { headers: opts.headers },
|
|
164
523
|
...opts.body !== void 0 && { body: opts.body },
|
|
165
|
-
...opts.signal !== void 0 && { signal: opts.signal }
|
|
524
|
+
...opts.signal !== void 0 && { signal: opts.signal },
|
|
525
|
+
...opts.redirect !== void 0 && { redirect: opts.redirect }
|
|
166
526
|
};
|
|
167
527
|
response = await this.fetchImpl(opts.url, init);
|
|
168
528
|
} catch (error) {
|
|
@@ -194,8 +554,13 @@ var BaseResource = class {
|
|
|
194
554
|
const res = await this.rawFetch(authedOpts);
|
|
195
555
|
if (res.status === 401 && this.canRefreshAccessToken) {
|
|
196
556
|
const refreshedToken = await this.getAccessToken({ forceRefresh: true });
|
|
197
|
-
|
|
198
|
-
|
|
557
|
+
return this.rawFetch({
|
|
558
|
+
...opts,
|
|
559
|
+
headers: {
|
|
560
|
+
...opts.headers,
|
|
561
|
+
Authorization: `Bearer ${refreshedToken}`
|
|
562
|
+
}
|
|
563
|
+
});
|
|
199
564
|
}
|
|
200
565
|
return res;
|
|
201
566
|
}
|
|
@@ -217,20 +582,350 @@ var BaseResource = class {
|
|
|
217
582
|
}
|
|
218
583
|
};
|
|
219
584
|
|
|
585
|
+
// src/resources/attestations.ts
|
|
586
|
+
var TOKEN_TYPE_BLIND_RSA = 2;
|
|
587
|
+
var CONTENT_TYPE_TOKEN_REQUEST = "application/private-token-generic-batch-request";
|
|
588
|
+
var CONTENT_TYPE_TOKEN_RESPONSE = "application/private-token-generic-batch-response";
|
|
589
|
+
var LINK_ISSUER = "https://api.link.com";
|
|
590
|
+
var LINK_ISSUER_HOSTNAME = "api.link.com";
|
|
591
|
+
var LINK_ISSUER_METADATA_URL = `${LINK_ISSUER}/.well-known/aap-issuer`;
|
|
592
|
+
var issuerMetadataSchema = z.looseObject({
|
|
593
|
+
issuer: z.literal(LINK_ISSUER),
|
|
594
|
+
token_issuance_endpoint: z.string(),
|
|
595
|
+
token_keys: z.string()
|
|
596
|
+
});
|
|
597
|
+
var tokenKeyDirectorySchema = z.looseObject({
|
|
598
|
+
"token-keys": z.array(
|
|
599
|
+
z.looseObject({
|
|
600
|
+
"token-type": z.number(),
|
|
601
|
+
"token-key": z.string(),
|
|
602
|
+
"not-before": z.number().int().nonnegative().optional()
|
|
603
|
+
})
|
|
604
|
+
)
|
|
605
|
+
});
|
|
606
|
+
function base64ToBytes(value) {
|
|
607
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
608
|
+
return new Uint8Array(Buffer.from(normalized, "base64"));
|
|
609
|
+
}
|
|
610
|
+
function minQuicVarintLength(value) {
|
|
611
|
+
if (value < 2 ** 6) return 1;
|
|
612
|
+
if (value < 2 ** 14) return 2;
|
|
613
|
+
if (value < 2 ** 30) return 4;
|
|
614
|
+
return 8;
|
|
615
|
+
}
|
|
616
|
+
function encodeQuicVarint(value) {
|
|
617
|
+
if (!Number.isSafeInteger(value) || value < 0 || value >= 2 ** 62) {
|
|
618
|
+
throw new Error(`Cannot encode ${value} as a QUIC variable-length integer`);
|
|
619
|
+
}
|
|
620
|
+
const length = minQuicVarintLength(value);
|
|
621
|
+
const encoded = Buffer.alloc(length);
|
|
622
|
+
let remaining = BigInt(value);
|
|
623
|
+
for (let index = length - 1; index >= 0; index--) {
|
|
624
|
+
encoded[index] = Number(remaining & 0xffn);
|
|
625
|
+
remaining >>= 8n;
|
|
626
|
+
}
|
|
627
|
+
encoded[0] = encoded.readUInt8(0) | Math.log2(length) << 6;
|
|
628
|
+
return encoded;
|
|
629
|
+
}
|
|
630
|
+
function readQuicVarint(body, offset = 0) {
|
|
631
|
+
const first = body[offset];
|
|
632
|
+
if (first === void 0) throw new Error("QUIC varint is absent");
|
|
633
|
+
const length = 1 << (first >> 6);
|
|
634
|
+
if (body.length < offset + length)
|
|
635
|
+
throw new Error("QUIC varint is truncated");
|
|
636
|
+
let value = BigInt(first & 63);
|
|
637
|
+
for (let index = 1; index < length; index++) {
|
|
638
|
+
value = value << 8n | BigInt(body.readUInt8(offset + index));
|
|
639
|
+
}
|
|
640
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
641
|
+
throw new Error("QUIC varint exceeds the JavaScript safe integer range");
|
|
642
|
+
}
|
|
643
|
+
const numeric = Number(value);
|
|
644
|
+
if (minQuicVarintLength(numeric) !== length) {
|
|
645
|
+
throw new Error("QUIC varint is not minimally encoded");
|
|
646
|
+
}
|
|
647
|
+
return { value: numeric, length };
|
|
648
|
+
}
|
|
649
|
+
function encodeBatchTokenRequest(blindedMessages, truncatedTokenKeyId) {
|
|
650
|
+
const entries = blindedMessages.map((blindedMessage) => {
|
|
651
|
+
const entry = Buffer.alloc(3 + blindedMessage.length);
|
|
652
|
+
entry.writeUInt16BE(TOKEN_TYPE_BLIND_RSA, 0);
|
|
653
|
+
entry.writeUInt8(truncatedTokenKeyId, 2);
|
|
654
|
+
Buffer.from(blindedMessage).copy(entry, 3);
|
|
655
|
+
return entry;
|
|
656
|
+
});
|
|
657
|
+
const vector = Buffer.concat(entries);
|
|
658
|
+
return Buffer.concat([encodeQuicVarint(vector.length), vector]);
|
|
659
|
+
}
|
|
660
|
+
function decodeBatchTokenResponse(body, elementSize, expectedCount) {
|
|
661
|
+
const prefix = readQuicVarint(body);
|
|
662
|
+
const vector = body.subarray(prefix.length);
|
|
663
|
+
if (vector.length !== prefix.value) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
`BatchTokenResponse length prefix says ${prefix.value} bytes but ${vector.length} bytes follow`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if (vector.length === 0)
|
|
669
|
+
throw new Error("BatchTokenResponse vector is empty");
|
|
670
|
+
const signatures = [];
|
|
671
|
+
let offset = 0;
|
|
672
|
+
while (offset < vector.length) {
|
|
673
|
+
const present = vector[offset++];
|
|
674
|
+
if (present === 0) {
|
|
675
|
+
throw new Error(
|
|
676
|
+
`Issuer refused token request at index ${signatures.length}`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (present !== 1) {
|
|
680
|
+
throw new Error(`Invalid OptionalTokenResponse presence byte ${present}`);
|
|
681
|
+
}
|
|
682
|
+
if (offset + 2 + elementSize > vector.length) {
|
|
683
|
+
throw new Error("Present GenericTokenResponse is truncated");
|
|
684
|
+
}
|
|
685
|
+
const tokenType = vector.readUInt16BE(offset);
|
|
686
|
+
offset += 2;
|
|
687
|
+
if (tokenType !== TOKEN_TYPE_BLIND_RSA) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`GenericTokenResponse has unsupported token type 0x${tokenType.toString(16).padStart(4, "0")}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
signatures.push(
|
|
693
|
+
base64urlEncode(
|
|
694
|
+
new Uint8Array(vector.subarray(offset, offset + elementSize))
|
|
695
|
+
)
|
|
696
|
+
);
|
|
697
|
+
offset += elementSize;
|
|
698
|
+
}
|
|
699
|
+
if (signatures.length !== expectedCount) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
`Issuer returned ${signatures.length} token responses for ${expectedCount} token requests`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
return signatures;
|
|
705
|
+
}
|
|
706
|
+
function requireLinkEndpoint(value, field) {
|
|
707
|
+
let url;
|
|
708
|
+
try {
|
|
709
|
+
url = new URL(value);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
throw new TypeError(`${field} is not a valid URL`, { cause: error });
|
|
712
|
+
}
|
|
713
|
+
if (url.protocol !== "https:" || url.origin !== LINK_ISSUER || url.username || url.password) {
|
|
714
|
+
throw new TypeError(`${field} must be an HTTPS URL on ${LINK_ISSUER}`);
|
|
715
|
+
}
|
|
716
|
+
return url.href;
|
|
717
|
+
}
|
|
718
|
+
var AttestationsResource = class extends BaseResource {
|
|
719
|
+
/** Creates an attestation resource using the SDK's authentication config. */
|
|
720
|
+
constructor(options) {
|
|
721
|
+
super(options, "");
|
|
722
|
+
}
|
|
723
|
+
/** Fetches JSON without following redirects and normalizes API errors. */
|
|
724
|
+
async fetchJson(url, operation) {
|
|
725
|
+
let response;
|
|
726
|
+
try {
|
|
727
|
+
response = await this.fetchImpl(url, { redirect: "manual" });
|
|
728
|
+
} catch (error) {
|
|
729
|
+
throw new LinkTransportError(`Request failed: GET ${url}`, {
|
|
730
|
+
cause: error
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
const rawBody = await response.text();
|
|
734
|
+
if (response.status >= 300 && response.status < 400) {
|
|
735
|
+
throw new LinkApiError(
|
|
736
|
+
`Refused redirect while attempting to ${operation} (${response.status})`,
|
|
737
|
+
{
|
|
738
|
+
status: response.status,
|
|
739
|
+
rawBody
|
|
740
|
+
}
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
let data = null;
|
|
744
|
+
try {
|
|
745
|
+
data = JSON.parse(rawBody);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
if (response.ok) {
|
|
748
|
+
throw new LinkResponseError(operation, response.status, {
|
|
749
|
+
cause: error
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (!response.ok) {
|
|
754
|
+
throw new LinkApiError(
|
|
755
|
+
`Failed to ${operation} (${response.status}): ${rawBody}`,
|
|
756
|
+
{
|
|
757
|
+
status: response.status,
|
|
758
|
+
rawBody,
|
|
759
|
+
details: data
|
|
760
|
+
}
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return { data, status: response.status };
|
|
764
|
+
}
|
|
765
|
+
/** Sends a blinded batch with the current or refreshed access token. */
|
|
766
|
+
async issueTokens(url, body, forceRefresh = false) {
|
|
767
|
+
const token = await this.getAccessToken(
|
|
768
|
+
forceRefresh ? { forceRefresh: true } : void 0
|
|
769
|
+
);
|
|
770
|
+
const requestBody = new ArrayBuffer(body.byteLength);
|
|
771
|
+
new Uint8Array(requestBody).set(body);
|
|
772
|
+
try {
|
|
773
|
+
return await this.fetchImpl(url, {
|
|
774
|
+
method: "POST",
|
|
775
|
+
redirect: "manual",
|
|
776
|
+
headers: {
|
|
777
|
+
"Content-Type": CONTENT_TYPE_TOKEN_REQUEST,
|
|
778
|
+
Accept: CONTENT_TYPE_TOKEN_RESPONSE,
|
|
779
|
+
Authorization: `Bearer ${token}`
|
|
780
|
+
},
|
|
781
|
+
body: requestBody
|
|
782
|
+
});
|
|
783
|
+
} catch (error) {
|
|
784
|
+
throw new LinkTransportError(`Request failed: POST ${url}`, {
|
|
785
|
+
cause: error
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
/** Requests, verifies, and returns a batch of Link attestation tokens. */
|
|
790
|
+
async request(params) {
|
|
791
|
+
const { count } = params;
|
|
792
|
+
if (!Number.isInteger(count) || count < 1 || count > 100) {
|
|
793
|
+
throw new LinkConfigurationError(
|
|
794
|
+
"Attestation token count must be an integer from 1 to 100"
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const metadataResponse = await this.fetchJson(
|
|
798
|
+
LINK_ISSUER_METADATA_URL,
|
|
799
|
+
"fetch issuer metadata"
|
|
800
|
+
);
|
|
801
|
+
const metadata = this.parseResponse(
|
|
802
|
+
"parse issuer metadata",
|
|
803
|
+
metadataResponse.status,
|
|
804
|
+
() => issuerMetadataSchema.parse(metadataResponse.data)
|
|
805
|
+
);
|
|
806
|
+
let tokenKeysUrl;
|
|
807
|
+
let issuanceUrl;
|
|
808
|
+
try {
|
|
809
|
+
tokenKeysUrl = requireLinkEndpoint(metadata.token_keys, "token_keys");
|
|
810
|
+
issuanceUrl = requireLinkEndpoint(
|
|
811
|
+
metadata.token_issuance_endpoint,
|
|
812
|
+
"token_issuance_endpoint"
|
|
813
|
+
);
|
|
814
|
+
} catch (error) {
|
|
815
|
+
throw new LinkResponseError(
|
|
816
|
+
"validate issuer metadata",
|
|
817
|
+
metadataResponse.status,
|
|
818
|
+
{ cause: error }
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
const directoryResponse = await this.fetchJson(
|
|
822
|
+
tokenKeysUrl,
|
|
823
|
+
"fetch token keys"
|
|
824
|
+
);
|
|
825
|
+
const directory = this.parseResponse(
|
|
826
|
+
"parse token key directory",
|
|
827
|
+
directoryResponse.status,
|
|
828
|
+
() => tokenKeyDirectorySchema.parse(directoryResponse.data)
|
|
829
|
+
);
|
|
830
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
831
|
+
const tokenKey = directory["token-keys"].find(
|
|
832
|
+
(entry) => entry["token-type"] === TOKEN_TYPE_BLIND_RSA && (entry["not-before"] === void 0 || entry["not-before"] <= now)
|
|
833
|
+
);
|
|
834
|
+
if (!tokenKey) {
|
|
835
|
+
throw new LinkResponseError(
|
|
836
|
+
"select Blind RSA token key",
|
|
837
|
+
directoryResponse.status,
|
|
838
|
+
{
|
|
839
|
+
cause: new Error(
|
|
840
|
+
"No active token key with type 0x0002 found in directory"
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
const spkiDer = base64ToBytes(tokenKey["token-key"]);
|
|
846
|
+
const challengeDigest = computeChallengeDigest(
|
|
847
|
+
TOKEN_TYPE_BLIND_RSA,
|
|
848
|
+
LINK_ISSUER_HOSTNAME
|
|
849
|
+
);
|
|
850
|
+
const blindingState = generateBlindedMessages(
|
|
851
|
+
spkiDer,
|
|
852
|
+
count,
|
|
853
|
+
challengeDigest
|
|
854
|
+
);
|
|
855
|
+
const tokenKeyIdBytes = new Uint8Array(
|
|
856
|
+
createHash2("sha256").update(spkiDer).digest()
|
|
857
|
+
);
|
|
858
|
+
const truncatedTokenKeyId = tokenKeyIdBytes.at(-1);
|
|
859
|
+
if (truncatedTokenKeyId === void 0) {
|
|
860
|
+
throw new LinkResponseError("derive Blind RSA token key ID", 200);
|
|
861
|
+
}
|
|
862
|
+
const requestBody = encodeBatchTokenRequest(
|
|
863
|
+
blindingState.tokens.map((token) => token.blindedMsg),
|
|
864
|
+
truncatedTokenKeyId
|
|
865
|
+
);
|
|
866
|
+
let issueResponse = await this.issueTokens(
|
|
867
|
+
issuanceUrl,
|
|
868
|
+
new Uint8Array(requestBody)
|
|
869
|
+
);
|
|
870
|
+
if (issueResponse.status === 401 && this.canRefreshAccessToken) {
|
|
871
|
+
issueResponse = await this.issueTokens(
|
|
872
|
+
issuanceUrl,
|
|
873
|
+
new Uint8Array(requestBody),
|
|
874
|
+
true
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
if (!issueResponse.ok) {
|
|
878
|
+
const rawBody = await issueResponse.text();
|
|
879
|
+
throw new LinkApiError(
|
|
880
|
+
`Failed to issue attestation tokens (${issueResponse.status}): ${rawBody}`,
|
|
881
|
+
{
|
|
882
|
+
status: issueResponse.status,
|
|
883
|
+
rawBody
|
|
884
|
+
}
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
let blindSignatures;
|
|
888
|
+
try {
|
|
889
|
+
blindSignatures = decodeBatchTokenResponse(
|
|
890
|
+
Buffer.from(await issueResponse.arrayBuffer()),
|
|
891
|
+
blindingState.publicKey.nLen,
|
|
892
|
+
count
|
|
893
|
+
);
|
|
894
|
+
} catch (error) {
|
|
895
|
+
throw new LinkResponseError(
|
|
896
|
+
"decode attestation token response",
|
|
897
|
+
issueResponse.status,
|
|
898
|
+
{ cause: error }
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
const finalTokens = unblindSignatures(
|
|
902
|
+
blindingState,
|
|
903
|
+
blindSignatures
|
|
904
|
+
);
|
|
905
|
+
return {
|
|
906
|
+
tokens: finalTokens.map((finalToken) => finalToken.base64url),
|
|
907
|
+
issuer: LINK_ISSUER,
|
|
908
|
+
token_key_id: base64urlEncode(blindingState.tokenKeyId),
|
|
909
|
+
count: finalTokens.length
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
|
|
220
914
|
// src/resources/balances.ts
|
|
221
|
-
|
|
222
|
-
var
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
915
|
+
import { z as z2 } from "zod";
|
|
916
|
+
var currencyAmountsSchema = z2.record(z2.string(), z2.number());
|
|
917
|
+
var balanceSchema = z2.looseObject({
|
|
918
|
+
source_id: z2.string(),
|
|
919
|
+
type: z2.enum(["cash", "credit"]),
|
|
920
|
+
cash: z2.looseObject({ available: currencyAmountsSchema }).nullable().optional(),
|
|
921
|
+
credit: z2.looseObject({ used: currencyAmountsSchema.nullable() }).nullable().optional(),
|
|
922
|
+
current: z2.number(),
|
|
923
|
+
currency: z2.string(),
|
|
924
|
+
as_of: z2.string()
|
|
230
925
|
});
|
|
231
|
-
var balancesPageSchema =
|
|
232
|
-
data:
|
|
233
|
-
has_more:
|
|
926
|
+
var balancesPageSchema = z2.looseObject({
|
|
927
|
+
data: z2.array(balanceSchema),
|
|
928
|
+
has_more: z2.boolean().optional()
|
|
234
929
|
});
|
|
235
930
|
var BalancesResource = class extends BaseResource {
|
|
236
931
|
constructor(options) {
|
|
@@ -270,17 +965,143 @@ var BalancesResource = class extends BaseResource {
|
|
|
270
965
|
}
|
|
271
966
|
};
|
|
272
967
|
|
|
968
|
+
// src/resources/identity-credentials.ts
|
|
969
|
+
import { z as z3 } from "zod";
|
|
970
|
+
|
|
971
|
+
// src/resources/holder-jwk.ts
|
|
972
|
+
import { createHash as createHash3 } from "crypto";
|
|
973
|
+
function isRecord2(value) {
|
|
974
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
975
|
+
}
|
|
976
|
+
function holderJwkThumbprint(jwk) {
|
|
977
|
+
const canonical = JSON.stringify({
|
|
978
|
+
crv: jwk.crv,
|
|
979
|
+
kty: jwk.kty,
|
|
980
|
+
x: jwk.x
|
|
981
|
+
});
|
|
982
|
+
return createHash3("sha256").update(canonical).digest("base64url");
|
|
983
|
+
}
|
|
984
|
+
function holderJwksEqual(left, right) {
|
|
985
|
+
return holderJwkThumbprint(left) === holderJwkThumbprint(right);
|
|
986
|
+
}
|
|
987
|
+
function parseHolderPublicJwk(value) {
|
|
988
|
+
if (!isRecord2(value)) {
|
|
989
|
+
throw new LinkConfigurationError("Holder public key must be a JWK object");
|
|
990
|
+
}
|
|
991
|
+
if (typeof value.kty !== "string") {
|
|
992
|
+
throw new LinkConfigurationError("Holder public key is missing kty");
|
|
993
|
+
}
|
|
994
|
+
if (value.kty !== "OKP" || value.crv !== "Ed25519" || typeof value.x !== "string" || value.x.length === 0) {
|
|
995
|
+
throw new LinkConfigurationError(
|
|
996
|
+
"Holder public key must be an Ed25519 OKP JWK with an x member"
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
return { kty: "OKP", crv: "Ed25519", x: value.x };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/resources/identity-credentials.ts
|
|
1003
|
+
var LINK_ISSUER2 = "https://api.link.com";
|
|
1004
|
+
var LINK_ISSUER_METADATA_URL2 = `${LINK_ISSUER2}/.well-known/aap-issuer`;
|
|
1005
|
+
var identityCredentialIssuerMetadataSchema = z3.looseObject({
|
|
1006
|
+
issuer: z3.literal(LINK_ISSUER2),
|
|
1007
|
+
credential_endpoint: z3.string()
|
|
1008
|
+
});
|
|
1009
|
+
var issueIdentityCredentialResponseSchema = z3.looseObject({
|
|
1010
|
+
credential: z3.string(),
|
|
1011
|
+
issuer: z3.literal(LINK_ISSUER2),
|
|
1012
|
+
expires_at: z3.string()
|
|
1013
|
+
});
|
|
1014
|
+
function requireLinkEndpoint2(value, field) {
|
|
1015
|
+
let url;
|
|
1016
|
+
try {
|
|
1017
|
+
url = new URL(value);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
throw new TypeError(`${field} is not a valid URL`, { cause: error });
|
|
1020
|
+
}
|
|
1021
|
+
if (url.protocol !== "https:" || url.origin !== LINK_ISSUER2 || url.username || url.password) {
|
|
1022
|
+
throw new TypeError(`${field} must be an HTTPS URL on ${LINK_ISSUER2}`);
|
|
1023
|
+
}
|
|
1024
|
+
return url.href;
|
|
1025
|
+
}
|
|
1026
|
+
var IdentityCredentialsResource = class extends BaseResource {
|
|
1027
|
+
constructor(options) {
|
|
1028
|
+
super(options, "");
|
|
1029
|
+
}
|
|
1030
|
+
async discoverCredentialEndpoint() {
|
|
1031
|
+
const { status, data, rawBody } = await this.rawFetch({
|
|
1032
|
+
method: "GET",
|
|
1033
|
+
url: LINK_ISSUER_METADATA_URL2,
|
|
1034
|
+
redirect: "manual"
|
|
1035
|
+
});
|
|
1036
|
+
if (status >= 300 && status < 400) {
|
|
1037
|
+
throw new LinkApiError(
|
|
1038
|
+
`Refused redirect while fetching identity credential issuer metadata (${status})`,
|
|
1039
|
+
{ status, rawBody }
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
if (status < 200 || status >= 300) {
|
|
1043
|
+
this.throwApiError(
|
|
1044
|
+
"fetch identity credential issuer metadata",
|
|
1045
|
+
status,
|
|
1046
|
+
data,
|
|
1047
|
+
rawBody
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
const metadata = this.parseResponse(
|
|
1051
|
+
"parse identity credential issuer metadata",
|
|
1052
|
+
status,
|
|
1053
|
+
() => identityCredentialIssuerMetadataSchema.parse(data)
|
|
1054
|
+
);
|
|
1055
|
+
return this.parseResponse(
|
|
1056
|
+
"validate identity credential issuer metadata",
|
|
1057
|
+
status,
|
|
1058
|
+
() => requireLinkEndpoint2(
|
|
1059
|
+
metadata.credential_endpoint,
|
|
1060
|
+
"credential_endpoint"
|
|
1061
|
+
)
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
async issue(params) {
|
|
1065
|
+
const publicJwk = parseHolderPublicJwk(params.cnf.jwk);
|
|
1066
|
+
const endpoint = await this.discoverCredentialEndpoint();
|
|
1067
|
+
const { status, data, rawBody } = await this.apiFetch({
|
|
1068
|
+
method: "POST",
|
|
1069
|
+
url: endpoint,
|
|
1070
|
+
redirect: "manual",
|
|
1071
|
+
headers: {
|
|
1072
|
+
"Content-Type": "application/json",
|
|
1073
|
+
Accept: "application/json"
|
|
1074
|
+
},
|
|
1075
|
+
body: JSON.stringify({ cnf: { jwk: publicJwk } })
|
|
1076
|
+
});
|
|
1077
|
+
if (status >= 300 && status < 400) {
|
|
1078
|
+
throw new LinkApiError(
|
|
1079
|
+
`Refused redirect while issuing identity credential (${status})`,
|
|
1080
|
+
{ status, rawBody }
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
if (status < 200 || status >= 300) {
|
|
1084
|
+
this.throwApiError("issue identity credential", status, data, rawBody);
|
|
1085
|
+
}
|
|
1086
|
+
return this.parseResponse(
|
|
1087
|
+
"issue identity credential",
|
|
1088
|
+
status,
|
|
1089
|
+
() => issueIdentityCredentialResponseSchema.parse(data)
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
|
|
273
1094
|
// src/resources/payment-methods.ts
|
|
274
|
-
import { z as
|
|
275
|
-
var paymentMethodSchema =
|
|
276
|
-
id:
|
|
277
|
-
type:
|
|
278
|
-
is_default:
|
|
279
|
-
name:
|
|
280
|
-
nickname:
|
|
1095
|
+
import { z as z4 } from "zod";
|
|
1096
|
+
var paymentMethodSchema = z4.looseObject({
|
|
1097
|
+
id: z4.string(),
|
|
1098
|
+
type: z4.string(),
|
|
1099
|
+
is_default: z4.boolean(),
|
|
1100
|
+
name: z4.string(),
|
|
1101
|
+
nickname: z4.optional(z4.string().nullable())
|
|
281
1102
|
});
|
|
282
|
-
var paymentMethodsResponseSchema =
|
|
283
|
-
payment_details:
|
|
1103
|
+
var paymentMethodsResponseSchema = z4.looseObject({
|
|
1104
|
+
payment_details: z4.array(paymentMethodSchema)
|
|
284
1105
|
});
|
|
285
1106
|
var PaymentMethodsResource = class extends BaseResource {
|
|
286
1107
|
constructor(options) {
|
|
@@ -303,14 +1124,14 @@ var PaymentMethodsResource = class extends BaseResource {
|
|
|
303
1124
|
};
|
|
304
1125
|
|
|
305
1126
|
// src/resources/report.ts
|
|
306
|
-
import { z as
|
|
307
|
-
var reportRecordSchema =
|
|
308
|
-
object:
|
|
309
|
-
created_at:
|
|
310
|
-
domain:
|
|
311
|
-
outcome:
|
|
312
|
-
spend_request_id:
|
|
313
|
-
status:
|
|
1127
|
+
import { z as z5 } from "zod";
|
|
1128
|
+
var reportRecordSchema = z5.looseObject({
|
|
1129
|
+
object: z5.string(),
|
|
1130
|
+
created_at: z5.string(),
|
|
1131
|
+
domain: z5.string(),
|
|
1132
|
+
outcome: z5.string(),
|
|
1133
|
+
spend_request_id: z5.string(),
|
|
1134
|
+
status: z5.string()
|
|
314
1135
|
});
|
|
315
1136
|
var ReportResource = class extends BaseResource {
|
|
316
1137
|
constructor(options) {
|
|
@@ -335,15 +1156,15 @@ var ReportResource = class extends BaseResource {
|
|
|
335
1156
|
};
|
|
336
1157
|
|
|
337
1158
|
// src/resources/shipping-address.ts
|
|
338
|
-
import { z as
|
|
339
|
-
var shippingAddressSchema =
|
|
340
|
-
id:
|
|
341
|
-
is_default:
|
|
342
|
-
nickname:
|
|
343
|
-
address:
|
|
1159
|
+
import { z as z6 } from "zod";
|
|
1160
|
+
var shippingAddressSchema = z6.looseObject({
|
|
1161
|
+
id: z6.string(),
|
|
1162
|
+
is_default: z6.boolean(),
|
|
1163
|
+
nickname: z6.optional(z6.string().nullable()),
|
|
1164
|
+
address: z6.looseObject({}).nullable()
|
|
344
1165
|
});
|
|
345
|
-
var shippingAddressesResponseSchema =
|
|
346
|
-
shipping_addresses:
|
|
1166
|
+
var shippingAddressesResponseSchema = z6.looseObject({
|
|
1167
|
+
shipping_addresses: z6.array(shippingAddressSchema)
|
|
347
1168
|
});
|
|
348
1169
|
var ShippingAddressResource = class extends BaseResource {
|
|
349
1170
|
constructor(options) {
|
|
@@ -366,21 +1187,21 @@ var ShippingAddressResource = class extends BaseResource {
|
|
|
366
1187
|
};
|
|
367
1188
|
|
|
368
1189
|
// src/resources/sources.ts
|
|
369
|
-
import { z as
|
|
370
|
-
var nullableRecordSchema =
|
|
371
|
-
var sourceSchema =
|
|
372
|
-
id:
|
|
373
|
-
name:
|
|
374
|
-
type:
|
|
1190
|
+
import { z as z7 } from "zod";
|
|
1191
|
+
var nullableRecordSchema = z7.record(z7.string(), z7.unknown()).nullable().optional();
|
|
1192
|
+
var sourceSchema = z7.looseObject({
|
|
1193
|
+
id: z7.string().nullable().optional(),
|
|
1194
|
+
name: z7.string().nullable().optional(),
|
|
1195
|
+
type: z7.string().nullable().optional(),
|
|
375
1196
|
capabilities: nullableRecordSchema,
|
|
376
1197
|
external_connection: nullableRecordSchema,
|
|
377
|
-
granted_actions:
|
|
1198
|
+
granted_actions: z7.array(z7.string()).nullable().optional(),
|
|
378
1199
|
bank_account: nullableRecordSchema,
|
|
379
1200
|
card: nullableRecordSchema
|
|
380
1201
|
});
|
|
381
|
-
var sourcesPageSchema =
|
|
382
|
-
data:
|
|
383
|
-
has_more:
|
|
1202
|
+
var sourcesPageSchema = z7.looseObject({
|
|
1203
|
+
data: z7.array(sourceSchema),
|
|
1204
|
+
has_more: z7.boolean().optional()
|
|
384
1205
|
});
|
|
385
1206
|
var SourcesResource = class extends BaseResource {
|
|
386
1207
|
constructor(options) {
|
|
@@ -416,34 +1237,34 @@ var SourcesResource = class extends BaseResource {
|
|
|
416
1237
|
};
|
|
417
1238
|
|
|
418
1239
|
// src/resources/spend-request.ts
|
|
419
|
-
import { z as
|
|
420
|
-
var sharedPaymentTokenSchema =
|
|
421
|
-
|
|
422
|
-
|
|
1240
|
+
import { z as z8 } from "zod";
|
|
1241
|
+
var sharedPaymentTokenSchema = z8.union([
|
|
1242
|
+
z8.string().transform((id) => ({ id })),
|
|
1243
|
+
z8.looseObject({ id: z8.string() })
|
|
423
1244
|
]);
|
|
424
|
-
var spendRequestSchema =
|
|
425
|
-
id:
|
|
426
|
-
payment_details:
|
|
427
|
-
status:
|
|
428
|
-
line_items:
|
|
429
|
-
totals:
|
|
430
|
-
created_at:
|
|
431
|
-
updated_at:
|
|
1245
|
+
var spendRequestSchema = z8.looseObject({
|
|
1246
|
+
id: z8.string(),
|
|
1247
|
+
payment_details: z8.string().optional(),
|
|
1248
|
+
status: z8.string(),
|
|
1249
|
+
line_items: z8.array(z8.unknown()).optional(),
|
|
1250
|
+
totals: z8.array(z8.unknown()).optional(),
|
|
1251
|
+
created_at: z8.string(),
|
|
1252
|
+
updated_at: z8.string(),
|
|
432
1253
|
shared_payment_token: sharedPaymentTokenSchema.nullable().optional()
|
|
433
1254
|
});
|
|
434
|
-
var spendRequestsResponseSchema =
|
|
435
|
-
data:
|
|
1255
|
+
var spendRequestsResponseSchema = z8.looseObject({
|
|
1256
|
+
data: z8.array(spendRequestSchema)
|
|
436
1257
|
});
|
|
437
|
-
var requestApprovalResponseSchema =
|
|
438
|
-
id:
|
|
439
|
-
approval_link:
|
|
1258
|
+
var requestApprovalResponseSchema = z8.looseObject({
|
|
1259
|
+
id: z8.string(),
|
|
1260
|
+
approval_link: z8.string()
|
|
440
1261
|
}).transform(({ approval_link, ...response }) => ({
|
|
441
1262
|
...response,
|
|
442
1263
|
approval_url: approval_link
|
|
443
1264
|
}));
|
|
444
|
-
var duplicateSpendRequestErrorSchema =
|
|
445
|
-
error:
|
|
446
|
-
duplicate_spend_request:
|
|
1265
|
+
var duplicateSpendRequestErrorSchema = z8.looseObject({
|
|
1266
|
+
error: z8.looseObject({
|
|
1267
|
+
duplicate_spend_request: z8.unknown().optional()
|
|
447
1268
|
})
|
|
448
1269
|
});
|
|
449
1270
|
function parseSpendRequest(value) {
|
|
@@ -562,24 +1383,83 @@ var SpendRequestResource = class extends BaseResource {
|
|
|
562
1383
|
}
|
|
563
1384
|
};
|
|
564
1385
|
|
|
1386
|
+
// src/resources/summaries.ts
|
|
1387
|
+
import { z as z9 } from "zod";
|
|
1388
|
+
var summaryValueSchema = z9.discriminatedUnion("unit", [
|
|
1389
|
+
z9.looseObject({ unit: z9.literal("count"), count: z9.number() }),
|
|
1390
|
+
z9.looseObject({
|
|
1391
|
+
unit: z9.literal("payment_volume"),
|
|
1392
|
+
amount: z9.number(),
|
|
1393
|
+
currency: z9.string()
|
|
1394
|
+
})
|
|
1395
|
+
]);
|
|
1396
|
+
var summaryDataValueSchema = z9.discriminatedUnion("unit", [
|
|
1397
|
+
z9.looseObject({ unit: z9.literal("count"), amount: z9.number() }),
|
|
1398
|
+
z9.looseObject({
|
|
1399
|
+
unit: z9.literal("payment_volume"),
|
|
1400
|
+
amount: z9.number(),
|
|
1401
|
+
currency: z9.string()
|
|
1402
|
+
})
|
|
1403
|
+
]);
|
|
1404
|
+
var summarySchema = z9.looseObject({
|
|
1405
|
+
id: z9.string(),
|
|
1406
|
+
description: z9.string(),
|
|
1407
|
+
created_at: z9.string().nullable().optional(),
|
|
1408
|
+
status: z9.enum(["ready", "pending", "no_data"]),
|
|
1409
|
+
entries: z9.array(z9.looseObject({ label: z9.string(), value: summaryValueSchema })).default([]),
|
|
1410
|
+
as_of: z9.number().optional(),
|
|
1411
|
+
data: z9.array(z9.looseObject({ label: z9.string(), value: summaryDataValueSchema })).nullable().optional()
|
|
1412
|
+
});
|
|
1413
|
+
var summariesPageSchema = z9.looseObject({
|
|
1414
|
+
data: z9.array(summarySchema),
|
|
1415
|
+
has_more: z9.boolean().optional()
|
|
1416
|
+
});
|
|
1417
|
+
var SummariesResource = class extends BaseResource {
|
|
1418
|
+
constructor(options) {
|
|
1419
|
+
super(options, "/summaries");
|
|
1420
|
+
}
|
|
1421
|
+
buildUrl(params) {
|
|
1422
|
+
const url = new URL(this.endpoint);
|
|
1423
|
+
if (params.starting_after !== void 0)
|
|
1424
|
+
url.searchParams.set("starting_after", params.starting_after);
|
|
1425
|
+
if (params.summaries !== void 0)
|
|
1426
|
+
for (const summary of params.summaries)
|
|
1427
|
+
url.searchParams.append("summaries[]", summary);
|
|
1428
|
+
return url.toString();
|
|
1429
|
+
}
|
|
1430
|
+
async list(params = {}) {
|
|
1431
|
+
const { status, data, rawBody } = await this.apiFetch({
|
|
1432
|
+
method: "GET",
|
|
1433
|
+
url: this.buildUrl(params)
|
|
1434
|
+
});
|
|
1435
|
+
if (status < 200 || status >= 300)
|
|
1436
|
+
this.throwApiError("list summaries", status, data, rawBody);
|
|
1437
|
+
return this.parseResponse(
|
|
1438
|
+
"list summaries",
|
|
1439
|
+
status,
|
|
1440
|
+
() => summariesPageSchema.parse(data)
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
|
|
565
1445
|
// src/resources/transactions.ts
|
|
566
|
-
import { z as
|
|
567
|
-
var transactionSchema =
|
|
568
|
-
id:
|
|
569
|
-
source_id:
|
|
570
|
-
amount:
|
|
571
|
-
currency:
|
|
572
|
-
created_date:
|
|
573
|
-
description:
|
|
574
|
-
origin:
|
|
575
|
-
category:
|
|
576
|
-
status:
|
|
1446
|
+
import { z as z10 } from "zod";
|
|
1447
|
+
var transactionSchema = z10.looseObject({
|
|
1448
|
+
id: z10.string(),
|
|
1449
|
+
source_id: z10.string().nullable(),
|
|
1450
|
+
amount: z10.number(),
|
|
1451
|
+
currency: z10.string(),
|
|
1452
|
+
created_date: z10.string(),
|
|
1453
|
+
description: z10.string(),
|
|
1454
|
+
origin: z10.enum(["link", "external_connection"]),
|
|
1455
|
+
category: z10.string().nullable(),
|
|
1456
|
+
status: z10.string()
|
|
577
1457
|
});
|
|
578
|
-
var transactionsPageSchema =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
data:
|
|
582
|
-
has_more:
|
|
1458
|
+
var transactionsPageSchema = z10.union([
|
|
1459
|
+
z10.array(transactionSchema).transform((data) => ({ data })),
|
|
1460
|
+
z10.looseObject({
|
|
1461
|
+
data: z10.array(transactionSchema),
|
|
1462
|
+
has_more: z10.boolean().optional()
|
|
583
1463
|
})
|
|
584
1464
|
]);
|
|
585
1465
|
var TransactionsResource = class extends BaseResource {
|
|
@@ -632,36 +1512,246 @@ var TransactionsResource = class extends BaseResource {
|
|
|
632
1512
|
}
|
|
633
1513
|
};
|
|
634
1514
|
|
|
1515
|
+
// src/resources/ucp.ts
|
|
1516
|
+
function extractApiError(data, rawBody) {
|
|
1517
|
+
if (data && typeof data === "object") {
|
|
1518
|
+
const body = data;
|
|
1519
|
+
if (body.error && typeof body.error === "object") {
|
|
1520
|
+
const err = body.error;
|
|
1521
|
+
if (typeof err.message === "string") return err.message;
|
|
1522
|
+
}
|
|
1523
|
+
if (typeof body.error === "string") return body.error;
|
|
1524
|
+
if (typeof body.message === "string") return body.message;
|
|
1525
|
+
}
|
|
1526
|
+
return rawBody || "unknown error";
|
|
1527
|
+
}
|
|
1528
|
+
function normalizeSearchResult(data) {
|
|
1529
|
+
const body = data ?? {};
|
|
1530
|
+
const items = Array.isArray(body.data) ? body.data : [];
|
|
1531
|
+
return { ...body, data: items };
|
|
1532
|
+
}
|
|
1533
|
+
function normalizeCheckout(data) {
|
|
1534
|
+
const body = data ?? {};
|
|
1535
|
+
return { ...body, id: String(body.id ?? "") };
|
|
1536
|
+
}
|
|
1537
|
+
var UcpResource = class {
|
|
1538
|
+
verbose;
|
|
1539
|
+
getAccessToken;
|
|
1540
|
+
fetchImpl;
|
|
1541
|
+
ucpEndpoint;
|
|
1542
|
+
logger;
|
|
1543
|
+
constructor(options) {
|
|
1544
|
+
const config = resolveLinkSdkConfig(options);
|
|
1545
|
+
this.verbose = config.verbose;
|
|
1546
|
+
this.getAccessToken = config.getAccessToken;
|
|
1547
|
+
this.fetchImpl = requireFetchImplementation(config);
|
|
1548
|
+
this.ucpEndpoint = `${config.apiBaseUrl}/ucp`;
|
|
1549
|
+
this.logger = config.logger;
|
|
1550
|
+
}
|
|
1551
|
+
async rawFetch(opts) {
|
|
1552
|
+
if (this.verbose) {
|
|
1553
|
+
const redactedHeaders = { ...opts.headers };
|
|
1554
|
+
if (redactedHeaders.Authorization)
|
|
1555
|
+
redactedHeaders.Authorization = "Bearer <redacted>";
|
|
1556
|
+
this.logger.debug(`> ${opts.method} ${opts.url}`);
|
|
1557
|
+
this.logger.debug(` Headers: ${JSON.stringify(redactedHeaders)}`);
|
|
1558
|
+
if (opts.body) this.logger.debug(opts.body);
|
|
1559
|
+
}
|
|
1560
|
+
const fetchOpts = {
|
|
1561
|
+
method: opts.method,
|
|
1562
|
+
...opts.headers ? { headers: opts.headers } : {}
|
|
1563
|
+
};
|
|
1564
|
+
if (opts.body) fetchOpts.body = opts.body;
|
|
1565
|
+
let response;
|
|
1566
|
+
try {
|
|
1567
|
+
response = await this.fetchImpl(opts.url, fetchOpts);
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
throw new LinkTransportError(
|
|
1570
|
+
`Request failed: ${opts.method} ${opts.url}`,
|
|
1571
|
+
{
|
|
1572
|
+
cause: error
|
|
1573
|
+
}
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
const rawBody = await response.text();
|
|
1577
|
+
let data = null;
|
|
1578
|
+
try {
|
|
1579
|
+
data = JSON.parse(rawBody);
|
|
1580
|
+
} catch {
|
|
1581
|
+
}
|
|
1582
|
+
if (this.verbose) {
|
|
1583
|
+
this.logger.debug(`< ${response.status} ${response.statusText}`);
|
|
1584
|
+
response.headers.forEach((value, key) => {
|
|
1585
|
+
this.logger.debug(` ${key}: ${value}`);
|
|
1586
|
+
});
|
|
1587
|
+
this.logger.debug(JSON.stringify(data, null, 2) ?? rawBody);
|
|
1588
|
+
}
|
|
1589
|
+
return { status: response.status, data, rawBody };
|
|
1590
|
+
}
|
|
1591
|
+
/** Injects the Bearer token; retries once on 401 after refreshing. */
|
|
1592
|
+
async apiFetch(opts) {
|
|
1593
|
+
const token = await this.getAccessToken();
|
|
1594
|
+
const authedOpts = {
|
|
1595
|
+
...opts,
|
|
1596
|
+
headers: { ...opts.headers, Authorization: `Bearer ${token}` }
|
|
1597
|
+
};
|
|
1598
|
+
const res = await this.rawFetch(authedOpts);
|
|
1599
|
+
if (res.status === 401) {
|
|
1600
|
+
const refreshedToken = await this.getAccessToken({ forceRefresh: true });
|
|
1601
|
+
authedOpts.headers.Authorization = `Bearer ${refreshedToken}`;
|
|
1602
|
+
return this.rawFetch(authedOpts);
|
|
1603
|
+
}
|
|
1604
|
+
return res;
|
|
1605
|
+
}
|
|
1606
|
+
buildSearchUrl(params) {
|
|
1607
|
+
const url = new URL(`${this.ucpEndpoint}/catalog/search`);
|
|
1608
|
+
const setIf = (key, value) => {
|
|
1609
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
1610
|
+
};
|
|
1611
|
+
setIf("query", params.query);
|
|
1612
|
+
setIf("profile_id", params.profile_id);
|
|
1613
|
+
setIf("sku", params.sku);
|
|
1614
|
+
setIf("price_min", params.price_min);
|
|
1615
|
+
setIf("price_max", params.price_max);
|
|
1616
|
+
setIf("currency", params.currency);
|
|
1617
|
+
setIf("availability", params.availability);
|
|
1618
|
+
setIf("sort", params.sort);
|
|
1619
|
+
setIf("group_by", params.group_by);
|
|
1620
|
+
setIf("limit", params.limit);
|
|
1621
|
+
setIf("offset", params.offset);
|
|
1622
|
+
if (params.include_facets) url.searchParams.set("include_facets", "true");
|
|
1623
|
+
const arrays = [
|
|
1624
|
+
["brand", params.brand],
|
|
1625
|
+
["category", params.category],
|
|
1626
|
+
["color", params.color],
|
|
1627
|
+
["size", params.size],
|
|
1628
|
+
["material", params.material],
|
|
1629
|
+
["gender", params.gender],
|
|
1630
|
+
["condition", params.condition]
|
|
1631
|
+
];
|
|
1632
|
+
for (const [key, values] of arrays) {
|
|
1633
|
+
if (values)
|
|
1634
|
+
for (const value of values) url.searchParams.append(`${key}[]`, value);
|
|
1635
|
+
}
|
|
1636
|
+
if (params.test) url.searchParams.set("test", "true");
|
|
1637
|
+
return url.toString();
|
|
1638
|
+
}
|
|
1639
|
+
async searchCatalog(params) {
|
|
1640
|
+
const { status, data, rawBody } = await this.apiFetch({
|
|
1641
|
+
method: "GET",
|
|
1642
|
+
url: this.buildSearchUrl(params)
|
|
1643
|
+
});
|
|
1644
|
+
if (status < 200 || status >= 300) {
|
|
1645
|
+
throw new LinkApiError(
|
|
1646
|
+
`Failed to search UCP catalog (${status}): ${extractApiError(data, rawBody)}`,
|
|
1647
|
+
{ status, rawBody, details: data }
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
return normalizeSearchResult(data);
|
|
1651
|
+
}
|
|
1652
|
+
async createCheckout(params) {
|
|
1653
|
+
const body = {
|
|
1654
|
+
profile_id: params.profile_id,
|
|
1655
|
+
line_items: params.line_items
|
|
1656
|
+
};
|
|
1657
|
+
if (params.currency !== void 0) body.currency = params.currency;
|
|
1658
|
+
if (params.fulfillment_details !== void 0)
|
|
1659
|
+
body.fulfillment_details = params.fulfillment_details;
|
|
1660
|
+
if (params.test) body.test = true;
|
|
1661
|
+
const { status, data, rawBody } = await this.apiFetch({
|
|
1662
|
+
method: "POST",
|
|
1663
|
+
url: `${this.ucpEndpoint}/checkout`,
|
|
1664
|
+
headers: { "Content-Type": "application/json" },
|
|
1665
|
+
body: JSON.stringify(body)
|
|
1666
|
+
});
|
|
1667
|
+
if (status < 200 || status >= 300) {
|
|
1668
|
+
throw new LinkApiError(
|
|
1669
|
+
`Failed to create UCP checkout (${status}): ${extractApiError(data, rawBody)}`,
|
|
1670
|
+
{ status, rawBody, details: data }
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
return normalizeCheckout(data);
|
|
1674
|
+
}
|
|
1675
|
+
async completeCheckout(id, params) {
|
|
1676
|
+
const body = {
|
|
1677
|
+
spend_request_id: params.spend_request_id,
|
|
1678
|
+
profile_id: params.profile_id
|
|
1679
|
+
};
|
|
1680
|
+
if (params.test) body.test = true;
|
|
1681
|
+
const { status, data, rawBody } = await this.apiFetch({
|
|
1682
|
+
method: "POST",
|
|
1683
|
+
url: `${this.ucpEndpoint}/checkout/${encodeURIComponent(id)}/complete`,
|
|
1684
|
+
headers: { "Content-Type": "application/json" },
|
|
1685
|
+
body: JSON.stringify(body)
|
|
1686
|
+
});
|
|
1687
|
+
if (status < 200 || status >= 300) {
|
|
1688
|
+
throw new LinkApiError(
|
|
1689
|
+
`Failed to complete UCP checkout (${status}): ${extractApiError(data, rawBody)}`,
|
|
1690
|
+
{ status, rawBody, details: data }
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
return normalizeCheckout(data);
|
|
1694
|
+
}
|
|
1695
|
+
async retrieveCheckout(id, params) {
|
|
1696
|
+
const url = new URL(
|
|
1697
|
+
`${this.ucpEndpoint}/checkout/${encodeURIComponent(id)}`
|
|
1698
|
+
);
|
|
1699
|
+
url.searchParams.set("spend_request_id", params.spend_request_id);
|
|
1700
|
+
if (params.test) url.searchParams.set("test", "true");
|
|
1701
|
+
const { status, data, rawBody } = await this.apiFetch({
|
|
1702
|
+
method: "GET",
|
|
1703
|
+
url: url.toString()
|
|
1704
|
+
});
|
|
1705
|
+
if (status < 200 || status >= 300) {
|
|
1706
|
+
throw new LinkApiError(
|
|
1707
|
+
`Failed to retrieve UCP checkout (${status}): ${extractApiError(data, rawBody)}`,
|
|
1708
|
+
{ status, rawBody, details: data }
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
return normalizeCheckout(data);
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
|
|
635
1715
|
// src/resources/user-info.ts
|
|
636
|
-
import { z as
|
|
637
|
-
var rollingSpendLimitSchema =
|
|
638
|
-
limit:
|
|
639
|
-
used:
|
|
640
|
-
remaining:
|
|
1716
|
+
import { z as z11 } from "zod";
|
|
1717
|
+
var rollingSpendLimitSchema = z11.object({
|
|
1718
|
+
limit: z11.number().int().nullable(),
|
|
1719
|
+
used: z11.number().int(),
|
|
1720
|
+
remaining: z11.number().int().nullable()
|
|
641
1721
|
});
|
|
642
|
-
var agentWalletSpendLimitsSchema =
|
|
643
|
-
per_transaction:
|
|
644
|
-
limit:
|
|
1722
|
+
var agentWalletSpendLimitsSchema = z11.object({
|
|
1723
|
+
per_transaction: z11.object({
|
|
1724
|
+
limit: z11.number().int().nullable()
|
|
645
1725
|
}),
|
|
646
1726
|
daily: rollingSpendLimitSchema,
|
|
647
1727
|
thirty_day: rollingSpendLimitSchema
|
|
648
1728
|
});
|
|
649
|
-
var agentWalletVerificationRequirementSchema =
|
|
650
|
-
status:
|
|
1729
|
+
var agentWalletVerificationRequirementSchema = z11.object({
|
|
1730
|
+
status: z11.enum([
|
|
651
1731
|
"not_required",
|
|
652
1732
|
"ssn_verification",
|
|
653
1733
|
"identity_verification",
|
|
654
1734
|
"contact_support",
|
|
655
1735
|
"complete"
|
|
656
1736
|
]),
|
|
657
|
-
action_url:
|
|
1737
|
+
action_url: z11.string().nullable()
|
|
658
1738
|
});
|
|
659
|
-
var
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1739
|
+
var userInfoAddressSchema = z11.object({
|
|
1740
|
+
line1: z11.string().nullable(),
|
|
1741
|
+
line2: z11.string().nullable(),
|
|
1742
|
+
city: z11.string().nullable(),
|
|
1743
|
+
state: z11.string().nullable(),
|
|
1744
|
+
postal_code: z11.string().nullable(),
|
|
1745
|
+
country: z11.string().nullable()
|
|
1746
|
+
});
|
|
1747
|
+
var userInfoSchema = z11.looseObject({
|
|
1748
|
+
email: z11.string().nullable().optional(),
|
|
1749
|
+
name: z11.string().nullable().optional(),
|
|
1750
|
+
first_name: z11.string().nullable().optional(),
|
|
1751
|
+
last_name: z11.string().nullable().optional(),
|
|
1752
|
+
phone: z11.string().nullable().optional(),
|
|
1753
|
+
address: userInfoAddressSchema.nullable().optional(),
|
|
1754
|
+
eligible_for_balance: z11.boolean().optional(),
|
|
665
1755
|
agent_wallet_spend_limits: agentWalletSpendLimitsSchema.optional(),
|
|
666
1756
|
agent_wallet_step_up: agentWalletVerificationRequirementSchema.optional()
|
|
667
1757
|
}).transform(
|
|
@@ -671,6 +1761,8 @@ var userInfoSchema = z8.looseObject({
|
|
|
671
1761
|
first_name,
|
|
672
1762
|
last_name,
|
|
673
1763
|
phone,
|
|
1764
|
+
address,
|
|
1765
|
+
eligible_for_balance,
|
|
674
1766
|
agent_wallet_spend_limits,
|
|
675
1767
|
agent_wallet_step_up
|
|
676
1768
|
}) => ({
|
|
@@ -679,6 +1771,8 @@ var userInfoSchema = z8.looseObject({
|
|
|
679
1771
|
first_name: first_name ?? null,
|
|
680
1772
|
last_name: last_name ?? null,
|
|
681
1773
|
phone: phone ?? null,
|
|
1774
|
+
...address === void 0 ? {} : { address },
|
|
1775
|
+
...eligible_for_balance === void 0 ? {} : { eligible_for_balance },
|
|
682
1776
|
...agent_wallet_spend_limits === void 0 ? {} : { agent_wallet_spend_limits },
|
|
683
1777
|
...agent_wallet_step_up === void 0 ? {} : { agent_wallet_verification_requirement: agent_wallet_step_up }
|
|
684
1778
|
})
|
|
@@ -704,16 +1798,16 @@ var UserInfoResource = class extends BaseResource {
|
|
|
704
1798
|
};
|
|
705
1799
|
|
|
706
1800
|
// src/resources/web-bot-auth.ts
|
|
707
|
-
import { z as
|
|
1801
|
+
import { z as z12 } from "zod";
|
|
708
1802
|
var EXPIRY_BUFFER_MS = 3e4;
|
|
709
|
-
var webBotAuthBlockSchema =
|
|
710
|
-
signature:
|
|
711
|
-
signature_input:
|
|
712
|
-
signature_agent:
|
|
713
|
-
authority:
|
|
714
|
-
expires_at:
|
|
1803
|
+
var webBotAuthBlockSchema = z12.looseObject({
|
|
1804
|
+
signature: z12.string(),
|
|
1805
|
+
signature_input: z12.string(),
|
|
1806
|
+
signature_agent: z12.string(),
|
|
1807
|
+
authority: z12.string(),
|
|
1808
|
+
expires_at: z12.string()
|
|
715
1809
|
});
|
|
716
|
-
var webBotAuthResponseSchema =
|
|
1810
|
+
var webBotAuthResponseSchema = z12.looseObject({
|
|
717
1811
|
web_bot_auth: webBotAuthBlockSchema
|
|
718
1812
|
});
|
|
719
1813
|
var WebBotAuthResource = class extends BaseResource {
|
|
@@ -759,6 +1853,8 @@ var WebBotAuthResource = class extends BaseResource {
|
|
|
759
1853
|
|
|
760
1854
|
// src/client.ts
|
|
761
1855
|
var Link = class {
|
|
1856
|
+
attestations;
|
|
1857
|
+
identityCredentials;
|
|
762
1858
|
spendRequests;
|
|
763
1859
|
paymentMethods;
|
|
764
1860
|
shippingAddresses;
|
|
@@ -766,9 +1862,13 @@ var Link = class {
|
|
|
766
1862
|
transactions;
|
|
767
1863
|
sources;
|
|
768
1864
|
balances;
|
|
1865
|
+
summaries;
|
|
769
1866
|
webBotAuth;
|
|
770
1867
|
reports;
|
|
1868
|
+
ucp;
|
|
771
1869
|
constructor(options) {
|
|
1870
|
+
this.attestations = new AttestationsResource(options);
|
|
1871
|
+
this.identityCredentials = new IdentityCredentialsResource(options);
|
|
772
1872
|
this.spendRequests = new SpendRequestResource(options);
|
|
773
1873
|
this.paymentMethods = new PaymentMethodsResource(options);
|
|
774
1874
|
this.shippingAddresses = new ShippingAddressResource(options);
|
|
@@ -776,8 +1876,10 @@ var Link = class {
|
|
|
776
1876
|
this.transactions = new TransactionsResource(options);
|
|
777
1877
|
this.sources = new SourcesResource(options);
|
|
778
1878
|
this.balances = new BalancesResource(options);
|
|
1879
|
+
this.summaries = new SummariesResource(options);
|
|
779
1880
|
this.webBotAuth = new WebBotAuthResource(options);
|
|
780
1881
|
this.reports = new ReportResource(options);
|
|
1882
|
+
this.ucp = new UcpResource(options);
|
|
781
1883
|
}
|
|
782
1884
|
};
|
|
783
1885
|
var client_default = Link;
|
|
@@ -802,6 +1904,7 @@ var REPORT_TAGS = [
|
|
|
802
1904
|
];
|
|
803
1905
|
var REPORT_ATTEMPT_TRACE_MAX_LENGTH = 8e3;
|
|
804
1906
|
export {
|
|
1907
|
+
AttestationsResource,
|
|
805
1908
|
Link,
|
|
806
1909
|
LinkApiError,
|
|
807
1910
|
LinkConfigurationError,
|
|
@@ -811,7 +1914,11 @@ export {
|
|
|
811
1914
|
REPORT_ATTEMPT_TRACE_MAX_LENGTH,
|
|
812
1915
|
REPORT_OUTCOMES,
|
|
813
1916
|
REPORT_TAGS,
|
|
1917
|
+
SummariesResource,
|
|
814
1918
|
client_default as default,
|
|
815
|
-
getDuplicateSpendRequest
|
|
1919
|
+
getDuplicateSpendRequest,
|
|
1920
|
+
holderJwkThumbprint,
|
|
1921
|
+
holderJwksEqual,
|
|
1922
|
+
parseHolderPublicJwk
|
|
816
1923
|
};
|
|
817
1924
|
//# sourceMappingURL=index.js.map
|