payment-kit 1.29.6 → 1.29.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/api/src/host-glue.ts +79 -0
  2. package/api/src/integrations/app-store/client.ts +7 -13
  3. package/api/src/integrations/app-store/native-api.ts +102 -0
  4. package/api/src/integrations/app-store/native-asn1.ts +49 -0
  5. package/api/src/integrations/app-store/native-jws.ts +222 -0
  6. package/api/src/integrations/app-store/native-receipt.ts +105 -0
  7. package/api/src/integrations/app-store/signed-data-verifier.ts +40 -79
  8. package/api/src/libs/env.ts +64 -22
  9. package/api/src/routes/hono/credit-grants.ts +1 -1
  10. package/api/tests/integrations/app-store/client.spec.ts +83 -57
  11. package/api/tests/integrations/app-store/fixtures/README.md +54 -0
  12. package/api/tests/integrations/app-store/fixtures/keys/int.key.pem +5 -0
  13. package/api/tests/integrations/app-store/fixtures/keys/leaf.key.pem +5 -0
  14. package/api/tests/integrations/app-store/fixtures/keys/root.key.pem +5 -0
  15. package/api/tests/integrations/app-store/fixtures/keys/wrongint.key.pem +5 -0
  16. package/api/tests/integrations/app-store/fixtures/make-chain.spec.ts +152 -0
  17. package/api/tests/integrations/app-store/fixtures/make-chain.ts +326 -0
  18. package/api/tests/integrations/app-store/fixtures/real-sandbox.json +43 -0
  19. package/api/tests/integrations/app-store/fixtures/real-sandbox.jws +1 -0
  20. package/api/tests/integrations/app-store/native-api.spec.ts +172 -0
  21. package/api/tests/integrations/app-store/native-integration.spec.ts +78 -0
  22. package/api/tests/integrations/app-store/native-jws.spec.ts +219 -0
  23. package/api/tests/integrations/app-store/native-receipt.spec.ts +161 -0
  24. package/api/tests/libs/notification-guard.spec.ts +25 -8
  25. package/blocklet.yml +1 -1
  26. package/cloudflare/cf-adapter.ts +11 -46
  27. package/cloudflare/tests/cf-adapter.spec.ts +2 -1
  28. package/cloudflare/tests/x509-probe.mjs +260 -0
  29. package/package.json +7 -10
  30. package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +0 -17
