payment-kit 1.29.11 → 1.29.13
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/libs/logger.ts +10 -1
- package/api/src/routes/hono/checkout-sessions.ts +12 -1
- package/api/src/routes/hono/tax-rates.ts +4 -1
- package/api/src/service.ts +12 -1
- package/api/tests/routes/hono/tax-rates.spec.ts +48 -0
- package/blocklet.yml +1 -1
- package/cloudflare/cf-adapter.ts +3 -0
- package/package.json +7 -7
- package/src/components/payment-link/preview.tsx +6 -1
package/api/src/libs/logger.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// The blocklet server (BLOCKLET_LOG_DIR set) and the CF worker (aliased shim)
|
|
10
10
|
// still get the real logger on first call — behavior unchanged for them.
|
|
11
11
|
|
|
12
|
-
interface Logger {
|
|
12
|
+
export interface Logger {
|
|
13
13
|
debug: (...args: any[]) => void;
|
|
14
14
|
info: (...args: any[]) => void;
|
|
15
15
|
error: (...args: any[]) => void;
|
|
@@ -18,6 +18,15 @@ interface Logger {
|
|
|
18
18
|
|
|
19
19
|
let resolved: Logger | undefined;
|
|
20
20
|
|
|
21
|
+
// Host-injection seam: an embedding host (e.g. arc) can supply its own structured
|
|
22
|
+
// logger so payment's lines join the host's unified log stream instead of the lazy
|
|
23
|
+
// @blocklet/logger / console fallback. MUST be set before the first log call (the
|
|
24
|
+
// embedded factory does this from its `logger` slot as its first action) so it wins
|
|
25
|
+
// over `resolveLogger()`'s lazy default.
|
|
26
|
+
export function setResolvedLogger(l: Logger): void {
|
|
27
|
+
resolved = l;
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
function resolveLogger(): Logger {
|
|
22
31
|
if (resolved) return resolved;
|
|
23
32
|
try {
|
|
@@ -1243,7 +1243,18 @@ export async function startCheckoutSessionFromPaymentLink(id: string, c: any) {
|
|
|
1243
1243
|
const body = c.get('sanitizedBody') ?? {};
|
|
1244
1244
|
const { metadata, needShortUrl = false } = body;
|
|
1245
1245
|
try {
|
|
1246
|
-
|
|
1246
|
+
let link = await PaymentLink.findByPk(id);
|
|
1247
|
+
// Preview-draft ids are `plink_<did>` (create page). The frontend builds them from the
|
|
1248
|
+
// BARE login_token did (e.g. z1cxgj7…), but /stash persisted the link with the
|
|
1249
|
+
// auth-normalized `did:abt:` prefix (or vice versa) — a bare/prefixed DID shape
|
|
1250
|
+
// mismatch. Fall back to the other shape so the preview iframe resolves its own draft.
|
|
1251
|
+
if (!link && id.startsWith('plink_')) {
|
|
1252
|
+
const didPart = id.slice('plink_'.length);
|
|
1253
|
+
const altId = didPart.startsWith('did:abt:')
|
|
1254
|
+
? `plink_${didPart.slice('did:abt:'.length)}`
|
|
1255
|
+
: `plink_did:abt:${didPart}`;
|
|
1256
|
+
link = await PaymentLink.findByPk(altId);
|
|
1257
|
+
}
|
|
1247
1258
|
if (!link) {
|
|
1248
1259
|
return c.json({ error: 'Payment link not found, please contact the source of the payment link.' }, 400);
|
|
1249
1260
|
}
|
|
@@ -77,10 +77,13 @@ app.get('/', auth, async (c) => {
|
|
|
77
77
|
attributes: {
|
|
78
78
|
include: [
|
|
79
79
|
[
|
|
80
|
+
// Sequelize aliases the primary table to its modelName (`TaxRate`)
|
|
81
|
+
// in findAndCountAll, so the bare table name `tax_rates.id` is not a
|
|
82
|
+
// valid column in the emitted SQL (#1395). Reference the alias.
|
|
80
83
|
literal(`(
|
|
81
84
|
SELECT COUNT(DISTINCT ii.invoice_id)
|
|
82
85
|
FROM invoice_items AS ii
|
|
83
|
-
WHERE ii.tax_rate_id =
|
|
86
|
+
WHERE ii.tax_rate_id = \`TaxRate\`.id
|
|
84
87
|
)`),
|
|
85
88
|
'invoice_count',
|
|
86
89
|
],
|
package/api/src/service.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Hono } from 'hono';
|
|
|
5
5
|
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
6
6
|
import { isProduction } from './libs/env';
|
|
7
7
|
|
|
8
|
-
import logger from './libs/logger';
|
|
8
|
+
import logger, { type Logger, setResolvedLogger } from './libs/logger';
|
|
9
9
|
import { context as requestContext } from './libs/context';
|
|
10
10
|
import { TENANT_CONTEXT_MISSING, TenantError, getTenantMode, getDefaultInstanceDid } from './libs/tenant';
|
|
11
11
|
import type { LocksDriver, QueueHostHooks, CronDriver, IdentityDriver, SecretsDriver } from './libs/drivers';
|
|
@@ -79,6 +79,14 @@ export interface PaymentCoreSlots {
|
|
|
79
79
|
* keeping the runtime-neutral `http.fetch` free of node:fs.
|
|
80
80
|
*/
|
|
81
81
|
staticHandler?: (app: Hono) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Host-injected logger (e.g. arc's structured `ns:"payment"` logger). When set,
|
|
84
|
+
* payment's lines join the host's unified log stream instead of the lazy
|
|
85
|
+
* @blocklet/logger / console fallback. Applied via `setResolvedLogger` as the
|
|
86
|
+
* factory's first action, before any core module logs. Omit on blocklet-server
|
|
87
|
+
* (keeps the @blocklet/logger default).
|
|
88
|
+
*/
|
|
89
|
+
logger?: Logger;
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
export interface PaymentCoreLifecycle {
|
|
@@ -661,6 +669,9 @@ async function stopBackgroundServices(): Promise<void> {
|
|
|
661
669
|
*/
|
|
662
670
|
export function createEmbeddedPaymentService(slots: PaymentCoreSlots): EmbeddedPaymentService {
|
|
663
671
|
if (!slots || typeof slots !== 'object') throw new PaymentCoreSlotError('config');
|
|
672
|
+
// Host-injected logger wins over the lazy @blocklet/logger / console fallback.
|
|
673
|
+
// Set FIRST so even slot-validation/init logging routes to the host stream.
|
|
674
|
+
if (slots.logger) setResolvedLogger(slots.logger);
|
|
664
675
|
if (!slots.config) throw new PaymentCoreSlotError('config');
|
|
665
676
|
if (!slots.db || !slots.db.sequelize) throw new PaymentCoreSlotError('db');
|
|
666
677
|
// D1 (S3.0): validate the tenancy slot and derive the effective config BEFORE
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Regression test for #1395 — GET /api/tax-rates returned 400
|
|
2
|
+
// `SQLITE_ERROR: no such column: tax_rates.id`.
|
|
3
|
+
//
|
|
4
|
+
// The list handler's `invoice_count` subquery used `literal()` with the bare
|
|
5
|
+
// table name `tax_rates.id`, but Sequelize aliases the primary table to its
|
|
6
|
+
// modelName (`TaxRate`) in findAndCountAll, so that qualifier doesn't exist in
|
|
7
|
+
// the emitted SQL. The error is in the compiled subquery itself, so it throws
|
|
8
|
+
// even on an empty table — which is exactly what makes a tiny DB-backed test
|
|
9
|
+
// (real sqlite, like store/tenant-model.spec.ts) enough to pin it.
|
|
10
|
+
//
|
|
11
|
+
// auth is stubbed to pass-through (gating is covered by
|
|
12
|
+
// middlewares/hono/security.spec.ts).
|
|
13
|
+
jest.mock('../../../src/middlewares/hono/security', () => ({
|
|
14
|
+
authenticate: () => (_c: any, next: any) => next(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { Hono } from 'hono';
|
|
18
|
+
import { Sequelize } from 'sequelize';
|
|
19
|
+
|
|
20
|
+
import { withTenant } from '../../../src/libs/context';
|
|
21
|
+
import { initialize } from '../../../src/store/models';
|
|
22
|
+
import { TENANT_A } from '../../fixtures/tenants';
|
|
23
|
+
import taxRates from '../../../src/routes/hono/tax-rates';
|
|
24
|
+
|
|
25
|
+
const sequelize = new Sequelize('sqlite::memory:', { logging: false });
|
|
26
|
+
initialize(sequelize);
|
|
27
|
+
|
|
28
|
+
const app = new Hono();
|
|
29
|
+
app.route('/api/tax-rates', taxRates);
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
await sequelize.sync({ force: true });
|
|
33
|
+
});
|
|
34
|
+
afterAll(() => sequelize.close());
|
|
35
|
+
|
|
36
|
+
describe('GET /api/tax-rates — list (regression #1395)', () => {
|
|
37
|
+
it('the invoice_count subquery alias resolves — 200, not 400 no-such-column', async () => {
|
|
38
|
+
const r = await withTenant(TENANT_A, () =>
|
|
39
|
+
app.fetch(new Request('http://app.local/api/tax-rates', { headers: { host: 'app.local' } })),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// pre-fix: 400 { error: "SQLITE_ERROR: no such column: tax_rates.id" }
|
|
43
|
+
expect(r.status).toBe(200);
|
|
44
|
+
const body = (await r.json()) as { count: number; list: unknown[] };
|
|
45
|
+
expect(body.count).toBe(0);
|
|
46
|
+
expect(Array.isArray(body.list)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/blocklet.yml
CHANGED
package/cloudflare/cf-adapter.ts
CHANGED
|
@@ -347,6 +347,9 @@ export async function createCloudflarePaymentAdapter(
|
|
|
347
347
|
};
|
|
348
348
|
|
|
349
349
|
const svc: EmbeddedPaymentService = createEmbeddedPaymentService({
|
|
350
|
+
// Forward the host-injected logger so CF payment lines join the worker's
|
|
351
|
+
// unified log stream (omit → lazy @blocklet/logger / console fallback).
|
|
352
|
+
logger: options.logger,
|
|
350
353
|
// Host-global csrf secret via the config boundary (payment-core's csrf reads
|
|
351
354
|
// PAYMENT_CSRF_SECRET through readConfig). NO sessionSecret (decision #5).
|
|
352
355
|
config: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.13",
|
|
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.1.
|
|
51
|
+
"@arcblock/did-connect-js": "^4.1.5",
|
|
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.13",
|
|
64
|
+
"@blocklet/payment-react": "1.29.13",
|
|
65
|
+
"@blocklet/payment-vendor": "1.29.13",
|
|
66
66
|
"@blocklet/sdk": "^1.17.13-beta-20260613-094425-b81920c8",
|
|
67
67
|
"@blocklet/ui-react": "^3.5.4",
|
|
68
68
|
"@blocklet/uploader": "^0.15.4",
|
|
@@ -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.13",
|
|
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": "e023f83ecfb07c0782e138a2802b5dec746fd7c6"
|
|
184
184
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
2
|
import { useFullscreen, useSize } from 'ahooks';
|
|
3
|
+
import { joinURL } from 'ufo';
|
|
3
4
|
import IframeResizer from 'iframe-resizer-react';
|
|
4
5
|
import { useTheme } from '@mui/material';
|
|
5
6
|
import Chrome from './chrome';
|
|
@@ -30,7 +31,11 @@ export default function PaymentLinkPreview({
|
|
|
30
31
|
}
|
|
31
32
|
}, [theme.palette.mode, mode]);
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
// joinURL (not string concat) so the payment prefix joins the path with exactly one
|
|
35
|
+
// slash. window.blocklet.prefix has NO trailing slash on embedded hosts
|
|
36
|
+
// (/.well-known/payment), so `${prefix}checkout/...` produced `/.well-known/paymentcheckout/...`
|
|
37
|
+
// → 404. Every other payment-link URL already uses joinURL; this was the lone hard-concat.
|
|
38
|
+
const iframeSrc = `${joinURL(window.blocklet.prefix, 'checkout/pay', id)}?preview=1&version=${version}&theme=${mode}`;
|
|
34
39
|
return (
|
|
35
40
|
<div ref={innerRef}>
|
|
36
41
|
{fullscreen ? (
|