payment-kit 1.29.13 → 1.29.15

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.
@@ -3,3 +3,15 @@ export const EVM_CHAIN_TYPES = ['ethereum', 'base'];
3
3
  export const CHARGE_SUPPORTED_CHAIN_TYPES = ['arcblock', 'ethereum', 'base'];
4
4
 
5
5
  export const VAULT_BUFFER_THRESHOLD = 10;
6
+
7
+ // Conservative cap on the number of past-due invoice ids embedded in a single
8
+ // arcblock batch-collect transaction's on-chain metadata (see getTxMetadata()
9
+ // in libs/util.ts, consumed by routes/connect/collect-batch.ts). Each invoice
10
+ // id is up to 30 chars (Invoice.GENESIS_ATTRIBUTES.id is STRING(30)), so at
11
+ // this cap the `invoices` JSON array alone stays in the low single-digit KB.
12
+ // We could not find a documented/exact byte-size limit for arcblock chain tx
13
+ // data in this repo or in @ocap/client, so this is a safety margin chosen to
14
+ // stay well clear of the "tx size exceeds chain limit" failure reported in
15
+ // https://github.com/blocklet/payment-kit/issues/1128, not a value verified
16
+ // against the protocol's exact threshold.
17
+ export const MAX_BATCH_COLLECT_INVOICES = 200;
@@ -386,7 +386,7 @@ setInterval(
386
386
  }
387
387
  },
388
388
  60 * 60 * 1000
389
- ); // 每小时清理一次
389
+ ).unref(); // 每小时清理一次;unref 避免该定时器阻塞进程退出(同 libs/reference-cache.ts 的既有模式)
390
390
 
