@stripe/link-sdk 0.4.2 → 0.6.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/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) {
@@ -217,20 +576,350 @@ var BaseResource = class {
217
576
  }
218
577
  };
219
578
 
579
+ // src/resources/attestations.ts
580
+ var TOKEN_TYPE_BLIND_RSA = 2;
581
+ var CONTENT_TYPE_TOKEN_REQUEST = "application/private-token-generic-batch-request";
582
+ var CONTENT_TYPE_TOKEN_RESPONSE = "application/private-token-generic-batch-response";
583
+ var LINK_ISSUER = "https://api.link.com";
584
+ var LINK_ISSUER_HOSTNAME = "api.link.com";
585
+ var LINK_ISSUER_METADATA_URL = `${LINK_ISSUER}/.well-known/aap-issuer`;
586
+ var issuerMetadataSchema = z.looseObject({
587
+ issuer: z.literal(LINK_ISSUER),
588
+ token_issuance_endpoint: z.string(),
589
+ token_keys: z.string()
590
+ });
591
+ var tokenKeyDirectorySchema = z.looseObject({
592
+ "token-keys": z.array(
593
+ z.looseObject({
594
+ "token-type": z.number(),
595
+ "token-key": z.string(),
596
+ "not-before": z.number().int().nonnegative().optional()
597
+ })
598
+ )
599
+ });
600
+ function base64ToBytes(value) {
601
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
602
+ return new Uint8Array(Buffer.from(normalized, "base64"));
603
+ }
604
+ function minQuicVarintLength(value) {
605
+ if (value < 2 ** 6) return 1;
606
+ if (value < 2 ** 14) return 2;
607
+ if (value < 2 ** 30) return 4;
608
+ return 8;
609
+ }
610
+ function encodeQuicVarint(value) {
611
+ if (!Number.isSafeInteger(value) || value < 0 || value >= 2 ** 62) {
612
+ throw new Error(`Cannot encode ${value} as a QUIC variable-length integer`);
613
+ }
614
+ const length = minQuicVarintLength(value);
615
+ const encoded = Buffer.alloc(length);
616
+ let remaining = BigInt(value);
617
+ for (let index = length - 1; index >= 0; index--) {
618
+ encoded[index] = Number(remaining & 0xffn);
619
+ remaining >>= 8n;
620
+ }
621
+ encoded[0] = encoded.readUInt8(0) | Math.log2(length) << 6;
622
+ return encoded;
623
+ }
624
+ function readQuicVarint(body, offset = 0) {
625
+ const first = body[offset];
626
+ if (first === void 0) throw new Error("QUIC varint is absent");
627
+ const length = 1 << (first >> 6);
628
+ if (body.length < offset + length)
629
+ throw new Error("QUIC varint is truncated");
630
+ let value = BigInt(first & 63);
631
+ for (let index = 1; index < length; index++) {
632
+ value = value << 8n | BigInt(body.readUInt8(offset + index));
633
+ }
634
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
635
+ throw new Error("QUIC varint exceeds the JavaScript safe integer range");
636
+ }
637
+ const numeric = Number(value);
638
+ if (minQuicVarintLength(numeric) !== length) {
639
+ throw new Error("QUIC varint is not minimally encoded");
640
+ }
641
+ return { value: numeric, length };
642
+ }
643
+ function encodeBatchTokenRequest(blindedMessages, truncatedTokenKeyId) {
644
+ const entries = blindedMessages.map((blindedMessage) => {
645
+ const entry = Buffer.alloc(3 + blindedMessage.length);
646
+ entry.writeUInt16BE(TOKEN_TYPE_BLIND_RSA, 0);
647
+ entry.writeUInt8(truncatedTokenKeyId, 2);
648
+ Buffer.from(blindedMessage).copy(entry, 3);
649
+ return entry;
650
+ });
651
+ const vector = Buffer.concat(entries);
652
+ return Buffer.concat([encodeQuicVarint(vector.length), vector]);
653
+ }
654
+ function decodeBatchTokenResponse(body, elementSize, expectedCount) {
655
+ const prefix = readQuicVarint(body);
656
+ const vector = body.subarray(prefix.length);
657
+ if (vector.length !== prefix.value) {
658
+ throw new Error(
659
+ `BatchTokenResponse length prefix says ${prefix.value} bytes but ${vector.length} bytes follow`
660
+ );
661
+ }
662
+ if (vector.length === 0)
663
+ throw new Error("BatchTokenResponse vector is empty");
664
+ const signatures = [];
665
+ let offset = 0;
666
+ while (offset < vector.length) {
667
+ const present = vector[offset++];
668
+ if (present === 0) {
669
+ throw new Error(
670
+ `Issuer refused token request at index ${signatures.length}`
671
+ );
672
+ }
673
+ if (present !== 1) {
674
+ throw new Error(`Invalid OptionalTokenResponse presence byte ${present}`);
675
+ }
676
+ if (offset + 2 + elementSize > vector.length) {
677
+ throw new Error("Present GenericTokenResponse is truncated");
678
+ }
679
+ const tokenType = vector.readUInt16BE(offset);
680
+ offset += 2;
681
+ if (tokenType !== TOKEN_TYPE_BLIND_RSA) {
682
+ throw new Error(
683
+ `GenericTokenResponse has unsupported token type 0x${tokenType.toString(16).padStart(4, "0")}`
684
+ );
685
+ }
686
+ signatures.push(
687
+ base64urlEncode(
688
+ new Uint8Array(vector.subarray(offset, offset + elementSize))
689
+ )
690
+ );
691
+ offset += elementSize;
692
+ }
693
+ if (signatures.length !== expectedCount) {
694
+ throw new Error(
695
+ `Issuer returned ${signatures.length} token responses for ${expectedCount} token requests`
696
+ );
697
+ }
698
+ return signatures;
699
+ }
700
+ function requireLinkEndpoint(value, field) {
701
+ let url;
702
+ try {
703
+ url = new URL(value);
704
+ } catch (error) {
705
+ throw new TypeError(`${field} is not a valid URL`, { cause: error });
706
+ }
707
+ if (url.protocol !== "https:" || url.origin !== LINK_ISSUER || url.username || url.password) {
708
+ throw new TypeError(`${field} must be an HTTPS URL on ${LINK_ISSUER}`);
709
+ }
710
+ return url.href;
711
+ }
712
+ var AttestationsResource = class extends BaseResource {
713
+ /** Creates an attestation resource using the SDK's authentication config. */
714
+ constructor(options) {
715
+ super(options, "");
716
+ }
717
+ /** Fetches JSON without following redirects and normalizes API errors. */
718
+ async fetchJson(url, operation) {
719
+ let response;
720
+ try {
721
+ response = await this.fetchImpl(url, { redirect: "manual" });
722
+ } catch (error) {
723
+ throw new LinkTransportError(`Request failed: GET ${url}`, {
724
+ cause: error
725
+ });
726
+ }
727
+ const rawBody = await response.text();
728
+ if (response.status >= 300 && response.status < 400) {
729
+ throw new LinkApiError(
730
+ `Refused redirect while attempting to ${operation} (${response.status})`,
731
+ {
732
+ status: response.status,
733
+ rawBody
734
+ }
735
+ );
736
+ }
737
+ let data = null;
738
+ try {
739
+ data = JSON.parse(rawBody);
740
+ } catch (error) {
741
+ if (response.ok) {
742
+ throw new LinkResponseError(operation, response.status, {
743
+ cause: error
744
+ });
745
+ }
746
+ }
747
+ if (!response.ok) {
748
+ throw new LinkApiError(
749
+ `Failed to ${operation} (${response.status}): ${rawBody}`,
750
+ {
751
+ status: response.status,
752
+ rawBody,
753
+ details: data
754
+ }
755
+ );
756
+ }
757
+ return { data, status: response.status };
758
+ }
759
+ /** Sends a blinded batch with the current or refreshed access token. */
760
+ async issueTokens(url, body, forceRefresh = false) {
761
+ const token = await this.getAccessToken(
762
+ forceRefresh ? { forceRefresh: true } : void 0
763
+ );
764
+ const requestBody = new ArrayBuffer(body.byteLength);
765
+ new Uint8Array(requestBody).set(body);
766
+ try {
767
+ return await this.fetchImpl(url, {
768
+ method: "POST",
769
+ redirect: "manual",
770
+ headers: {
771
+ "Content-Type": CONTENT_TYPE_TOKEN_REQUEST,
772
+ Accept: CONTENT_TYPE_TOKEN_RESPONSE,
773
+ Authorization: `Bearer ${token}`
774
+ },
775
+ body: requestBody
776
+ });
777
+ } catch (error) {
778
+ throw new LinkTransportError(`Request failed: POST ${url}`, {
779
+ cause: error
780
+ });
781
+ }
782
+ }
783
+ /** Requests, verifies, and returns a batch of Link attestation tokens. */
784
+ async request(params) {
785
+ const { count } = params;
786
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
787
+ throw new LinkConfigurationError(
788
+ "Attestation token count must be an integer from 1 to 100"
789
+ );
790
+ }
791
+ const metadataResponse = await this.fetchJson(
792
+ LINK_ISSUER_METADATA_URL,
793
+ "fetch issuer metadata"
794
+ );
795
+ const metadata = this.parseResponse(
796
+ "parse issuer metadata",
797
+ metadataResponse.status,
798
+ () => issuerMetadataSchema.parse(metadataResponse.data)
799
+ );
800
+ let tokenKeysUrl;
801
+ let issuanceUrl;
802
+ try {
803
+ tokenKeysUrl = requireLinkEndpoint(metadata.token_keys, "token_keys");
804
+ issuanceUrl = requireLinkEndpoint(
805
+ metadata.token_issuance_endpoint,
806
+ "token_issuance_endpoint"
807
+ );
808
+ } catch (error) {
809
+ throw new LinkResponseError(
810
+ "validate issuer metadata",
811
+ metadataResponse.status,
812
+ { cause: error }
813
+ );
814
+ }
815
+ const directoryResponse = await this.fetchJson(
816
+ tokenKeysUrl,
817
+ "fetch token keys"
818
+ );
819
+ const directory = this.parseResponse(
820
+ "parse token key directory",
821
+ directoryResponse.status,
822
+ () => tokenKeyDirectorySchema.parse(directoryResponse.data)
823
+ );
824
+ const now = Math.floor(Date.now() / 1e3);
825
+ const tokenKey = directory["token-keys"].find(
826
+ (entry) => entry["token-type"] === TOKEN_TYPE_BLIND_RSA && (entry["not-before"] === void 0 || entry["not-before"] <= now)
827
+ );
828
+ if (!tokenKey) {
829
+ throw new LinkResponseError(
830
+ "select Blind RSA token key",
831
+ directoryResponse.status,
832
+ {
833
+ cause: new Error(
834
+ "No active token key with type 0x0002 found in directory"
835
+ )
836
+ }
837
+ );
838
+ }
839
+ const spkiDer = base64ToBytes(tokenKey["token-key"]);
840
+ const challengeDigest = computeChallengeDigest(
841
+ TOKEN_TYPE_BLIND_RSA,
842
+ LINK_ISSUER_HOSTNAME
843
+ );
844
+ const blindingState = generateBlindedMessages(
845
+ spkiDer,
846
+ count,
847
+ challengeDigest
848
+ );
849
+ const tokenKeyIdBytes = new Uint8Array(
850
+ createHash2("sha256").update(spkiDer).digest()
851
+ );
852
+ const truncatedTokenKeyId = tokenKeyIdBytes.at(-1);
853
+ if (truncatedTokenKeyId === void 0) {
854
+ throw new LinkResponseError("derive Blind RSA token key ID", 200);
855
+ }
856
+ const requestBody = encodeBatchTokenRequest(
857
+ blindingState.tokens.map((token) => token.blindedMsg),
858
+ truncatedTokenKeyId
859
+ );
860
+ let issueResponse = await this.issueTokens(
861
+ issuanceUrl,
862
+ new Uint8Array(requestBody)
863
+ );
864
+ if (issueResponse.status === 401 && this.canRefreshAccessToken) {
865
+ issueResponse = await this.issueTokens(
866
+ issuanceUrl,
867
+ new Uint8Array(requestBody),
868
+ true
869
+ );
870
+ }
871
+ if (!issueResponse.ok) {
872
+ const rawBody = await issueResponse.text();
873
+ throw new LinkApiError(
874
+ `Failed to issue attestation tokens (${issueResponse.status}): ${rawBody}`,
875
+ {
876
+ status: issueResponse.status,
877
+ rawBody
878
+ }
879
+ );
880
+ }
881
+ let blindSignatures;
882
+ try {
883
+ blindSignatures = decodeBatchTokenResponse(
884
+ Buffer.from(await issueResponse.arrayBuffer()),
885
+ blindingState.publicKey.nLen,
886
+ count
887
+ );
888
+ } catch (error) {
889
+ throw new LinkResponseError(
890
+ "decode attestation token response",
891
+ issueResponse.status,
892
+ { cause: error }
893
+ );
894
+ }
895
+ const finalTokens = unblindSignatures(
896
+ blindingState,
897
+ blindSignatures
898
+ );
899
+ return {
900
+ tokens: finalTokens.map((finalToken) => finalToken.base64url),
901
+ issuer: LINK_ISSUER,
902
+ token_key_id: base64urlEncode(blindingState.tokenKeyId),
903
+ count: finalTokens.length
904
+ };
905
+ }
906
+ };
907
+
220
908
  // 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()
