payment-kit 1.29.2 → 1.29.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/bootstrap.ts +11 -0
- package/api/src/crons/index.ts +14 -13
- package/api/src/crons/tenant-fanout.ts +82 -0
- package/api/src/host-node/did-connect-runtime-node.ts +33 -0
- package/api/src/host-node/serve-static-arc.ts +68 -0
- package/api/src/host-node/serve-static.ts +41 -0
- package/api/src/libs/auth.ts +166 -27
- package/api/src/libs/context.ts +11 -0
- package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
- package/api/src/libs/did-connect/tenant-identity.ts +221 -0
- package/api/src/libs/drivers/identity.ts +61 -0
- package/api/src/libs/drivers/index.ts +1 -1
- package/api/src/libs/http-fetch-adapter.ts +11 -1
- package/api/src/libs/queue/index.ts +14 -2
- package/api/src/middlewares/hono/context.ts +7 -0
- package/api/src/middlewares/hono/csrf.ts +13 -2
- package/api/src/middlewares/hono/security.ts +6 -11
- package/api/src/queues/checkout-session.ts +21 -9
- package/api/src/queues/event.ts +29 -7
- package/api/src/queues/payment.ts +23 -9
- package/api/src/queues/payout.ts +28 -16
- package/api/src/queues/refund.ts +18 -6
- package/api/src/routes/hono/customers.ts +6 -1
- package/api/src/routes/hono/refunds.ts +2 -3
- package/api/src/service.ts +178 -31
- package/api/src/store/sequelize.ts +16 -1
- package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
- package/api/tests/crons/tenant-fanout.spec.ts +158 -0
- package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
- package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
- package/api/tests/libs/service-host.spec.ts +37 -0
- package/api/tests/queues/event-tenant.spec.ts +60 -4
- package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
- package/api/tests/service/fail-closed-http.spec.ts +79 -0
- package/api/tests/service/static-arc-handler.spec.ts +101 -0
- package/api/tests/service/static-externalized.spec.ts +48 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
- package/cloudflare/README.md +8 -21
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
- package/cloudflare/build.ts +10 -5
- package/cloudflare/cf-adapter.ts +419 -0
- package/cloudflare/did-connect-runtime.ts +96 -0
- package/cloudflare/did-connect-token-storage.ts +151 -0
- package/cloudflare/esbuild-cf-config.cjs +407 -0
- package/cloudflare/run-build.js +33 -357
- package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
- package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
- package/cloudflare/tests/cf-adapter.spec.ts +244 -0
- package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +35 -10
- package/cloudflare/vite.config.ts +53 -45
- package/cloudflare/worker.ts +98 -56
- package/cloudflare/wrangler.json +0 -6
- package/cloudflare/wrangler.jsonc +0 -6
- package/cloudflare/wrangler.local-e2e.jsonc +0 -1
- package/cloudflare/wrangler.staging.json +0 -6
- package/package.json +7 -7
- package/scripts/bootstrap-inject.ts +166 -0
- package/src/app.tsx +2 -1
- package/src/libs/service-host.ts +13 -0
- package/vite.arc.config.ts +159 -0
- package/cloudflare/did-connect-auth.ts +0 -310
- package/cloudflare/shims/blocklet-sdk/util-csrf.ts +0 -13
- package/cloudflare/shims/blocklet-sdk/util-wallet.ts +0 -8
package/cloudflare/worker.ts
CHANGED
|
@@ -19,13 +19,13 @@ import { Sequelize } from './shims/sequelize-d1/sequelize-class';
|
|
|
19
19
|
// loop). Side-effect import — keep it ahead of service/crons/queues below.
|
|
20
20
|
import './queue-runtime-mode';
|
|
21
21
|
|
|
22
|
-
// Phase 12a (Option 3 seam): the worker's /api
|
|
23
|
-
// the embedded payment service factory
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
22
|
+
// Phase 12a (Option 3 seam) + S3-CF Phase 1B: the worker's /api surface comes from
|
|
23
|
+
// the embedded payment service factory. The factory performs assembly (config slot
|
|
24
|
+
// authoritative via setCoreConfig + model initialize). Phase 1B: the worker forwards
|
|
25
|
+
// every payment /api/* (business resource routes + DID payment actions) through the
|
|
26
|
+
// runtime-neutral `http.fetch` (the core full app + full pipeline) — a SINGLE
|
|
27
|
+
// surface. The factory's lazy `handler` getter is never touched here (hard-gated),
|
|
28
|
+
// so the worker only ever drives the core via `http.fetch`.
|
|
29
29
|
import { createEmbeddedPaymentService } from '../api/src/service';
|
|
30
30
|
import type { PaymentCoreService } from '../api/src/service';
|
|
31
31
|
|
|
@@ -66,11 +66,10 @@ import { resetD1Timing, getD1Timing } from './shims/sequelize-d1/timing';
|
|
|
66
66
|
import { withD1Retry } from './shims/sequelize-d1/retry';
|
|
67
67
|
|
|
68
68
|
// DID Connect: login routes proxied to blocklet-service, business actions (pay/subscribe) handled locally
|
|
69
|
-
import {
|
|
69
|
+
import { createCloudflareDidConnectRuntime, createCloudflareIdentityDriver } from './did-connect-runtime';
|
|
70
70
|
|
|
71
71
|
// Phase 7: tenant-context middleware — resolves Host -> tenant (single point)
|
|
72
72
|
// and wraps the request chain in context.withTenant (multi-mode fail-closed).
|
|
73
|
-
import { tenantMiddleware } from './tenant-middleware';
|
|
74
73
|
|
|
75
74
|
FetchRequest.registerGetUrl(async (req: FetchRequest) => {
|
|
76
75
|
const resp = await fetch(req.url, {
|
|
@@ -100,7 +99,6 @@ interface CallerIdentityDTO {
|
|
|
100
99
|
|
|
101
100
|
interface Env {
|
|
102
101
|
DB: D1Database;
|
|
103
|
-
DID_CONNECT_KV: KVNamespace;
|
|
104
102
|
JOB_QUEUE: Queue;
|
|
105
103
|
ASSETS: { fetch: (request: Request | string) => Promise<Response> };
|
|
106
104
|
APP_SK: string;
|
|
@@ -128,6 +126,17 @@ interface Env {
|
|
|
128
126
|
role?: string;
|
|
129
127
|
approved?: number;
|
|
130
128
|
} | null>;
|
|
129
|
+
// S3-CF (DID convergence): did-connect-service@4.0.3 — the per-instance app
|
|
130
|
+
// signing identity for the DID-Connect authenticator. The RPC resolves the arc
|
|
131
|
+
// run-mode internally (instance app:sk → instance identity; else auth-service
|
|
132
|
+
// root APP_SK/APP_PSK; else fail-closed), so payment-core never reimplements it.
|
|
133
|
+
getInstanceAppIdentity: (instanceDid: string) => Promise<{
|
|
134
|
+
appSk: string;
|
|
135
|
+
appPsk?: string;
|
|
136
|
+
appInfo?: { name?: string; description?: string; icon?: string; link?: string };
|
|
137
|
+
}>;
|
|
138
|
+
getAppEk?: (instanceDid: string) => Promise<string | null>;
|
|
139
|
+
resolveInstanceDidForHost?: (host: string) => Promise<string | null>;
|
|
131
140
|
};
|
|
132
141
|
HYPERDRIVE: { connectionString: string };
|
|
133
142
|
[key: string]: any;
|
|
@@ -270,18 +279,26 @@ function ensurePaymentService(env: Env): PaymentCoreService {
|
|
|
270
279
|
const svc = createEmbeddedPaymentService({
|
|
271
280
|
config: envToPaymentCoreConfig(env),
|
|
272
281
|
db: { sequelize },
|
|
282
|
+
// S3-CF (DID convergence): inject the CF DID-Connect runtime (real
|
|
283
|
+
// @arcblock/did-connect-js + CF chain/txEncoder/timeout + tenant-aware D1 token
|
|
284
|
+
// store) and the AUTH_SERVICE-backed identity driver (getInstanceAppIdentity).
|
|
285
|
+
// The core buildConnectRoutesHono then registers the 14 payment DID actions with
|
|
286
|
+
// a per-tenant signing identity — the worker no longer owns a private DID surface.
|
|
287
|
+
identity: createCloudflareIdentityDriver(),
|
|
288
|
+
didConnectRuntime: createCloudflareDidConnectRuntime(),
|
|
273
289
|
});
|
|
274
|
-
// Phase 12c HARD GATE: the CF worker is a host adapter
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
290
|
+
// Phase 12c HARD GATE (updated S3-CF Phase 1B): the CF worker is a host adapter
|
|
291
|
+
// that drives the core via the runtime-neutral `svc.http.fetch`. It must NEVER
|
|
292
|
+
// read `svc.handler` directly — that lazy getter is the node-host convenience
|
|
293
|
+
// entry (it would wire the node staticHandler if one were injected). The worker
|
|
294
|
+
// forwards through `svc.http.fetch`, which builds the same full hono app but is
|
|
295
|
+
// the sanctioned runtime-neutral seam. Trap `handler` so a future regression
|
|
296
|
+
// fails loud instead of silently taking the node-convenience path.
|
|
280
297
|
paymentService = new Proxy(svc, {
|
|
281
298
|
get(target, prop, receiver) {
|
|
282
299
|
if (prop === 'handler') {
|
|
283
300
|
throw new Error(
|
|
284
|
-
'[worker hard-gate] svc.handler is forbidden in the CF worker —
|
|
301
|
+
'[worker hard-gate] svc.handler is forbidden in the CF worker — use svc.http.fetch instead'
|
|
285
302
|
);
|
|
286
303
|
}
|
|
287
304
|
return Reflect.get(target, prop, receiver);
|
|
@@ -340,16 +357,19 @@ function buildApp(env: Env): Hono<HonoEnv> {
|
|
|
340
357
|
return cachedApp;
|
|
341
358
|
}
|
|
342
359
|
|
|
343
|
-
// Phase 12a: assemble the embedded payment service
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
360
|
+
// Phase 12a + S3-CF Phase 1B: assemble the embedded payment service
|
|
361
|
+
// (config/db/slots incl. the injected CF DID-Connect runtime). The worker drives
|
|
362
|
+
// it through `service.http.fetch` (the single runtime-neutral surface); the
|
|
363
|
+
// node-only `handler` getter is hard-gated and never touched.
|
|
347
364
|
const service = ensurePaymentService(env);
|
|
348
365
|
|
|
349
366
|
const app = new Hono<HonoEnv>();
|
|
350
367
|
|
|
351
368
|
// CORS
|
|
352
|
-
|
|
369
|
+
// S3-CF Phase 1B: payment `/api/*` CORS is owned by the CORE full pipeline
|
|
370
|
+
// (cors/xss/csrf/i18n/cdn/context), reached via service.http.fetch. The worker no
|
|
371
|
+
// longer pre-applies cors() to /api/* — that would double the CORS headers on
|
|
372
|
+
// every payment route. Worker-owned cross-origin surfaces keep their own cors:
|
|
353
373
|
app.use('/.well-known/*', cors());
|
|
354
374
|
// /__blocklet__.js is fetched by external wallets (e.g. abtwallet.io, localhost
|
|
355
375
|
// dev wallets) to resolve app metadata + chain info before starting DID Connect.
|
|
@@ -413,7 +433,23 @@ function buildApp(env: Env): Hono<HonoEnv> {
|
|
|
413
433
|
if (caller) {
|
|
414
434
|
authSource = 'cache';
|
|
415
435
|
} else {
|
|
416
|
-
caller
|
|
436
|
+
// S3-CF Phase 1B: parameterize the caller RPC with the request's actual
|
|
437
|
+
// tenant via the SAME Host→tenant resolver the core uses — not a fixed
|
|
438
|
+
// env.APP_PID — so a multi-tenant embed verifies the caller against the
|
|
439
|
+
// right instance. Non-throwing: an unresolved host (multi) keeps the
|
|
440
|
+
// APP_PID fallback. Lazily required (NOT a top-level import) to avoid
|
|
441
|
+
// pulling the identity module into the worker's eager startup graph. This
|
|
442
|
+
// does NOT wrap the business handler's ALS context (the core owns that).
|
|
443
|
+
let callerInstanceDid: string | undefined = c.env.APP_PID;
|
|
444
|
+
try {
|
|
445
|
+
// eslint-disable-next-line global-require
|
|
446
|
+
const { resolveTenantForHost } = require('../api/src/libs/drivers/identity');
|
|
447
|
+
const resolved = await resolveTenantForHost(c.req.header('host'));
|
|
448
|
+
if (resolved) callerInstanceDid = resolved;
|
|
449
|
+
} catch {
|
|
450
|
+
/* unresolved host → keep the APP_PID fallback for the caller RPC param */
|
|
451
|
+
}
|
|
452
|
+
caller = await authService.resolveIdentity(jwt, authHeader, callerInstanceDid);
|
|
417
453
|
authSource = 'rpc';
|
|
418
454
|
if (caller && cacheKey) {
|
|
419
455
|
cacheIdentity(cacheKey, caller);
|
|
@@ -449,11 +485,13 @@ function buildApp(env: Env): Hono<HonoEnv> {
|
|
|
449
485
|
c.res.headers.append('Server-Timing', timings.join(', '));
|
|
450
486
|
});
|
|
451
487
|
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
|
|
488
|
+
// S3-CF Phase 1B: the ALS tenant context for payment `/api/*` business requests
|
|
489
|
+
// is owned by the CORE full pipeline (contextMiddleware), reached via
|
|
490
|
+
// service.http.fetch. The worker no longer wraps /api/* in tenantMiddleware() —
|
|
491
|
+
// that would double-resolve + double-wrap the tenant. The few worker-owned /api/*
|
|
492
|
+
// endpoints (dev/debug/proxies) run in single-mode default and never query under
|
|
493
|
+
// a per-request tenant. (DID payment actions get their context from the core
|
|
494
|
+
// buildConnectRoutesHono tenant middleware, also via http.fetch.)
|
|
457
495
|
|
|
458
496
|
// Health check
|
|
459
497
|
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
@@ -656,8 +694,9 @@ function buildApp(env: Env): Hono<HonoEnv> {
|
|
|
656
694
|
|
|
657
695
|
// === DID Auth Login routes ===
|
|
658
696
|
// Only proxy login-related /api/did/* paths to blocklet-service.
|
|
659
|
-
// Other /api/did/* paths (subscription, pay, collect, etc.) are Payment Kit's
|
|
660
|
-
//
|
|
697
|
+
// Other /api/did/* paths (subscription, pay, collect, etc.) are Payment Kit's own
|
|
698
|
+
// DID Connect payment actions — they fall through to the /api/* → service.http.fetch
|
|
699
|
+
// dispatcher (the core full app's DID payment actions).
|
|
661
700
|
const DID_AUTH_PROXY_PATHS = [
|
|
662
701
|
'/api/did/login/',
|
|
663
702
|
'/api/did/session',
|
|
@@ -668,7 +707,10 @@ function buildApp(env: Env): Hono<HonoEnv> {
|
|
|
668
707
|
app.all('/api/did/*', async (c, next) => {
|
|
669
708
|
const path = new URL(c.req.url).pathname;
|
|
670
709
|
const shouldProxy = DID_AUTH_PROXY_PATHS.some((p) => path.startsWith(p) || path === p);
|
|
671
|
-
|
|
710
|
+
// S3-CF Phase 1B: non-proxy payment DID actions fall through to the /api/*
|
|
711
|
+
// dispatcher below → service.http.fetch (the core full app includes the DID
|
|
712
|
+
// payment actions via buildConnectRoutesHono).
|
|
713
|
+
if (!shouldProxy) return next();
|
|
672
714
|
|
|
673
715
|
if (!c.env.AUTH_SERVICE) {
|
|
674
716
|
return c.json({ error: 'AUTH_SERVICE not configured' }, 503);
|
|
@@ -685,14 +727,10 @@ function buildApp(env: Env): Hono<HonoEnv> {
|
|
|
685
727
|
});
|
|
686
728
|
|
|
687
729
|
// === DID Connect business actions (subscription, pay, collect, etc.) ===
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
} catch (e: any) {
|
|
693
|
-
console.error('DID Connect init error:', e?.message || e);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
730
|
+
// S3-CF Phase 1B: the 14 payment DID actions are part of the CORE full app and
|
|
731
|
+
// are reached through the /api/* → service.http.fetch dispatcher below (they were
|
|
732
|
+
// converged onto buildConnectRoutesHono in Phase 1A, backed by the CF DID-Connect
|
|
733
|
+
// runtime injected into the service above). No separate worker mount — one surface.
|
|
696
734
|
|
|
697
735
|
// Notification unread count
|
|
698
736
|
app.get('/api/notifications/unread-count', (c) => c.json({ unReadCount: 0 }));
|
|
@@ -730,11 +768,9 @@ function buildApp(env: Env): Hono<HonoEnv> {
|
|
|
730
768
|
// for creating PaymentMethods is owner-gated, and staging has no owner yet
|
|
731
769
|
// (Pengfei's blocklet-service role is `member`). Gated by PAYMENT_LIVEMODE
|
|
732
770
|
// === 'false' so this only ever touches testmode data.
|
|
733
|
-
// Phase
|
|
734
|
-
//
|
|
735
|
-
// AFTER the worker's own /api/__dev__ routes
|
|
736
|
-
// "not implemented" catch-all). The old express-compat mountExpressRoutes shim
|
|
737
|
-
// is gone.
|
|
771
|
+
// S3-CF Phase 1B: payment business routes (and DID payment actions) are served by
|
|
772
|
+
// the core full app via the /api/* → service.http.fetch dispatcher, registered
|
|
773
|
+
// AFTER the worker's own /api/__dev__ routes so those match first.
|
|
738
774
|
|
|
739
775
|
// Dev endpoint: D1 admin operations
|
|
740
776
|
// Test CF Queue send directly
|
|
@@ -1079,17 +1115,21 @@ function buildApp(env: Env): Hono<HonoEnv> {
|
|
|
1079
1115
|
}
|
|
1080
1116
|
});
|
|
1081
1117
|
|
|
1082
|
-
// Phase
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1092
|
-
|
|
1118
|
+
// S3-CF Phase 1B: the single payment HTTP surface. Every payment `/api/*` request
|
|
1119
|
+
// (business resource routes + non-proxy DID payment actions) that wasn't handled
|
|
1120
|
+
// by a worker-owned route above is forwarded to the CORE full app via
|
|
1121
|
+
// service.http.fetch. The core full pipeline owns cors/xss/csrf/i18n/cdn/context
|
|
1122
|
+
// + the resource routes + the DID payment actions — there is no more LITE
|
|
1123
|
+
// resourceRoutes dispatcher and no second surface.
|
|
1124
|
+
//
|
|
1125
|
+
// The worker stays the HOST GLUE: it injects the caller identity it resolved
|
|
1126
|
+
// (caller RPC) as canonical x-user-* request headers — the core authenticate()
|
|
1127
|
+
// reads those — and STRIPS any client-supplied x-user-* first so it can never be
|
|
1128
|
+
// forged (component auth via x-component-sig is verified cryptographically
|
|
1129
|
+
// downstream and left intact). Raw body bytes are forwarded unconsumed (the
|
|
1130
|
+
// worker never reads the body here), preserving Stripe webhook signature fidelity
|
|
1131
|
+
// through to the core webhook route. flushQueueWork() drains the workerd deferred
|
|
1132
|
+
// queue work before responding.
|
|
1093
1133
|
const USER_HEADERS = ['x-user-did', 'x-user-role', 'x-user-provider', 'x-user-fullname', 'x-user-wallet-os'];
|
|
1094
1134
|
app.all('/api/*', async (c) => {
|
|
1095
1135
|
const headers = new Headers(c.req.raw.headers);
|
|
@@ -1103,7 +1143,9 @@ function buildApp(env: Env): Hono<HonoEnv> {
|
|
|
1103
1143
|
headers.set('x-user-fullname', encodeURIComponent(caller.displayName || ''));
|
|
1104
1144
|
headers.set('x-user-wallet-os', '');
|
|
1105
1145
|
}
|
|
1106
|
-
|
|
1146
|
+
// No basePath: the standalone worker serves payment routes at the root /api/*,
|
|
1147
|
+
// so the core full app matches them directly (raw bytes/headers carried verbatim).
|
|
1148
|
+
const res = await service.http.fetch(new Request(c.req.raw, { headers }));
|
|
1107
1149
|
await flushQueueWork(); // drain workerd deferred queue work before responding
|
|
1108
1150
|
return res;
|
|
1109
1151
|
});
|
package/cloudflare/wrangler.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.4",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"prelint": "npm run types",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@abtnode/cron": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
50
50
|
"@apple/app-store-server-library": "^3.1.0",
|
|
51
51
|
"@arcblock/did": "^1.30.24",
|
|
52
|
-
"@arcblock/did-connect-js": "4.0.
|
|
52
|
+
"@arcblock/did-connect-js": "^4.0.6",
|
|
53
53
|
"@arcblock/did-connect-react": "^3.5.4",
|
|
54
54
|
"@arcblock/did-connect-storage-nedb": "^1.8.0",
|
|
55
55
|
"@arcblock/did-util": "^1.30.24",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"@blocklet/error": "^0.3.5",
|
|
62
62
|
"@blocklet/js-sdk": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
63
63
|
"@blocklet/logger": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
64
|
-
"@blocklet/payment-broker-client": "1.29.
|
|
65
|
-
"@blocklet/payment-react": "1.29.
|
|
66
|
-
"@blocklet/payment-vendor": "1.29.
|
|
64
|
+
"@blocklet/payment-broker-client": "1.29.4",
|
|
65
|
+
"@blocklet/payment-react": "1.29.4",
|
|
66
|
+
"@blocklet/payment-vendor": "1.29.4",
|
|
67
67
|
"@blocklet/sdk": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
68
68
|
"@blocklet/ui-react": "^3.5.4",
|
|
69
69
|
"@blocklet/uploader": "^0.3.20",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"devDependencies": {
|
|
136
136
|
"@abtnode/types": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
137
137
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
138
|
-
"@blocklet/payment-types": "1.29.
|
|
138
|
+
"@blocklet/payment-types": "1.29.4",
|
|
139
139
|
"@types/connect": "^3.4.38",
|
|
140
140
|
"@types/debug": "^4.1.12",
|
|
141
141
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -183,5 +183,5 @@
|
|
|
183
183
|
"parser": "typescript"
|
|
184
184
|
}
|
|
185
185
|
},
|
|
186
|
-
"gitHead": "
|
|
186
|
+
"gitHead": "ffde8059827d7688a7e5a1e65830912e9093bb7c"
|
|
187
187
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// P2 (README D2 / F2) — runtime-neutral bootstrap helper.
|
|
2
|
+
//
|
|
3
|
+
// Parameterizes the formerly CF-only `cf-inject-blocklet` logic
|
|
4
|
+
// (cloudflare/vite.config.ts) so the node arc build (vite.arc.config.ts) and the
|
|
5
|
+
// CF build share ONE source for the build-time HTML bootstrap. The pure
|
|
6
|
+
// functions `buildBootstrap` / `mergeRemote` are jest-assertable
|
|
7
|
+
// (api/tests/bootstrap/bootstrap.spec.ts); `buildBootstrapScript` serializes
|
|
8
|
+
// `mergeRemote` (via .toString()) into the inline <script> so the browser runs
|
|
9
|
+
// the identical merge at runtime.
|
|
10
|
+
//
|
|
11
|
+
// D-3 (user-confirmed 2026-06-14) put this under blocklets/core/build/. That
|
|
12
|
+
// path is .gitignored (`blocklets/core/.gitignore:13` — `build` = production
|
|
13
|
+
// outputs), so the file could never be tracked there. It lives in scripts/
|
|
14
|
+
// instead — same intent (under blocklets/core, adjacent to both vite configs,
|
|
15
|
+
// zero cross-package dep, where build tooling already lives: jest.js,
|
|
16
|
+
// build-clean.js) but tracked. Extension is `.ts` (not `.mjs`) so ts-jest can
|
|
17
|
+
// unit-test the pure functions directly; both vite configs are `.ts` and import
|
|
18
|
+
// it natively.
|
|
19
|
+
|
|
20
|
+
// Source of truth: packages/react/src/libs/util.ts:33. Re-declared here to keep
|
|
21
|
+
// this build helper free of the heavy browser util graph (the only thing the
|
|
22
|
+
// build needs from it is the bare DID literal).
|
|
23
|
+
export const PAYMENT_KIT_DID = 'z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk';
|
|
24
|
+
|
|
25
|
+
export interface BootstrapOptions {
|
|
26
|
+
/** the host's UI mount prefix (node arc = '/.well-known/payment'; cf custom). */
|
|
27
|
+
uiPrefix: string;
|
|
28
|
+
/** PAYMENT_KIT_DID so getPrefix() (packages/react/src/libs/util.ts:87) takes
|
|
29
|
+
* the `componentId === PAYMENT_KIT_DID` branch and resolves to uiPrefix. */
|
|
30
|
+
componentId: string;
|
|
31
|
+
/** root-exact `/__blocklet__.js?type=json` — NEVER under uiPrefix (T2.3). */
|
|
32
|
+
remoteBlockletUrl: string;
|
|
33
|
+
/** session service host (node arc = arc '/.well-known/service'); CF root-deploy
|
|
34
|
+
* omits it so window.blocklet.serviceHost stays undefined (app.tsx falls back
|
|
35
|
+
* to prefix — P5 T5.1). */
|
|
36
|
+
serviceHost?: string;
|
|
37
|
+
/** baked appUrl; defaults to window.location.origin at runtime when omitted. */
|
|
38
|
+
appUrl?: string;
|
|
39
|
+
/** keys the remote __blocklet__.js must NOT clobber (G3 — `prefix` included). */
|
|
40
|
+
localOnly?: string[];
|
|
41
|
+
/** host-owned extras merged into the base bootstrap (navigation, mountpoints). */
|
|
42
|
+
extra?: Record<string, any>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// G3: `prefix` MUST be local-only so the app-root prefix from the remote
|
|
46
|
+
// __blocklet__.js never overwrites the payment reserved prefix.
|
|
47
|
+
const DEFAULT_LOCAL_ONLY = ['prefix', 'navigation', 'componentMountPoints'];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the synchronous window.blocklet skeleton. Pure — no IO, no globals.
|
|
51
|
+
* Throws (T2.3 regression lock) when remoteBlockletUrl is not root-exact, so a
|
|
52
|
+
* misconfigured build fails loudly at build time instead of silently fetching
|
|
53
|
+
* `/.well-known/payment/__blocklet__.js` (inside the reserved prefix).
|
|
54
|
+
*/
|
|
55
|
+
export function buildBootstrap(opts: BootstrapOptions): Record<string, any> {
|
|
56
|
+
const { uiPrefix, componentId, remoteBlockletUrl, serviceHost, appUrl, extra } = opts;
|
|
57
|
+
const localOnly = opts.localOnly || DEFAULT_LOCAL_ONLY;
|
|
58
|
+
|
|
59
|
+
if (!remoteBlockletUrl.startsWith('/__blocklet__')) {
|
|
60
|
+
throw new Error(`remoteBlockletUrl must be root-exact (start with /__blocklet__), got: ${remoteBlockletUrl}`);
|
|
61
|
+
}
|
|
62
|
+
if (uiPrefix !== '/' && remoteBlockletUrl.startsWith(uiPrefix)) {
|
|
63
|
+
throw new Error(`remoteBlockletUrl must not fall under uiPrefix ${uiPrefix}: ${remoteBlockletUrl}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
prefix: uiPrefix,
|
|
68
|
+
groupPrefix: uiPrefix,
|
|
69
|
+
componentId,
|
|
70
|
+
serviceHost,
|
|
71
|
+
appUrl: appUrl || '',
|
|
72
|
+
remoteBlockletUrl,
|
|
73
|
+
localOnly,
|
|
74
|
+
// @blocklet/ui-react nav/dashboard iterate these with .forEach — default to []
|
|
75
|
+
// so a host that doesn't override via `extra` never crashes on undefined. They
|
|
76
|
+
// are in localOnly, so the remote __blocklet__.js merge can't clobber them
|
|
77
|
+
// (AUTH_SERVICE doesn't know this blocklet's nav / mount points).
|
|
78
|
+
navigation: [],
|
|
79
|
+
componentMountPoints: [],
|
|
80
|
+
...(extra || {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Merge the remote per-app __blocklet__.js JSON onto the local bootstrap.
|
|
86
|
+
*
|
|
87
|
+
* SELF-CONTAINED (no module-scope refs) so `buildBootstrapScript` can embed it
|
|
88
|
+
* via `.toString()` and the browser runs the identical merge. Guards:
|
|
89
|
+
* - localOnly keys (prefix/navigation/componentMountPoints) are never clobbered
|
|
90
|
+
* - __proto__/constructor/prototype keys are dropped (prototype-pollution)
|
|
91
|
+
* - string values are angle-bracket escaped (<,> → entities) — blocks tag
|
|
92
|
+
* injection without corrupting URL query strings (& preserved)
|
|
93
|
+
* - payloads > 256KB are refused (resource exhaustion)
|
|
94
|
+
*/
|
|
95
|
+
export function mergeRemote(
|
|
96
|
+
wb: Record<string, any>,
|
|
97
|
+
remote: Record<string, any>,
|
|
98
|
+
localOnlyArg?: string[]
|
|
99
|
+
): Record<string, any> {
|
|
100
|
+
var localOnly = localOnlyArg || (wb && wb.localOnly) || ['prefix', 'navigation', 'componentMountPoints'];
|
|
101
|
+
|
|
102
|
+
if (remote && typeof remote === 'object') {
|
|
103
|
+
var size = 0;
|
|
104
|
+
try {
|
|
105
|
+
size = JSON.stringify(remote).length;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
size = Infinity;
|
|
108
|
+
}
|
|
109
|
+
if (size > 256 * 1024) {
|
|
110
|
+
throw new Error('remote bootstrap payload too large (>256KB), refusing merge');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var out: Record<string, any> = {};
|
|
115
|
+
Object.keys(wb || {}).forEach(function (k) {
|
|
116
|
+
out[k] = wb[k];
|
|
117
|
+
});
|
|
118
|
+
Object.keys(remote || {}).forEach(function (k) {
|
|
119
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') return;
|
|
120
|
+
// componentId is a structural invariant (getPrefix's PAYMENT_KIT_DID branch),
|
|
121
|
+
// protected independently of the configurable localOnly list.
|
|
122
|
+
if (k === 'componentId') return;
|
|
123
|
+
if (localOnly.indexOf(k) !== -1) return;
|
|
124
|
+
var v = remote[k];
|
|
125
|
+
if (typeof v === 'string') {
|
|
126
|
+
v = v.replace(/</g, '<').replace(/>/g, '>');
|
|
127
|
+
}
|
|
128
|
+
out[k] = v;
|
|
129
|
+
});
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Produce the inline <script> for transformIndexHtml. Bakes the validated
|
|
135
|
+
* buildBootstrap() result as the synchronous window.blocklet, then embeds
|
|
136
|
+
* mergeRemote (serialized) so the runtime XHR merge is the same code jest tests.
|
|
137
|
+
*/
|
|
138
|
+
export function buildBootstrapScript(opts: BootstrapOptions): string {
|
|
139
|
+
const wb = buildBootstrap(opts);
|
|
140
|
+
const initial = JSON.stringify(wb);
|
|
141
|
+
const remoteUrl = JSON.stringify(wb.remoteBlockletUrl);
|
|
142
|
+
return `<script>
|
|
143
|
+
window.global = globalThis;
|
|
144
|
+
if (!window.blocklet) {
|
|
145
|
+
window.blocklet = ${initial};
|
|
146
|
+
window.blocklet.appUrl = window.blocklet.appUrl || window.location.origin;
|
|
147
|
+
}
|
|
148
|
+
(function () {
|
|
149
|
+
try {
|
|
150
|
+
var xhr = new XMLHttpRequest();
|
|
151
|
+
var url = ${remoteUrl};
|
|
152
|
+
xhr.open('GET', url + (url.indexOf('?') === -1 ? '?' : '&') + '_t=' + Date.now(), false);
|
|
153
|
+
xhr.send();
|
|
154
|
+
if (xhr.status === 200) {
|
|
155
|
+
var remote = JSON.parse(xhr.responseText);
|
|
156
|
+
window.blocklet = (${mergeRemote.toString()})(window.blocklet, remote);
|
|
157
|
+
if (!window.blocklet.env) window.blocklet.env = {};
|
|
158
|
+
window.blocklet.env.appName = window.blocklet.appName || '';
|
|
159
|
+
window.blocklet.env.appDescription = window.blocklet.appDescription || '';
|
|
160
|
+
window.blocklet.env.appLogo = window.blocklet.appLogo || '';
|
|
161
|
+
window.blocklet.env.appUrl = window.blocklet.appUrl || '';
|
|
162
|
+
}
|
|
163
|
+
} catch (e) { /* ignore — fall back to sync bootstrap */ }
|
|
164
|
+
})();
|
|
165
|
+
</script>`;
|
|
166
|
+
}
|
package/src/app.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { Navigate, Route, BrowserRouter as Router, Routes, useNavigate } from 'r
|
|
|
13
13
|
import { joinURL } from 'ufo';
|
|
14
14
|
|
|
15
15
|
import ErrorFallback from './components/error-fallback';
|
|
16
|
+
import { resolveServiceHost } from './libs/service-host';
|
|
16
17
|
import UserLayoutDefault from './components/layout/user';
|
|
17
18
|
import UserLayoutCF from './components/layout/user-cf';
|
|
18
19
|
import { TransitionProvider } from './components/progress-bar';
|
|
@@ -250,7 +251,7 @@ export default function WrappedApp() {
|
|
|
250
251
|
<ToastProvider>
|
|
251
252
|
<PaymentThemeProvider>
|
|
252
253
|
<SessionProvider
|
|
253
|
-
serviceHost={prefix}
|
|
254
|
+
serviceHost={resolveServiceHost(prefix)}
|
|
254
255
|
useSocket={!(window as any).blocklet?.cloudflareWorker}
|
|
255
256
|
protectedRoutes={['/admin/*', '/customer/*', '/integrations/*'].map((item) => joinURL(prefix, item))}>
|
|
256
257
|
<Router basename={prefix}>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// P5 T5.1 (README D4 / F5) — resolve the SessionProvider serviceHost.
|
|
2
|
+
//
|
|
3
|
+
// arc injects window.blocklet.serviceHost (the arc /.well-known/service endpoint)
|
|
4
|
+
// at build time via the P2 bootstrap. The blocklet-server form does NOT inject it,
|
|
5
|
+
// so we fall back to the app prefix — TM-8 regression: zero behavior change for the
|
|
6
|
+
// blocklet-server deploy (serviceHost stays = prefix exactly as before).
|
|
7
|
+
//
|
|
8
|
+
// Zero imports on purpose so the rule is unit-testable in a node jest env without
|
|
9
|
+
// pulling the frontend React graph.
|
|
10
|
+
export function resolveServiceHost(prefix: string): string {
|
|
11
|
+
const injected = (typeof window !== 'undefined' && (window as any)?.blocklet?.serviceHost) || '';
|
|
12
|
+
return injected || prefix;
|
|
13
|
+
}
|