@@ -0,0 +1,152 @@
1
+ // Structural coverage for the synthetic chain generator (Phase 0, task 0.1).
2
+ // These assertions ARE the fixtures' contract: if they hold, the negative
3
+ // vectors the Phase 1 verifier specs rely on are sound.
4
+
5
+ import { X509Certificate } from 'node:crypto';
6
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ import { APPLE_INT_OID, APPLE_LEAF_OID, buildJws, makeChain } from './make-chain';
10
+
11
+ // Full DER OID tag (mirrors native-asn1.ts, see 0.3) — the strict form.
12
+ function oidToDerTag(oid: string): Buffer {
13
+ const parts = oid.split('.').map(Number);
14
+ const body = [40 * parts[0]! + parts[1]!];
15
+ for (let i = 2; i < parts.length; i++) {
16
+ let v = parts[i]!;
17
+ const stack = [v & 0x7f];
18
+ v = Math.floor(v / 128);
19
+ while (v > 0) {
20
+ stack.unshift((v & 0x7f) | 0x80);
21
+ v = Math.floor(v / 128);
22
+ }
23
+ body.push(...stack);
24
+ }
25
+ return Buffer.from([0x06, body.length, ...body]);
26
+ }
27
+
28
+ const certOf = (b64: string): X509Certificate => new X509Certificate(Buffer.from(b64, 'base64'));
29
+
30
+ describe('make-chain — happy path', () => {
31
+ const chain = makeChain({ signedDate: 1_781_521_334_379 });
32
+
33
+ it('leaf verifies against intermediate, intermediate against root', () => {
34
+ expect(chain.certs.leaf.verify(chain.certs.intermediate.publicKey)).toBe(true);
35
+ expect(chain.certs.intermediate.verify(chain.certs.root.publicKey)).toBe(true);
36
+ });
37
+
38
+ it('issuer/subject equality holds up the chain', () => {
39
+ expect(chain.certs.leaf.issuer).toBe(chain.certs.intermediate.subject);
40
+ expect(chain.certs.intermediate.issuer).toBe(chain.certs.root.subject);
41
+ expect(chain.certs.root.issuer).toBe(chain.certs.root.subject); // self-signed
42
+ });
43
+
44
+ it('intermediate is a CA and carries the Apple intermediate OID', () => {
45
+ expect(chain.certs.intermediate.ca).toBe(true);
46
+ expect(Buffer.from(chain.certs.intermediate.raw).includes(oidToDerTag(APPLE_INT_OID))).toBe(true);
47
+ });
48
+
49
+ it('leaf is not a CA and carries the Apple leaf OID', () => {
50
+ expect(chain.certs.leaf.ca).toBe(false);
51
+ expect(Buffer.from(chain.certs.leaf.raw).includes(oidToDerTag(APPLE_LEAF_OID))).toBe(true);
52
+ });
53
+
54
+ it('emits a 3-segment JWS with a 64-byte P1363 signature', () => {
55
+ expect(chain.jws.split('.')).toHaveLength(3);
56
+ expect(chain.p1363Sig).toHaveLength(64);
57
+ expect(chain.payload.signedDate).toBe(1_781_521_334_379);
58
+ });
59
+ });
60
+
61
+ describe('make-chain — security negative controls', () => {
62
+ const chain = makeChain();
63
+
64
+ it('cross-signed leaf fails leaf.verify(intermediate.publicKey) but keeps issuer/subject equality', () => {
65
+ const cross = certOf(chain.crossSignedLeaf);
66
+ expect(cross.issuer).toBe(chain.certs.intermediate.subject); // same DN — isolates the signature failure
67
+ expect(cross.verify(chain.certs.intermediate.publicKey)).toBe(false);
68
+ });
69
+
70
+ it('an OID we did NOT add is not present (mirrors probe intOidPresent === false)', () => {
71
+ // The leaf carries the LEAF OID, not the INTERMEDIATE OID — negative control.
72
+ expect(Buffer.from(chain.certs.leaf.raw).includes(oidToDerTag(APPLE_INT_OID))).toBe(false);
73
+ });
74
+
75
+ it('omits the leaf OID on demand (leafOid: false)', () => {
76
+ const noOid = makeChain({ leafOid: false });
77
+ expect(Buffer.from(noOid.certs.leaf.raw).includes(oidToDerTag(APPLE_LEAF_OID))).toBe(false);
78
+ });
79
+ });
80
+
81
+ describe('make-chain — data damage / round-trip', () => {
82
+ const chain = makeChain();
83
+
84
+ it('decode(x5c[i]) byte-equals the DER we encoded', () => {
85
+ expect(Buffer.from(chain.x5c[0]!, 'base64').equals(Buffer.from(chain.certs.leaf.raw))).toBe(true);
86
+ expect(Buffer.from(chain.x5c[1]!, 'base64').equals(Buffer.from(chain.certs.intermediate.raw))).toBe(true);
87
+ expect(Buffer.from(chain.x5c[2]!, 'base64').equals(Buffer.from(chain.certs.root.raw))).toBe(true);
88
+ });
89
+
90
+ it('the b64url JWS header round-trips to the header object', () => {
91
+ const [h] = chain.jws.split('.');
92
+ const header = JSON.parse(Buffer.from(h!, 'base64url').toString('utf8'));
93
+ expect(header.alg).toBe('ES256');
94
+ expect(header.x5c).toEqual(chain.x5c);
95
+ });
96
+ });
97
+
98
+ describe('make-chain — validity window pinning', () => {
99
+ it('pins notBefore/notAfter relative to signedDate', () => {
100
+ const signedDate = 1_700_000_000_000;
101
+ const chain = makeChain({
102
+ signedDate,
103
+ certValidFrom: new Date(signedDate - 86_400_000),
104
+ certValidTo: new Date(signedDate + 86_400_000),
105
+ });
106
+ const vf = Date.parse(chain.certs.leaf.validFrom);
107
+ const vt = Date.parse(chain.certs.leaf.validTo);
108
+ expect(vf).toBeLessThanOrEqual(signedDate);
109
+ expect(vt).toBeGreaterThanOrEqual(signedDate);
110
+ // window is ~2 days wide (second-granularity rounding aside)
111
+ expect(vt - vf).toBeGreaterThan(86_400_000);
112
+ });
113
+ });
114
+
115
+ describe('make-chain — data leak (committed test keys never reach src/)', () => {
116
+ // The synthetic signing keys under fixtures/keys/ are test-only. Production
117
+ // code must NEVER import the fixtures or the keys. This is a structural gate
118
+ // so a future regression (e.g. someone wiring a fixture into src) fails CI.
119
+ const srcDir = path.join(__dirname, '..', '..', '..', '..', 'src');
120
+
121
+ function walk(dir: string): string[] {
122
+ return readdirSync(dir).flatMap((name) => {
123
+ const full = path.join(dir, name);
124
+ if (statSync(full).isDirectory()) return walk(full);
125
+ return /\.(ts|tsx|js|jsx|mjs)$/.test(name) ? [full] : [];
126
+ });
127
+ }
128
+
129
+ it('no src/ file references the fixtures, the generator, or the test keys', () => {
130
+ const offenders = walk(srcDir).filter((file) => {
131
+ const text = readFileSync(file, 'utf8');
132
+ return /make-chain|app-store\/fixtures|fixtures\/keys|real-sandbox/.test(text);
133
+ });
134
+ expect(offenders).toEqual([]);
135
+ });
136
+ });
137
+
138
+ describe('make-chain — buildJws crafts variants', () => {
139
+ const chain = makeChain();
140
+
141
+ it('re-signs a custom header/payload with the leaf key', () => {
142
+ const { jws, p1363Sig } = buildJws({ alg: 'ES256', x5c: chain.x5c }, { foo: 'bar' }, chain.keys.leafKeyPem);
143
+ expect(jws.split('.')).toHaveLength(3);
144
+ expect(p1363Sig).toHaveLength(64);
145
+ });
146
+
147
+ it('can forge a wrong-length x5c header (negative vector source)', () => {
148
+ const { jws } = buildJws({ alg: 'ES256', x5c: chain.x5c.slice(0, 2) }, chain.payload, chain.keys.leafKeyPem);
149
+ const header = JSON.parse(Buffer.from(jws.split('.')[0]!, 'base64url').toString('utf8'));
150
+ expect(header.x5c).toHaveLength(2);
151
+ });
152
+ });
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Synthetic Apple-style 3-cert chain + ES256 JWS generator for the App Store
3
+ * native-verify tests (and mirrored by the workerd probe's chain round).
4
+ *
5
+ * CONTRACT — read before using in validity / clock-skew tests:
6
+ * - `verifyAppleJws` (Phase 1) uses the JWS payload's `signedDate` as the
7
+ * `effectiveDate` for ALL certificate validity checks — NEVER Date.now().
8
+ * So cert windows here are pinned with explicit notBefore/notAfter
9
+ * (`certValidFrom`/`certValidTo`) and the spec positions `signedDate`
10
+ * relative to them to exercise the ±60s skew boundary precisely.
11
+ * - Production OID presence (native-asn1.ts) matches the FULL DER tag
12
+ * `[0x06, len, ...content]`, STRICTER than the probe's content-only scan
13
+ * (see 0.3). The certs below carry the real Apple OIDs as `DER:0500` exts.
14
+ *
15
+ * This generator NEVER validates — by design it can emit deliberately
16
+ * malformed inputs (missing OID, cross-signed leaf, and via `buildJws` any
17
+ * wrong-length x5c / tampered sig / forged alg) so the verifier's negative
18
+ * vectors have inputs to reject.
19
+ *
20
+ * "Callable from the probe": the probe is a raw-node `.mjs` that cannot import
21
+ * this `.ts` module under the ts-jest (CJS) setup, so
22
+ * `cloudflare/tests/x509-probe.mjs` mirrors this chain-building inline. Keep the
23
+ * two in sync — the algorithm (root → CA intermediate w/ Apple OID → leaf w/
24
+ * Apple OID, ES256 P1363 sig over `header.payload`) is identical.
25
+ *
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
28
+ * pinned validity windows — so golden-vector VALUE assertions are stable and
29
+ * the certs never expire. Pass `freshKeys: true` to generate unique keys for a
30
+ * single call (e.g. to make two chains demonstrably distinct). The committed
31
+ * keys are test-only and MUST never be imported by `src/` — a data-leak spec
32
+ * (`make-chain.spec.ts`) guards that invariant. The committed REAL Apple
33
+ * fixture (`real-sandbox.jws`) is the golden vector for real-chain field values.
34
+ */
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';
39
+ import path from 'node:path';
40
+
41
+ export const APPLE_LEAF_OID = '1.2.840.113635.100.6.11.1';
42
+ export const APPLE_INT_OID = '1.2.840.113635.100.6.2.1';
43
+
44
+ const ONE_DAY_MS = 86_400_000;
45
+ const TEN_YEARS_MS = 10 * 365 * ONE_DAY_MS;
46
+
47
+ export interface MakeChainOptions {
48
+ /** ms; embedded as payload.signedDate (= effectiveDate verifyAppleJws uses). Default: Date.now(). */
49
+ signedDate?: number;
50
+ /** notBefore for all 3 certs. Default: signedDate − 1 day. */
51
+ certValidFrom?: Date;
52
+ /** notAfter for all 3 certs. Default: signedDate + 10 years. */
53
+ certValidTo?: Date;
54
+ /**
55
+ * Per-cert validity overrides, so a test can expire / not-yet-validate ONE cert
56
+ * (leaf, intermediate, or root) while the others stay valid — proving the
57
+ * verifier checks all three. Falls back to certValidFrom/certValidTo.
58
+ */
59
+ certWindows?: {
60
+ leaf?: { from?: Date; to?: Date };
61
+ intermediate?: { from?: Date; to?: Date };
62
+ root?: { from?: Date; to?: Date };
63
+ };
64
+ /** extra/override payload fields merged into the default transaction payload. */
65
+ payload?: Record<string, unknown>;
66
+ /** header `alg`; default 'ES256'. Override to forge alg-confusion vectors. */
67
+ alg?: string;
68
+ /** include the Apple leaf OID on the leaf (default true). */
69
+ leafOid?: boolean;
70
+ /** include the Apple intermediate OID on the intermediate (default true). */
71
+ intOid?: boolean;
72
+ /** generate unique keys instead of reusing the committed fixtures/keys/ PEMs (default false). */
73
+ freshKeys?: boolean;
74
+ }
75
+
76
+ export interface Chain {
77
+ /** [leafDER_b64, intermediateDER_b64, rootDER_b64] — standard base64 (as in a JWS header). */
78
+ x5c: string[];
79
+ /** valid `header.payload.p1363sig` JWS signed by the leaf key. */
80
+ jws: string;
81
+ /** `header.payload` — the ASCII bytes the ES256 signature covers. */
82
+ signingInput: string;
83
+ /** raw 64-byte IEEE-P1363 (r‖s) signature over `signingInput`. */
84
+ p1363Sig: Buffer;
85
+ /** b64 DER of a leaf signed by a DIFFERENT intermediate key (same subject DN): passes
86
+ * issuer/subject equality but fails `leaf.verify(realIntermediate.publicKey)`. */
87
+ crossSignedLeaf: string;
88
+ keys: { rootKeyPem: string; intKeyPem: string; leafKeyPem: string; wrongIntKeyPem: string };
89
+ certs: { leaf: X509Certificate; intermediate: X509Certificate; root: X509Certificate };
90
+ payload: Record<string, unknown>;
91
+ header: Record<string, unknown>;
92
+ }
93
+
94
+ 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',
98
+ };
99
+
100
+ /** Committed synthetic signing keys (test-only, never imported by src/). */
101
+ const KEYS_DIR = path.join(__dirname, 'keys');
102
+
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
+ /** Sign `signingInput` (ASCII) with an EC P-256 key, returning a raw 64-byte P1363 sig. */
113
+ export function signES256(signingInput: string, privateKeyPem: string): Buffer {
114
+ return nodeSign('sha256', Buffer.from(signingInput, 'ascii'), { key: privateKeyPem, dsaEncoding: 'ieee-p1363' });
115
+ }
116
+
117
+ /** Build a `header.payload.p1363sig` JWS from arbitrary header/payload objects (for crafting variants). */
118
+ export function buildJws(
119
+ header: Record<string, unknown>,
120
+ payload: Record<string, unknown>,
121
+ leafKeyPem: string
122
+ ): { jws: string; signingInput: string; p1363Sig: Buffer } {
123
+ const h = Buffer.from(JSON.stringify(header)).toString('base64url');
124
+ const p = Buffer.from(JSON.stringify(payload)).toString('base64url');
125
+ const signingInput = `${h}.${p}`;
126
+ const p1363Sig = signES256(signingInput, leafKeyPem);
127
+ return { jws: `${signingInput}.${p1363Sig.toString('base64url')}`, signingInput, p1363Sig };
128
+ }
129
+
130
+ export function makeChain(opts: MakeChainOptions = {}): Chain {
131
+ const signedDate = opts.signedDate ?? Date.now();
132
+ const validFrom = opts.certValidFrom ?? new Date(signedDate - ONE_DAY_MS);
133
+ 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 } => {
136
+ const o = opts.certWindows?.[cert];
137
+ return { nb: fmtTime(o?.from ?? validFrom), na: fmtTime(o?.to ?? validTo) };
138
+ };
139
+
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
+ };
155
+
156
+ try {
157
+ const rootW = windowFor('root');
158
+ const intW = windowFor('intermediate');
159
+ const leafW = windowFor('leaf');
160
+
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
+ ]);
182
+
183
+ // intermediate — CA + 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
+ ]);
210
+
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
+ ]);
238
+
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
+ ]);
283
+
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')));
288
+
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);
306
+
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
+ }
326
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "_meta": {
3
+ "source": "Apple App Store Server API getTransactionInfo()",
4
+ "fetched_at": "2026-06-15T11:02:00Z",
5
+ "bundle_id": "io.arcblock.ai.stro",
6
+ "environment": "Sandbox",
7
+ "transaction_id": "2000001186283296",
8
+ "verify_url": "https://payment-demo.ofind.cn/api/integrations/app-store/verify",
9
+ "notes": "Apple-signed (ES256). Verifiable against Apple Root CA G3. Use as a fixture for unit tests that need a real signed transaction."
10
+ },
11
+ "decoded_header": {
12
+ "alg": "ES256",
13
+ "x5c_chain": [
14
+ "Prod ECC Mac App Store and iTunes Store Receipt Signing (leaf, expires 2027-10-13)",
15
+ "Apple Worldwide Developer Relations Certification Authority G6 (intermediate)",
16
+ "Apple Root CA - G3 (root, expires 2039-04-30)"
17
+ ]
18
+ },
19
+ "decoded_payload": {
20
+ "transactionId": "2000001186283296",
21
+ "originalTransactionId": "2000001185803701",
22
+ "webOrderLineItemId": "2000000145344971",
23
+ "bundleId": "io.arcblock.ai.stro",
24
+ "productId": "io.aistro.monthly",
25
+ "subscriptionGroupIdentifier": "21331532",
26
+ "purchaseDate": 1781142078000,
27
+ "originalPurchaseDate": 1781080008000,
28
+ "expiresDate": 1781142258000,
29
+ "quantity": 1,
30
+ "type": "Auto-Renewable Subscription",
31
+ "appAccountToken": "040288cc-5062-5af3-ae64-1b1576bd3af7",
32
+ "inAppOwnershipType": "PURCHASED",
33
+ "signedDate": 1781521334379,
34
+ "environment": "Sandbox",
35
+ "transactionReason": "PURCHASE",
36
+ "storefront": "USA",
37
+ "storefrontId": "143441",
38
+ "price": 9990,
39
+ "currency": "USD",
40
+ "appTransactionId": "705611184395056381",
41
+ "billingPlanType": "BILLED_UPFRONT"
42
+ }
43
+ }
@@ -0,0 +1 @@
1
+ eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTVRDQ0E3YWdBd0lCQWdJUVI4S0h6ZG41NTRaL1VvcmFkTng5dHpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJMU1Ea3hPVEU1TkRRMU1Wb1hEVEkzTVRBeE16RTNORGN5TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCTm5WdmhjdjdpVCs3RXg1dEJNQmdyUXNwSHpJc1hSaTBZeGZlazdsdjh3RW1qL2JIaVd0TndKcWMyQm9IenNRaUVqUDdLRklJS2c0WTh5MC9ueW51QW1qZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRklGaW9HNHdNTVZBMWt1OXpKbUdOUEFWbjNlcU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TnBBREJtQWpFQStxWG5SRUM3aFhJV1ZMc0x4em5qUnBJelBmN1ZIejlWL0NUbTgrTEpsclFlcG5tY1B2R0xOY1g2WFBubGNnTEFBakVBNUlqTlpLZ2c1cFE3OWtuRjRJYlRYZEt2OHZ1dElETVhEbWpQVlQzZEd2RnRzR1J3WE95d1Iya1pDZFNyZmVvdCIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMTE4NjI4MzI5NiIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjIwMDAwMDExODU4MDM3MDEiLCJ3ZWJPcmRlckxpbmVJdGVtSWQiOiIyMDAwMDAwMTQ1MzQ0OTcxIiwiYnVuZGxlSWQiOiJpby5hcmNibG9jay5haS5zdHJvIiwicHJvZHVjdElkIjoiaW8uYWlzdHJvLm1vbnRobHkiLCJzdWJzY3JpcHRpb25Hcm91cElkZW50aWZpZXIiOiIyMTMzMTUzMiIsInB1cmNoYXNlRGF0ZSI6MTc4MTE0MjA3ODAwMCwib3JpZ2luYWxQdXJjaGFzZURhdGUiOjE3ODEwODAwMDgwMDAsImV4cGlyZXNEYXRlIjoxNzgxMTQyMjU4MDAwLCJxdWFudGl0eSI6MSwidHlwZSI6IkF1dG8tUmVuZXdhYmxlIFN1YnNjcmlwdGlvbiIsImFwcEFjY291bnRUb2tlbiI6IjA0MDI4OGNjLTUwNjItNWFmMy1hZTY0LTFiMTU3NmJkM2FmNyIsImluQXBwT3duZXJzaGlwVHlwZSI6IlBVUkNIQVNFRCIsInNpZ25lZERhdGUiOjE3ODE1MjEzMzQzNzksImVudmlyb25tZW50IjoiU2FuZGJveCIsInRyYW5zYWN0aW9uUmVhc29uIjoiUFVSQ0hBU0UiLCJzdG9yZWZyb250IjoiVVNBIiwic3RvcmVmcm9udElkIjoiMTQzNDQxIiwicHJpY2UiOjk5OTAsImN1cnJlbmN5IjoiVVNEIiwiYXBwVHJhbnNhY3Rpb25JZCI6IjcwNTYxMTE4NDM5NTA1NjM4MSIsImJpbGxpbmdQbGFuVHlwZSI6IkJJTExFRF9VUEZST05UIn0.uy7PLqCBuoT9vcUjr8S5qkxpzRGZVwoB9wMum67HXonX7zMrP-VjIzOcWlkt0_WFnjDaMzE0plvEiMkFSWVBoA