909
+ import { z as z2 } from "zod";
910
+ var currencyAmountsSchema = z2.record(z2.string(), z2.number());
911
+ var balanceSchema = z2.looseObject({
912
+ source_id: z2.string(),
913
+ type: z2.enum(["cash", "credit"]),
914
+ cash: z2.looseObject({ available: currencyAmountsSchema }).nullable().optional(),
915
+ credit: z2.looseObject({ used: currencyAmountsSchema.nullable() }).nullable().optional(),
916
+ current: z2.number(),
917
+ currency: z2.string(),
918
+ as_of: z2.string()
230
919
  });
231
- var balancesPageSchema = z.looseObject({
232
- data: z.array(balanceSchema),
233
- has_more: z.boolean().optional()
920
+ var balancesPageSchema = z2.looseObject({
921
+ data: z2.array(balanceSchema),
922
+ has_more: z2.boolean().optional()
234
923
  });
235
924
  var BalancesResource = class extends BaseResource {
236
925
  constructor(options) {
@@ -271,16 +960,16 @@ var BalancesResource = class extends BaseResource {
271
960
  };
272
961
 
273
962
  // 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())
963
+ import { z as z3 } from "zod";
964
+ var paymentMethodSchema = z3.looseObject({
965
+ id: z3.string(),
966
+ type: z3.string(),
967
+ is_default: z3.boolean(),
968
+ name: z3.string(),
969
+ nickname: z3.optional(z3.string().nullable())
281
970
  });
282
- var paymentMethodsResponseSchema = z2.looseObject({
283
- payment_details: z2.array(paymentMethodSchema)
971
+ var paymentMethodsResponseSchema = z3.looseObject({
972
+ payment_details: z3.array(paymentMethodSchema)
284
973
  });
285
974
  var PaymentMethodsResource = class extends BaseResource {
286
975
  constructor(options) {
@@ -303,14 +992,14 @@ var PaymentMethodsResource = class extends BaseResource {
303
992
  };
304
993
 
305
994
  // 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()
995
+ import { z as z4 } from "zod";
996
+ var reportRecordSchema = z4.looseObject({
997
+ object: z4.string(),
998
+ created_at: z4.string(),
999
+ domain: z4.string(),
1000
+ outcome: z4.string(),
1001
+ spend_request_id: z4.string(),
1002
+ status: z4.string()
314
1003
  });
315
1004
  var ReportResource = class extends BaseResource {
316
1005
  constructor(options) {
@@ -335,15 +1024,15 @@ var ReportResource = class extends BaseResource {
335
1024
  };
336
1025
 
337
1026
  // 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()
1027
+ import { z as z5 } from "zod";
1028
+ var shippingAddressSchema = z5.looseObject({
1029
+ id: z5.string(),
1030
+ is_default: z5.boolean(),
1031
+ nickname: z5.optional(z5.string().nullable()),
1032
+ address: z5.looseObject({}).nullable()
344
1033
  });
345
- var shippingAddressesResponseSchema = z4.looseObject({
346
- shipping_addresses: z4.array(shippingAddressSchema)
1034
+ var shippingAddressesResponseSchema = z5.looseObject({
1035
+ shipping_addresses: z5.array(shippingAddressSchema)
347
1036
  });
348
1037
  var ShippingAddressResource = class extends BaseResource {
349
1038
  constructor(options) {
@@ -366,21 +1055,21 @@ var ShippingAddressResource = class extends BaseResource {
366
1055
  };
367
1056
 
368
1057
  // 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(),
1058
+ import { z as z6 } from "zod";
1059
+ var nullableRecordSchema = z6.record(z6.string(), z6.unknown()).nullable().optional();
1060
+ var sourceSchema = z6.looseObject({
1061
+ id: z6.string().nullable().optional(),
1062
+ name: z6.string().nullable().optional(),
1063
+ type: z6.string().nullable().optional(),
375
1064
  capabilities: nullableRecordSchema,
376
1065
  external_connection: nullableRecordSchema,
377
- granted_actions: z5.array(z5.string()).nullable().optional(),
1066
+ granted_actions: z6.array(z6.string()).nullable().optional(),
378
1067
  bank_account: nullableRecordSchema,
379
1068
  card: nullableRecordSchema
380
1069
  });
381
- var sourcesPageSchema = z5.looseObject({
382
- data: z5.array(sourceSchema),
383
- has_more: z5.boolean().optional()
1070
+ var sourcesPageSchema = z6.looseObject({
1071
+ data: z6.array(sourceSchema),
1072
+ has_more: z6.boolean().optional()
384
1073
  });
385
1074
  var SourcesResource = class extends BaseResource {
386
1075
  constructor(options) {
@@ -416,34 +1105,34 @@ var SourcesResource = class extends BaseResource {
416
1105
  };
417
1106
 
418
1107
  // 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() })
1108
+ import { z as z7 } from "zod";
1109
+ var sharedPaymentTokenSchema = z7.union([
1110
+ z7.string().transform((id) => ({ id })),
1111
+ z7.looseObject({ id: z7.string() })
423
1112
  ]);
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(),
1113
+ var spendRequestSchema = z7.looseObject({
1114
+ id: z7.string(),
1115
+ payment_details: z7.string().optional(),
1116
+ status: z7.string(),
1117
+ line_items: z7.array(z7.unknown()).optional(),
1118
+ totals: z7.array(z7.unknown()).optional(),
1119
+ created_at: z7.string(),
1120
+ updated_at: z7.string(),
432
1121
  shared_payment_token: sharedPaymentTokenSchema.nullable().optional()
433
1122
  });
434
- var spendRequestsResponseSchema = z6.looseObject({
435
- data: z6.array(spendRequestSchema)
1123
+ var spendRequestsResponseSchema = z7.looseObject({
1124
+ data: z7.array(spendRequestSchema)
436
1125
  });
437
- var requestApprovalResponseSchema = z6.looseObject({
438
- id: z6.string(),
439
- approval_link: z6.string()
1126
+ var requestApprovalResponseSchema = z7.looseObject({
1127
+ id: z7.string(),
1128
+ approval_link: z7.string()
440
1129
  }).transform(({ approval_link, ...response }) => ({
441
1130
  ...response,
442
1131
  approval_url: approval_link
443
1132
  }));
444
- var duplicateSpendRequestErrorSchema = z6.looseObject({
445
- error: z6.looseObject({
446
- duplicate_spend_request: z6.unknown().optional()
1133
+ var duplicateSpendRequestErrorSchema = z7.looseObject({
1134
+ error: z7.looseObject({
1135
+ duplicate_spend_request: z7.unknown().optional()
447
1136
  })
448
1137
  });
449
1138
  function parseSpendRequest(value) {
@@ -562,24 +1251,83 @@ var SpendRequestResource = class extends BaseResource {
562
1251
  }
563
1252
  };
564
1253
 
1254
+ // src/resources/summaries.ts
1255
+ import { z as z8 } from "zod";
1256
+ var summaryValueSchema = z8.discriminatedUnion("unit", [
1257
+ z8.looseObject({ unit: z8.literal("count"), count: z8.number() }),
1258
+ z8.looseObject({
1259
+ unit: z8.literal("payment_volume"),
1260
+ amount: z8.number(),
1261
+ currency: z8.string()
1262
+ })
1263
+ ]);
1264
+ var summaryDataValueSchema = z8.discriminatedUnion("unit", [
1265
+ z8.looseObject({ unit: z8.literal("count"), amount: z8.number() }),
1266
+ z8.looseObject({
1267
+ unit: z8.literal("payment_volume"),
1268
+ amount: z8.number(),
1269
+ currency: z8.string()
1270
+ })
1271
+ ]);
1272
+ var summarySchema = z8.looseObject({
1273
+ id: z8.string(),
1274
+ description: z8.string(),
1275
+ created_at: z8.string().nullable().optional(),
1276
+ status: z8.enum(["ready", "pending", "no_data"]),
1277
+ entries: z8.array(z8.looseObject({ label: z8.string(), value: summaryValueSchema })).default([]),
1278
+ as_of: z8.number().optional(),
1279
+ data: z8.array(z8.looseObject({ label: z8.string(), value: summaryDataValueSchema })).nullable().optional()
1280
+ });
1281
+ var summariesPageSchema = z8.looseObject({
1282
+ data: z8.array(summarySchema),
1283
+ has_more: z8.boolean().optional()
1284
+ });
1285
+ var SummariesResource = class extends BaseResource {
1286
+ constructor(options) {
1287
+ super(options, "/summaries");
1288
+ }
1289
+ buildUrl(params) {
1290
+ const url = new URL(this.endpoint);
1291
+ if (params.starting_after !== void 0)
1292
+ url.searchParams.set("starting_after", params.starting_after);
1293
+ if (params.summaries !== void 0)
1294
+ for (const summary of params.summaries)
1295
+ url.searchParams.append("summaries[]", summary);
1296
+ return url.toString();
1297
+ }
1298
+ async list(params = {}) {
1299
+ const { status, data, rawBody } = await this.apiFetch({
1300
+ method: "GET",
1301
+ url: this.buildUrl(params)
1302
+ });
1303
+ if (status < 200 || status >= 300)
1304
+ this.throwApiError("list summaries", status, data, rawBody);
1305
+ return this.parseResponse(
1306
+ "list summaries",
1307
+ status,
1308
+ () => summariesPageSchema.parse(data)
1309
+ );
1310
+ }
1311
+ };
1312
+
565
1313
  // 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()
1314
+ import { z as z9 } from "zod";
1315
+ var transactionSchema = z9.looseObject({
1316
+ id: z9.string(),
1317
+ source_id: z9.string().nullable(),
1318
+ amount: z9.number(),
1319
+ currency: z9.string(),
1320
+ created_date: z9.string(),
1321
+ description: z9.string(),
1322
+ origin: z9.enum(["link", "external_connection"]),
1323
+ category: z9.string().nullable(),
1324
+ status: z9.string()
577
1325
  });
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()
1326
+ var transactionsPageSchema = z9.union([
1327
+ z9.array(transactionSchema).transform((data) => ({ data })),
1328
+ z9.looseObject({
1329
+ data: z9.array(transactionSchema),
1330
+ has_more: z9.boolean().optional()
583
1331
  })
584
1332
  ]);
585
1333
  var TransactionsResource = class extends BaseResource {
@@ -633,35 +1381,35 @@ var TransactionsResource = class extends BaseResource {
633
1381
  };
634
1382
 
635
1383
  // 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()
1384
+ import { z as z10 } from "zod";
1385
+ var rollingSpendLimitSchema = z10.object({
1386
+ limit: z10.number().int().nullable(),
1387
+ used: z10.number().int(),
1388
+ remaining: z10.number().int().nullable()
641
1389
  });
642
- var agentWalletSpendLimitsSchema = z8.object({
643
- per_transaction: z8.object({
644
- limit: z8.number().int().nullable()
1390
+ var agentWalletSpendLimitsSchema = z10.object({
1391
+ per_transaction: z10.object({
1392
+ limit: z10.number().int().nullable()
645
1393
  }),
646
1394
  daily: rollingSpendLimitSchema,
647
1395
  thirty_day: rollingSpendLimitSchema
648
1396
  });
649
- var agentWalletVerificationRequirementSchema = z8.object({
650
- status: z8.enum([
1397
+ var agentWalletVerificationRequirementSchema = z10.object({
1398
+ status: z10.enum([
651
1399
  "not_required",
652
1400
  "ssn_verification",
653
1401
  "identity_verification",
654
1402
  "contact_support",
655
1403
  "complete"
656
1404
  ]),
657
- action_url: z8.string().nullable()
1405
+ action_url: z10.string().nullable()
658
1406
  });
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(),
1407
+ var userInfoSchema = z10.looseObject({
1408
+ email: z10.string().nullable().optional(),
1409
+ name: z10.string().nullable().optional(),
1410
+ first_name: z10.string().nullable().optional(),
1411
+ last_name: z10.string().nullable().optional(),
1412
+ phone: z10.string().nullable().optional(),
665
1413
  agent_wallet_spend_limits: agentWalletSpendLimitsSchema.optional(),
666
1414
  agent_wallet_step_up: agentWalletVerificationRequirementSchema.optional()
667
1415
  }).transform(
@@ -704,16 +1452,16 @@ var UserInfoResource = class extends BaseResource {
704
1452
  };
705
1453
 
706
1454
  // src/resources/web-bot-auth.ts
707
- import { z as z9 } from "zod";
1455
+ import { z as z11 } from "zod";
708
1456
  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()
1457
+ var webBotAuthBlockSchema = z11.looseObject({
1458
+ signature: z11.string(),
1459
+ signature_input: z11.string(),
1460
+ signature_agent: z11.string(),
1461
+ authority: z11.string(),
1462
+ expires_at: z11.string()
715
1463
  });
716
- var webBotAuthResponseSchema = z9.looseObject({
1464
+ var webBotAuthResponseSchema = z11.looseObject({
717
1465
  web_bot_auth: webBotAuthBlockSchema
718
1466
  });
719
1467
  var WebBotAuthResource = class extends BaseResource {
@@ -759,6 +1507,7 @@ var WebBotAuthResource = class extends BaseResource {
759
1507
 
760
1508
  // src/client.ts
761
1509
  var Link = class {
1510
+ attestations;
762
1511
  spendRequests;
763
1512
  paymentMethods;
764
1513
  shippingAddresses;
@@ -766,9 +1515,11 @@ var Link = class {
766
1515
  transactions;
767
1516
  sources;
768
1517
  balances;
1518
+ summaries;
769
1519
  webBotAuth;
770
1520
  reports;
771
1521
  constructor(options) {
1522
+ this.attestations = new AttestationsResource(options);
772
1523
  this.spendRequests = new SpendRequestResource(options);
773
1524
  this.paymentMethods = new PaymentMethodsResource(options);
774
1525
  this.shippingAddresses = new ShippingAddressResource(options);
@@ -776,6 +1527,7 @@ var Link = class {
776
1527
  this.transactions = new TransactionsResource(options);
777
1528
  this.sources = new SourcesResource(options);
778
1529
  this.balances = new BalancesResource(options);
1530
+ this.summaries = new SummariesResource(options);
779
1531
  this.webBotAuth = new WebBotAuthResource(options);
780
1532
  this.reports = new ReportResource(options);
781
1533
  }
@@ -802,6 +1554,7 @@ var REPORT_TAGS = [
802
1554
  ];
803
1555
  var REPORT_ATTEMPT_TRACE_MAX_LENGTH = 8e3;
804
1556
  export {
1557
+ AttestationsResource,
805
1558
  Link,
806
1559
  LinkApiError,
807
1560
  LinkConfigurationError,
@@ -811,6 +1564,7 @@ export {
811
1564
  REPORT_ATTEMPT_TRACE_MAX_LENGTH,
812
1565
  REPORT_OUTCOMES,
813
1566
  REPORT_TAGS,
1567
+ SummariesResource,
814
1568
  client_default as default,
815
1569
  getDuplicateSpendRequest
816
1570
  };