391
391
  /**
392
392
  * Clear notification cache entries that match given prefix
@@ -45,7 +45,7 @@ import { PriceQuote } from '../../store/models/price-quote';
45
45
  import { SetupIntent } from '../../store/models/setup-intent';
46
46
  import { Subscription } from '../../store/models/subscription';
47
47
  import { ensureInvoiceAndItems } from '../../libs/invoice';
48
- import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../../libs/constants';
48
+ import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES, MAX_BATCH_COLLECT_INVOICES } from '../../libs/constants';
49
49
  import { returnStakeQueue } from '../../queues/subscription';
50
50
 
51
51
  type Result = {
@@ -1511,12 +1511,30 @@ export async function ensureSubscriptionForCollectBatch(
1511
1511
  throw new Error(`Payment method not found for currency ${currencyId}`);
1512
1512
  }
1513
1513
 
1514
+ const invoices = detail[currencyId] || [];
1515
+
1516
+ // arcblock batch-collect embeds the full `invoices` id list into the on-chain tx
1517
+ // metadata (see getTxMetadata() usage in routes/connect/collect-batch.ts). That
1518
+ // JSON blob has no length limit of its own, so an unbounded invoice count can
1519
+ // grow the tx data past the chain's tx size limit (issue #1128). Fail fast with a
1520
+ // clear error instead of silently truncating the list — the invoices field is
1521
+ // display-only on chain (payment reconciliation re-queries this function from the
1522
+ // DB, it never decodes the invoice list back out of the tx), so truncating it
1523
+ // would be safe for reconciliation, but it would also make the on-chain memo
1524
+ // permanently misleading/incomplete. Rejecting the request keeps the ledger
1525
+ // trustworthy and asks the customer/admin to collect in smaller batches instead.
1526
+ if (paymentMethod.type === 'arcblock' && invoices.length > MAX_BATCH_COLLECT_INVOICES) {
1527
+ throw new Error(
1528
+ `Too many past due invoices to batch collect in a single on-chain transaction (${invoices.length} invoices, limit is ${MAX_BATCH_COLLECT_INVOICES}). Please pay a smaller subset of invoices, or contact support to collect the remainder separately.`
1529
+ );
1530
+ }
1531
+
1514
1532
  return {
1515
1533
  subscription,
1516
1534
  paymentCurrency: paymentCurrency as PaymentCurrency,
1517
1535
  paymentMethod: paymentMethod as PaymentMethod,
1518
1536
  amount: summary[currencyId],
1519
- invoices: detail[currencyId],
1537
+ invoices,
1520
1538
  };
1521
1539
  }
1522
1540
 
@@ -2922,7 +2922,7 @@ app.put('/:id/submit', user, ensureCheckoutSessionOpen, async (c) => {
2922
2922
  }
2923
2923
 
2924
2924
  // check if customer can make new purchase
2925
- const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
2925
+ const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id, checkoutSession.livemode);
2926
2926
  if (!canMakeNewPurchase) {
2927
2927
  return c.json(
2928
2928
  {
@@ -3621,7 +3621,7 @@ app.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async (c
3621
3621
  }
3622
3622
  }
3623
3623
 
3624
- const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
3624
+ const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id, checkoutSession.livemode);
3625
3625
  if (!canMakeNewPurchase) {
3626
3626
  return c.json(
3627
3627
  {
@@ -212,10 +212,10 @@ export class Customer extends TenantModel<InferAttributes<Customer>, InferCreati
212
212
  return { paid, due, refunded };
213
213
  }
214
214
 
215
- public async canMakeNewPurchase(excludedInvoiceId: string = '') {
215
+ public async canMakeNewPurchase(excludedInvoiceId: string = '', livemode?: boolean) {
216
216
  const { Invoice } = this.sequelize.models;
217
217
  // @ts-ignore
218
- const [summary] = await Invoice!.getUncollectibleAmount({ customerId: this.id, excludedInvoiceId });
218
+ const [summary] = await Invoice!.getUncollectibleAmount({ customerId: this.id, excludedInvoiceId, livemode });
219
219
  return Object.entries(summary).every(([, amount]) => new BN(amount as string).lte(new BN(0)));
220
220
  }
221
221
 
@@ -6,7 +6,7 @@ import { X509Certificate } from 'node:crypto';
6
6
  import { readdirSync, readFileSync, statSync } from 'node:fs';
7
7
  import path from 'node:path';
8
8
 
9
- import { APPLE_INT_OID, APPLE_LEAF_OID, buildJws, makeChain } from './make-chain';
9
+ import { APPLE_INT_OID, APPLE_LEAF_OID, buildJws, Chain, makeChain } from './make-chain';
10
10
 
11
11
  // Full DER OID tag (mirrors native-asn1.ts, see 0.3) — the strict form.
12
12
  function oidToDerTag(oid: string): Buffer {
@@ -28,7 +28,10 @@ function oidToDerTag(oid: string): Buffer {
28
28
  const certOf = (b64: string): X509Certificate => new X509Certificate(Buffer.from(b64, 'base64'));
29
29
 
30
30
  describe('make-chain — happy path', () => {
31
- const chain = makeChain({ signedDate: 1_781_521_334_379 });
31
+ let chain: Chain;
32
+ beforeAll(async () => {
33
+ chain = await makeChain({ signedDate: 1_781_521_334_379 });
34
+ });
32
35
 
33
36
  it('leaf verifies against intermediate, intermediate against root', () => {
34
37
  expect(chain.certs.leaf.verify(chain.certs.intermediate.publicKey)).toBe(true);
@@ -59,7 +62,10 @@ describe('make-chain — happy path', () => {
59
62
  });
60
63
 
61
64
  describe('make-chain — security negative controls', () => {
62
- const chain = makeChain();
65
+ let chain: Chain;
66
+ beforeAll(async () => {
67
+ chain = await makeChain();
68
+ });
63
69
 
64
70
  it('cross-signed leaf fails leaf.verify(intermediate.publicKey) but keeps issuer/subject equality', () => {
65
71
  const cross = certOf(chain.crossSignedLeaf);
@@ -72,14 +78,17 @@ describe('make-chain — security negative controls', () => {
72
78
  expect(Buffer.from(chain.certs.leaf.raw).includes(oidToDerTag(APPLE_INT_OID))).toBe(false);
73
79
  });
74
80
 
75
- it('omits the leaf OID on demand (leafOid: false)', () => {
76
- const noOid = makeChain({ leafOid: false });
81
+ it('omits the leaf OID on demand (leafOid: false)', async () => {
82
+ const noOid = await makeChain({ leafOid: false });
77
83
  expect(Buffer.from(noOid.certs.leaf.raw).includes(oidToDerTag(APPLE_LEAF_OID))).toBe(false);
78
84
  });
79
85
  });
80
86
 
81
87
  describe('make-chain — data damage / round-trip', () => {
82
- const chain = makeChain();
88
+ let chain: Chain;
89
+ beforeAll(async () => {
90
+ chain = await makeChain();
91
+ });
83
92
 
84
93
  it('decode(x5c[i]) byte-equals the DER we encoded', () => {
85
94
  expect(Buffer.from(chain.x5c[0]!, 'base64').equals(Buffer.from(chain.certs.leaf.raw))).toBe(true);
@@ -96,9 +105,9 @@ describe('make-chain — data damage / round-trip', () => {
96
105
  });
97
106
 
98
107
  describe('make-chain — validity window pinning', () => {
99
- it('pins notBefore/notAfter relative to signedDate', () => {
108
+ it('pins notBefore/notAfter relative to signedDate', async () => {
100
109
  const signedDate = 1_700_000_000_000;
101
- const chain = makeChain({
110
+ const chain = await makeChain({
102
111
  signedDate,
103
112
  certValidFrom: new Date(signedDate - 86_400_000),
104
113
  certValidTo: new Date(signedDate + 86_400_000),
@@ -136,7 +145,10 @@ describe('make-chain — data leak (committed test keys never reach src/)', () =
136
145
  });
137
146
 
138
147
  describe('make-chain — buildJws crafts variants', () => {
139
- const chain = makeChain();
148
+ let chain: Chain;
149
+ beforeAll(async () => {
150
+ chain = await makeChain();
151
+ });
140
152
 
141
153
  it('re-signs a custom header/payload with the leaf key', () => {
142
154
  const { jws, p1363Sig } = buildJws({ alg: 'ES256', x5c: chain.x5c }, { foo: 'bar' }, chain.keys.leafKeyPem);
@@ -24,19 +24,27 @@
24
24
  * Apple OID, ES256 P1363 sig over `header.payload`) is identical.
25
25
  *
26
26
  * Determinism: the EC P-256 signing keys are COMMITTED under `fixtures/keys/`
27
- * (synthetic, test-only) and the certs are derived from them at runtime with
27
+ * (synthetic, test-only) and the certs are minted from them at runtime with
28
28
  * pinned validity windows — so golden-vector VALUE assertions are stable and
29
29
  * the certs never expire. Pass `freshKeys: true` to generate unique keys for a
30
30
  * single call (e.g. to make two chains demonstrably distinct). The committed
31
31
  * keys are test-only and MUST never be imported by `src/` — a data-leak spec
32
32
  * (`make-chain.spec.ts`) guards that invariant. The committed REAL Apple
33
33
  * fixture (`real-sandbox.jws`) is the golden vector for real-chain field values.
34
+ *
35
+ * Certs are minted in-process with `@peculiar/x509` (no `openssl` shell-out),
36
+ * so the fixtures do not depend on the host toolchain's OpenSSL version.
34
37
  */
