@harrylabsj/kiwi-merchant-cloud 0.12.0 → 0.12.1
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/app/cloud/bootstrap.d.ts +2 -0
- package/app/cloud/bootstrap.js +92 -10
- package/app/cloud/bootstrap.js.map +1 -1
- package/app/cloud/connect-service.d.ts +144 -0
- package/app/cloud/connect-service.js +776 -0
- package/app/cloud/connect-service.js.map +1 -0
- package/app/cloud/file-listing-publication.d.ts +105 -0
- package/app/cloud/file-listing-publication.js +436 -0
- package/app/cloud/file-listing-publication.js.map +1 -0
- package/app/cloud/merchant-connect.d.ts +19 -13
- package/app/cloud/merchant-connect.js +43 -284
- package/app/cloud/merchant-connect.js.map +1 -1
- package/app/cloud-sample/products.json +3 -3
- package/app/http/merchant-management/api.d.ts +21 -1
- package/app/http/merchant-management/api.js +136 -1
- package/app/http/merchant-management/api.js.map +1 -1
- package/app/http/merchant-management/page.d.ts +4 -1
- package/app/http/merchant-management/page.js +113 -7
- package/app/http/merchant-management/page.js.map +1 -1
- package/build-manifest.json +98 -68
- package/node_modules/@hono/node-server/dist/early-hints.cjs +1 -1
- package/node_modules/@hono/node-server/dist/early-hints.mjs +1 -1
- package/node_modules/@hono/node-server/dist/index.cjs +41 -19
- package/node_modules/@hono/node-server/dist/index.mjs +42 -20
- package/node_modules/@hono/node-server/dist/serve-static.cjs +1 -0
- package/node_modules/@hono/node-server/dist/serve-static.d.cts +2 -1
- package/node_modules/@hono/node-server/dist/serve-static.d.mts +2 -1
- package/node_modules/@hono/node-server/dist/serve-static.mjs +1 -0
- package/node_modules/@hono/node-server/package.json +3 -2
- package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/language/language.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/serve-static/index.js +1 -0
- package/node_modules/hono/dist/middleware/language/language.js +1 -1
- package/node_modules/hono/dist/middleware/serve-static/index.js +1 -0
- package/node_modules/hono/package.json +1 -1
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +2 -2
- package/runtime-package.json +2 -2
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
41
41
|
import { contentHash } from "../../agent/merchant/action-candidate.js";
|
|
42
42
|
import { ADMIN_SESSION_COOKIE } from "../../auth/merchant-sessions.js";
|
|
43
|
+
import { FileListingPublicationError, } from "../../cloud/file-listing-publication.js";
|
|
43
44
|
import { parseProductTable, ProductTableError, productTableDigest, } from "../../cloud/product-source.js";
|
|
44
45
|
import { convertTabularImport, renderImportTemplateCsv, renderImportTemplateXlsx, TABULAR_IMPORT_MAX_BYTES, } from "../../cloud/product-import-tabular.js";
|
|
45
46
|
import { MerchantImportDraftStore } from "./draft-store.js";
|
|
@@ -67,6 +68,11 @@ import { WorkbenchEventProjectionError, } from "./event-projection.js";
|
|
|
67
68
|
import { GRANT_ACTIONS, MerchantGrantError, } from "../../merchant/grant-store.js";
|
|
68
69
|
const API_PREFIX = "/merchant/api";
|
|
69
70
|
const WORKBENCH_API_PREFIX = "/merchant/api/v1";
|
|
71
|
+
const CATALOG_CONNECTION_SAFE_CODES = new Set([
|
|
72
|
+
"PUBLICATION_NOT_ACTIVE", "PUBLICATION_NOT_VERIFIED", "CATALOG_UNREACHABLE",
|
|
73
|
+
"CATALOG_MISMATCH", "BINDING_MERCHANT_MISMATCH", "BINDING_CLAIM_EXPIRED",
|
|
74
|
+
"BINDING_EXPIRED", "AUTHORIZATION_PENDING", "PREVIEW_CHANGED", "RUNTIME_CARD_CHANGED",
|
|
75
|
+
]);
|
|
70
76
|
const MAX_BODY_BYTES = 1_048_576;
|
|
71
77
|
/** 非候选写命令确认引用的有效期(BD §7.3:短时、单次)。 */
|
|
72
78
|
const CONFIRMATION_TTL_MS = 5 * 60 * 1000;
|
|
@@ -189,6 +195,50 @@ export function createMerchantManagementApiHandler(options) {
|
|
|
189
195
|
// ── 只读路由(认证即鉴权入口;业务授权在服务层逐次复核)────────────────
|
|
190
196
|
/** Workbench v1 读取面复用同一应用服务;未实现路由明确失败,不返回假数据。 */
|
|
191
197
|
async function routeWorkbenchV1Get(req, res, url, rest, requestId) {
|
|
198
|
+
const listingDraftMatch = /^\/products\/publication-drafts\/(flp_[a-z0-9]+_[0-9a-f]{12})$/.exec(rest);
|
|
199
|
+
if (listingDraftMatch !== null) {
|
|
200
|
+
const auth = requireActor(req);
|
|
201
|
+
authorizeOrThrow(auth.ctx, "products:import");
|
|
202
|
+
if (auth.ctx.role !== "owner")
|
|
203
|
+
throw new ManagementError("forbidden", "only owner can publish products");
|
|
204
|
+
const channel = options.fileListingPublication;
|
|
205
|
+
if (channel === undefined)
|
|
206
|
+
throw new ManagementError("unavailable", "product publication is unavailable");
|
|
207
|
+
const result = await channel.getDraft(listingDraftMatch[1]);
|
|
208
|
+
if (!result.found)
|
|
209
|
+
throw new ManagementError("not_found", "publication draft does not exist");
|
|
210
|
+
writeJson(res, 200, result, { "x-request-id": requestId });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (rest === "/catalog/connect" || rest === "/catalog/connect/pairing") {
|
|
214
|
+
const auth = requireActor(req);
|
|
215
|
+
authorizeOrThrow(auth.ctx, "onboarding:manage");
|
|
216
|
+
if (auth.ctx.role !== "owner")
|
|
217
|
+
throw new ManagementError("forbidden", "only owner can manage Catalog connection");
|
|
218
|
+
const channel = options.catalogConnection;
|
|
219
|
+
if (channel === undefined)
|
|
220
|
+
throw new ManagementError("unavailable", "Catalog connection is not configured");
|
|
221
|
+
if (rest.endsWith("/pairing")) {
|
|
222
|
+
const pairing = await channel.getPairing();
|
|
223
|
+
writeJson(res, 200, { pairing: pairing === null ? null : {
|
|
224
|
+
user_code: pairing.user_code,
|
|
225
|
+
verification_uri: pairing.verification_uri,
|
|
226
|
+
expires_at: pairing.expires_at,
|
|
227
|
+
} }, { "x-request-id": requestId });
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const summary = await channel.getSummary();
|
|
231
|
+
// 默认状态响应白名单:配对码与设备凭据不能混入助手可读取的摘要。
|
|
232
|
+
writeJson(res, 200, {
|
|
233
|
+
status: summary.status, published: summary.published,
|
|
234
|
+
agentId: summary.agentId ?? null, bindingId: summary.bindingId ?? null,
|
|
235
|
+
bindingExpiresAt: summary.bindingExpiresAt ?? null,
|
|
236
|
+
errorCode: summary.errorCode == null ? null : CATALOG_CONNECTION_SAFE_CODES.has(summary.errorCode)
|
|
237
|
+
? summary.errorCode : "CATALOG_CONNECTION_UNAVAILABLE",
|
|
238
|
+
}, { "x-request-id": requestId });
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
192
242
|
if (rest === "/overview") {
|
|
193
243
|
const auth = requireActor(req);
|
|
194
244
|
const runtime = await readService.getStatus(auth.ctx);
|
|
@@ -524,6 +574,87 @@ export function createMerchantManagementApiHandler(options) {
|
|
|
524
574
|
writeWorkbenchProblem(res, "RESOURCE_NOT_FOUND", requestId, "资源不存在", "Workbench API 路由不存在或尚未实现。");
|
|
525
575
|
}
|
|
526
576
|
async function routeWorkbenchV1Post(req, res, rest, requestId, url) {
|
|
577
|
+
const publicationCommitMatch = /^\/products\/publication-drafts\/(flp_[a-z0-9]+_[0-9a-f]{12})\/commit$/.exec(rest);
|
|
578
|
+
if (rest === "/products/publication-drafts" || publicationCommitMatch !== null) {
|
|
579
|
+
const auth = requireActor(req);
|
|
580
|
+
assertWriteGuards(req, auth.sessionId);
|
|
581
|
+
authorizeOrThrow(auth.ctx, "products:import");
|
|
582
|
+
if (auth.ctx.role !== "owner")
|
|
583
|
+
throw new ManagementError("forbidden", "only owner can publish products");
|
|
584
|
+
const channel = options.fileListingPublication;
|
|
585
|
+
if (channel === undefined)
|
|
586
|
+
throw new ManagementError("unavailable", "product publication is unavailable");
|
|
587
|
+
try {
|
|
588
|
+
if (publicationCommitMatch !== null) {
|
|
589
|
+
const fields = objectFields(await readJsonBody(req), ["expected_digest", "confirm_publication"]);
|
|
590
|
+
if (fields["confirm_publication"] !== true)
|
|
591
|
+
throw new ManagementError("invalid_input", "explicit publication confirmation is required");
|
|
592
|
+
const result = await channel.commit(publicationCommitMatch[1], requireString(fields["expected_digest"], "expected_digest"));
|
|
593
|
+
writeJson(res, result.status === "pending" ? 202 : 200, result, { "x-request-id": requestId });
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
const fields = objectFields(await readJsonBody(req), ["selections"]);
|
|
597
|
+
const selections = fields["selections"];
|
|
598
|
+
if (!Array.isArray(selections) || selections.length === 0 || selections.length > 100) {
|
|
599
|
+
throw new ManagementError("invalid_input", "select between 1 and 100 products");
|
|
600
|
+
}
|
|
601
|
+
const selected = selections.map((item) => {
|
|
602
|
+
const entry = objectFields(item, ["sku", "category"]);
|
|
603
|
+
return { sku: requireString(entry["sku"], "sku"), category: requireString(entry["category"], "category") };
|
|
604
|
+
});
|
|
605
|
+
writeJson(res, 200, await channel.preview(selected), { "x-request-id": requestId });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch (err) {
|
|
609
|
+
if (err instanceof FileListingPublicationError) {
|
|
610
|
+
// Never reflect remote errors or credential-bearing URLs to the page.
|
|
611
|
+
throw new ManagementError("precondition_changed", "商品发布条件已变化或目录暂不可用,请检查连接状态并重新预览。");
|
|
612
|
+
}
|
|
613
|
+
if (err instanceof ManagementError)
|
|
614
|
+
throw err;
|
|
615
|
+
throw new ManagementError("unavailable", "商品发布结果暂无法确认,请查看原草稿回执后决定是否续办。");
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (rest === "/catalog/connect/begin") {
|
|
620
|
+
const auth = requireActor(req);
|
|
621
|
+
assertWriteGuards(req, auth.sessionId);
|
|
622
|
+
authorizeOrThrow(auth.ctx, "onboarding:manage");
|
|
623
|
+
if (auth.ctx.role !== "owner")
|
|
624
|
+
throw new ManagementError("forbidden", "only owner can begin Catalog connection");
|
|
625
|
+
const channel = options.catalogConnection;
|
|
626
|
+
if (channel === undefined)
|
|
627
|
+
throw new ManagementError("unavailable", "Catalog connection is not configured");
|
|
628
|
+
const fields = objectFields(await readJsonBody(req), ["idempotency_key"]);
|
|
629
|
+
const begun = operations.begin({
|
|
630
|
+
merchantId: auth.ctx.merchantId, actorId: auth.ctx.actorId,
|
|
631
|
+
commandType: "catalog.connection.begin",
|
|
632
|
+
idempotencyKey: requireString(fields["idempotency_key"], "idempotency_key"),
|
|
633
|
+
requestDigest: managementRequestDigest({ generation: options.generation() }),
|
|
634
|
+
});
|
|
635
|
+
if (begun.kind === "conflict")
|
|
636
|
+
throw new ManagementError("conflict", "idempotency key was reused with another request");
|
|
637
|
+
if (begun.kind === "replay") {
|
|
638
|
+
writeJson(res, 200, begun.receipt, { "x-request-id": requestId });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
let status = "succeeded";
|
|
642
|
+
try {
|
|
643
|
+
await channel.begin();
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// 远端创建可能已发生:保留幂等回执,先查连接状态,不盲目重复创建。
|
|
647
|
+
status = "unknown";
|
|
648
|
+
console.warn("[merchant-management] catalog.connection.begin result unknown; query connection status before retry");
|
|
649
|
+
}
|
|
650
|
+
const receipt = {
|
|
651
|
+
...staticReceipt(begun.operationId, "catalog.connection.begin", status),
|
|
652
|
+
resource_ref: "catalog:connection", completed_at: now().toISOString(),
|
|
653
|
+
};
|
|
654
|
+
operations.complete(begun.operationId, status, receipt);
|
|
655
|
+
writeJson(res, status === "unknown" ? 202 : 200, receipt, { "x-request-id": requestId });
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
527
658
|
if (rest === "/runtime/safety-stops") {
|
|
528
659
|
const auth = requireActor(req);
|
|
529
660
|
assertWriteGuards(req, auth.sessionId);
|
|
@@ -1594,6 +1725,10 @@ export function createMerchantManagementApiHandler(options) {
|
|
|
1594
1725
|
async function postOnboardingIntent(req, res) {
|
|
1595
1726
|
const auth = requireActor(req);
|
|
1596
1727
|
assertWriteGuards(req, auth.sessionId);
|
|
1728
|
+
authorizeOrThrow(auth.ctx, "onboarding:manage");
|
|
1729
|
+
if (options.catalogConnection !== undefined) {
|
|
1730
|
+
throw new ManagementError("conflict", "本实例已启用目录直连,请在连接 Kiwi 目录入口继续,不能同时启动旧部署向导。");
|
|
1731
|
+
}
|
|
1597
1732
|
const body = await readJsonBody(req);
|
|
1598
1733
|
const fields = objectFields(body, [
|
|
1599
1734
|
"intent_id",
|
|
@@ -2171,7 +2306,7 @@ export function createMerchantManagementApiHandler(options) {
|
|
|
2171
2306
|
let table;
|
|
2172
2307
|
try {
|
|
2173
2308
|
table = parseProductTable(fields["table"], "request body");
|
|
2174
|
-
table = importChannel.prepareTable?.(table) ?? table;
|
|
2309
|
+
table = await importChannel.prepareTable?.(table) ?? table;
|
|
2175
2310
|
}
|
|
2176
2311
|
catch (err) {
|
|
2177
2312
|
if (err instanceof ProductTableError) {
|