payment-kit 1.29.5 → 1.29.7

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 (25) hide show
  1. package/api/src/integrations/app-store/client.ts +7 -13
  2. package/api/src/integrations/app-store/native-api.ts +102 -0
  3. package/api/src/integrations/app-store/native-asn1.ts +49 -0
  4. package/api/src/integrations/app-store/native-jws.ts +222 -0
  5. package/api/src/integrations/app-store/native-receipt.ts +105 -0
  6. package/api/src/integrations/app-store/signed-data-verifier.ts +40 -79
  7. package/api/tests/integrations/app-store/client.spec.ts +83 -57
  8. package/api/tests/integrations/app-store/fixtures/README.md +54 -0
  9. package/api/tests/integrations/app-store/fixtures/keys/int.key.pem +5 -0
  10. package/api/tests/integrations/app-store/fixtures/keys/leaf.key.pem +5 -0
  11. package/api/tests/integrations/app-store/fixtures/keys/root.key.pem +5 -0
  12. package/api/tests/integrations/app-store/fixtures/keys/wrongint.key.pem +5 -0
  13. package/api/tests/integrations/app-store/fixtures/make-chain.spec.ts +152 -0
  14. package/api/tests/integrations/app-store/fixtures/make-chain.ts +326 -0
  15. package/api/tests/integrations/app-store/fixtures/real-sandbox.json +43 -0
  16. package/api/tests/integrations/app-store/fixtures/real-sandbox.jws +1 -0
  17. package/api/tests/integrations/app-store/native-api.spec.ts +172 -0
  18. package/api/tests/integrations/app-store/native-integration.spec.ts +78 -0
  19. package/api/tests/integrations/app-store/native-jws.spec.ts +219 -0
  20. package/api/tests/integrations/app-store/native-receipt.spec.ts +161 -0
  21. package/blocklet.yml +1 -1
  22. package/cloudflare/cf-adapter.ts +14 -1
  23. package/cloudflare/tests/x509-probe.mjs +260 -0
  24. package/package.json +7 -10
  25. package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +0 -17
