@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/README.md +1 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +863 -109
- package/dist/index.js.map +1 -1
- package/dist/resources/__tests__/attestations-crypto.test.d.ts +2 -0
- package/dist/resources/__tests__/attestations-crypto.test.d.ts.map +1 -0
- package/dist/resources/__tests__/attestations.test.d.ts +2 -0
- package/dist/resources/__tests__/attestations.test.d.ts.map +1 -0
- package/dist/resources/__tests__/summaries.test.d.ts +2 -0
- package/dist/resources/__tests__/summaries.test.d.ts.map +1 -0
- package/dist/resources/attestations-crypto.d.ts +35 -0
- package/dist/resources/attestations-crypto.d.ts.map +1 -0
- package/dist/resources/attestations.d.ts +14 -0
- package/dist/resources/attestations.d.ts.map +1 -0
- package/dist/resources/balances.d.ts.map +1 -1
- package/dist/resources/interfaces.d.ts +20 -1
- package/dist/resources/interfaces.d.ts.map +1 -1
- package/dist/resources/summaries.d.ts +10 -0
- package/dist/resources/summaries.d.ts.map +1 -0
- package/dist/types/index.d.ts +48 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// src/resources/
|
|
1
|
+
// src/resources/attestations.ts
|
|
2
|
+
import { createHash as createHash2 } from "crypto";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
|
|
4
5
|
// src/errors.ts
|
|
@@ -44,6 +45,364 @@ var LinkApiError = class extends LinkSdkError {
|
|
|
44
45
|
}
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
// src/resources/attestations-crypto.ts
|
|
49
|
+
import {
|
|
50
|
+
createHash,
|
|
51
|
+
createPublicKey,
|
|
52
|
+
randomBytes,
|
|
53
|
+
timingSafeEqual
|
|
54
|
+
} from "crypto";
|
|
55
|
+
var TOKEN_TYPE = 2;
|
|
56
|
+
var NONCE_SIZE = 32;
|
|
57
|
+
var CHALLENGE_DIGEST_SIZE = 32;
|
|
58
|
+
var TOKEN_KEY_ID_SIZE = 32;
|
|
59
|
+
function base64urlEncode(buf) {
|
|
60
|
+
return Buffer.from(buf).toString("base64url");
|
|
61
|
+
}
|
|
62
|
+
function base64urlDecode(str) {
|
|
63
|
+
return new Uint8Array(Buffer.from(str, "base64url"));
|
|
64
|
+
}
|
|
65
|
+
function bytesToBigInt(bytes) {
|
|
66
|
+
let hex = "";
|
|
67
|
+
for (const b of bytes) {
|
|
68
|
+
hex += b.toString(16).padStart(2, "0");
|
|
69
|
+
}
|
|
70
|
+
return hex.length === 0 ? 0n : BigInt(`0x${hex}`);
|
|
71
|
+
}
|
|
72
|
+
function bigIntToBytes(n, length) {
|
|
73
|
+
if (!Number.isInteger(length) || length < 0) {
|
|
74
|
+
throw new Error("Integer encoding length must be a non-negative integer");
|
|
75
|
+
}
|
|
76
|
+
if (n < 0n || n >= 1n << BigInt(length * 8)) {
|
|
77
|
+
throw new Error(`Integer does not fit in ${length} bytes`);
|
|
78
|
+
}
|
|
79
|
+
const hex = n.toString(16).padStart(length * 2, "0");
|
|
80
|
+
const bytes = new Uint8Array(length);
|
|
81
|
+
for (let i = 0; i < length; i++) {
|
|
82
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
83
|
+
}
|
|
84
|
+
return bytes;
|
|
85
|
+
}
|
|
86
|
+
function bitLength(value) {
|
|
87
|
+
if (value <= 0n) throw new Error("Bit length requires a positive integer");
|
|
88
|
+
return value.toString(2).length;
|
|
89
|
+
}
|
|
90
|
+
function modPow(base, exp, mod) {
|
|
91
|
+
let result = 1n;
|
|
92
|
+
let b = (base % mod + mod) % mod;
|
|
93
|
+
let e = exp;
|
|
94
|
+
while (e > 0n) {
|
|
95
|
+
if (e & 1n) {
|
|
96
|
+
result = result * b % mod;
|
|
97
|
+
}
|
|
98
|
+
e >>= 1n;
|
|
99
|
+
b = b * b % mod;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function modInverse(a, m) {
|
|
104
|
+
let [oldR, r] = [a, m];
|
|
105
|
+
let [oldS, s] = [1n, 0n];
|
|
106
|
+
while (r !== 0n) {
|
|
107
|
+
const q = oldR / r;
|
|
108
|
+
[oldR, r] = [r, oldR - q * r];
|
|
109
|
+
[oldS, s] = [s, oldS - q * s];
|
|
110
|
+
}
|
|
111
|
+
if (oldR !== 1n) {
|
|
112
|
+
throw new Error("Value has no modular inverse");
|
|
113
|
+
}
|
|
114
|
+
return (oldS % m + m) % m;
|
|
115
|
+
}
|
|
116
|
+
function greatestCommonDivisor(a, b) {
|
|
117
|
+
let x = a < 0n ? -a : a;
|
|
118
|
+
let y = b < 0n ? -b : b;
|
|
119
|
+
while (y !== 0n) {
|
|
120
|
+
[x, y] = [y, x % y];
|
|
121
|
+
}
|
|
122
|
+
return x;
|
|
123
|
+
}
|
|
124
|
+
function parseSpkiPublicKey(spkiDer) {
|
|
125
|
+
let keyObject;
|
|
126
|
+
try {
|
|
127
|
+
keyObject = createPublicKey({
|
|
128
|
+
key: Buffer.from(spkiDer),
|
|
129
|
+
format: "der",
|
|
130
|
+
type: "spki"
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new Error("Invalid RSA issuer public key", { cause: error });
|
|
134
|
+
}
|
|
135
|
+
const details = keyObject.asymmetricKeyDetails;
|
|
136
|
+
if (keyObject.asymmetricKeyType !== "rsa-pss" || details?.modulusLength !== 2048 || details.hashAlgorithm !== "sha384" || details.mgf1HashAlgorithm !== "sha384" || details.saltLength !== 48) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"Issuer key must use 2048-bit RSA-PSS with SHA-384, MGF1-SHA-384, and a 48-byte salt"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const spki = readSequence(spkiDer, 0);
|
|
142
|
+
const algorithm = readTlv(spkiDer, spki.contentStart);
|
|
143
|
+
const bitString = readTlv(spkiDer, algorithm.end);
|
|
144
|
+
if (bitString.tag !== 3) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Expected BIT STRING in SPKI, got 0x${bitString.tag.toString(16)}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (bitString.contentStart >= bitString.end || spkiDer[bitString.contentStart] !== 0) {
|
|
150
|
+
throw new Error("RSA SPKI BIT STRING must have zero unused bits");
|
|
151
|
+
}
|
|
152
|
+
const rsaPublicKeyDer = spkiDer.slice(
|
|
153
|
+
bitString.contentStart + 1,
|
|
154
|
+
bitString.end
|
|
155
|
+
);
|
|
156
|
+
const rsaPublicKey = readSequence(rsaPublicKeyDer, 0);
|
|
157
|
+
const modulus = readInteger(rsaPublicKeyDer, rsaPublicKey.contentStart);
|
|
158
|
+
const exponent = readInteger(rsaPublicKeyDer, modulus.end);
|
|
159
|
+
const n = bytesToBigInt(modulus.value);
|
|
160
|
+
const e = bytesToBigInt(exponent.value);
|
|
161
|
+
if (modulus.value.length !== 256 || bitLength(n) !== 2048) {
|
|
162
|
+
throw new Error("Token type 0x0002 requires a 2048-bit RSA modulus");
|
|
163
|
+
}
|
|
164
|
+
if (e < 3n || e % 2n === 0n) {
|
|
165
|
+
throw new Error("RSA public exponent must be an odd integer of at least 3");
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
n,
|
|
169
|
+
e,
|
|
170
|
+
nLen: modulus.value.length
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function readTlv(data, offset) {
|
|
174
|
+
if (offset >= data.length) {
|
|
175
|
+
throw new Error(`Unexpected end of DER at offset ${offset}`);
|
|
176
|
+
}
|
|
177
|
+
const tag = data[offset];
|
|
178
|
+
if (tag === void 0) {
|
|
179
|
+
throw new Error(`Unexpected end of DER at offset ${offset}`);
|
|
180
|
+
}
|
|
181
|
+
const { value: length, bytesRead } = parseDerLength(data, offset + 1);
|
|
182
|
+
const contentStart = offset + 1 + bytesRead;
|
|
183
|
+
const end = contentStart + length;
|
|
184
|
+
if (end > data.length) {
|
|
185
|
+
throw new Error(`DER element at offset ${offset} overruns the buffer`);
|
|
186
|
+
}
|
|
187
|
+
return { tag, contentStart, end };
|
|
188
|
+
}
|
|
189
|
+
function readSequence(data, offset) {
|
|
190
|
+
const tlv = readTlv(data, offset);
|
|
191
|
+
if (tlv.tag !== 48) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Expected SEQUENCE at offset ${offset}, got 0x${tlv.tag.toString(16)}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return tlv;
|
|
197
|
+
}
|
|
198
|
+
function readInteger(data, offset) {
|
|
199
|
+
const tlv = readTlv(data, offset);
|
|
200
|
+
if (tlv.tag !== 2) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Expected INTEGER at offset ${offset}, got 0x${tlv.tag.toString(16)}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
let value = data.slice(tlv.contentStart, tlv.end);
|
|
206
|
+
if (value.length > 1 && value[0] === 0) {
|
|
207
|
+
value = value.slice(1);
|
|
208
|
+
}
|
|
209
|
+
return { value, end: tlv.end };
|
|
210
|
+
}
|
|
211
|
+
function parseDerLength(data, offset) {
|
|
212
|
+
const first = data[offset];
|
|
213
|
+
if (first === void 0) {
|
|
214
|
+
throw new Error(`Unexpected end of DER at offset ${offset}`);
|
|
215
|
+
}
|
|
216
|
+
if (first < 128) {
|
|
217
|
+
return { value: first, bytesRead: 1 };
|
|
218
|
+
}
|
|
219
|
+
const numBytes = first & 127;
|
|
220
|
+
if (numBytes === 0 || offset + numBytes >= data.length) {
|
|
221
|
+
throw new Error(`Invalid DER length at offset ${offset}`);
|
|
222
|
+
}
|
|
223
|
+
let value = 0;
|
|
224
|
+
for (let i = 0; i < numBytes; i++) {
|
|
225
|
+
const byte = data[offset + 1 + i];
|
|
226
|
+
if (byte === void 0) {
|
|
227
|
+
throw new Error(`Unexpected end of DER at offset ${offset + 1 + i}`);
|
|
228
|
+
}
|
|
229
|
+
value = value * 256 + byte;
|
|
230
|
+
}
|
|
231
|
+
return { value, bytesRead: 1 + numBytes };
|
|
232
|
+
}
|
|
233
|
+
function emsaPssEncode(message, emBits) {
|
|
234
|
+
const hashAlg = "sha384";
|
|
235
|
+
const hLen = 48;
|
|
236
|
+
const sLen = 48;
|
|
237
|
+
const emLen = Math.ceil(emBits / 8);
|
|
238
|
+
const mHash = createHash(hashAlg).update(message).digest();
|
|
239
|
+
if (emLen < hLen + sLen + 2) {
|
|
240
|
+
throw new Error("Encoding error: emLen too small");
|
|
241
|
+
}
|
|
242
|
+
const salt = randomBytes(sLen);
|
|
243
|
+
const mPrime = Buffer.concat([Buffer.alloc(8), mHash, salt]);
|
|
244
|
+
const h = createHash(hashAlg).update(mPrime).digest();
|
|
245
|
+
const ps = Buffer.alloc(emLen - sLen - hLen - 2);
|
|
246
|
+
const db = Buffer.concat([ps, Buffer.from([1]), salt]);
|
|
247
|
+
const dbMask = mgf1(h, db.length, hashAlg);
|
|
248
|
+
const maskedDb = Buffer.alloc(db.length);
|
|
249
|
+
for (let i = 0; i < db.length; i++) {
|
|
250
|
+
maskedDb.writeUInt8(db.readUInt8(i) ^ dbMask.readUInt8(i), i);
|
|
251
|
+
}
|
|
252
|
+
const topBits = 8 * emLen - emBits;
|
|
253
|
+
maskedDb.writeUInt8(maskedDb.readUInt8(0) & 255 >> topBits, 0);
|
|
254
|
+
return new Uint8Array(Buffer.concat([maskedDb, h, Buffer.from([188])]));
|
|
255
|
+
}
|
|
256
|
+
function mgf1(seed, length, hashAlg) {
|
|
257
|
+
const hLen = hashAlg === "sha384" ? 48 : 32;
|
|
258
|
+
const result = Buffer.alloc(length);
|
|
259
|
+
let offset = 0;
|
|
260
|
+
let counter = 0;
|
|
261
|
+
while (offset < length) {
|
|
262
|
+
const c = Buffer.alloc(4);
|
|
263
|
+
c.writeUInt32BE(counter);
|
|
264
|
+
const hash = createHash(hashAlg).update(seed).update(c).digest();
|
|
265
|
+
const toCopy = Math.min(hLen, length - offset);
|
|
266
|
+
hash.copy(result, offset, 0, toCopy);
|
|
267
|
+
offset += toCopy;
|
|
268
|
+
counter++;
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
function generateBlindingFactor(n, nLen) {
|
|
273
|
+
while (true) {
|
|
274
|
+
const rBytes = randomBytes(nLen);
|
|
275
|
+
const r = bytesToBigInt(new Uint8Array(rBytes));
|
|
276
|
+
if (r < 1n || r >= n) continue;
|
|
277
|
+
try {
|
|
278
|
+
const rInv = modInverse(r, n);
|
|
279
|
+
return { r, rInv };
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function generateBlindedMessages(spkiDer, count, challengeDigest) {
|
|
285
|
+
if (!Number.isInteger(count) || count < 1 || count > 100) {
|
|
286
|
+
throw new Error("Token count must be an integer from 1 to 100");
|
|
287
|
+
}
|
|
288
|
+
if (challengeDigest.length !== CHALLENGE_DIGEST_SIZE) {
|
|
289
|
+
throw new Error("Challenge digest must be exactly 32 bytes");
|
|
290
|
+
}
|
|
291
|
+
const publicKey = parseSpkiPublicKey(spkiDer);
|
|
292
|
+
const tokenKeyId = new Uint8Array(
|
|
293
|
+
createHash("sha256").update(spkiDer).digest()
|
|
294
|
+
);
|
|
295
|
+
const emBits = bitLength(publicKey.n) - 1;
|
|
296
|
+
const tokens = [];
|
|
297
|
+
for (let i = 0; i < count; i++) {
|
|
298
|
+
const nonce = new Uint8Array(randomBytes(NONCE_SIZE));
|
|
299
|
+
const tokenInput = new Uint8Array(
|
|
300
|
+
2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE + TOKEN_KEY_ID_SIZE
|
|
301
|
+
);
|
|
302
|
+
tokenInput[0] = TOKEN_TYPE >> 8 & 255;
|
|
303
|
+
tokenInput[1] = TOKEN_TYPE & 255;
|
|
304
|
+
tokenInput.set(nonce, 2);
|
|
305
|
+
tokenInput.set(challengeDigest, 2 + NONCE_SIZE);
|
|
306
|
+
tokenInput.set(tokenKeyId, 2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE);
|
|
307
|
+
const encoded = emsaPssEncode(tokenInput, emBits);
|
|
308
|
+
const message = bytesToBigInt(encoded);
|
|
309
|
+
if (greatestCommonDivisor(message, publicKey.n) !== 1n) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
"PSS-encoded token input is not coprime to the RSA modulus"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const { r, rInv } = generateBlindingFactor(publicKey.n, publicKey.nLen);
|
|
315
|
+
const blindedMessage = message * modPow(r, publicKey.e, publicKey.n) % publicKey.n;
|
|
316
|
+
tokens.push({
|
|
317
|
+
nonce,
|
|
318
|
+
blindedMsg: bigIntToBytes(blindedMessage, publicKey.nLen),
|
|
319
|
+
blindInverse: rInv,
|
|
320
|
+
encodedMessage: encoded
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return { tokens, publicKey, challengeDigest, tokenKeyId };
|
|
324
|
+
}
|
|
325
|
+
function unblindSignatures(state, blindSigs) {
|
|
326
|
+
const { tokens, publicKey, tokenKeyId } = state;
|
|
327
|
+
if (blindSigs.length !== tokens.length) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Expected ${tokens.length} blind signatures, got ${blindSigs.length}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const results = [];
|
|
333
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
334
|
+
const token = tokens[i];
|
|
335
|
+
const blindSignature = blindSigs[i];
|
|
336
|
+
if (!token || blindSignature === void 0) {
|
|
337
|
+
throw new Error(`Missing blind signature state at index ${i}`);
|
|
338
|
+
}
|
|
339
|
+
const blindSigBytes = base64urlDecode(blindSignature);
|
|
340
|
+
if (blindSigBytes.length !== publicKey.nLen) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Blind signature ${i} must be exactly ${publicKey.nLen} bytes`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const blindSigInt = bytesToBigInt(blindSigBytes);
|
|
346
|
+
const sigInt = blindSigInt * token.blindInverse % publicKey.n;
|
|
347
|
+
const authenticator = bigIntToBytes(sigInt, publicKey.nLen);
|
|
348
|
+
const recoveredMessage = bigIntToBytes(
|
|
349
|
+
modPow(sigInt, publicKey.e, publicKey.n),
|
|
350
|
+
publicKey.nLen
|
|
351
|
+
);
|
|
352
|
+
if (!timingSafeEqual(
|
|
353
|
+
Buffer.from(recoveredMessage),
|
|
354
|
+
Buffer.from(token.encodedMessage)
|
|
355
|
+
)) {
|
|
356
|
+
throw new Error(`Blind signature ${i} failed verification`);
|
|
357
|
+
}
|
|
358
|
+
const raw = new Uint8Array(
|
|
359
|
+
2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE + TOKEN_KEY_ID_SIZE + publicKey.nLen
|
|
360
|
+
);
|
|
361
|
+
raw[0] = TOKEN_TYPE >> 8 & 255;
|
|
362
|
+
raw[1] = TOKEN_TYPE & 255;
|
|
363
|
+
raw.set(token.nonce, 2);
|
|
364
|
+
raw.set(state.challengeDigest, 2 + NONCE_SIZE);
|
|
365
|
+
raw.set(tokenKeyId, 2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE);
|
|
366
|
+
raw.set(
|
|
367
|
+
authenticator,
|
|
368
|
+
2 + NONCE_SIZE + CHALLENGE_DIGEST_SIZE + TOKEN_KEY_ID_SIZE
|
|
369
|
+
);
|
|
370
|
+
results.push({
|
|
371
|
+
raw,
|
|
372
|
+
base64url: base64urlEncode(raw)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return results;
|
|
376
|
+
}
|
|
377
|
+
function computeChallengeDigest(tokenType, issuerName) {
|
|
378
|
+
const issuerBytes = Buffer.from(issuerName, "utf-8");
|
|
379
|
+
const originBytes = Buffer.alloc(0);
|
|
380
|
+
if (!Number.isInteger(tokenType) || tokenType < 0 || tokenType > 65535) {
|
|
381
|
+
throw new Error("Token type must fit in an unsigned 16-bit integer");
|
|
382
|
+
}
|
|
383
|
+
if (issuerBytes.length > 65535) {
|
|
384
|
+
throw new Error("Issuer name must fit in an unsigned 16-bit length");
|
|
385
|
+
}
|
|
386
|
+
const challenge = Buffer.alloc(
|
|
387
|
+
2 + 2 + issuerBytes.length + 1 + 2 + originBytes.length
|
|
388
|
+
);
|
|
389
|
+
let offset = 0;
|
|
390
|
+
challenge.writeUInt16BE(tokenType, offset);
|
|
391
|
+
offset += 2;
|
|
392
|
+
challenge.writeUInt16BE(issuerBytes.length, offset);
|
|
393
|
+
offset += 2;
|
|
394
|
+
issuerBytes.copy(challenge, offset);
|
|
395
|
+
offset += issuerBytes.length;
|
|
396
|
+
challenge.writeUInt8(0, offset);
|
|
397
|
+
offset += 1;
|
|
398
|
+
challenge.writeUInt16BE(originBytes.length, offset);
|
|
399
|
+
offset += 2;
|
|
400
|
+
if (originBytes.length > 0) {
|
|
401
|
+
originBytes.copy(challenge, offset);
|
|
402
|
+
}
|
|
403
|
+
return new Uint8Array(createHash("sha256").update(challenge).digest());
|
|
404
|
+
}
|
|
405
|
+
|
|
47
406
|
// src/config.ts
|
|
48
407
|
var DEFAULT_API_BASE_URL = "https://api.link.com";
|
|
49
408
|
function createDefaultHeadersFetch(baseFetch, defaultHeaders) {
|
|
@@ -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
|
-
|
|
222
|
-
var
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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 =
|
|
232
|
-
data:
|
|
233
|
-
has_more:
|
|
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
|
|
275
|
-
var paymentMethodSchema =
|
|
276
|
-
id:
|
|
277
|
-
type:
|
|
278
|
-
is_default:
|
|
279
|
-
name:
|
|
280
|
-
nickname:
|
|
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 =
|
|
283
|
-
payment_details:
|
|
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
|
|
307
|
-
var reportRecordSchema =
|
|
308
|
-
object:
|
|
309
|
-
created_at:
|
|
310
|
-
domain:
|
|
311
|
-
outcome:
|
|
312
|
-
spend_request_id:
|
|
313
|
-
status:
|
|
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
|
|
339
|
-
var shippingAddressSchema =
|
|
340
|
-
id:
|
|
341
|
-
is_default:
|
|
342
|
-
nickname:
|
|
343
|
-
address:
|
|
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 =
|
|
346
|
-
shipping_addresses:
|
|
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
|
|
370
|
-
var nullableRecordSchema =
|
|
371
|
-
var sourceSchema =
|
|
372
|
-
id:
|
|
373
|
-
name:
|
|
374
|
-
type:
|
|
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:
|
|
1066
|
+
granted_actions: z6.array(z6.string()).nullable().optional(),
|
|
378
1067
|
bank_account: nullableRecordSchema,
|
|
379
1068
|
card: nullableRecordSchema
|
|
380
1069
|
});
|
|
381
|
-
var sourcesPageSchema =
|
|
382
|
-
data:
|
|
383
|
-
has_more:
|
|
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
|
|
420
|
-
var sharedPaymentTokenSchema =
|
|
421
|
-
|
|
422
|
-
|
|
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 =
|
|
425
|
-
id:
|
|
426
|
-
payment_details:
|
|
427
|
-
status:
|
|
428
|
-
line_items:
|
|
429
|
-
totals:
|
|
430
|
-
created_at:
|
|
431
|
-
updated_at:
|
|
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 =
|
|
435
|
-
data:
|
|
1123
|
+
var spendRequestsResponseSchema = z7.looseObject({
|
|
1124
|
+
data: z7.array(spendRequestSchema)
|
|
436
1125
|
});
|
|
437
|
-
var requestApprovalResponseSchema =
|
|
438
|
-
id:
|
|
439
|
-
approval_link:
|
|
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 =
|
|
445
|
-
error:
|
|
446
|
-
duplicate_spend_request:
|
|
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
|
|
567
|
-
var transactionSchema =
|
|
568
|
-
id:
|
|
569
|
-
source_id:
|
|
570
|
-
amount:
|
|
571
|
-
currency:
|
|
572
|
-
created_date:
|
|
573
|
-
description:
|
|
574
|
-
origin:
|
|
575
|
-
category:
|
|
576
|
-
status:
|
|
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 =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
data:
|
|
582
|
-
has_more:
|
|
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
|
|
637
|
-
var rollingSpendLimitSchema =
|
|
638
|
-
limit:
|
|
639
|
-
used:
|
|
640
|
-
remaining:
|
|
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 =
|
|
643
|
-
per_transaction:
|
|
644
|
-
limit:
|
|
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 =
|
|
650
|
-
status:
|
|
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:
|
|
1405
|
+
action_url: z10.string().nullable()
|
|
658
1406
|
});
|
|
659
|
-
var userInfoSchema =
|
|
660
|
-
email:
|
|
661
|
-
name:
|
|
662
|
-
first_name:
|
|
663
|
-
last_name:
|
|
664
|
-
phone:
|
|
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
|
|
1455
|
+
import { z as z11 } from "zod";
|
|
708
1456
|
var EXPIRY_BUFFER_MS = 3e4;
|
|
709
|
-
var webBotAuthBlockSchema =
|
|
710
|
-
signature:
|
|
711
|
-
signature_input:
|
|
712
|
-
signature_agent:
|
|
713
|
-
authority:
|
|
714
|
-
expires_at:
|
|
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 =
|
|
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
|
};
|