@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.
Files changed (42) hide show
  1. package/app/cloud/bootstrap.d.ts +2 -0
  2. package/app/cloud/bootstrap.js +92 -10
  3. package/app/cloud/bootstrap.js.map +1 -1
  4. package/app/cloud/connect-service.d.ts +144 -0
  5. package/app/cloud/connect-service.js +776 -0
  6. package/app/cloud/connect-service.js.map +1 -0
  7. package/app/cloud/file-listing-publication.d.ts +105 -0
  8. package/app/cloud/file-listing-publication.js +436 -0
  9. package/app/cloud/file-listing-publication.js.map +1 -0
  10. package/app/cloud/merchant-connect.d.ts +19 -13
  11. package/app/cloud/merchant-connect.js +43 -284
  12. package/app/cloud/merchant-connect.js.map +1 -1
  13. package/app/cloud-sample/products.json +3 -3
  14. package/app/http/merchant-management/api.d.ts +21 -1
  15. package/app/http/merchant-management/api.js +136 -1
  16. package/app/http/merchant-management/api.js.map +1 -1
  17. package/app/http/merchant-management/page.d.ts +4 -1
  18. package/app/http/merchant-management/page.js +113 -7
  19. package/app/http/merchant-management/page.js.map +1 -1
  20. package/build-manifest.json +98 -68
  21. package/node_modules/@hono/node-server/dist/early-hints.cjs +1 -1
  22. package/node_modules/@hono/node-server/dist/early-hints.mjs +1 -1
  23. package/node_modules/@hono/node-server/dist/index.cjs +41 -19
  24. package/node_modules/@hono/node-server/dist/index.mjs +42 -20
  25. package/node_modules/@hono/node-server/dist/serve-static.cjs +1 -0
  26. package/node_modules/@hono/node-server/dist/serve-static.d.cts +2 -1
  27. package/node_modules/@hono/node-server/dist/serve-static.d.mts +2 -1
  28. package/node_modules/@hono/node-server/dist/serve-static.mjs +1 -0
  29. package/node_modules/@hono/node-server/package.json +3 -2
  30. package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
  31. package/node_modules/hono/dist/cjs/middleware/language/language.js +1 -1
  32. package/node_modules/hono/dist/cjs/middleware/serve-static/index.js +1 -0
  33. package/node_modules/hono/dist/middleware/language/language.js +1 -1
  34. package/node_modules/hono/dist/middleware/serve-static/index.js +1 -0
  35. package/node_modules/hono/package.json +1 -1
  36. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  37. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  38. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  39. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  40. package/node_modules/yaml/package.json +1 -1
  41. package/package.json +2 -2
  42. 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) {