@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.
Files changed (39) hide show
  1. package/dist/client.d.ts +5 -1
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1220 -113
  6. package/dist/index.js.map +1 -1
  7. package/dist/resources/__tests__/attestations-crypto.test.d.ts +2 -0
  8. package/dist/resources/__tests__/attestations-crypto.test.d.ts.map +1 -0
  9. package/dist/resources/__tests__/attestations.test.d.ts +2 -0
  10. package/dist/resources/__tests__/attestations.test.d.ts.map +1 -0
  11. package/dist/resources/__tests__/credentials.test.d.ts +2 -0
  12. package/dist/resources/__tests__/credentials.test.d.ts.map +1 -0
  13. package/dist/resources/__tests__/holder-jwk.test.d.ts +2 -0
  14. package/dist/resources/__tests__/holder-jwk.test.d.ts.map +1 -0
  15. package/dist/resources/__tests__/summaries.test.d.ts +2 -0
  16. package/dist/resources/__tests__/summaries.test.d.ts.map +1 -0
  17. package/dist/resources/__tests__/ucp.test.d.ts +2 -0
  18. package/dist/resources/__tests__/ucp.test.d.ts.map +1 -0
  19. package/dist/resources/attestations-crypto.d.ts +35 -0
  20. package/dist/resources/attestations-crypto.d.ts.map +1 -0
  21. package/dist/resources/attestations.d.ts +14 -0
  22. package/dist/resources/attestations.d.ts.map +1 -0
  23. package/dist/resources/balances.d.ts.map +1 -1
  24. package/dist/resources/base.d.ts +1 -0
  25. package/dist/resources/base.d.ts.map +1 -1
  26. package/dist/resources/holder-jwk.d.ts +11 -0
  27. package/dist/resources/holder-jwk.d.ts.map +1 -0
  28. package/dist/resources/identity-credentials.d.ts +9 -0
  29. package/dist/resources/identity-credentials.d.ts.map +1 -0
  30. package/dist/resources/interfaces.d.ts +86 -1
  31. package/dist/resources/interfaces.d.ts.map +1 -1
  32. package/dist/resources/summaries.d.ts +10 -0
  33. package/dist/resources/summaries.d.ts.map +1 -0
  34. package/dist/resources/ucp.d.ts +25 -0
  35. package/dist/resources/ucp.d.ts.map +1 -0
  36. package/dist/resources/user-info.d.ts.map +1 -1
  37. package/dist/types/index.d.ts +151 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- // src/resources/balances.ts
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
- authedOpts.headers.Authorization = `Bearer ${refreshedToken}`;
198
- return this.rawFetch(authedOpts);
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
- var currencyAmountsSchema = z.record(z.string(), z.number());
222
- var balanceSchema = z.looseObject({
223
- source_id: z.string(),
224
- type: z.enum(["cash", "credit"]),
225
- cash: z.looseObject({ available: currencyAmountsSchema }).nullable().optional(),
226
- credit: z.looseObject({ used: currencyAmountsSchema }).nullable().optional(),
227
- current: z.number(),
228
- currency: z.string(),
229
- as_of: z.string()
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 = z.looseObject({
232
- data: z.array(balanceSchema),
233
- has_more: z.boolean().optional()
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 z2 } from "zod";
275
- var paymentMethodSchema = z2.looseObject({
276
- id: z2.string(),
277
- type: z2.string(),
278
- is_default: z2.boolean(),
279
- name: z2.string(),
280
- nickname: z2.optional(z2.string().nullable())
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 = z2.looseObject({
283
- payment_details: z2.array(paymentMethodSchema)
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 z3 } from "zod";
307
- var reportRecordSchema = z3.looseObject({
308
- object: z3.string(),
309
- created_at: z3.string(),
310
- domain: z3.string(),
311
- outcome: z3.string(),
312
- spend_request_id: z3.string(),
313
- status: z3.string()
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 z4 } from "zod";
339
- var shippingAddressSchema = z4.looseObject({
340
- id: z4.string(),
341
- is_default: z4.boolean(),
342
- nickname: z4.optional(z4.string().nullable()),
343
- address: z4.looseObject({}).nullable()
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 = z4.looseObject({
346
- shipping_addresses: z4.array(shippingAddressSchema)
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 z5 } from "zod";
370
- var nullableRecordSchema = z5.record(z5.string(), z5.unknown()).nullable().optional();
371
- var sourceSchema = z5.looseObject({
372
- id: z5.string().nullable().optional(),
373
- name: z5.string().nullable().optional(),
374
- type: z5.string().nullable().optional(),
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: z5.array(z5.string()).nullable().optional(),
1198
+ granted_actions: z7.array(z7.string()).nullable().optional(),
378
1199
  bank_account: nullableRecordSchema,
379
1200
  card: nullableRecordSchema
380
1201
  });
381
- var sourcesPageSchema = z5.looseObject({
382
- data: z5.array(sourceSchema),
383
- has_more: z5.boolean().optional()
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 z6 } from "zod";
420
- var sharedPaymentTokenSchema = z6.union([
421
- z6.string().transform((id) => ({ id })),
422
- z6.looseObject({ id: z6.string() })
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 = z6.looseObject({
425
- id: z6.string(),
426
- payment_details: z6.string().optional(),
427
- status: z6.string(),
428
- line_items: z6.array(z6.unknown()).optional(),
429
- totals: z6.array(z6.unknown()).optional(),
430
- created_at: z6.string(),
431
- updated_at: z6.string(),
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 = z6.looseObject({
435
- data: z6.array(spendRequestSchema)
1255
+ var spendRequestsResponseSchema = z8.looseObject({
1256
+ data: z8.array(spendRequestSchema)
436
1257
  });
437
- var requestApprovalResponseSchema = z6.looseObject({
438
- id: z6.string(),
439
- approval_link: z6.string()
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 = z6.looseObject({
445
- error: z6.looseObject({
446
- duplicate_spend_request: z6.unknown().optional()
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 z7 } from "zod";
567
- var transactionSchema = z7.looseObject({
568
- id: z7.string(),
569
- source_id: z7.string().nullable(),
570
- amount: z7.number(),
571
- currency: z7.string(),
572
- created_date: z7.string(),
573
- description: z7.string(),
574
- origin: z7.enum(["link", "external_connection"]),
575
- category: z7.string().nullable(),
576
- status: z7.string()
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 = z7.union([
579
- z7.array(transactionSchema).transform((data) => ({ data })),
580
- z7.looseObject({
581
- data: z7.array(transactionSchema),
582
- has_more: z7.boolean().optional()
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 z8 } from "zod";
637
- var rollingSpendLimitSchema = z8.object({
638
- limit: z8.number().int().nullable(),
639
- used: z8.number().int(),
640
- remaining: z8.number().int().nullable()
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 = z8.object({
643
- per_transaction: z8.object({
644
- limit: z8.number().int().nullable()
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 = z8.object({
650
- status: z8.enum([
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: z8.string().nullable()
1737
+ action_url: z11.string().nullable()
658
1738
  });
659
- var userInfoSchema = z8.looseObject({
660
- email: z8.string().nullable().optional(),
661
- name: z8.string().nullable().optional(),
662
- first_name: z8.string().nullable().optional(),
663
- last_name: z8.string().nullable().optional(),
664
- phone: z8.string().nullable().optional(),
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 z9 } from "zod";
1801
+ import { z as z12 } from "zod";
708
1802
  var EXPIRY_BUFFER_MS = 3e4;
709
- var webBotAuthBlockSchema = z9.looseObject({
710
- signature: z9.string(),
711
- signature_input: z9.string(),
712
- signature_agent: z9.string(),
713
- authority: z9.string(),
714
- expires_at: z9.string()
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 = z9.looseObject({
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