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
@@ -5,13 +5,17 @@
5
5
  // environments" off Blocklet Server (arc-node / CF). The guard no-ops both SDK
6
6
  // transports there — at the module level, so app-side logic still runs and only
7
7
  // the send/publish is skipped — and leaves Blocklet Server untouched. Gated on the
8
- // single reusable predicate isBlockletServer() (= BLOCKLET_APP_ID present).
8
+ // single reusable predicate isBlockletServer() (= BLOCKLET_DID present; the CF
9
+ // worker injects BLOCKLET_APP_ID = APP_PID so that can NOT be the marker).
9
10
  describe('ensureBlockletSdkTransportGuard', () => {
10
- const ORIG = process.env.BLOCKLET_APP_ID;
11
+ const ORIG = process.env.BLOCKLET_DID;
12
+ const ORIG_APP_ID = process.env.BLOCKLET_APP_ID;
11
13
 
12
14
  afterEach(() => {
13
- if (ORIG === undefined) delete process.env.BLOCKLET_APP_ID;
14
- else process.env.BLOCKLET_APP_ID = ORIG;
15
+ if (ORIG === undefined) delete process.env.BLOCKLET_DID;
16
+ else process.env.BLOCKLET_DID = ORIG;
17
+ if (ORIG_APP_ID === undefined) delete process.env.BLOCKLET_APP_ID;
18
+ else process.env.BLOCKLET_APP_ID = ORIG_APP_ID;
15
19
  jest.resetModules();
16
20
  });
17
21
 
@@ -33,8 +37,8 @@ describe('ensureBlockletSdkTransportGuard', () => {
33
37
  return { notification, eventbus };
34
38
  }
35
39
 
36
- it('off Blocklet Server (no BLOCKLET_APP_ID): no-ops notification + eventbus on module + default', async () => {
37
- delete process.env.BLOCKLET_APP_ID;
40
+ it('off Blocklet Server (no BLOCKLET_DID): no-ops notification + eventbus on module + default', async () => {
41
+ delete process.env.BLOCKLET_DID;
38
42
  const { notification, eventbus } = loadAndGuard();
39
43
  for (const target of [notification, notification.default]) {
40
44
  expect(target.sendToUser).not.toBe('REAL');
@@ -50,12 +54,25 @@ describe('ensureBlockletSdkTransportGuard', () => {
50
54
  }
51
55
  });
52
56
 
53
- it('on Blocklet Server (BLOCKLET_APP_ID set): leaves both transports intact', () => {
54
- process.env.BLOCKLET_APP_ID = 'z_app_pid';
57
+ it('on Blocklet Server (BLOCKLET_DID set): leaves both transports intact', () => {
58
+ process.env.BLOCKLET_DID = 'z_node_did';
55
59
  const { notification, eventbus } = loadAndGuard();
56
60
  expect(notification.sendToUser).toBe('REAL');
57
61
  expect(notification.default.sendToUser).toBe('REAL');
58
62
  expect(eventbus.publish).toBe('REAL');
59
63
  expect(eventbus.default.publish).toBe('REAL');
60
64
  });
65
+
66
+ // Regression: the CF worker injects BLOCKLET_APP_ID = APP_PID for app identity,
67
+ // which used to make isBlockletServer() spuriously true and skip the guard — then
68
+ // the SDK transport crashed at send time. With the marker on BLOCKLET_DID, an
69
+ // APP_ID without a DID is correctly treated as off-Blocklet-Server.
70
+ it('CF worker (BLOCKLET_APP_ID set, no BLOCKLET_DID): still no-ops the transports', async () => {
71
+ delete process.env.BLOCKLET_DID;
72
+ process.env.BLOCKLET_APP_ID = 'z_app_pid';
73
+ const { notification, eventbus } = loadAndGuard();
74
+ expect(notification.sendToUser).not.toBe('REAL');
75
+ expect(eventbus.publish).not.toBe('REAL');
76
+ await expect(notification.sendToUser('zUser', { title: 't' })).resolves.toBeUndefined();
77
+ });
61
78
  });
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.6
17
+ version: 1.29.8
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
@@ -54,6 +54,11 @@ import { setDB } from './shims/sequelize-d1/model';
54
54
  import { withD1Retry } from './shims/sequelize-d1/retry';
55
55
  import { cronInstance } from './shims/cron';
56
56
  import { createCloudflareDidConnectRuntime, createCloudflareIdentityDriver } from './did-connect-runtime';
57
+ // Runtime-neutral host glue — single source of truth in the core source tree,
58
+ // shared with the arc-node daemon (via the package main entry's `as string`
59
+ // facade). Re-exported below so existing importers (cf-adapter.spec.ts) keep
60
+ // resolving them from this module.
61
+ import { injectCaller, createTenantProvisioner } from '../api/src/host-glue';
57
62
 
58
63
  /**
59
64
  * The reserved mount prefix for the embedded payment service. The internal
@@ -142,52 +147,12 @@ export interface CloudflarePaymentAdapter {
142
147
  ensureSchema: () => Promise<string[]>;
143
148
  }
144
149
 
145
- /** Client-forged identity headers always stripped before the host injects the real caller. */
146
- const USER_HEADERS = ['x-user-did', 'x-user-role', 'x-user-provider', 'x-user-fullname', 'x-user-wallet-os'];
147
-
148
- /**
149
- * CF API gate caller header glue (decision #5). STRIP any client-supplied
150
- * `x-user-*` first (a forged `x-user-did` would be an auth bypass), then inject the
151
- * host-resolved caller as canonical headers the core `authenticate()` trusts. No
152
- * caller → the request runs anonymous (the strip already happened, so it is never
153
- * forged). Mirrors the standalone worker's identical strip-then-inject.
154
- */
155
- export function injectCaller(headers: Headers, caller: CloudflareCallerIdentity | null): void {
156
- for (const h of USER_HEADERS) headers.delete(h);
157
- if (!caller) return;
158
- const canonicalDid = caller.did?.startsWith('did:abt:') ? caller.did : `did:abt:${caller.did}`;
159
- headers.set('x-user-did', canonicalDid);
160
- headers.set('x-user-role', `blocklet-${caller.role || 'guest'}`);
161
- headers.set('x-user-provider', caller.authMethod === 'access-key' ? 'access-key' : caller.authMethod || 'wallet');
162
- headers.set('x-user-fullname', encodeURIComponent(caller.displayName || ''));
163
- headers.set('x-user-wallet-os', '');
164
- }
165
-
166
- /**
167
- * Lazy per-tenant provisioning with in-flight dedup. Concurrent first-requests for
168
- * the same tenant await the SAME provision() call — a Set would let a burst at
169
- * isolate start double-seed before the DB idempotency guard commits. A resolved
170
- * promise stays cached so later requests are instant; a FAILED provision is dropped
171
- * so the next request retries. Null tenant is a no-op. Copied from the node witness
172
- * (runtimes/node/src/daemon/payment/index.ts:72-105).
173
- */
174
- export function createTenantProvisioner(
175
- provision: (instanceDid: string) => Promise<void>,
176
- ): (instanceDid: string | null) => Promise<void> {
177
- const inflight = new Map<string, Promise<void>>();
178
- return (instanceDid) => {
179
- if (!instanceDid) return Promise.resolve();
180
- let p = inflight.get(instanceDid);
181
- if (!p) {
182
- p = provision(instanceDid).catch((err) => {
183
- inflight.delete(instanceDid); // drop so the next request retries
184
- throw err;
185
- });
186
- inflight.set(instanceDid, p);
187
- }
188
- return p;
189
- };
190
- }
150
+ // `injectCaller` (CF API gate caller header glue) and `createTenantProvisioner`
151
+ // (lazy-provision in-flight dedup) now live in the runtime-neutral
152
+ // `../api/src/host-glue` — a single source shared with the arc-node daemon (via
153
+ // the package main entry). Re-exported here so existing importers keep resolving
154
+ // them from this module.
155
+ export { injectCaller, createTenantProvisioner };
191
156
 
192
157
  /** Dependencies for the request handler (factored out so it is testable with a fake service). */
193
158
  export interface FetchDeps {
@@ -16,7 +16,8 @@ describe('injectCaller — CF API gate caller header glue (decision #5)', () =>
16
16
  const h = new Headers();
17
17
  injectCaller(h, { did: 'zUSER', role: 'admin', authMethod: 'passkey', displayName: 'Alice' });
18
18
  expect(h.get('x-user-did')).toBe('did:abt:zUSER');
19
- expect(h.get('x-user-role')).toBe('blocklet-admin');
19
+ // raw role — NO `blocklet-` prefix (the core strips it anyway; a prefixed role is wrong)
20
+ expect(h.get('x-user-role')).toBe('admin');
20
21
  expect(h.get('x-user-provider')).toBe('passkey');
21
22
  expect(h.get('x-user-fullname')).toBe(encodeURIComponent('Alice'));
22
23
  });
@@ -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.6",
3
+ "version": "1.29.8",
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.7",
51
+ "@arcblock/did-connect-js": "^4.1.0",
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.6",
65
- "@blocklet/payment-react": "1.29.6",
66
- "@blocklet/payment-vendor": "1.29.6",
63
+ "@blocklet/payment-broker-client": "1.29.8",
64
+ "@blocklet/payment-react": "1.29.8",
65
+ "@blocklet/payment-vendor": "1.29.8",
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.6",
136
+ "@blocklet/payment-types": "1.29.8",
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": "ee97756d3e9ba56def933580a6cb6bb93d97b55c"
183
+ "gitHead": "547c0614f1f83abad80ea08452d2f5a9f4f5aa07"
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
- }