payment-kit 1.29.7 → 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.
- package/api/src/host-glue.ts +79 -0
- package/api/src/libs/env.ts +64 -22
- package/api/src/routes/hono/credit-grants.ts +1 -1
- package/api/tests/libs/notification-guard.spec.ts +25 -8
- package/blocklet.yml +1 -1
- package/cloudflare/cf-adapter.ts +11 -46
- package/cloudflare/tests/cf-adapter.spec.ts +2 -1
- package/package.json +7 -7
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-neutral payment host glue (lives in the canonical core source tree).
|
|
3
|
+
*
|
|
4
|
+
* Zero runtime dependency (only the Web `Headers` global and `Map`), so it is the
|
|
5
|
+
* SINGLE source of truth for the host API gate header glue and the lazy-provision
|
|
6
|
+
* dedup. Two hosts consume it:
|
|
7
|
+
* - the CF adapter (`../../cloudflare/cf-adapter.ts`, sibling import), and
|
|
8
|
+
* - the arc-node daemon, via the published `@arcblock/payment-service` main
|
|
9
|
+
* entry (the barrel re-exposes these through the same `as string` facade it
|
|
10
|
+
* uses for the other node-side helpers).
|
|
11
|
+
* They previously kept hand-mirrored copies that had already drifted (the node
|
|
12
|
+
* host injected a raw role, the CF adapter prefixed it with `blocklet-`); a single
|
|
13
|
+
* source removes that drift class.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The reserved mount prefix for the embedded payment service. */
|
|
17
|
+
export const PAYMENT_PREFIX = '/.well-known/payment';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The caller identity the host resolved (host injects it as `x-user-*`; the core
|
|
21
|
+
* never verifies a `login_token` in-process — the host owns caller resolution).
|
|
22
|
+
*/
|
|
23
|
+
export interface HostCallerIdentity {
|
|
24
|
+
did: string;
|
|
25
|
+
role?: string;
|
|
26
|
+
authMethod?: string;
|
|
27
|
+
displayName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Client-forged identity headers — always stripped before the host injects the real caller. */
|
|
31
|
+
export const USER_HEADERS = ['x-user-did', 'x-user-role', 'x-user-provider', 'x-user-fullname', 'x-user-wallet-os'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Host API gate caller header glue (decision #5). STRIP any client-supplied
|
|
35
|
+
* `x-user-*` first (a forged `x-user-did` would be an auth bypass), then inject the
|
|
36
|
+
* host-resolved caller as the canonical headers the core `authenticate()` trusts.
|
|
37
|
+
* No caller → the request runs anonymous (the strip already happened, so it is
|
|
38
|
+
* never forged).
|
|
39
|
+
*
|
|
40
|
+
* `x-user-role` carries the RAW role (e.g. `owner` / `admin`). The core normalizes
|
|
41
|
+
* it as `(role || '').replace('blocklet-', '') || 'guest'`, so the legacy
|
|
42
|
+
* `blocklet-` prefix is deliberately NOT added — it was vestigial (the core strips
|
|
43
|
+
* it either way) and a prefixed role is wrong.
|
|
44
|
+
*/
|
|
45
|
+
export function injectCaller(headers: Headers, caller: HostCallerIdentity | null): void {
|
|
46
|
+
for (const h of USER_HEADERS) headers.delete(h);
|
|
47
|
+
if (!caller) return;
|
|
48
|
+
const canonicalDid = caller.did?.startsWith('did:abt:') ? caller.did : `did:abt:${caller.did}`;
|
|
49
|
+
headers.set('x-user-did', canonicalDid);
|
|
50
|
+
headers.set('x-user-role', caller.role || 'guest');
|
|
51
|
+
headers.set('x-user-provider', caller.authMethod === 'access-key' ? 'access-key' : caller.authMethod || 'wallet');
|
|
52
|
+
headers.set('x-user-fullname', encodeURIComponent(caller.displayName || ''));
|
|
53
|
+
headers.set('x-user-wallet-os', '');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Lazy per-tenant provisioning with in-flight dedup. Concurrent first-requests for
|
|
58
|
+
* the same tenant await the SAME provision() call — a Set would let a burst at
|
|
59
|
+
* isolate/daemon start double-seed before the DB idempotency guard commits. A
|
|
60
|
+
* resolved promise stays cached so later requests are instant; a FAILED provision
|
|
61
|
+
* is dropped so the next request retries. Null tenant is a no-op.
|
|
62
|
+
*/
|
|
63
|
+
export function createTenantProvisioner(
|
|
64
|
+
provision: (instanceDid: string) => Promise<void>
|
|
65
|
+
): (instanceDid: string | null) => Promise<void> {
|
|
66
|
+
const inflight = new Map<string, Promise<void>>();
|
|
67
|
+
return (instanceDid) => {
|
|
68
|
+
if (!instanceDid) return Promise.resolve();
|
|
69
|
+
let p = inflight.get(instanceDid);
|
|
70
|
+
if (!p) {
|
|
71
|
+
p = provision(instanceDid).catch((err) => {
|
|
72
|
+
inflight.delete(instanceDid); // drop so the next request retries
|
|
73
|
+
throw err;
|
|
74
|
+
});
|
|
75
|
+
inflight.set(instanceDid, p);
|
|
76
|
+
}
|
|
77
|
+
return p;
|
|
78
|
+
};
|
|
79
|
+
}
|
package/api/src/libs/env.ts
CHANGED
|
@@ -57,6 +57,31 @@ const numConfig = (key: string, fallback: number): number => {
|
|
|
57
57
|
return v ? +v : fallback;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// Unified boolean env parsing. Accepts 1/true/yes/on (truthy) and 0/false/no/off
|
|
61
|
+
// (falsy), case-insensitive; unset / empty / unrecognized → `fallback`. Replaces
|
|
62
|
+
// the ad-hoc `=== '1'` / `=== 'true'` / `!== 'false'` conventions that used to be
|
|
63
|
+
// scattered across these accessors, so operators no longer have to guess whether a
|
|
64
|
+
// flag wants `1` or `true`.
|
|
65
|
+
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
66
|
+
const FALSY = new Set(['0', 'false', 'no', 'off']);
|
|
67
|
+
const parseBool = (value: string | undefined, fallback = false): boolean => {
|
|
68
|
+
if (value === undefined || value === '') return fallback;
|
|
69
|
+
const v = value.trim().toLowerCase();
|
|
70
|
+
if (TRUTHY.has(v)) return true;
|
|
71
|
+
if (FALSY.has(v)) return false;
|
|
72
|
+
return fallback;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// First non-empty value among keys — used to honor a renamed key while still
|
|
76
|
+
// reading the deprecated alias (new name wins, old name kept as fallback).
|
|
77
|
+
const readConfigAny = (...keys: string[]): string | undefined => {
|
|
78
|
+
for (const key of keys) {
|
|
79
|
+
const v = readConfig(key);
|
|
80
|
+
if (v !== undefined && v !== '') return v;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
60
85
|
export const paymentStatCronTime = (): string => '0 1 0 * * *'; // 默认每天一次,计算前一天的
|
|
61
86
|
export const subscriptionCronTime = (): string => readConfig('SUBSCRIPTION_CRON_TIME') || '0 */30 * * * *';
|
|
62
87
|
export const notificationCronTime = (): string => readConfig('NOTIFICATION_CRON_TIME') || '0 5 */6 * * *';
|
|
@@ -67,7 +92,6 @@ export const stripeInvoiceCronTime = (): string => readConfig('STRIPE_INVOICE_CR
|
|
|
67
92
|
export const stripePaymentCronTime = (): string => readConfig('STRIPE_PAYMENT_CRON_TIME') || '0 */20 * * * *';
|
|
68
93
|
export const stripeSubscriptionCronTime = (): string => readConfig('STRIPE_SUBSCRIPTION_CRON_TIME') || '0 10 */8 * * *';
|
|
69
94
|
export const revokeStakeCronTime = (): string => readConfig('REVOKE_STAKE_CRON_TIME') || '0 */5 * * * *';
|
|
70
|
-
export const daysUntilCancel = (): string | undefined => readConfig('DAYS_UNTIL_CANCEL');
|
|
71
95
|
export const meteringSubscriptionDetectionCronTime = (): string =>
|
|
72
96
|
readConfig('METERING_SUBSCRIPTION_DETECTION_CRON_TIME') || '0 0 10 * * *';
|
|
73
97
|
export const overdueDetectionCronTime = (): string => readConfig('OVERDUE_DETECTION_CRON_TIME') || '0 0 10 * * *';
|
|
@@ -93,20 +117,26 @@ export const sequelizeOptionsPoolIdle = (): number => numConfig('SEQUELIZE_OPTIO
|
|
|
93
117
|
|
|
94
118
|
export const updateDataConcurrency = (): number => numConfig('UPDATE_DATA_CONCURRENCY', 5);
|
|
95
119
|
|
|
96
|
-
// When
|
|
97
|
-
//
|
|
120
|
+
// When truthy, the system stops accepting new orders. Existing checkout sessions
|
|
121
|
+
// can still be viewed but new submissions will be rejected.
|
|
122
|
+
// Renamed PAYMENT_KIT_STOP_ACCEPTING_ORDERS → PAYMENT_STOP_ACCEPTING_ORDERS; the
|
|
123
|
+
// old name is still honored as a deprecated alias.
|
|
98
124
|
export const stopAcceptingOrders = (): boolean =>
|
|
99
|
-
|
|
125
|
+
parseBool(readConfigAny('PAYMENT_STOP_ACCEPTING_ORDERS', 'PAYMENT_KIT_STOP_ACCEPTING_ORDERS'));
|
|
100
126
|
|
|
101
127
|
export const exchangeRateCacheTTLSeconds = (): number => numConfig('EXCHANGE_RATE_CACHE_TTL_SECONDS', 10 * 60);
|
|
102
128
|
|
|
103
|
-
// System-level maximum pending amount limit (in token format, e.g., "10")
|
|
104
|
-
//
|
|
105
|
-
|
|
129
|
+
// System-level maximum pending amount limit (in token format, e.g., "10"). Default 5.
|
|
130
|
+
// Renamed PAYMENT_KIT_MAX_PENDING_AMOUNT → PAYMENT_MAX_PENDING_AMOUNT; the old name
|
|
131
|
+
// is still honored as a deprecated alias.
|
|
132
|
+
export const systemMaxPendingAmount = (): number => {
|
|
133
|
+
const v = readConfigAny('PAYMENT_MAX_PENDING_AMOUNT', 'PAYMENT_KIT_MAX_PENDING_AMOUNT');
|
|
134
|
+
return v ? +v : 5;
|
|
135
|
+
};
|
|
106
136
|
|
|
107
137
|
// Whether a locked price may still be edited. Lazy (reads injected config via
|
|
108
138
|
// the boundary at call time) like every other Phase 8 accessor.
|
|
109
|
-
export const allowChangeLockedPrice = (): boolean => readConfig('PAYMENT_CHANGE_LOCKED_PRICE')
|
|
139
|
+
export const allowChangeLockedPrice = (): boolean => parseBool(readConfig('PAYMENT_CHANGE_LOCKED_PRICE'));
|
|
110
140
|
|
|
111
141
|
// ──────────────────────────────────────────────────────────────────────────
|
|
112
142
|
// Phase 8 (W2′): converged accessors. Every read below used to live inline in
|
|
@@ -122,7 +152,7 @@ export const isProduction = (): boolean => blockletMode() === 'production';
|
|
|
122
152
|
export const nodeEnv = (): string | undefined => readConfig('NODE_ENV');
|
|
123
153
|
export const isTestEnv = (): boolean => nodeEnv() === 'test';
|
|
124
154
|
export const isDevelopmentEnv = (): boolean => nodeEnv() === 'development';
|
|
125
|
-
export const enableDevFakeAuth = (): boolean => readConfig('ENABLE_DEV_FAKE_AUTH')
|
|
155
|
+
export const enableDevFakeAuth = (): boolean => parseBool(readConfig('ENABLE_DEV_FAKE_AUTH'));
|
|
126
156
|
|
|
127
157
|
// -- tenant mode-source (getTenantMode / getDefaultInstanceDid read these) --
|
|
128
158
|
export const tenantModeRaw = (): string | undefined => readConfig('PAYMENT_TENANT_MODE');
|
|
@@ -138,14 +168,14 @@ export const blockletPort = (): string | undefined => readConfig('BLOCKLET_PORT'
|
|
|
138
168
|
export const blockletMountPoints = (): string | undefined => readConfig('BLOCKLET_MOUNT_POINTS');
|
|
139
169
|
|
|
140
170
|
// -- integrations --
|
|
141
|
-
export const appStoreWriteEnabled = (): boolean => readConfig('APP_STORE_WRITE_ENABLED')
|
|
142
|
-
export const appStoreSkipSignatureVerify = (): boolean => readConfig('APP_STORE_SKIP_SIGNATURE_VERIFY')
|
|
171
|
+
export const appStoreWriteEnabled = (): boolean => parseBool(readConfig('APP_STORE_WRITE_ENABLED'));
|
|
172
|
+
export const appStoreSkipSignatureVerify = (): boolean => parseBool(readConfig('APP_STORE_SKIP_SIGNATURE_VERIFY'));
|
|
143
173
|
export const googlePubsubSkipSignatureVerify = (): boolean =>
|
|
144
|
-
readConfig('GOOGLE_PUBSUB_SKIP_SIGNATURE_VERIFY')
|
|
174
|
+
parseBool(readConfig('GOOGLE_PUBSUB_SKIP_SIGNATURE_VERIFY'));
|
|
145
175
|
export const googlePubsubPushServiceAccount = (): string | undefined =>
|
|
146
176
|
readConfig('GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT');
|
|
147
177
|
export const googlePubsubAllowUnverifiedSender = (): boolean =>
|
|
148
|
-
readConfig('GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER')
|
|
178
|
+
parseBool(readConfig('GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER'));
|
|
149
179
|
export const googlePlayWebhookUrl = (): string | undefined => readConfig('GOOGLE_PLAY_WEBHOOK_URL');
|
|
150
180
|
export const stripeWebhookSecret = (): string | undefined => readConfig('STRIPE_WEBHOOK_SECRET');
|
|
151
181
|
export const iapReconcileBatchSize = (): number => Number(readConfig('IAP_RECONCILE_BATCH_SIZE') ?? '100');
|
|
@@ -155,9 +185,12 @@ export const paymentBillingThreshold = (): number => +(readConfig('PAYMENT_BILLI
|
|
|
155
185
|
export const paymentMinStakeAmount = (): number => +(readConfig('PAYMENT_MIN_STAKE_AMOUNT') as string);
|
|
156
186
|
export const paymentDaysUntilDue = (): string | undefined => readConfig('PAYMENT_DAYS_UNTIL_DUE');
|
|
157
187
|
export const paymentDaysUntilCancel = (): string | undefined => readConfig('PAYMENT_DAYS_UNTIL_CANCEL');
|
|
158
|
-
export const paymentReloadSubscriptionJobs = (): boolean => readConfig('PAYMENT_RELOAD_SUBSCRIPTION_JOBS')
|
|
188
|
+
export const paymentReloadSubscriptionJobs = (): boolean => parseBool(readConfig('PAYMENT_RELOAD_SUBSCRIPTION_JOBS'));
|
|
159
189
|
export const paymentRateVolatilityThreshold = (): string | undefined => readConfig('PAYMENT_RATE_VOLATILITY_THRESHOLD');
|
|
160
|
-
|
|
190
|
+
// Defaults to livemode (true) when unset. NOTE: with the unified parser, an
|
|
191
|
+
// explicit falsy value (false/0/no/off) now means testmode — previously ONLY the
|
|
192
|
+
// literal string 'false' did, so PAYMENT_LIVEMODE=0 used to (mis)read as livemode.
|
|
193
|
+
export const paymentLivemode = (): boolean => parseBool(readConfig('PAYMENT_LIVEMODE'), true);
|
|
161
194
|
|
|
162
195
|
// -- credit queue --
|
|
163
196
|
export const creditLowBalanceThresholdPercentage = (): number =>
|
|
@@ -172,8 +205,8 @@ export const creditQueueConcurrency = (): number =>
|
|
|
172
205
|
export const exchangeRateCacheTTLFromEnv = (): boolean => hasConfig('EXCHANGE_RATE_CACHE_TTL_SECONDS');
|
|
173
206
|
|
|
174
207
|
// -- store / sequelize logging --
|
|
175
|
-
export const sqlLog = (): boolean => readConfig('SQL_LOG')
|
|
176
|
-
export const sqlBenchmark = (): boolean => readConfig('SQL_BENCHMARK')
|
|
208
|
+
export const sqlLog = (): boolean => parseBool(readConfig('SQL_LOG'));
|
|
209
|
+
export const sqlBenchmark = (): boolean => parseBool(readConfig('SQL_BENCHMARK'));
|
|
177
210
|
|
|
178
211
|
// -- CF worker runtime detection + env mirror (set by cloudflare/worker.ts on
|
|
179
212
|
// globalThis; the boundary so core never reads the global directly) --
|
|
@@ -184,17 +217,26 @@ export const isCfWorker = (): boolean => !!cfEnv();
|
|
|
184
217
|
* THE single, reusable gate for Blocklet-Server-ONLY features.
|
|
185
218
|
*
|
|
186
219
|
* True only under Blocklet Server, where the full @blocklet/sdk host runtime is
|
|
187
|
-
* present —
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
220
|
+
* present — the env the SDK's `checkBlockletEnvironment` requires (BLOCKLET_APP_ID /
|
|
221
|
+
* BLOCKLET_DID / BLOCKLET_APP_EK / ABT_NODE_*).
|
|
222
|
+
*
|
|
223
|
+
* Keyed on BLOCKLET_DID, NOT BLOCKLET_APP_ID: the CF worker injects
|
|
224
|
+
* BLOCKLET_APP_ID = APP_PID into the config slot (cloudflare/worker.ts
|
|
225
|
+
* envToPaymentCoreConfig — blockletAppId() legitimately needs it for app identity
|
|
226
|
+
* in auth.ts / util.ts), which made the old BLOCKLET_APP_ID check spuriously true
|
|
227
|
+
* on CF and defeated the transport guard below. BLOCKLET_DID is supplied only by
|
|
228
|
+
* the real Blocklet Server runtime — the CF worker uses APP_PID for the node DID
|
|
229
|
+
* and never sets BLOCKLET_DID in config, and arc-node never injects it — so it
|
|
230
|
+
* cleanly distinguishes the three hosts. (BLOCKLET_APP_EK would also be BS-only in
|
|
231
|
+
* production, but the test harness sets it for the crypto shim, so DID is the
|
|
232
|
+
* unambiguous discriminator.)
|
|
191
233
|
*
|
|
192
234
|
* Any integration that only works on Blocklet Server (the @blocklet/sdk
|
|
193
235
|
* notification / eventbus / relay transports, and any future host-service feature)
|
|
194
236
|
* MUST gate on this and skip itself off Blocklet Server — never branch on
|
|
195
237
|
* isCfWorker()/hasDynamicIdentity() ad hoc, so "what is BS-only" stays in one place.
|
|
196
238
|
*/
|
|
197
|
-
export const isBlockletServer = (): boolean => hasConfig('
|
|
239
|
+
export const isBlockletServer = (): boolean => hasConfig('BLOCKLET_DID');
|
|
198
240
|
|
|
199
241
|
export default {
|
|
200
242
|
...env,
|
|
@@ -394,7 +394,7 @@ app.get('/verify-availability', authMine, async (c) => {
|
|
|
394
394
|
const pendingAmountBN = new BN(pendingAmount);
|
|
395
395
|
|
|
396
396
|
// 5. Check system-level maximum pending amount limit (highest priority, cannot be bypassed)
|
|
397
|
-
// systemMaxPendingAmount() is configured via
|
|
397
|
+
// systemMaxPendingAmount() is configured via PAYMENT_MAX_PENDING_AMOUNT (in token format)
|
|
398
398
|
if (systemMaxPendingAmount() > 0 && pendingAmountBN.gt(new BN(0))) {
|
|
399
399
|
const systemMaxPendingAmountBN = fromTokenToUnit(
|
|
400
400
|
trimDecimals(String(systemMaxPendingAmount()), currency.decimal),
|
|
@@ -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() (=
|
|
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.
|
|
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.
|
|
14
|
-
else process.env.
|
|
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
|
|
37
|
-
delete process.env.
|
|
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 (
|
|
54
|
-
process.env.
|
|
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
package/cloudflare/cf-adapter.ts
CHANGED
|
@@ -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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.8",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"prelint": "npm run types",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@abtnode/cron": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
50
50
|
"@arcblock/did": "^1.30.24",
|
|
51
|
-
"@arcblock/did-connect-js": "^4.0
|
|
51
|
+
"@arcblock/did-connect-js": "^4.1.0",
|
|
52
52
|
"@arcblock/did-connect-react": "^3.5.4",
|
|
53
53
|
"@arcblock/did-connect-storage-nedb": "^1.8.0",
|
|
54
54
|
"@arcblock/did-util": "^1.30.24",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"@blocklet/error": "^0.3.5",
|
|
61
61
|
"@blocklet/js-sdk": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
62
62
|
"@blocklet/logger": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
63
|
-
"@blocklet/payment-broker-client": "1.29.
|
|
64
|
-
"@blocklet/payment-react": "1.29.
|
|
65
|
-
"@blocklet/payment-vendor": "1.29.
|
|
63
|
+
"@blocklet/payment-broker-client": "1.29.8",
|
|
64
|
+
"@blocklet/payment-react": "1.29.8",
|
|
65
|
+
"@blocklet/payment-vendor": "1.29.8",
|
|
66
66
|
"@blocklet/sdk": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
67
67
|
"@blocklet/ui-react": "^3.5.4",
|
|
68
68
|
"@blocklet/uploader": "^0.3.20",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"devDependencies": {
|
|
134
134
|
"@abtnode/types": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
135
135
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
136
|
-
"@blocklet/payment-types": "1.29.
|
|
136
|
+
"@blocklet/payment-types": "1.29.8",
|
|
137
137
|
"@types/connect": "^3.4.38",
|
|
138
138
|
"@types/debug": "^4.1.12",
|
|
139
139
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -180,5 +180,5 @@
|
|
|
180
180
|
"parser": "typescript"
|
|
181
181
|
}
|
|
182
182
|
},
|
|
183
|
-
"gitHead": "
|
|
183
|
+
"gitHead": "547c0614f1f83abad80ea08452d2f5a9f4f5aa07"
|
|
184
184
|
}
|