@harrylabsj/kiwi-merchant-cloud 0.12.0 → 0.12.2

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