@@ -0,0 +1,260 @@
1
+ // X509 / crypto probe — runs inside REAL workerd (via miniflare) with the EXACT
2
+ // production CF config (compatibility_date 2024-12-01 + nodejs_compat).
3
+ //
4
+ // Settles the Apple-IAP native-verify plan (intent/apple-iap-native-verify-plan/)
5
+ // AND is the committed regression gate proving the §5 algorithm holds under
6
+ // workerd. Rounds:
7
+ // Round 1 (baseline): X509Certificate construct/publicKey/verify, node ES256
8
+ // (DER sig), subtle ECDSA P-256, subtle importKey spki.
9
+ // Round 2 (§4 prep): the verify-path specifics NOT covered by round 1 —
10
+ // P1: leafCert.publicKey.export({type:'spki',format:'der'}) in workerd
11
+ // P2: verify a REAL P1363 (raw r||s) ES256 sig — (a) via crypto.subtle,
12
+ // (b) via createVerify dsaEncoding:'ieee-p1363'
13
+ // P3: read an Apple custom extension OID (1.2.840.113635.*) WITHOUT an ASN.1
14
+ // lib — DER-encode the OID and indexOf() it in cert.raw. (Node's
15
+ // X509Certificate has NO generic extension-by-OID accessor.)
16
+ // chain_round (0.2): a synthetic 3-cert chain (root → CA int w/ Apple OID →
17
+ // leaf w/ Apple OID) — proves §5.4 walk (intermediate.verify(root),
18
+ // leaf.verify(int)) + §5.5 OID presence on BOTH certs under workerd,
19
+ // plus a cross-signed leaf that MUST fail leaf.verify(int).
20
+ // Mirrors api/tests/integrations/app-store/fixtures/make-chain.ts —
21
+ // keep the two in sync.
22
+ // pkcs8_sign_round (0.5): importKey('pkcs8') for SIGN + round-trip verify —
23
+ // de-risks the App Store Server API JWT bearer (Phase 2) BEFORE it is
24
+ // built. If workerd rejects pkcs8-for-sign, Phase 2 takes Approach J.
25
+ //
26
+ // Vectors are generated at runtime (openssl + node:crypto) so cert validity is
27
+ // always current and the P1363 sig is real.
28
+ //
29
+ // Run: node cloudflare/tests/x509-probe.mjs (cwd = blocklets/core)
30
+ import { Miniflare } from 'miniflare';
31
+ import { execSync, execFileSync } from 'node:child_process';
32
+ import { generateKeyPairSync, sign as nodeSign, X509Certificate } from 'node:crypto';
33
+ import fs from 'node:fs';
34
+ import os from 'node:os';
35
+ import path from 'node:path';
36
+
37
+ const LEAF_OID = '1.2.840.113635.100.6.11.1'; // Apple: present on the leaf
38
+ const INT_OID = '1.2.840.113635.100.6.2.1'; // Apple: present on the intermediate (ABSENT from our leaf — negative control)
39
+ const SIGNING_INPUT = 'header.payload';
40
+
41
+ // DER-encode an OID's *content* octets (no 0x06 tag / length) — enough to scan
42
+ // for inside a cert's DER with indexOf (the content is unique enough).
43
+ function oidContentHex(oid) {
44
+ const parts = oid.split('.').map(Number);
45
+ const bytes = [40 * parts[0] + parts[1]];
46
+ for (let i = 2; i < parts.length; i++) {
47
+ let v = parts[i];
48
+ const stack = [v & 0x7f];
49
+ v = Math.floor(v / 128);
50
+ while (v > 0) {
51
+ stack.unshift((v & 0x7f) | 0x80);
52
+ v = Math.floor(v / 128);
53
+ }
54
+ bytes.push(...stack);
55
+ }
56
+ return Buffer.from(bytes).toString('hex');
57
+ }
58
+
59
+ // ---- generate vectors (node side) ----
60
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'x509-probe-'));
61
+ const keyPath = path.join(dir, 'leaf.key');
62
+ const certPem = path.join(dir, 'leaf.pem');
63
+ const certDer = path.join(dir, 'leaf.der');
64
+ execSync(`openssl ecparam -name prime256v1 -genkey -noout -out ${keyPath}`, { stdio: 'ignore' });
65
+ // self-signed leaf carrying the Apple LEAF extension OID (value = ASN.1 NULL)
66
+ execSync(
67
+ `openssl req -new -x509 -key ${keyPath} -out ${certPem} -days 3650 -subj "/CN=apple-leaf-probe" -addext "${LEAF_OID}=DER:0500"`,
68
+ { stdio: 'ignore' }
69
+ );
70
+ execSync(`openssl x509 -in ${certPem} -outform DER -out ${certDer}`, { stdio: 'ignore' });
71
+
72
+ const certDerB64 = fs.readFileSync(certDer).toString('base64');
73
+ const keyPem = fs.readFileSync(keyPath, 'utf8');
74
+ // REAL P1363 ES256 signature (raw 64-byte r||s — the JWS form), NOT DER.
75
+ const p1363SigB64 = nodeSign('sha256', Buffer.from(SIGNING_INPUT), { key: keyPem, dsaEncoding: 'ieee-p1363' }).toString(
76
+ 'base64'
77
+ );
78
+
79
+ // ---- chain_round vectors (0.2): synthetic root → CA int (Apple OID) → leaf (Apple OID) ----
80
+ // Mirrors fixtures/make-chain.ts. The cross-signed leaf shares the int's subject
81
+ // DN but is signed by a DIFFERENT key, so leaf.verify(int.publicKey) MUST fail.
82
+ function buildChain() {
83
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'x509-chain-'));
84
+ const f = (n) => path.join(d, n);
85
+ const run = (args) => execFileSync('openssl', args, { cwd: d, stdio: ['ignore', 'pipe', 'pipe'] });
86
+ const key = (n) => run(['ecparam', '-name', 'prime256v1', '-genkey', '-noout', '-out', f(n)]);
87
+ const SUBJ_INT = '/CN=Probe Apple WWDR Intermediate';
88
+ try {
89
+ key('root.key');
90
+ run(['req', '-new', '-x509', '-key', f('root.key'), '-out', f('root.pem'), '-days', '3650',
91
+ '-subj', '/CN=Probe Apple Root CA', '-addext', 'basicConstraints=critical,CA:TRUE']);
92
+ key('int.key');
93
+ run(['req', '-new', '-key', f('int.key'), '-out', f('int.csr'), '-subj', SUBJ_INT]);
94
+ fs.writeFileSync(f('int.ext'), `[v3]\nbasicConstraints=critical,CA:TRUE\n${INT_OID}=DER:0500`);
95
+ run(['x509', '-req', '-in', f('int.csr'), '-CA', f('root.pem'), '-CAkey', f('root.key'), '-CAcreateserial',
96
+ '-out', f('int.pem'), '-days', '3650', '-extfile', f('int.ext'), '-extensions', 'v3']);
97
+ key('leaf.key');
98
+ run(['req', '-new', '-key', f('leaf.key'), '-out', f('leaf.csr'), '-subj', '/CN=Probe Apple Leaf']);
99
+ fs.writeFileSync(f('leaf.ext'), `[v3]\nbasicConstraints=critical,CA:FALSE\n${LEAF_OID}=DER:0500`);
100
+ run(['x509', '-req', '-in', f('leaf.csr'), '-CA', f('int.pem'), '-CAkey', f('int.key'), '-CAcreateserial',
101
+ '-out', f('leaf.pem'), '-days', '3650', '-extfile', f('leaf.ext'), '-extensions', 'v3']);
102
+ // wrong intermediate: same subject DN, different key
103
+ key('wrongint.key');
104
+ run(['req', '-new', '-x509', '-key', f('wrongint.key'), '-out', f('wrongint.pem'), '-days', '3650',
105
+ '-subj', SUBJ_INT, '-addext', 'basicConstraints=critical,CA:TRUE']);
106
+ key('crossleaf.key');
107
+ run(['req', '-new', '-key', f('crossleaf.key'), '-out', f('crossleaf.csr'), '-subj', '/CN=Probe Apple Leaf']);
108
+ run(['x509', '-req', '-in', f('crossleaf.csr'), '-CA', f('wrongint.pem'), '-CAkey', f('wrongint.key'),
109
+ '-CAcreateserial', '-out', f('crossleaf.pem'), '-days', '3650', '-extfile', f('leaf.ext'), '-extensions', 'v3']);
110
+ const der = (n) => Buffer.from(new X509Certificate(fs.readFileSync(f(n))).raw).toString('base64');
111
+ return { leaf: der('leaf.pem'), int: der('int.pem'), root: der('root.pem'), crossLeaf: der('crossleaf.pem') };
112
+ } finally {
113
+ fs.rmSync(d, { recursive: true, force: true });
114
+ }
115
+ }
116
+ const chain = buildChain();
117
+
118
+ // ---- pkcs8_sign_round vectors (0.5): EC P-256 pkcs8 (DER) + its spki, for SIGN ----
119
+ const { privateKey: ecPriv, publicKey: ecPub } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
120
+ const pkcs8B64 = ecPriv.export({ type: 'pkcs8', format: 'der' }).toString('base64');
121
+ const spkiSignB64 = ecPub.export({ type: 'spki', format: 'der' }).toString('base64');
122
+ const badPkcs8B64 = Buffer.from('not-a-real-pkcs8-key').toString('base64');
123
+
124
+ const script = `
125
+ import { X509Certificate, createVerify } from 'node:crypto';
126
+ import { Buffer } from 'node:buffer';
127
+
128
+ const CERT_DER_B64 = ${JSON.stringify(certDerB64)};
129
+ const P1363_SIG_B64 = ${JSON.stringify(p1363SigB64)};
130
+ const LEAF_OID_HEX = ${JSON.stringify(oidContentHex(LEAF_OID))};
131
+ const INT_OID_HEX = ${JSON.stringify(oidContentHex(INT_OID))};
132
+ const MSG = ${JSON.stringify(SIGNING_INPUT)};
133
+
134
+ // chain_round (0.2)
135
+ const CHAIN = ${JSON.stringify(chain)};
136
+ // pkcs8_sign_round (0.5)
137
+ const PKCS8_B64 = ${JSON.stringify(pkcs8B64)};
138
+ const SPKI_SIGN_B64 = ${JSON.stringify(spkiSignB64)};
139
+ const BAD_PKCS8_B64 = ${JSON.stringify(badPkcs8B64)};
140
+
141
+ function ok(v){ return { ok: true, ...v }; }
142
+ function err(e){ return { ok: false, error: String(e && e.message || e) }; }
143
+ function hex(buf){ return Buffer.from(buf).toString('hex'); }
144
+
145
+ export default {
146
+ async fetch() {
147
+ const out = {};
148
+ const cert = new X509Certificate(Buffer.from(CERT_DER_B64, 'base64'));
149
+
150
+ // P1 — export leaf public key as SPKI DER (needed to feed crypto.subtle)
151
+ let spkiDer = null;
152
+ try {
153
+ spkiDer = cert.publicKey.export({ type: 'spki', format: 'der' });
154
+ out.P1_export_spki_der = ok({ bytes: spkiDer.byteLength ?? spkiDer.length });
155
+ } catch (e) { out.P1_export_spki_der = err(e); }
156
+
157
+ // P2a — verify REAL P1363 ES256 sig via crypto.subtle (preferred)
158
+ try {
159
+ if (!spkiDer) throw new Error('no spki from P1');
160
+ const key = await crypto.subtle.importKey(
161
+ 'spki', spkiDer, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']
162
+ );
163
+ const valid = await crypto.subtle.verify(
164
+ { name: 'ECDSA', hash: 'SHA-256' }, key,
165
+ Buffer.from(P1363_SIG_B64, 'base64'), new TextEncoder().encode(MSG)
166
+ );
167
+ out.P2a_subtle_p1363_verify = ok({ valid });
168
+ } catch (e) { out.P2a_subtle_p1363_verify = err(e); }
169
+
170
+ // P2b — verify REAL P1363 sig via node createVerify dsaEncoding:'ieee-p1363' (fallback)
171
+ try {
172
+ const v = createVerify('SHA256');
173
+ v.update(MSG); v.end();
174
+ const valid = v.verify({ key: cert.publicKey, dsaEncoding: 'ieee-p1363' }, Buffer.from(P1363_SIG_B64, 'base64'));
175
+ out.P2b_node_p1363_verify = ok({ valid });
176
+ } catch (e) { out.P2b_node_p1363_verify = err(e); }
177
+
178
+ // P3 — Apple extension-OID presence via indexOf in cert.raw (no ASN.1 lib)
179
+ try {
180
+ const rawHex = hex(cert.raw);
181
+ const leafPresent = rawHex.includes(LEAF_OID_HEX); // expect TRUE (we added it)
182
+ const intPresent = rawHex.includes(INT_OID_HEX); // expect FALSE (negative control)
183
+ // also confirm Node has NO generic extension accessor (documents the gap)
184
+ const hasGenericExtAccessor =
185
+ typeof cert.getExtension === 'function' || typeof cert.extensions !== 'undefined';
186
+ out.P3_oid_scan = ok({
187
+ rawIsBuffer: cert.raw instanceof Uint8Array,
188
+ leafOidPresent: leafPresent,
189
+ intOidPresent: intPresent,
190
+ hasGenericExtAccessor,
191
+ });
192
+ } catch (e) { out.P3_oid_scan = err(e); }
193
+
194
+ // chain_round (0.2) — full §5.4 walk + §5.5 OID presence on a 3-cert chain
195
+ try {
196
+ const leaf = new X509Certificate(Buffer.from(CHAIN.leaf, 'base64'));
197
+ const intermediate = new X509Certificate(Buffer.from(CHAIN.int, 'base64'));
198
+ const root = new X509Certificate(Buffer.from(CHAIN.root, 'base64'));
199
+ const crossLeaf = new X509Certificate(Buffer.from(CHAIN.crossLeaf, 'base64'));
200
+ out.chain_round = ok({
201
+ intVerifiesRoot: intermediate.verify(root.publicKey),
202
+ leafVerifiesInt: leaf.verify(intermediate.publicKey),
203
+ leafOidPresent: hex(leaf.raw).includes(LEAF_OID_HEX),
204
+ intOidPresent: hex(intermediate.raw).includes(INT_OID_HEX),
205
+ intIsCa: intermediate.ca === true,
206
+ crossSignedLeafVerifies: crossLeaf.verify(intermediate.publicKey), // expect FALSE
207
+ });
208
+ } catch (e) { out.chain_round = err(e); }
209
+
210
+ // pkcs8_sign_round (0.5) — importKey('pkcs8') for SIGN + round-trip verify
211
+ try {
212
+ const signKey = await crypto.subtle.importKey(
213
+ 'pkcs8', Buffer.from(PKCS8_B64, 'base64'), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']
214
+ );
215
+ const sig = await crypto.subtle.sign(
216
+ { name: 'ECDSA', hash: 'SHA-256' }, signKey, new TextEncoder().encode(MSG)
217
+ );
218
+ const verifyKey = await crypto.subtle.importKey(
219
+ 'spki', Buffer.from(SPKI_SIGN_B64, 'base64'), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']
220
+ );
221
+ const roundTripVerifyOk = await crypto.subtle.verify(
222
+ { name: 'ECDSA', hash: 'SHA-256' }, verifyKey, sig, new TextEncoder().encode(MSG)
223
+ );
224
+ // negative control: a malformed pkcs8 must surface as importPkcs8Ok:false, not a silent unsigned key
225
+ let importBadOk = true;
226
+ try {
227
+ await crypto.subtle.importKey(
228
+ 'pkcs8', Buffer.from(BAD_PKCS8_B64, 'base64'), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']
229
+ );
230
+ } catch { importBadOk = false; }
231
+ out.pkcs8_sign_round = ok({
232
+ importPkcs8Ok: true,
233
+ signOk: sig.byteLength > 0,
234
+ roundTripVerifyOk,
235
+ malformedPkcs8Rejected: importBadOk === false,
236
+ });
237
+ } catch (e) { out.pkcs8_sign_round = err(e); }
238
+
239
+ return new Response(JSON.stringify(out, null, 2), { headers: { 'content-type': 'application/json' } });
240
+ }
241
+ };
242
+ `;
243
+
244
+ const mf = new Miniflare({
245
+ modules: true,
246
+ script,
247
+ compatibilityDate: '2024-12-01',
248
+ compatibilityFlags: ['nodejs_compat'],
249
+ });
250
+
251
+ try {
252
+ const res = await mf.dispatchFetch('http://localhost/');
253
+ console.log(await res.text());
254
+ } catch (e) {
255
+ console.error('PROBE FAILED TO RUN:', e);
256
+ process.exitCode = 1;
257
+ } finally {
258
+ await mf.dispose();
259
+ fs.rmSync(dir, { recursive: true, force: true });
260
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.29.5",
3
+ "version": "1.29.7",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "prelint": "npm run types",
@@ -47,9 +47,8 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@abtnode/cron": "^1.17.13-beta-20260613-094425-b81920c8",
50
- "@apple/app-store-server-library": "^3.1.0",
51
50
  "@arcblock/did": "^1.30.24",
52
- "@arcblock/did-connect-js": "^4.0.6",
51
+ "@arcblock/did-connect-js": "^4.0.7",
53
52
  "@arcblock/did-connect-react": "^3.5.4",
54
53
  "@arcblock/did-connect-storage-nedb": "^1.8.0",
55
54
  "@arcblock/did-util": "^1.30.24",
@@ -61,9 +60,9 @@
61
60
  "@blocklet/error": "^0.3.5",
62
61
  "@blocklet/js-sdk": "^1.17.13-beta-20260613-094425-b81920c8",
63
62
  "@blocklet/logger": "^1.17.13-beta-20260613-094425-b81920c8",
64
- "@blocklet/payment-broker-client": "1.29.5",
65
- "@blocklet/payment-react": "1.29.5",
66
- "@blocklet/payment-vendor": "1.29.5",
63
+ "@blocklet/payment-broker-client": "1.29.7",
64
+ "@blocklet/payment-react": "1.29.7",
65
+ "@blocklet/payment-vendor": "1.29.7",
67
66
  "@blocklet/sdk": "^1.17.13-beta-20260613-094425-b81920c8",
68
67
  "@blocklet/ui-react": "^3.5.4",
69
68
  "@blocklet/uploader": "^0.3.20",
@@ -105,7 +104,6 @@
105
104
  "morgan": "^1.10.0",
106
105
  "mui-daterange-picker": "^1.0.5",
107
106
  "nanoid": "^3.3.11",
108
- "node-apple-receipt-verify": "^1.15.0",
109
107
  "numbro": "^2.5.0",
110
108
  "p-all": "3.0.0",
111
109
  "p-wait-for": "^3.2.0",
@@ -135,12 +133,11 @@
135
133
  "devDependencies": {
136
134
  "@abtnode/types": "^1.17.13-beta-20260613-094425-b81920c8",
137
135
  "@arcblock/eslint-config-ts": "^0.3.3",
138
- "@blocklet/payment-types": "1.29.5",
136
+ "@blocklet/payment-types": "1.29.7",
139
137
  "@types/connect": "^3.4.38",
140
138
  "@types/debug": "^4.1.12",
141
139
  "@types/dotenv-flow": "^3.3.3",
142
140
  "@types/node": "^18.19.112",
143
- "@types/node-apple-receipt-verify": "^1.7.5",
144
141
  "@types/react": "^18.3.23",
145
142
  "@types/react-dom": "^18.3.7",
146
143
  "@vitejs/plugin-react": "^4.6.0",
@@ -183,5 +180,5 @@
183
180
  "parser": "typescript"
184
181
  }
185
182
  },
186
- "gitHead": "b392a84d8557fd6f32f47540d76d642d62e59dd3"
183
+ "gitHead": "e1999aecfa9ce64bfd56da14bf47b255a19ef394"
187
184
  }
@@ -1,17 +0,0 @@
1
- // Minimal ambient declarations for `node-apple-receipt-verify`.
2
- // The library ships no @types package; we only use config() + validate(),
3
- // and accept that validate() returns `unknown` items we narrow ourselves.
4
-
5
- declare module 'node-apple-receipt-verify' {
6
- export interface AppleReceiptConfig {
7
- secret?: string;
8
- verbose?: boolean;
9
- environment?: Array<'production' | 'sandbox'>;
10
- /** ignore expired items, default false */
11
- ignoreExpired?: boolean;
12
- /** extended fields, etc. */
13
- [k: string]: any;
14
- }
15
- export function config(options: AppleReceiptConfig): void;
16
- export function validate(options: { receipt: string }): Promise<unknown[]>;
17
- }