35
- import { execFileSync } from 'node:child_process';
36
- import { X509Certificate, sign as nodeSign } from 'node:crypto';
37
- import { copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
38
- import os from 'node:os';
38
+ // eslint-disable-next-line import/no-extraneous-dependencies
39
+ import 'reflect-metadata';
40
+ import { X509Certificate, createPrivateKey, createPublicKey, sign as nodeSign, webcrypto } from 'node:crypto';
41
+ import { readFileSync } from 'node:fs';
39
42
  import path from 'node:path';
43
+ // eslint-disable-next-line import/no-extraneous-dependencies
44
+ import * as x509 from '@peculiar/x509';
45
+
46
+ x509.cryptoProvider.set(webcrypto as unknown as Crypto);
47
+ const CERT_ALG = { name: 'ECDSA', namedCurve: 'P-256', hash: 'SHA-256' } as const;
40
48
 
41
49
  export const APPLE_LEAF_OID = '1.2.840.113635.100.6.11.1';
42
50
  export const APPLE_INT_OID = '1.2.840.113635.100.6.2.1';
@@ -92,23 +100,14 @@ export interface Chain {
92
100
  }
93
101
 
94
102
  const SUBJ = {
95
- root: '/CN=Test Apple Root CA/O=did-pay-test',
96
- int: '/CN=Test Apple WWDR Intermediate/O=did-pay-test',
97
- leaf: '/CN=Test Apple Leaf/O=did-pay-test',
103
+ root: 'CN=Test Apple Root CA, O=did-pay-test',
104
+ int: 'CN=Test Apple WWDR Intermediate, O=did-pay-test',
105
+ leaf: 'CN=Test Apple Leaf, O=did-pay-test',
98
106
  };
99
107
 
100
108
  /** Committed synthetic signing keys (test-only, never imported by src/). */
101
109
  const KEYS_DIR = path.join(__dirname, 'keys');
102
110
 
103
- /** openssl `-not_before`/`-not_after` want `[CC]YYMMDDHHMMSSZ`. */
104
- function fmtTime(d: Date): string {
105
- return `${d
106
- .toISOString()
107
- .replace(/[-:]/g, '')
108
- .replace('T', '')
109
- .replace(/\.\d+Z$/, '')}Z`;
110
- }
111
-
112
111
  /** Sign `signingInput` (ASCII) with an EC P-256 key, returning a raw 64-byte P1363 sig. */
113
112
  export function signES256(signingInput: string, privateKeyPem: string): Buffer {
114
113
  return nodeSign('sha256', Buffer.from(signingInput, 'ascii'), { key: privateKeyPem, dsaEncoding: 'ieee-p1363' });
@@ -127,200 +126,169 @@ export function buildJws(
127
126
  return { jws: `${signingInput}.${p1363Sig.toString('base64url')}`, signingInput, p1363Sig };
128
127
  }
129
128
 
130
- export function makeChain(opts: MakeChainOptions = {}): Chain {
129
+ /** A committed (or fresh) EC P-256 key as a WebCrypto pair + its PEM (for JWS signing / return). */
130
+ async function loadKeyPair(committedStem: string, fresh: boolean): Promise<{ pair: CryptoKeyPair; pem: string }> {
131
+ if (fresh) {
132
+ const pair = (await webcrypto.subtle.generateKey(CERT_ALG, true, ['sign', 'verify'])) as CryptoKeyPair;
133
+ const der = Buffer.from(await webcrypto.subtle.exportKey('pkcs8', pair.privateKey));
134
+ const pem = createPrivateKey({ key: der, format: 'der', type: 'pkcs8' }).export({
135
+ type: 'sec1',
136
+ format: 'pem',
137
+ }) as string;
138
+ return { pair, pem };
139
+ }
140
+ const pem = readFileSync(path.join(KEYS_DIR, `${committedStem}.key.pem`), 'utf8');
141
+ const nodeKey = createPrivateKey(pem);
142
+ const privateKey = await webcrypto.subtle.importKey(
143
+ 'pkcs8',
144
+ nodeKey.export({ type: 'pkcs8', format: 'der' }),
145
+ CERT_ALG,
146
+ true,
147
+ ['sign']
148
+ );
149
+ const publicKey = await webcrypto.subtle.importKey(
150
+ 'spki',
151
+ createPublicKey(nodeKey).export({ type: 'spki', format: 'der' }),
152
+ CERT_ALG,
153
+ true,
154
+ ['verify']
155
+ );
156
+ return { pair: { privateKey, publicKey }, pem };
157
+ }
158
+
159
+ /** Apple marker OID carried as a critical=false extension whose value is ASN.1 NULL (`05 00`). */
160
+ const appleOidExt = (oid: string): x509.Extension => new x509.Extension(oid, false, new Uint8Array([0x05, 0x00]));
161
+
162
+ export async function makeChain(opts: MakeChainOptions = {}): Promise<Chain> {
131
163
  const signedDate = opts.signedDate ?? Date.now();
132
164
  const validFrom = opts.certValidFrom ?? new Date(signedDate - ONE_DAY_MS);
133
165
  const validTo = opts.certValidTo ?? new Date(signedDate + TEN_YEARS_MS);
134
- // Resolve the [notBefore, notAfter] pair for a given cert, honoring per-cert overrides.
135
- const windowFor = (cert: 'leaf' | 'intermediate' | 'root'): { nb: string; na: string } => {
166
+ const windowFor = (cert: 'leaf' | 'intermediate' | 'root'): { nb: Date; na: Date } => {
136
167
  const o = opts.certWindows?.[cert];
137
- return { nb: fmtTime(o?.from ?? validFrom), na: fmtTime(o?.to ?? validTo) };
168
+ return { nb: o?.from ?? validFrom, na: o?.to ?? validTo };
138
169
  };
170
+ const rootW = windowFor('root');
171
+ const intW = windowFor('intermediate');
172
+ const leafW = windowFor('leaf');
139
173
 
140
- const dir = mkdtempSync(path.join(os.tmpdir(), 'make-chain-'));
141
- const file = (name: string): string => path.join(dir, name);
142
- const ossl = (args: string[]): void => {
143
- execFileSync('openssl', args, { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
144
- };
145
- // Reuse the committed synthetic keys (deterministic) unless freshKeys is set.
146
- // `tmpName` is the working filename (e.g. 'root.key'); `committed` is the
147
- // fixtures/keys/<committed>.key.pem stem.
148
- const provisionKey = (tmpName: string, committed: string): void => {
149
- if (opts.freshKeys) {
150
- ossl(['ecparam', '-name', 'prime256v1', '-genkey', '-noout', '-out', file(tmpName)]);
151
- } else {
152
- copyFileSync(path.join(KEYS_DIR, `${committed}.key.pem`), file(tmpName));
153
- }
154
- };
174
+ const fresh = !!opts.freshKeys;
175
+ const root = await loadKeyPair('root', fresh);
176
+ const int = await loadKeyPair('int', fresh);
177
+ const leaf = await loadKeyPair('leaf', fresh);
178
+ const wrongInt = await loadKeyPair('wrongint', fresh);
155
179
 
156
- try {
157
- const rootW = windowFor('root');
158
- const intW = windowFor('intermediate');
159
- const leafW = windowFor('leaf');
180
+ const caUsage = (): x509.Extension =>
181
+ // eslint-disable-next-line no-bitwise
182
+ new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true);
160
183
 
161
- // root self-signed CA
162
- provisionKey('root.key', 'root');
163
- ossl([
164
- 'req',
165
- '-new',
166
- '-x509',
167
- '-key',
168
- file('root.key'),
169
- '-out',
170
- file('root.pem'),
171
- '-subj',
172
- SUBJ.root,
173
- '-not_before',
174
- rootW.nb,
175
- '-not_after',
176
- rootW.na,
177
- '-addext',
178
- 'basicConstraints=critical,CA:TRUE',
179
- '-addext',
180
- 'keyUsage=critical,keyCertSign,cRLSign',
181
- ]);
184
+ const mk = (o: {
185
+ serial: string;
186
+ subject: string;
187
+ issuer: string;
188
+ nb: Date;
189
+ na: Date;
190
+ pub: CryptoKey;
191
+ sign: CryptoKey;
192
+ exts: x509.Extension[];
193
+ }): Promise<x509.X509Certificate> =>
194
+ x509.X509CertificateGenerator.create({
195
+ serialNumber: o.serial,
196
+ subject: o.subject,
197
+ issuer: o.issuer,
198
+ notBefore: o.nb,
199
+ notAfter: o.na,
200
+ signingAlgorithm: CERT_ALG,
201
+ publicKey: o.pub,
202
+ signingKey: o.sign,
203
+ extensions: o.exts,
204
+ });
182
205
 
183
- // intermediateCA + Apple intermediate OID, signed by root
184
- provisionKey('int.key', 'int');
185
- ossl(['req', '-new', '-key', file('int.key'), '-out', file('int.csr'), '-subj', SUBJ.int]);
186
- const intExt = ['[v3]', 'basicConstraints=critical,CA:TRUE', 'keyUsage=critical,keyCertSign,cRLSign'];
187
- if (opts.intOid !== false) intExt.push(`${APPLE_INT_OID}=DER:0500`);
188
- writeFileSync(file('int.ext'), intExt.join('\n'));
189
- ossl([
190
- 'x509',
191
- '-req',
192
- '-in',
193
- file('int.csr'),
194
- '-CA',
195
- file('root.pem'),
196
- '-CAkey',
197
- file('root.key'),
198
- '-CAcreateserial',
199
- '-out',
200
- file('int.pem'),
201
- '-not_before',
202
- intW.nb,
203
- '-not_after',
204
- intW.na,
205
- '-extfile',
206
- file('int.ext'),
207
- '-extensions',
208
- 'v3',
209
- ]);
206
+ // rootself-signed CA
207
+ const rootX = await mk({
208
+ serial: '01',
209
+ subject: SUBJ.root,
210
+ issuer: SUBJ.root,
211
+ nb: rootW.nb,
212
+ na: rootW.na,
213
+ pub: root.pair.publicKey,
214
+ sign: root.pair.privateKey,
215
+ exts: [new x509.BasicConstraintsExtension(true, undefined, true), caUsage()],
216
+ });
210
217
 
211
- // leaf — Apple leaf OID, signed by intermediate
212
- provisionKey('leaf.key', 'leaf');
213
- ossl(['req', '-new', '-key', file('leaf.key'), '-out', file('leaf.csr'), '-subj', SUBJ.leaf]);
214
- const leafExt = ['[v3]', 'basicConstraints=critical,CA:FALSE'];
215
- if (opts.leafOid !== false) leafExt.push(`${APPLE_LEAF_OID}=DER:0500`);
216
- writeFileSync(file('leaf.ext'), leafExt.join('\n'));
217
- ossl([
218
- 'x509',
219
- '-req',
220
- '-in',
221
- file('leaf.csr'),
222
- '-CA',
223
- file('int.pem'),
224
- '-CAkey',
225
- file('int.key'),
226
- '-CAcreateserial',
227
- '-out',
228
- file('leaf.pem'),
229
- '-not_before',
230
- leafW.nb,
231
- '-not_after',
232
- leafW.na,
233
- '-extfile',
234
- file('leaf.ext'),
235
- '-extensions',
236
- 'v3',
237
- ]);
218
+ // intermediateCA + Apple intermediate OID, signed by root
219
+ const intExts: x509.Extension[] = [new x509.BasicConstraintsExtension(true, undefined, true), caUsage()];
220
+ if (opts.intOid !== false) intExts.push(appleOidExt(APPLE_INT_OID));
221
+ const intX = await mk({
222
+ serial: '02',
223
+ subject: SUBJ.int,
224
+ issuer: SUBJ.root,
225
+ nb: intW.nb,
226
+ na: intW.na,
227
+ pub: int.pair.publicKey,
228
+ sign: root.pair.privateKey,
229
+ exts: intExts,
230
+ });
238
231
 
239
- // wrong intermediate SAME subject DN, DIFFERENT key. A leaf it signs passes
240
- // issuer/subject equality but fails leaf.verify(realIntermediate.publicKey),
241
- // isolating the chain-signature failure for the negative control.
242
- provisionKey('wrongint.key', 'wrongint');
243
- ossl([
244
- 'req',
245
- '-new',
246
- '-x509',
247
- '-key',
248
- file('wrongint.key'),
249
- '-out',
250
- file('wrongint.pem'),
251
- '-subj',
252
- SUBJ.int,
253
- '-not_before',
254
- intW.nb,
255
- '-not_after',
256
- intW.na,
257
- '-addext',
258
- 'basicConstraints=critical,CA:TRUE',
259
- ]);
260
- provisionKey('crossleaf.key', 'leaf');
261
- ossl(['req', '-new', '-key', file('crossleaf.key'), '-out', file('crossleaf.csr'), '-subj', SUBJ.leaf]);
262
- ossl([
263
- 'x509',
264
- '-req',
265
- '-in',
266
- file('crossleaf.csr'),
267
- '-CA',
268
- file('wrongint.pem'),
269
- '-CAkey',
270
- file('wrongint.key'),
271
- '-CAcreateserial',
272
- '-out',
273
- file('crossleaf.pem'),
274
- '-not_before',
275
- leafW.nb,
276
- '-not_after',
277
- leafW.na,
278
- '-extfile',
279
- file('leaf.ext'),
280
- '-extensions',
281
- 'v3',
282
- ]);
232
+ // leafApple leaf OID, signed by intermediate
233
+ const leafExts: x509.Extension[] = [new x509.BasicConstraintsExtension(false, undefined, true)];
234
+ if (opts.leafOid !== false) leafExts.push(appleOidExt(APPLE_LEAF_OID));
235
+ const leafX = await mk({
236
+ serial: '03',
237
+ subject: SUBJ.leaf,
238
+ issuer: SUBJ.int,
239
+ nb: leafW.nb,
240
+ na: leafW.na,
241
+ pub: leaf.pair.publicKey,
242
+ sign: int.pair.privateKey,
243
+ exts: leafExts,
244
+ });
283
245
 
284
- const leafCert = new X509Certificate(readFileSync(file('leaf.pem')));
285
- const intCert = new X509Certificate(readFileSync(file('int.pem')));
286
- const rootCert = new X509Certificate(readFileSync(file('root.pem')));
287
- const crossLeafCert = new X509Certificate(readFileSync(file('crossleaf.pem')));
246
+ // cross-signed leaf the SAME leaf key + subject, but issued (signed) by a
247
+ // different key under the intermediate's DN (`wrongint`). It keeps
248
+ // issuer/subject equality up the chain yet fails leaf.verify(int.publicKey).
249
+ const crossLeafX = await mk({
250
+ serial: '05',
251
+ subject: SUBJ.leaf,
252
+ issuer: SUBJ.int,
253
+ nb: leafW.nb,
254
+ na: leafW.na,
255
+ pub: leaf.pair.publicKey,
256
+ sign: wrongInt.pair.privateKey,
257
+ exts: leafExts,
258
+ });
288
259
 
289
- const x5c = [
290
- Buffer.from(leafCert.raw).toString('base64'),
291
- Buffer.from(intCert.raw).toString('base64'),
292
- Buffer.from(rootCert.raw).toString('base64'),
293
- ];
294
- const header = { alg: opts.alg ?? 'ES256', x5c };
295
- const payload: Record<string, unknown> = {
296
- transactionId: 'txn_synth_1',
297
- originalTransactionId: 'orig_synth_1',
298
- productId: 'sub_synthetic',
299
- bundleId: 'com.example.app',
300
- environment: 'Sandbox',
301
- signedDate,
302
- ...opts.payload,
303
- };
304
- const leafKeyPem = readFileSync(file('leaf.key'), 'utf8');
305
- const { jws, signingInput, p1363Sig } = buildJws(header, payload, leafKeyPem);
260
+ const toNode = (c: x509.X509Certificate): X509Certificate => new X509Certificate(Buffer.from(c.rawData));
261
+ const rootCert = toNode(rootX);
262
+ const intCert = toNode(intX);
263
+ const leafCert = toNode(leafX);
264
+ const crossLeafCert = toNode(crossLeafX);
306
265
 
307
- return {
308
- x5c,
309
- jws,
310
- signingInput,
311
- p1363Sig,
312
- crossSignedLeaf: Buffer.from(crossLeafCert.raw).toString('base64'),
313
- keys: {
314
- rootKeyPem: readFileSync(file('root.key'), 'utf8'),
315
- intKeyPem: readFileSync(file('int.key'), 'utf8'),
316
- leafKeyPem,
317
- wrongIntKeyPem: readFileSync(file('wrongint.key'), 'utf8'),
318
- },
319
- certs: { leaf: leafCert, intermediate: intCert, root: rootCert },
320
- payload,
321
- header,
322
- };
323
- } finally {
324
- rmSync(dir, { recursive: true, force: true });
325
- }
266
+ const x5c = [
267
+ Buffer.from(leafX.rawData).toString('base64'),
268
+ Buffer.from(intX.rawData).toString('base64'),
269
+ Buffer.from(rootX.rawData).toString('base64'),
270
+ ];
271
+ const header = { alg: opts.alg ?? 'ES256', x5c };
272
+ const payload: Record<string, unknown> = {
273
+ transactionId: 'txn_synth_1',
274
+ originalTransactionId: 'orig_synth_1',
275
+ productId: 'sub_synthetic',
276
+ bundleId: 'com.example.app',
277
+ environment: 'Sandbox',
278
+ signedDate,
279
+ ...opts.payload,
280
+ };
281
+ const { jws, signingInput, p1363Sig } = buildJws(header, payload, leaf.pem);
282
+
283
+ return {
284
+ x5c,
285
+ jws,
286
+ signingInput,
287
+ p1363Sig,
288
+ crossSignedLeaf: Buffer.from(crossLeafCert.raw).toString('base64'),
289
+ keys: { rootKeyPem: root.pem, intKeyPem: int.pem, leafKeyPem: leaf.pem, wrongIntKeyPem: wrongInt.pem },
290
+ certs: { leaf: leafCert, intermediate: intCert, root: rootCert },
291
+ payload,
292
+ header,
293
+ };
326
294
  }
@@ -15,8 +15,8 @@ import type { JwsTransactionPayload } from '../../../src/integrations/app-store/
15
15
  const FIXED_SIGNED_DATE = 1_700_000_000_000;
16
16
 
17
17
  /** A valid chain + the trustedRoots list that anchors it (the chain's own root). */
18
- function freshChainCtx(opts: Parameters<typeof makeChain>[0] = {}) {
19
- const chain = makeChain({ signedDate: FIXED_SIGNED_DATE, ...opts });
18
+ async function freshChainCtx(opts: Parameters<typeof makeChain>[0] = {}) {
19
+ const chain = await makeChain({ signedDate: FIXED_SIGNED_DATE, ...opts });
20
20
  const trustedRoots = [Buffer.from(chain.x5c[2]!, 'base64')];
21
21
  return { chain, trustedRoots };
22
22
  }
@@ -27,7 +27,7 @@ async function expectCode(promise: Promise<unknown>, code: string): Promise<void
27
27
 
28
28
  describe('verifyAppleJws — happy path', () => {
29
29
  it('decodes a synthetic chain when its root is trusted; fields match the signed payload', async () => {
30
- const { chain, trustedRoots } = freshChainCtx({
30
+ const { chain, trustedRoots } = await freshChainCtx({
31
31
  payload: { productId: 'sub_happy', expiresDate: 1_900_000_000_000, appAccountToken: 'uuid-happy' },
32
32
  });
33
33
  const decoded = await verifyAppleJws<JwsTransactionPayload>(chain.jws, { trustedRoots });
@@ -56,7 +56,7 @@ describe('verifyAppleJws — bad input', () => {
56
56
  });
57
57
 
58
58
  it('rejects x5c.length !== 3 with INVALID_CHAIN_LENGTH', async () => {
59
- const { chain, trustedRoots } = freshChainCtx();
59
+ const { chain, trustedRoots } = await freshChainCtx();
60
60
  const { jws } = buildJws({ alg: 'ES256', x5c: chain.x5c.slice(0, 2) }, chain.payload, chain.keys.leafKeyPem);
61
61
  await expectCode(verifyAppleJws(jws, { trustedRoots }), 'INVALID_CHAIN_LENGTH');
62
62
  });
@@ -66,7 +66,7 @@ describe('verifyAppleJws — bad input', () => {
66
66
  });
67
67
 
68
68
  it('rejects a payload that is not valid JSON (after a valid chain)', async () => {
69
- const { chain, trustedRoots } = freshChainCtx();
69
+ const { chain, trustedRoots } = await freshChainCtx();
70
70
  const headerSeg = chain.jws.split('.')[0]!;
71
71
  const badPayload = Buffer.from('not json at all').toString('base64url');
72
72
  const jws = `${headerSeg}.${badPayload}.${chain.jws.split('.')[2]}`;
@@ -76,14 +76,14 @@ describe('verifyAppleJws — bad input', () => {
76
76
 
77
77
  describe('verifyAppleJws — security', () => {
78
78
  it('rejects a tampered payload (signature no longer matches)', async () => {
79
- const { chain, trustedRoots } = freshChainCtx();
79
+ const { chain, trustedRoots } = await freshChainCtx();
80
80
  const [h, , s] = chain.jws.split('.');
81
81
  const tampered = Buffer.from(JSON.stringify({ ...chain.payload, productId: 'EVIL' })).toString('base64url');
82
82
  await expectCode(verifyAppleJws(`${h}.${tampered}.${s}`, { trustedRoots }), 'SIGNATURE');
83
83
  });
84
84
 
85
85
  it('rejects a tampered signature', async () => {
86
- const { chain, trustedRoots } = freshChainCtx();
86
+ const { chain, trustedRoots } = await freshChainCtx();
87
87
  const [h, p] = chain.jws.split('.');
88
88
  const sigBytes = Buffer.from(chain.jws.split('.')[2]!, 'base64url');
89
89
  sigBytes[0] = sigBytes[0]! ^ 0xff;
@@ -91,37 +91,37 @@ describe('verifyAppleJws — security', () => {
91
91
  });
92
92
 
93
93
  it('rejects alg: "none" and alg: "RS256" (alg whitelist)', async () => {
94
- const none = freshChainCtx({ alg: 'none' });
94
+ const none = await freshChainCtx({ alg: 'none' });
95
95
  await expectCode(verifyAppleJws(none.chain.jws, { trustedRoots: none.trustedRoots }), 'ALG');
96
- const rs = freshChainCtx({ alg: 'RS256' });
96
+ const rs = await freshChainCtx({ alg: 'RS256' });
97
97
  await expectCode(verifyAppleJws(rs.chain.jws, { trustedRoots: rs.trustedRoots }), 'ALG');
98
98
  });
99
99
 
100
100
  it('rejects when the presented root is NOT a trusted anchor', async () => {
101
- const { chain } = freshChainCtx();
101
+ const { chain } = await freshChainCtx();
102
102
  // default APPLE_ROOT_CERTS does NOT contain the synthetic root
103
103
  await expectCode(verifyAppleJws(chain.jws), 'ANCHOR');
104
104
  });
105
105
 
106
106
  it('rejects a cross-signed leaf (chain break) before signature', async () => {
107
- const { chain, trustedRoots } = freshChainCtx();
107
+ const { chain, trustedRoots } = await freshChainCtx();
108
108
  const header = { alg: 'ES256', x5c: [chain.crossSignedLeaf, chain.x5c[1], chain.x5c[2]] };
109
109
  const { jws } = buildJws(header, chain.payload, chain.keys.leafKeyPem);
110
110
  await expectCode(verifyAppleJws(jws, { trustedRoots }), 'CHAIN');
111
111
  });
112
112
 
113
113
  it('rejects when the leaf is missing the Apple OID', async () => {
114
- const { chain, trustedRoots } = freshChainCtx({ leafOid: false });
114
+ const { chain, trustedRoots } = await freshChainCtx({ leafOid: false });
115
115
  await expectCode(verifyAppleJws(chain.jws, { trustedRoots }), 'OID');
116
116
  });
117
117
 
118
118
  it('rejects when the intermediate is missing the Apple OID', async () => {
119
- const { chain, trustedRoots } = freshChainCtx({ intOid: false });
119
+ const { chain, trustedRoots } = await freshChainCtx({ intOid: false });
120
120
  await expectCode(verifyAppleJws(chain.jws, { trustedRoots }), 'OID');
121
121
  });
122
122
 
123
123
  it('does not pollute Object.prototype from a hostile __proto__ payload', async () => {
124
- const { chain, trustedRoots } = freshChainCtx();
124
+ const { chain, trustedRoots } = await freshChainCtx();
125
125
  const headerSeg = chain.jws.split('.')[0]!;
126
126
  // own __proto__ key via JSON.parse, then sign it so the chain is valid
127
127
  const hostile = JSON.parse(
@@ -142,29 +142,29 @@ describe('verifyAppleJws — validity & clock skew (±60s, all 3 certs)', () =>
142
142
  // boundary: reject if validTo < effectiveDate − 60s; pass at exactly −60s.
143
143
  it('passes at the expiry boundary (effectiveDate = validTo + 60s) and rejects 1ms past it', async () => {
144
144
  const validTo = new Date(FIXED_SIGNED_DATE);
145
- const ctxPass = freshChainCtx({ certValidTo: validTo, signedDate: FIXED_SIGNED_DATE + 60_000 });
145
+ const ctxPass = await freshChainCtx({ certValidTo: validTo, signedDate: FIXED_SIGNED_DATE + 60_000 });
146
146
  await expect(verifyAppleJws(ctxPass.chain.jws, { trustedRoots: ctxPass.trustedRoots })).resolves.toBeDefined();
147
- const ctxFail = freshChainCtx({ certValidTo: validTo, signedDate: FIXED_SIGNED_DATE + 60_000 + 1 });
147
+ const ctxFail = await freshChainCtx({ certValidTo: validTo, signedDate: FIXED_SIGNED_DATE + 60_000 + 1 });
148
148
  await expectCode(verifyAppleJws(ctxFail.chain.jws, { trustedRoots: ctxFail.trustedRoots }), 'CERT_EXPIRED');
149
149
  });
150
150
 
151
151
  it('passes at the not-yet-valid boundary (effectiveDate = validFrom − 60s) and rejects 1ms before it', async () => {
152
152
  const validFrom = new Date(FIXED_SIGNED_DATE);
153
- const ctxPass = freshChainCtx({ certValidFrom: validFrom, signedDate: FIXED_SIGNED_DATE - 60_000 });
153
+ const ctxPass = await freshChainCtx({ certValidFrom: validFrom, signedDate: FIXED_SIGNED_DATE - 60_000 });
154
154
  await expect(verifyAppleJws(ctxPass.chain.jws, { trustedRoots: ctxPass.trustedRoots })).resolves.toBeDefined();
155
- const ctxFail = freshChainCtx({ certValidFrom: validFrom, signedDate: FIXED_SIGNED_DATE - 60_000 - 1 });
155
+ const ctxFail = await freshChainCtx({ certValidFrom: validFrom, signedDate: FIXED_SIGNED_DATE - 60_000 - 1 });
156
156
  await expectCode(verifyAppleJws(ctxFail.chain.jws, { trustedRoots: ctxFail.trustedRoots }), 'CERT_NOT_YET_VALID');
157
157
  });
158
158
 
159
159
  it('rejects when ONLY the intermediate is expired (proves int is checked, not just leaf)', async () => {
160
- const { chain, trustedRoots } = freshChainCtx({
160
+ const { chain, trustedRoots } = await freshChainCtx({
161
161
  certWindows: { intermediate: { to: new Date(FIXED_SIGNED_DATE - 3_600_000) } },
162
162
  });
163
163
  await expectCode(verifyAppleJws(chain.jws, { trustedRoots }), 'CERT_EXPIRED');
164
164
  });
165
165
 
166
166
  it('rejects when ONLY the root is expired (proves root is checked)', async () => {
167
- const { chain, trustedRoots } = freshChainCtx({
167
+ const { chain, trustedRoots } = await freshChainCtx({
168
168
  certWindows: { root: { to: new Date(FIXED_SIGNED_DATE - 3_600_000) } },
169
169
  });
170
170
  await expectCode(verifyAppleJws(chain.jws, { trustedRoots }), 'CERT_EXPIRED');
@@ -173,7 +173,7 @@ describe('verifyAppleJws — validity & clock skew (±60s, all 3 certs)', () =>
173
173
 
174
174
  describe('verifyAppleJws — data loss (concurrency / statelessness)', () => {
175
175
  it('resolves all concurrent verifications correctly (no shared mutable state)', async () => {
176
- const { chain, trustedRoots } = freshChainCtx();
176
+ const { chain, trustedRoots } = await freshChainCtx();
177
177
  const results = await Promise.all(
178
178
  Array.from({ length: 12 }, () => verifyAppleJws<JwsTransactionPayload>(chain.jws, { trustedRoots }))
179
179
  );
@@ -183,7 +183,7 @@ describe('verifyAppleJws — data loss (concurrency / statelessness)', () => {
183
183
 
184
184
  describe('verifyAppleJws — data damage (type/encoding fidelity)', () => {
185
185
  it('keeps numbers as numbers and round-trips unicode byte-faithfully', async () => {
186
- const { chain, trustedRoots } = freshChainCtx({
186
+ const { chain, trustedRoots } = await freshChainCtx({
187
187
  payload: { productId: '产品_🎉', expiresDate: 1_888_888_888_888, appAccountToken: 'uuid-😀' },
188
188
  });
189
189
  const decoded = await verifyAppleJws<JwsTransactionPayload>(chain.jws, { trustedRoots });
@@ -194,7 +194,7 @@ describe('verifyAppleJws — data damage (type/encoding fidelity)', () => {
194
194
  });
195
195
 
196
196
  it('leaves a missing expiresDate as undefined (never coerced to 0/null)', async () => {
197
- const { chain, trustedRoots } = freshChainCtx(); // default payload has no expiresDate
197
+ const { chain, trustedRoots } = await freshChainCtx(); // default payload has no expiresDate
198
198
  const decoded = await verifyAppleJws<JwsTransactionPayload>(chain.jws, { trustedRoots });
199
199
  expect('expiresDate' in decoded).toBe(false);
200
200
  expect(decoded.expiresDate).toBeUndefined();
@@ -203,7 +203,7 @@ describe('verifyAppleJws — data damage (type/encoding fidelity)', () => {
203
203
 
204
204
  describe('verifyAppleJws — data leak (errors name the failure, not secrets)', () => {
205
205
  it('throws AppleJwsVerifyError with a code and never echoes the raw cert/JWS material', async () => {
206
- const { chain } = freshChainCtx();
206
+ const { chain } = await freshChainCtx();
207
207
  try {
208
208
  await verifyAppleJws(chain.jws); // ANCHOR (synthetic root not trusted)
209
209
  throw new Error('should have rejected');
@@ -0,0 +1,82 @@
1
+ // #1128 — arcblock batch-collect embeds the FULL past-due invoice id list into
2
+ // the on-chain tx metadata (getTxMetadata() in libs/util.ts, consumed by
3
+ // routes/connect/collect-batch.ts). That JSON blob has no length limit of its
4
+ // own, so a customer/subscription with many uncollectible invoices could build
5
+ // a tx whose `data` field exceeds the chain's tx size limit and fails on-chain.
6
+ //
7
+ // ensureSubscriptionForCollectBatch() (routes/connect/shared.ts) is the single
8
+ // choke point used by collect-batch's claims.authPrincipal / onConnect / onAuth,
9
+ // so the size guard lives there: reject the request up front with a clear error
10
+ // instead of silently truncating the invoice list (which would make the
11
+ // on-chain memo permanently misleading) or letting an oversized tx reach the
12
+ // chain and fail there.
13
+ import { ensureSubscriptionForCollectBatch } from '../../../src/routes/connect/shared';
14
+ import { Invoice, PaymentCurrency, PaymentMethod, Subscription } from '../../../src/store/models';
15
+ import { MAX_BATCH_COLLECT_INVOICES } from '../../../src/libs/constants';
16
+
17
+ const SUBSCRIPTION_ID = 'sub_test_1128';
18
+ const CURRENCY_ID = 'cur_test_1128';
19
+
20
+ const buildInvoiceIds = (count: number) => Array.from({ length: count }, (_, i) => `in_test_${i}`);
21
+
22
+ function mockUncollectible({ invoiceCount, paymentMethodType }: { invoiceCount: number; paymentMethodType: string }) {
23
+ const invoiceIds = buildInvoiceIds(invoiceCount);
24
+
25
+ jest.spyOn(Subscription, 'findByPk').mockResolvedValue({
26
+ id: SUBSCRIPTION_ID,
27
+ customer_id: 'cus_test_1128',
28
+ } as any);
29
+
30
+ jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue({
31
+ id: CURRENCY_ID,
32
+ payment_method_id: 'pm_test_1128',
33
+ } as any);
34
+
35
+ jest.spyOn(PaymentMethod, 'findByPk').mockResolvedValue({
36
+ id: 'pm_test_1128',
37
+ type: paymentMethodType,
38
+ } as any);
39
+
40
+ jest
41
+ .spyOn(Invoice, 'getUncollectibleAmount')
42
+ .mockResolvedValue([{ [CURRENCY_ID]: String(invoiceCount * 100) }, { [CURRENCY_ID]: invoiceIds }, []] as any);
43
+
44
+ return invoiceIds;
45
+ }
46
+
47
+ describe('ensureSubscriptionForCollectBatch — batch size guard (#1128)', () => {
48
+ afterEach(() => {
49
+ jest.restoreAllMocks();
50
+ });
51
+
52
+ it('resolves normally when the arcblock invoice count is within the limit', async () => {
53
+ const invoiceIds = mockUncollectible({ invoiceCount: MAX_BATCH_COLLECT_INVOICES, paymentMethodType: 'arcblock' });
54
+
55
+ const result = await ensureSubscriptionForCollectBatch(SUBSCRIPTION_ID, CURRENCY_ID, undefined);
56
+
57
+ expect(result.invoices).toEqual(invoiceIds);
58
+ });
59
+
60
+ it('rejects with a clear error instead of building an oversized tx when the arcblock invoice count exceeds the limit', async () => {
61
+ mockUncollectible({ invoiceCount: MAX_BATCH_COLLECT_INVOICES + 1, paymentMethodType: 'arcblock' });
62
+
63
+ await expect(ensureSubscriptionForCollectBatch(SUBSCRIPTION_ID, CURRENCY_ID, undefined)).rejects.toThrow(
64
+ /Too many past due invoices.*on-chain transaction/
65
+ );
66
+ });
67
+
68
+ it('does not enforce the on-chain tx-size guard for non-arcblock (EVM) payment methods', async () => {
69
+ // EVM batch-collect (see collect-batch.ts onConnect) never embeds the
70
+ // invoices array into the signed payload — only `network` + the encoded
71
+ // transfer itx — so the on-chain size risk this guard protects against
72
+ // does not apply there.
73
+ const invoiceIds = mockUncollectible({
74
+ invoiceCount: MAX_BATCH_COLLECT_INVOICES + 1,
75
+ paymentMethodType: 'ethereum',
76
+ });
77
+
78
+ const result = await ensureSubscriptionForCollectBatch(SUBSCRIPTION_ID, CURRENCY_ID, undefined);
79
+
80
+ expect(result.invoices).toEqual(invoiceIds);
81
+ });
82
+ });
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.29.13
17
+ version: 1.29.15
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.29.13",
3
+ "version": "1.29.15",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "prelint": "npm run types",
@@ -60,9 +60,9 @@
60
60
  "@blocklet/error": "^0.3.5",
61
61
  "@blocklet/js-sdk": "^1.17.13-beta-20260613-094425-b81920c8",
62
62
  "@blocklet/logger": "^1.17.13-beta-20260613-094425-b81920c8",
63
- "@blocklet/payment-broker-client": "1.29.13",
64
- "@blocklet/payment-react": "1.29.13",
65
- "@blocklet/payment-vendor": "1.29.13",
63
+ "@blocklet/payment-broker-client": "1.29.15",
64
+ "@blocklet/payment-react": "1.29.15",
65
+ "@blocklet/payment-vendor": "1.29.15",
66
66
  "@blocklet/sdk": "^1.17.13-beta-20260613-094425-b81920c8",
67
67
  "@blocklet/ui-react": "^3.5.4",
68
68
  "@blocklet/uploader": "^0.15.4",
@@ -133,7 +133,8 @@
133
133
  "devDependencies": {
134
134
  "@abtnode/types": "^1.17.13-beta-20260613-094425-b81920c8",
135
135
  "@arcblock/eslint-config-ts": "^0.3.3",
136
- "@blocklet/payment-types": "1.29.13",
136
+ "@blocklet/payment-types": "1.29.15",
137
+ "@peculiar/x509": "^2.0.0",
137
138
  "@types/connect": "^3.4.38",
138
139
  "@types/debug": "^4.1.12",
139
140
  "@types/dotenv-flow": "^3.3.3",
@@ -154,6 +155,7 @@
154
155
  "npm-run-all": "^4.1.5",
155
156
  "prettier": "^3.6.0",
156
157
  "prettier-plugin-import-sort": "^0.0.7",
158
+ "reflect-metadata": "^0.2.2",
157
159
  "rollup-plugin-visualizer": "^6.0.3",
158
160
  "ts-jest": "^29.4.0",
159
161
  "ts-node": "^10.9.2",
@@ -180,5 +182,5 @@
180
182
  "parser": "typescript"
181
183
  }
182
184
  },
183
- "gitHead": "e023f83ecfb07c0782e138a2802b5dec746fd7c6"
185
+ "gitHead": "438312658874b67c7936eaf74192bd2c958d51b0"
184
186
  }