punchout-simulator 0.1.4 → 0.2.0

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.
@@ -6,7 +6,7 @@ import { serve } from "@hono/node-server";
6
6
 
7
7
  // src/server/app.ts
8
8
  import { existsSync as existsSync3 } from "fs";
9
- import { Hono as Hono7 } from "hono";
9
+ import { Hono as Hono8 } from "hono";
10
10
  import { logger } from "hono/logger";
11
11
  import { serveStatic } from "@hono/node-server/serve-static";
12
12
 
@@ -50,29 +50,80 @@ var db = null;
50
50
  async function initConfig() {
51
51
  ensureDirs();
52
52
  const adapter = new JSONFile(configPath());
53
- db = new Low(adapter, { connections: [] });
53
+ db = new Low(adapter, { buyers: [], suppliers: [], connections: [] });
54
54
  await db.read();
55
- db.data ||= { connections: [] };
55
+ db.data ||= { buyers: [], suppliers: [], connections: [] };
56
+ db.data.buyers ||= [];
57
+ db.data.suppliers ||= [];
58
+ db.data.connections ||= [];
59
+ migrateLegacy(db.data);
56
60
  await db.write();
57
61
  }
58
62
  function requireDb() {
59
63
  if (!db) throw new Error("config store not initialized \u2014 call initConfig() first");
60
64
  return db;
61
65
  }
62
- function listConnections() {
63
- return requireDb().data.connections;
66
+ var now = () => (/* @__PURE__ */ new Date()).toISOString();
67
+ var listBuyers = () => requireDb().data.buyers;
68
+ var getBuyer = (id) => requireDb().data.buyers.find((b) => b.id === id);
69
+ async function createBuyer(input) {
70
+ const buyer = { ...input, id: input.id ?? nanoid(8), createdAt: now(), updatedAt: now() };
71
+ const d = requireDb();
72
+ d.data.buyers.push(buyer);
73
+ await d.write();
74
+ return buyer;
75
+ }
76
+ async function updateBuyer(id, patch) {
77
+ const d = requireDb();
78
+ const existing = d.data.buyers.find((b) => b.id === id);
79
+ if (!existing) return void 0;
80
+ Object.assign(existing, patch, { id, updatedAt: now() });
81
+ await d.write();
82
+ return existing;
83
+ }
84
+ async function deleteBuyer(id) {
85
+ const d = requireDb();
86
+ if (d.data.connections.some((c) => c.buyerId === id)) {
87
+ throw new Error("buyer is referenced by a connection");
88
+ }
89
+ const before = d.data.buyers.length;
90
+ d.data.buyers = d.data.buyers.filter((b) => b.id !== id);
91
+ const removed = d.data.buyers.length < before;
92
+ if (removed) await d.write();
93
+ return removed;
94
+ }
95
+ var listSuppliers = () => requireDb().data.suppliers;
96
+ var getSupplier = (id) => requireDb().data.suppliers.find((s) => s.id === id);
97
+ async function createSupplier(input) {
98
+ const supplier = { ...input, id: input.id ?? nanoid(8), createdAt: now(), updatedAt: now() };
99
+ const d = requireDb();
100
+ d.data.suppliers.push(supplier);
101
+ await d.write();
102
+ return supplier;
103
+ }
104
+ async function updateSupplier(id, patch) {
105
+ const d = requireDb();
106
+ const existing = d.data.suppliers.find((s) => s.id === id);
107
+ if (!existing) return void 0;
108
+ Object.assign(existing, patch, { id, updatedAt: now() });
109
+ await d.write();
110
+ return existing;
64
111
  }
65
- function getConnection(id) {
66
- return requireDb().data.connections.find((c) => c.id === id);
112
+ async function deleteSupplier(id) {
113
+ const d = requireDb();
114
+ if (d.data.connections.some((c) => c.supplierId === id)) {
115
+ throw new Error("supplier is referenced by a connection");
116
+ }
117
+ const before = d.data.suppliers.length;
118
+ d.data.suppliers = d.data.suppliers.filter((s) => s.id !== id);
119
+ const removed = d.data.suppliers.length < before;
120
+ if (removed) await d.write();
121
+ return removed;
67
122
  }
123
+ var listConnections = () => requireDb().data.connections;
124
+ var getConnection = (id) => requireDb().data.connections.find((c) => c.id === id);
68
125
  async function createConnection(input) {
69
- const now = (/* @__PURE__ */ new Date()).toISOString();
70
- const conn = {
71
- ...input,
72
- id: input.id ?? nanoid(8),
73
- createdAt: now,
74
- updatedAt: now
75
- };
126
+ const conn = { ...input, id: input.id ?? nanoid(8), createdAt: now(), updatedAt: now() };
76
127
  const d = requireDb();
77
128
  d.data.connections.push(conn);
78
129
  await d.write();
@@ -82,7 +133,7 @@ async function updateConnection(id, patch) {
82
133
  const d = requireDb();
83
134
  const existing = d.data.connections.find((c) => c.id === id);
84
135
  if (!existing) return void 0;
85
- Object.assign(existing, patch, { id, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
136
+ Object.assign(existing, patch, { id, updatedAt: now() });
86
137
  await d.write();
87
138
  return existing;
88
139
  }
@@ -94,65 +145,229 @@ async function deleteConnection(id) {
94
145
  if (removed) await d.write();
95
146
  return removed;
96
147
  }
148
+ function resolveConnection(id) {
149
+ const connection = getConnection(id);
150
+ if (!connection) return void 0;
151
+ const buyer = getBuyer(connection.buyerId);
152
+ const supplier = getSupplier(connection.supplierId);
153
+ if (!buyer || !supplier) return void 0;
154
+ return { connection, buyer, supplier };
155
+ }
156
+ function findConnectionBySupplierAndBuyerIdentity(supplierId, from) {
157
+ const d = requireDb();
158
+ for (const connection of d.data.connections) {
159
+ if (connection.supplierId !== supplierId) continue;
160
+ const buyer = getBuyer(connection.buyerId);
161
+ if (!buyer) continue;
162
+ if (from && buyer.identity.domain === from.domain && buyer.identity.identity === from.identity) {
163
+ const supplier = getSupplier(supplierId);
164
+ if (supplier) return { connection, buyer, supplier };
165
+ }
166
+ }
167
+ return void 0;
168
+ }
169
+ function migrateLegacy(data) {
170
+ const legacy = data.connections.filter((c) => "from" in c || "to" in c);
171
+ if (legacy.length === 0) return;
172
+ const buyerKey = (c) => `${c.domain}|${c.identity}`;
173
+ const findOrAddBuyer = (name, identity) => {
174
+ const found = data.buyers.find((b) => buyerKey(b.identity) === buyerKey(identity));
175
+ if (found) return found.id;
176
+ const buyer = { id: nanoid(8), name, identity, createdAt: now(), updatedAt: now() };
177
+ data.buyers.push(buyer);
178
+ return buyer.id;
179
+ };
180
+ const findOrAddSupplier = (s) => {
181
+ const found = data.suppliers.find((x) => buyerKey(x.identity) === buyerKey(s.identity));
182
+ if (found) return found.id;
183
+ const supplier = {
184
+ id: nanoid(8),
185
+ name: s.name ?? "Supplier",
186
+ identity: s.identity,
187
+ punchoutUrl: s.punchoutUrl,
188
+ orderUrl: s.orderUrl,
189
+ catalog: s.catalog,
190
+ createdAt: now(),
191
+ updatedAt: now()
192
+ };
193
+ data.suppliers.push(supplier);
194
+ return supplier.id;
195
+ };
196
+ const migrated = [];
197
+ for (const c of data.connections) {
198
+ if (!("from" in c) && !("to" in c)) {
199
+ migrated.push(c);
200
+ continue;
201
+ }
202
+ const isSupplierMode = c.mode === "virtual-supplier";
203
+ const buyerCred = isSupplierMode ? c.to : c.from;
204
+ const supplierCred = isSupplierMode ? c.from : c.to;
205
+ const buyerId = findOrAddBuyer(isSupplierMode ? "Buyer" : c.name, buyerCred);
206
+ const supplierId = findOrAddSupplier({
207
+ name: isSupplierMode ? c.name : "Supplier",
208
+ identity: supplierCred,
209
+ punchoutUrl: c.punchoutUrl,
210
+ orderUrl: c.orderUrl,
211
+ catalog: c.catalog
212
+ });
213
+ migrated.push({
214
+ id: c.id ?? nanoid(8),
215
+ name: c.name ?? "Connection",
216
+ buyerId,
217
+ supplierId,
218
+ mode: c.mode ?? "virtual-buyer",
219
+ sharedSecret: c.sharedSecret ?? "",
220
+ senderIdentity: c.sender,
221
+ deploymentMode: c.deploymentMode ?? "test",
222
+ authStyle: c.authStyle ?? "SharedSecret",
223
+ createdAt: c.createdAt ?? now(),
224
+ updatedAt: now()
225
+ });
226
+ }
227
+ data.connections = migrated;
228
+ }
97
229
 
98
230
  // src/server/routes/connections.ts
99
231
  var connectionsRoute = new Hono();
100
- var emptyCredential = () => ({ domain: "", identity: "" });
101
232
  function normalize(body) {
102
- const cred = (c) => c && typeof c === "object" ? { domain: String(c.domain ?? ""), identity: String(c.identity ?? "") } : emptyCredential();
233
+ const sender = body?.senderIdentity && (body.senderIdentity.domain || body.senderIdentity.identity) ? { domain: String(body.senderIdentity.domain ?? ""), identity: String(body.senderIdentity.identity ?? "") } : void 0;
103
234
  return {
104
- name: String(body?.name ?? "Untitled connection"),
235
+ name: String(body?.name ?? ""),
236
+ buyerId: String(body?.buyerId ?? ""),
237
+ supplierId: String(body?.supplierId ?? ""),
105
238
  mode: body?.mode === "virtual-supplier" ? "virtual-supplier" : "virtual-buyer",
106
- from: cred(body?.from),
107
- to: cred(body?.to),
108
- sender: cred(body?.sender),
109
239
  sharedSecret: String(body?.sharedSecret ?? ""),
240
+ senderIdentity: sender,
110
241
  deploymentMode: body?.deploymentMode === "production" ? "production" : "test",
111
- authStyle: body?.authStyle === "MAC" ? "MAC" : "SharedSecret",
112
- punchoutUrl: body?.punchoutUrl ? String(body.punchoutUrl) : void 0,
113
- orderUrl: body?.orderUrl ? String(body.orderUrl) : void 0,
114
- catalog: Array.isArray(body?.catalog) ? body.catalog : void 0
242
+ authStyle: body?.authStyle === "MAC" ? "MAC" : "SharedSecret"
115
243
  };
116
244
  }
117
- function validateConnection(input) {
245
+ function validate(input) {
118
246
  const errors = [];
119
- if (!input.name.trim()) errors.push("name is required");
120
- if (input.mode === "virtual-buyer") {
121
- if (!input.punchoutUrl) errors.push("punchoutUrl is required for virtual-buyer");
122
- if (!input.orderUrl) errors.push("orderUrl is required for virtual-buyer");
123
- }
247
+ if (!input.buyerId || !getBuyer(input.buyerId)) errors.push("a valid buyer is required");
248
+ if (!input.supplierId || !getSupplier(input.supplierId)) errors.push("a valid supplier is required");
124
249
  return errors;
125
250
  }
126
- connectionsRoute.get("/", (c) => c.json(listConnections()));
251
+ function withLabel(input) {
252
+ if (input.name.trim()) return input;
253
+ const buyer = getBuyer(input.buyerId);
254
+ const supplier = getSupplier(input.supplierId);
255
+ return { ...input, name: `${buyer?.name ?? "Buyer"} \u2192 ${supplier?.name ?? "Supplier"}` };
256
+ }
257
+ connectionsRoute.get(
258
+ "/",
259
+ (c) => c.json(
260
+ listConnections().map((conn) => ({
261
+ ...conn,
262
+ buyer: getBuyer(conn.buyerId),
263
+ supplier: getSupplier(conn.supplierId)
264
+ }))
265
+ )
266
+ );
127
267
  connectionsRoute.post("/", async (c) => {
128
268
  const input = normalize(await c.req.json().catch(() => ({})));
129
- const errors = validateConnection(input);
269
+ const errors = validate(input);
130
270
  if (errors.length) return c.json({ errors }, 400);
131
- const created = await createConnection(input);
132
- return c.json(created, 201);
271
+ return c.json(await createConnection(withLabel(input)), 201);
133
272
  });
134
273
  connectionsRoute.get("/:id", (c) => {
274
+ const resolved = resolveConnection(c.req.param("id"));
275
+ if (resolved) return c.json({ ...resolved.connection, buyer: resolved.buyer, supplier: resolved.supplier });
135
276
  const conn = getConnection(c.req.param("id"));
136
277
  return conn ? c.json(conn) : c.json({ error: "not found" }, 404);
137
278
  });
138
279
  connectionsRoute.put("/:id", async (c) => {
139
- const id = c.req.param("id");
140
- const existing = getConnection(id);
280
+ const existing = getConnection(c.req.param("id"));
141
281
  if (!existing) return c.json({ error: "not found" }, 404);
142
- const merged = { ...existing, ...await c.req.json().catch(() => ({})) };
143
- const input = normalize(merged);
144
- const errors = validateConnection(input);
282
+ const input = normalize({ ...existing, ...await c.req.json().catch(() => ({})) });
283
+ const errors = validate(input);
145
284
  if (errors.length) return c.json({ errors }, 400);
146
- const updated = await updateConnection(id, input);
147
- return c.json(updated);
285
+ return c.json(await updateConnection(c.req.param("id"), withLabel(input)));
148
286
  });
149
287
  connectionsRoute.delete("/:id", async (c) => {
150
288
  const ok = await deleteConnection(c.req.param("id"));
151
289
  return ok ? c.json({ ok: true }) : c.json({ error: "not found" }, 404);
152
290
  });
153
291
 
154
- // src/server/routes/flow.ts
292
+ // src/server/routes/parties.ts
155
293
  import { Hono as Hono2 } from "hono";
294
+ var cred = (c) => ({
295
+ domain: String(c?.domain ?? ""),
296
+ identity: String(c?.identity ?? "")
297
+ });
298
+ var buyersRoute = new Hono2();
299
+ function normalizeBuyer(body) {
300
+ return { name: String(body?.name ?? "Untitled buyer"), identity: cred(body?.identity) };
301
+ }
302
+ buyersRoute.get("/", (c) => c.json(listBuyers()));
303
+ buyersRoute.post("/", async (c) => {
304
+ const input = normalizeBuyer(await c.req.json().catch(() => ({})));
305
+ if (!input.name.trim()) return c.json({ errors: ["name is required"] }, 400);
306
+ return c.json(await createBuyer(input), 201);
307
+ });
308
+ buyersRoute.get("/:id", (c) => {
309
+ const b = getBuyer(c.req.param("id"));
310
+ return b ? c.json(b) : c.json({ error: "not found" }, 404);
311
+ });
312
+ buyersRoute.put("/:id", async (c) => {
313
+ if (!getBuyer(c.req.param("id"))) return c.json({ error: "not found" }, 404);
314
+ const input = normalizeBuyer(await c.req.json().catch(() => ({})));
315
+ return c.json(await updateBuyer(c.req.param("id"), input));
316
+ });
317
+ buyersRoute.delete("/:id", async (c) => {
318
+ try {
319
+ const ok = await deleteBuyer(c.req.param("id"));
320
+ return ok ? c.json({ ok: true }) : c.json({ error: "not found" }, 404);
321
+ } catch (e) {
322
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 409);
323
+ }
324
+ });
325
+ var suppliersRoute = new Hono2();
326
+ function normalizeSupplier(body) {
327
+ const catalog = Array.isArray(body?.catalog) ? body.catalog.map((it) => ({
328
+ supplierPartId: String(it?.supplierPartId ?? ""),
329
+ description: String(it?.description ?? ""),
330
+ unitPrice: Number(it?.unitPrice ?? 0) || 0,
331
+ currency: String(it?.currency ?? "USD"),
332
+ uom: String(it?.uom ?? "EA"),
333
+ unspsc: String(it?.unspsc ?? ""),
334
+ manufacturerPartId: it?.manufacturerPartId ? String(it.manufacturerPartId) : void 0,
335
+ manufacturerName: it?.manufacturerName ? String(it.manufacturerName) : void 0
336
+ })) : void 0;
337
+ return {
338
+ name: String(body?.name ?? "Untitled supplier"),
339
+ identity: cred(body?.identity),
340
+ punchoutUrl: body?.punchoutUrl ? String(body.punchoutUrl) : void 0,
341
+ orderUrl: body?.orderUrl ? String(body.orderUrl) : void 0,
342
+ catalog
343
+ };
344
+ }
345
+ suppliersRoute.get("/", (c) => c.json(listSuppliers()));
346
+ suppliersRoute.post("/", async (c) => {
347
+ const input = normalizeSupplier(await c.req.json().catch(() => ({})));
348
+ if (!input.name.trim()) return c.json({ errors: ["name is required"] }, 400);
349
+ return c.json(await createSupplier(input), 201);
350
+ });
351
+ suppliersRoute.get("/:id", (c) => {
352
+ const s = getSupplier(c.req.param("id"));
353
+ return s ? c.json(s) : c.json({ error: "not found" }, 404);
354
+ });
355
+ suppliersRoute.put("/:id", async (c) => {
356
+ if (!getSupplier(c.req.param("id"))) return c.json({ error: "not found" }, 404);
357
+ const input = normalizeSupplier(await c.req.json().catch(() => ({})));
358
+ return c.json(await updateSupplier(c.req.param("id"), input));
359
+ });
360
+ suppliersRoute.delete("/:id", async (c) => {
361
+ try {
362
+ const ok = await deleteSupplier(c.req.param("id"));
363
+ return ok ? c.json({ ok: true }) : c.json({ error: "not found" }, 404);
364
+ } catch (e) {
365
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 409);
366
+ }
367
+ });
368
+
369
+ // src/server/routes/flow.ts
370
+ import { Hono as Hono3 } from "hono";
156
371
  import { nanoid as nanoid5 } from "nanoid";
157
372
 
158
373
  // src/server/store/log.ts
@@ -718,9 +933,9 @@ function getDocType(doc) {
718
933
  return "Unknown";
719
934
  }
720
935
  function credentialOf(node) {
721
- const cred = node?.Credential;
722
- if (!cred) return void 0;
723
- const first = Array.isArray(cred) ? cred[0] : cred;
936
+ const cred2 = node?.Credential;
937
+ if (!cred2) return void 0;
938
+ const first = Array.isArray(cred2) ? cred2[0] : cred2;
724
939
  return {
725
940
  domain: attr(first, "domain") ?? "",
726
941
  identity: text(first?.Identity) ?? ""
@@ -859,9 +1074,9 @@ function credEq(a, b) {
859
1074
  if (!a || !b) return false;
860
1075
  return a.domain === b.domain && a.identity === b.identity;
861
1076
  }
862
- function credKnown(c, conn) {
1077
+ function credKnown(c, exp) {
863
1078
  if (!c) return false;
864
- return [conn.from, conn.to, conn.sender].some((k) => credEq(c, k));
1079
+ return [exp.from, exp.to, exp.sender].some((k) => credEq(c, k));
865
1080
  }
866
1081
  function checkGeneral(doc, ctx, issues) {
867
1082
  const payloadId = getPayloadId(doc);
@@ -877,8 +1092,8 @@ function checkGeneral(doc, ctx, issues) {
877
1092
  if (!getTimestamp(doc)) {
878
1093
  issues.error("missing-timestamp", "cXML/@timestamp is missing", "cXML/@timestamp");
879
1094
  }
880
- if (!ctx.connection) return;
881
- const conn = ctx.connection;
1095
+ if (!ctx.expected) return;
1096
+ const exp = ctx.expected;
882
1097
  const creds = getHeaderCredentials(doc);
883
1098
  for (const [name, c] of [
884
1099
  ["From", creds.from],
@@ -895,7 +1110,7 @@ function checkGeneral(doc, ctx, issues) {
895
1110
  if (!c.identity) {
896
1111
  issues.warn("credential-identity", `Header/${name}/Credential/Identity is empty`, `cXML/Header/${name}`);
897
1112
  }
898
- if (c.domain && c.identity && !credKnown(c, conn)) {
1113
+ if (c.domain && c.identity && !credKnown(c, exp)) {
899
1114
  issues.warn(
900
1115
  "credential-mismatch",
901
1116
  `Header/${name} (${c.domain}/${c.identity}) does not match any identity configured on the connection`,
@@ -905,9 +1120,8 @@ function checkGeneral(doc, ctx, issues) {
905
1120
  }
906
1121
  }
907
1122
  function checkSharedSecret(doc, ctx, issues) {
908
- if (!ctx.connection) return;
909
- const conn = ctx.connection;
910
- if (conn.authStyle !== "SharedSecret") return;
1123
+ const exp = ctx.expected;
1124
+ if (!exp || exp.authStyle !== "SharedSecret") return;
911
1125
  const creds = getHeaderCredentials(doc);
912
1126
  if (!creds.sharedSecret) {
913
1127
  issues.warn(
@@ -915,7 +1129,7 @@ function checkSharedSecret(doc, ctx, issues) {
915
1129
  "Sender/Credential/SharedSecret is absent (required for SharedSecret auth)",
916
1130
  "cXML/Header/Sender/Credential/SharedSecret"
917
1131
  );
918
- } else if (conn.sharedSecret && creds.sharedSecret !== conn.sharedSecret) {
1132
+ } else if (exp.sharedSecret && creds.sharedSecret !== exp.sharedSecret) {
919
1133
  issues.error(
920
1134
  "sharedsecret-mismatch",
921
1135
  "Sender SharedSecret does not match the connection's configured shared secret",
@@ -1149,7 +1363,7 @@ function validateDocument(raw, ctx = {}) {
1149
1363
  }
1150
1364
 
1151
1365
  // src/server/routes/flow.ts
1152
- var flowRoute = new Hono2();
1366
+ var flowRoute = new Hono3();
1153
1367
  function host() {
1154
1368
  try {
1155
1369
  return new URL(getPublicUrl()).host;
@@ -1157,54 +1371,69 @@ function host() {
1157
1371
  return "punchout-simulator";
1158
1372
  }
1159
1373
  }
1160
- function requireVirtualBuyer(conn) {
1161
- if (!conn) return "connection not found";
1162
- if (conn.mode !== "virtual-buyer") return "connection is not in virtual-buyer mode";
1163
- return null;
1374
+ function buyerContext(r) {
1375
+ const { connection, buyer, supplier } = r;
1376
+ const from = buyer.identity;
1377
+ const to = supplier.identity;
1378
+ const sender = connection.senderIdentity ?? buyer.identity;
1379
+ return {
1380
+ from,
1381
+ to,
1382
+ sender,
1383
+ sharedSecret: connection.sharedSecret,
1384
+ punchoutUrl: supplier.punchoutUrl,
1385
+ orderUrl: supplier.orderUrl,
1386
+ deploymentMode: connection.deploymentMode,
1387
+ connectionId: connection.id,
1388
+ expected: { from, to, sender, sharedSecret: connection.sharedSecret, authStyle: connection.authStyle }
1389
+ };
1390
+ }
1391
+ function resolveVirtualBuyer(id) {
1392
+ const r = resolveConnection(id);
1393
+ if (!r) return { error: "connection not found (or its buyer/supplier is missing)" };
1394
+ if (r.connection.mode !== "virtual-buyer") return { error: "connection is not in virtual-buyer mode" };
1395
+ return { ctx: buyerContext(r) };
1164
1396
  }
1165
1397
  flowRoute.get("/:id/setup/preview", (c) => {
1166
- const conn = getConnection(c.req.param("id"));
1167
- const err = requireVirtualBuyer(conn);
1168
- if (err) return c.json({ error: err }, 400);
1398
+ const r = resolveVirtualBuyer(c.req.param("id"));
1399
+ if ("error" in r) return c.json({ error: r.error }, 400);
1400
+ const { ctx } = r;
1169
1401
  const buyerCookie = c.req.query("buyerCookie") || `pos-${nanoid5(16)}`;
1170
1402
  const xml = buildSetupRequest({
1171
- from: conn.from,
1172
- to: conn.to,
1173
- sender: conn.sender,
1174
- sharedSecret: conn.sharedSecret,
1403
+ from: ctx.from,
1404
+ to: ctx.to,
1405
+ sender: ctx.sender,
1406
+ sharedSecret: ctx.sharedSecret,
1175
1407
  buyerCookie,
1176
1408
  browserFormPostUrl: browserFormPostUrl(),
1177
1409
  payloadId: makePayloadId(host(), (/* @__PURE__ */ new Date()).toISOString()),
1178
1410
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1179
- deploymentMode: conn.deploymentMode
1411
+ deploymentMode: ctx.deploymentMode
1180
1412
  });
1181
1413
  return c.json({ buyerCookie, xml, browserFormPostUrl: browserFormPostUrl() });
1182
1414
  });
1183
1415
  flowRoute.post("/:id/setup", async (c) => {
1184
- const conn = getConnection(c.req.param("id"));
1185
- const err = requireVirtualBuyer(conn);
1186
- if (err) return c.json({ error: err }, 400);
1416
+ const r = resolveVirtualBuyer(c.req.param("id"));
1417
+ if ("error" in r) return c.json({ error: r.error }, 400);
1418
+ const { ctx } = r;
1187
1419
  const body = await c.req.json().catch(() => ({}));
1188
1420
  const buyerCookie = body.buyerCookie || `pos-${nanoid5(16)}`;
1189
1421
  const xml = body.xml || buildSetupRequest({
1190
- from: conn.from,
1191
- to: conn.to,
1192
- sender: conn.sender,
1193
- sharedSecret: conn.sharedSecret,
1422
+ from: ctx.from,
1423
+ to: ctx.to,
1424
+ sender: ctx.sender,
1425
+ sharedSecret: ctx.sharedSecret,
1194
1426
  buyerCookie,
1195
1427
  browserFormPostUrl: browserFormPostUrl(),
1196
1428
  payloadId: makePayloadId(host(), (/* @__PURE__ */ new Date()).toISOString()),
1197
1429
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1198
- deploymentMode: conn.deploymentMode
1199
- });
1200
- rememberSessionConnection(buyerCookie, conn.id);
1201
- const reqValidation = validateDocument(xml, {
1202
- connection: conn,
1203
- forceDocType: "SetupRequest"
1430
+ deploymentMode: ctx.deploymentMode
1204
1431
  });
1432
+ rememberSessionConnection(buyerCookie, ctx.connectionId);
1433
+ const reqValidation = validateDocument(xml, { expected: ctx.expected, forceDocType: "SetupRequest" });
1205
1434
  const reqLog = appendLog({
1206
1435
  sessionId: buyerCookie,
1207
- connectionId: conn.id,
1436
+ connectionId: ctx.connectionId,
1208
1437
  direction: "out",
1209
1438
  docType: "SetupRequest",
1210
1439
  headers: { "Content-Type": "text/xml; charset=UTF-8" },
@@ -1212,11 +1441,12 @@ flowRoute.post("/:id/setup", async (c) => {
1212
1441
  contentType: "text/xml",
1213
1442
  validation: reqValidation
1214
1443
  });
1215
- const res = await sendCxml(conn.punchoutUrl, xml);
1216
- const respValidation = res.error ? void 0 : validateDocument(res.body, { connection: conn, forceDocType: "SetupResponse" });
1444
+ if (!ctx.punchoutUrl) return c.json({ error: "supplier has no punchoutUrl configured" }, 400);
1445
+ const res = await sendCxml(ctx.punchoutUrl, xml);
1446
+ const respValidation = res.error ? void 0 : validateDocument(res.body, { expected: ctx.expected, forceDocType: "SetupResponse" });
1217
1447
  const respLog = appendLog({
1218
1448
  sessionId: buyerCookie,
1219
- connectionId: conn.id,
1449
+ connectionId: ctx.connectionId,
1220
1450
  direction: "in",
1221
1451
  docType: "SetupResponse",
1222
1452
  status: res.status,
@@ -1237,32 +1467,25 @@ flowRoute.post("/:id/setup", async (c) => {
1237
1467
  response: respLog
1238
1468
  });
1239
1469
  });
1240
- flowRoute.post("/:id/order", async (c) => {
1241
- const conn = getConnection(c.req.param("id"));
1242
- const err = requireVirtualBuyer(conn);
1243
- if (err) return c.json({ error: err }, 400);
1244
- const body = await c.req.json().catch(() => ({}));
1245
- const sessionId = body.sessionId || `pos-${nanoid5(16)}`;
1470
+ function buildOrderXml(ctx, body) {
1246
1471
  const items = body.items ?? [];
1247
1472
  const currency = body.currency || items[0]?.currency || "USD";
1248
1473
  const total = body.total ?? items.reduce((s, it) => s + (it.unitPriceAmount ?? 0) * it.quantity, 0);
1249
1474
  const orderId = body.orderId || `PO-${nanoid5(8)}`;
1250
- const dangling = !!body.danglingCid;
1251
- const inputAtts = body.attachments ?? [];
1252
- const attMeta = inputAtts.map((a) => ({
1475
+ const attMeta = (body.attachments ?? []).map((a) => ({
1253
1476
  contentId: a.contentId,
1254
1477
  scope: a.scope === "order" || a.scope == null ? "order" : { itemIndex: Number(a.scope) }
1255
1478
  }));
1256
- const xml = body.xml || buildOrderRequest({
1257
- from: conn.from,
1258
- to: conn.to,
1259
- sender: conn.sender,
1260
- sharedSecret: conn.sharedSecret,
1479
+ const xml = buildOrderRequest({
1480
+ from: ctx.from,
1481
+ to: ctx.to,
1482
+ sender: ctx.sender,
1483
+ sharedSecret: ctx.sharedSecret,
1261
1484
  orderId,
1262
1485
  orderDate: (/* @__PURE__ */ new Date()).toISOString(),
1263
1486
  payloadId: makePayloadId(host(), (/* @__PURE__ */ new Date()).toISOString()),
1264
1487
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1265
- deploymentMode: conn.deploymentMode,
1488
+ deploymentMode: ctx.deploymentMode,
1266
1489
  currency,
1267
1490
  total,
1268
1491
  items,
@@ -1270,6 +1493,24 @@ flowRoute.post("/:id/order", async (c) => {
1270
1493
  billTo: body.billTo,
1271
1494
  attachments: attMeta
1272
1495
  });
1496
+ return { xml, orderId };
1497
+ }
1498
+ flowRoute.post("/:id/order/preview", async (c) => {
1499
+ const r = resolveVirtualBuyer(c.req.param("id"));
1500
+ if ("error" in r) return c.json({ error: r.error }, 400);
1501
+ const body = await c.req.json().catch(() => ({}));
1502
+ const { xml, orderId } = buildOrderXml(r.ctx, body);
1503
+ return c.json({ xml, orderId });
1504
+ });
1505
+ flowRoute.post("/:id/order", async (c) => {
1506
+ const r = resolveVirtualBuyer(c.req.param("id"));
1507
+ if ("error" in r) return c.json({ error: r.error }, 400);
1508
+ const { ctx } = r;
1509
+ const body = await c.req.json().catch(() => ({}));
1510
+ const sessionId = body.sessionId || `pos-${nanoid5(16)}`;
1511
+ const dangling = !!body.danglingCid;
1512
+ const inputAtts = body.attachments ?? [];
1513
+ const xml = body.xml || buildOrderXml(ctx, body).xml;
1273
1514
  let wireBody = xml;
1274
1515
  let wireContentType = "text/xml; charset=UTF-8";
1275
1516
  const availableContentIds = /* @__PURE__ */ new Set();
@@ -1298,13 +1539,13 @@ flowRoute.post("/:id/order", async (c) => {
1298
1539
  wireContentType = built.contentType;
1299
1540
  }
1300
1541
  const reqValidation = validateDocument(xml, {
1301
- connection: conn,
1542
+ expected: ctx.expected,
1302
1543
  forceDocType: "OrderRequest",
1303
1544
  availableContentIds: inputAtts.length > 0 ? availableContentIds : void 0
1304
1545
  });
1305
1546
  const reqLog = appendLog({
1306
1547
  sessionId,
1307
- connectionId: conn.id,
1548
+ connectionId: ctx.connectionId,
1308
1549
  direction: "out",
1309
1550
  docType: "OrderRequest",
1310
1551
  headers: { "Content-Type": wireContentType },
@@ -1314,11 +1555,12 @@ flowRoute.post("/:id/order", async (c) => {
1314
1555
  attachments: savedRefs,
1315
1556
  note: dangling ? "dangling-cid test" : void 0
1316
1557
  });
1317
- const res = await sendCxml(conn.orderUrl, wireBody, wireContentType);
1318
- const respValidation = res.error ? void 0 : validateDocument(res.body, { connection: conn, forceDocType: "OrderResponse" });
1558
+ if (!ctx.orderUrl) return c.json({ error: "supplier has no orderUrl configured" }, 400);
1559
+ const res = await sendCxml(ctx.orderUrl, wireBody, wireContentType);
1560
+ const respValidation = res.error ? void 0 : validateDocument(res.body, { expected: ctx.expected, forceDocType: "OrderResponse" });
1319
1561
  const respLog = appendLog({
1320
1562
  sessionId,
1321
- connectionId: conn.id,
1563
+ connectionId: ctx.connectionId,
1322
1564
  direction: "in",
1323
1565
  docType: "OrderResponse",
1324
1566
  status: res.status,
@@ -1339,8 +1581,8 @@ flowRoute.post("/:id/order", async (c) => {
1339
1581
  });
1340
1582
 
1341
1583
  // src/server/routes/punchout-return.ts
1342
- import { Hono as Hono3 } from "hono";
1343
- var punchoutReturnRoute = new Hono3();
1584
+ import { Hono as Hono4 } from "hono";
1585
+ var punchoutReturnRoute = new Hono4();
1344
1586
  async function extractCxml(c) {
1345
1587
  const ct = (c.req.header("content-type") ?? "").toLowerCase();
1346
1588
  if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) {
@@ -1380,9 +1622,15 @@ punchoutReturnRoute.post("/return", async (c) => {
1380
1622
  const cart = parseCart(doc);
1381
1623
  const sessionId = cart.sessionId || text(root(doc)?.Message?.PunchOutOrderMessage?.BuyerCookie) || "unknown";
1382
1624
  const connectionId = connectionForSession(sessionId);
1383
- const conn = connectionId ? getConnection(connectionId) : void 0;
1625
+ const resolved = connectionId ? resolveConnection(connectionId) : void 0;
1626
+ const expected = resolved ? {
1627
+ from: resolved.buyer.identity,
1628
+ to: resolved.supplier.identity,
1629
+ sender: resolved.connection.senderIdentity ?? resolved.supplier.identity,
1630
+ authStyle: resolved.connection.authStyle
1631
+ } : void 0;
1384
1632
  const validation = validateDocument(xml, {
1385
- connection: conn,
1633
+ expected,
1386
1634
  expectedBuyerCookie: sessionId,
1387
1635
  forceDocType: "PunchOutOrderMessage"
1388
1636
  });
@@ -1403,9 +1651,9 @@ punchoutReturnRoute.post("/return", async (c) => {
1403
1651
  });
1404
1652
 
1405
1653
  // src/server/routes/stream.ts
1406
- import { Hono as Hono4 } from "hono";
1654
+ import { Hono as Hono5 } from "hono";
1407
1655
  import { streamSSE } from "hono/streaming";
1408
- var streamRoute = new Hono4();
1656
+ var streamRoute = new Hono5();
1409
1657
  streamRoute.get("/stream", (c) => {
1410
1658
  return streamSSE(c, async (stream) => {
1411
1659
  let open = true;
@@ -1432,8 +1680,8 @@ streamRoute.get("/stream", (c) => {
1432
1680
  });
1433
1681
 
1434
1682
  // src/server/routes/data.ts
1435
- import { Hono as Hono5 } from "hono";
1436
- var dataRoute = new Hono5();
1683
+ import { Hono as Hono6 } from "hono";
1684
+ var dataRoute = new Hono6();
1437
1685
  dataRoute.get("/health", (c) => c.json({ ok: true }));
1438
1686
  dataRoute.get(
1439
1687
  "/runtime",
@@ -1457,38 +1705,13 @@ dataRoute.get("/attachments/:hash", (c) => {
1457
1705
  });
1458
1706
 
1459
1707
  // src/server/routes/sim.ts
1460
- import { Hono as Hono6 } from "hono";
1708
+ import { Hono as Hono7 } from "hono";
1461
1709
  import { nanoid as nanoid6 } from "nanoid";
1462
- var simRoute = new Hono6();
1710
+ var simRoute = new Hono7();
1463
1711
  var DEMO_CATALOG = [
1464
- {
1465
- supplierPartId: "WIDGET-001",
1466
- description: "Premium Steel Widget",
1467
- unitPrice: 12.5,
1468
- currency: "USD",
1469
- uom: "EA",
1470
- unspsc: "31161500",
1471
- manufacturerPartId: "MFR-W001",
1472
- manufacturerName: "Acme Manufacturing"
1473
- },
1474
- {
1475
- supplierPartId: "BOLT-250",
1476
- description: "M8 Hex Bolt (pack of 250)",
1477
- unitPrice: 34,
1478
- currency: "USD",
1479
- uom: "PK",
1480
- unspsc: "31161600",
1481
- manufacturerPartId: "MFR-B250",
1482
- manufacturerName: "Acme Manufacturing"
1483
- },
1484
- {
1485
- supplierPartId: "TAPE-RED",
1486
- description: "Industrial Marking Tape, Red",
1487
- unitPrice: 5.75,
1488
- currency: "USD",
1489
- uom: "RL",
1490
- unspsc: "31201500"
1491
- }
1712
+ { supplierPartId: "WIDGET-001", description: "Premium Steel Widget", unitPrice: 12.5, currency: "USD", uom: "EA", unspsc: "31161500", manufacturerPartId: "MFR-W001", manufacturerName: "Acme Manufacturing" },
1713
+ { supplierPartId: "BOLT-250", description: "M8 Hex Bolt (pack of 250)", unitPrice: 34, currency: "USD", uom: "PK", unspsc: "31161600", manufacturerPartId: "MFR-B250", manufacturerName: "Acme Manufacturing" },
1714
+ { supplierPartId: "TAPE-RED", description: "Industrial Marking Tape, Red", unitPrice: 5.75, currency: "USD", uom: "RL", unspsc: "31201500" }
1492
1715
  ];
1493
1716
  function host2() {
1494
1717
  try {
@@ -1497,59 +1720,58 @@ function host2() {
1497
1720
  return "punchout-simulator";
1498
1721
  }
1499
1722
  }
1723
+ var catalogOf = (s) => s.catalog && s.catalog.length > 0 ? s.catalog : DEMO_CATALOG;
1500
1724
  function safeHttpUrl(u) {
1501
1725
  if (!u) return "";
1502
1726
  try {
1503
- const parsed = new URL(u.trim());
1504
- return parsed.protocol === "http:" || parsed.protocol === "https:" ? u.trim() : "";
1727
+ const p = new URL(u.trim());
1728
+ return p.protocol === "http:" || p.protocol === "https:" ? u.trim() : "";
1505
1729
  } catch {
1506
1730
  return "";
1507
1731
  }
1508
1732
  }
1509
- function catalogOf(conn) {
1510
- return conn.catalog && conn.catalog.length > 0 ? conn.catalog : DEMO_CATALOG;
1511
- }
1512
- function requireSupplier(conn) {
1513
- if (!conn) return "connection not found";
1514
- if (conn.mode !== "virtual-supplier") return "connection is not in virtual-supplier mode";
1515
- return null;
1733
+ function expectedFor(supplierId, from) {
1734
+ const r = findConnectionBySupplierAndBuyerIdentity(supplierId, from);
1735
+ if (!r) return void 0;
1736
+ return {
1737
+ from: r.buyer.identity,
1738
+ to: r.supplier.identity,
1739
+ sender: r.connection.senderIdentity ?? r.buyer.identity,
1740
+ sharedSecret: r.connection.sharedSecret,
1741
+ authStyle: r.connection.authStyle
1742
+ };
1516
1743
  }
1517
1744
  simRoute.post("/:id/punchout", async (c) => {
1518
- const conn = getConnection(c.req.param("id"));
1519
- const err = requireSupplier(conn);
1520
- if (err) return c.text(err, 400);
1745
+ const supplier = getSupplier(c.req.param("id"));
1746
+ if (!supplier) return c.text("supplier not found", 404);
1521
1747
  const reqXml = await c.req.text();
1522
1748
  const doc = parseXml(reqXml);
1523
1749
  const reqRoot = root(doc)?.Request?.PunchOutSetupRequest;
1524
1750
  const buyerCookie = text(reqRoot?.BuyerCookie) || `pos-${nanoid6(16)}`;
1525
1751
  const formPost = safeHttpUrl(text(reqRoot?.BrowserFormPost?.URL));
1526
- const reqValidation = validateDocument(reqXml, {
1527
- connection: conn,
1528
- forceDocType: "SetupRequest"
1529
- });
1752
+ const from = getHeaderCredentials(doc).from;
1530
1753
  appendLog({
1531
1754
  sessionId: buyerCookie,
1532
- connectionId: conn.id,
1755
+ connectionId: supplier.id,
1533
1756
  direction: "in",
1534
1757
  docType: "SetupRequest",
1535
1758
  headers: { "Content-Type": c.req.header("content-type") ?? "" },
1536
1759
  body: reqXml,
1537
- validation: reqValidation
1760
+ validation: validateDocument(reqXml, { expected: expectedFor(supplier.id, from), forceDocType: "SetupRequest" })
1538
1761
  });
1539
- const startPageUrl = `${getPublicUrl()}/sim/${conn.id}/catalog?cookie=${encodeURIComponent(
1540
- buyerCookie
1541
- )}&formpost=${encodeURIComponent(formPost)}`;
1762
+ const buyerCred = from ?? { domain: "", identity: "" };
1763
+ const startPageUrl = `${getPublicUrl()}/sim/${supplier.id}/catalog?cookie=${encodeURIComponent(buyerCookie)}&formpost=${encodeURIComponent(formPost)}&bd=${encodeURIComponent(buyerCred.domain)}&bi=${encodeURIComponent(buyerCred.identity)}`;
1542
1764
  const respXml = buildSetupResponse({
1543
1765
  payloadId: makePayloadId(host2(), (/* @__PURE__ */ new Date()).toISOString()),
1544
1766
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1545
1767
  startPageUrl,
1546
- from: conn.from,
1547
- to: conn.to,
1548
- sender: conn.sender
1768
+ from: supplier.identity,
1769
+ to: buyerCred,
1770
+ sender: supplier.identity
1549
1771
  });
1550
1772
  appendLog({
1551
1773
  sessionId: buyerCookie,
1552
- connectionId: conn.id,
1774
+ connectionId: supplier.id,
1553
1775
  direction: "out",
1554
1776
  docType: "SetupResponse",
1555
1777
  headers: { "Content-Type": "text/xml; charset=UTF-8" },
@@ -1560,24 +1782,23 @@ simRoute.post("/:id/punchout", async (c) => {
1560
1782
  return c.body(respXml);
1561
1783
  });
1562
1784
  simRoute.get("/:id/catalog", (c) => {
1563
- const conn = getConnection(c.req.param("id"));
1564
- const err = requireSupplier(conn);
1565
- if (err) return c.text(err, 400);
1785
+ const supplier = getSupplier(c.req.param("id"));
1786
+ if (!supplier) return c.text("supplier not found", 404);
1566
1787
  const cookie = c.req.query("cookie") ?? "";
1567
- const formpost = c.req.query("formpost") ?? "";
1568
- const items = catalogOf(conn);
1788
+ const formpost = safeHttpUrl(c.req.query("formpost") ?? "");
1789
+ const bd = c.req.query("bd") ?? "";
1790
+ const bi = c.req.query("bi") ?? "";
1791
+ const items = catalogOf(supplier);
1569
1792
  const rows = items.map(
1570
1793
  (it, i) => `<tr>
1571
- <td><strong>${escapeHtml(it.description)}</strong><br><small>${escapeHtml(
1572
- it.supplierPartId
1573
- )} \xB7 ${escapeHtml(it.uom)} \xB7 UNSPSC ${escapeHtml(it.unspsc)}</small></td>
1574
- <td class="price">${it.currency} ${it.unitPrice.toFixed(2)}</td>
1794
+ <td><strong>${escapeXml(it.description)}</strong><br><small>${escapeXml(it.supplierPartId)} \xB7 ${escapeXml(it.uom)} \xB7 UNSPSC ${escapeXml(it.unspsc)}</small></td>
1795
+ <td class="price">${escapeXml(it.currency)} ${it.unitPrice.toFixed(2)}</td>
1575
1796
  <td><input type="number" name="q_${i}" value="0" min="0" step="1" inputmode="numeric"></td>
1576
1797
  </tr>`
1577
1798
  ).join("\n");
1578
1799
  return c.html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
1579
1800
  <meta name="viewport" content="width=device-width, initial-scale=1">
1580
- <title>${escapeHtml(conn.name)} \u2014 mock catalog</title>
1801
+ <title>${escapeXml(supplier.name)} \u2014 mock catalog</title>
1581
1802
  <style>
1582
1803
  body{font-family:system-ui,sans-serif;background:#0f172a;color:#e2e8f0;margin:0;padding:2rem}
1583
1804
  .wrap{max-width:720px;margin:0 auto}
@@ -1586,17 +1807,17 @@ simRoute.get("/:id/catalog", (c) => {
1586
1807
  th,td{padding:.75rem 1rem;text-align:left;border-bottom:1px solid #334155}
1587
1808
  th{background:#0b1220;font-size:.8rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8}
1588
1809
  .price{white-space:nowrap;color:#fbbf24}
1589
- input[type=number]{width:5rem;background:#0b1220;border:1px solid #334155;color:#e2e8f0;
1590
- border-radius:6px;padding:.35rem .5rem}
1591
- button{margin-top:1.5rem;background:#6366f1;color:#fff;border:0;border-radius:8px;
1592
- padding:.7rem 1.4rem;font-size:1rem;cursor:pointer}
1810
+ input[type=number]{width:5rem;background:#0b1220;border:1px solid #334155;color:#e2e8f0;border-radius:6px;padding:.35rem .5rem}
1811
+ button{margin-top:1.5rem;background:#6366f1;color:#fff;border:0;border-radius:8px;padding:.7rem 1.4rem;font-size:1rem;cursor:pointer}
1593
1812
  button:hover{background:#4f46e5}
1594
1813
  </style></head><body><div class="wrap">
1595
- <h1>${escapeHtml(conn.name)} <small>(virtual supplier)</small></h1>
1814
+ <h1>${escapeXml(supplier.name)} <small>(virtual supplier)</small></h1>
1596
1815
  <div class="sub">Mock catalog served by punchout-simulator. Set quantities and return the cart.</div>
1597
- <form method="post" action="${getPublicUrl()}/sim/${conn.id}/checkout">
1598
- <input type="hidden" name="cookie" value="${escapeHtml(cookie)}">
1599
- <input type="hidden" name="formpost" value="${escapeHtml(formpost)}">
1816
+ <form method="post" action="${getPublicUrl()}/sim/${supplier.id}/checkout">
1817
+ <input type="hidden" name="cookie" value="${escapeXml(cookie)}">
1818
+ <input type="hidden" name="formpost" value="${escapeXml(formpost)}">
1819
+ <input type="hidden" name="bd" value="${escapeXml(bd)}">
1820
+ <input type="hidden" name="bi" value="${escapeXml(bi)}">
1600
1821
  <table><thead><tr><th>Item</th><th>Price</th><th>Qty</th></tr></thead>
1601
1822
  <tbody>${rows}</tbody></table>
1602
1823
  <button type="submit">Return cart to buyer \u2192</button>
@@ -1604,13 +1825,13 @@ simRoute.get("/:id/catalog", (c) => {
1604
1825
  </div></body></html>`);
1605
1826
  });
1606
1827
  simRoute.post("/:id/checkout", async (c) => {
1607
- const conn = getConnection(c.req.param("id"));
1608
- const err = requireSupplier(conn);
1609
- if (err) return c.text(err, 400);
1828
+ const supplier = getSupplier(c.req.param("id"));
1829
+ if (!supplier) return c.text("supplier not found", 404);
1610
1830
  const form = await c.req.parseBody();
1611
1831
  const cookie = String(form.cookie ?? `pos-${nanoid6(16)}`);
1612
1832
  const formpost = safeHttpUrl(String(form.formpost ?? ""));
1613
- const catalog = catalogOf(conn);
1833
+ const buyerCred = { domain: String(form.bd ?? ""), identity: String(form.bi ?? "") };
1834
+ const catalog = catalogOf(supplier);
1614
1835
  const items = [];
1615
1836
  catalog.forEach((it, i) => {
1616
1837
  const qty = Number(form[`q_${i}`] ?? 0);
@@ -1631,9 +1852,9 @@ simRoute.post("/:id/checkout", async (c) => {
1631
1852
  });
1632
1853
  const currency = items[0]?.currency ?? "USD";
1633
1854
  const xml = buildPunchOutOrderMessage({
1634
- from: conn.from,
1635
- to: conn.to,
1636
- sender: conn.sender,
1855
+ from: supplier.identity,
1856
+ to: buyerCred,
1857
+ sender: supplier.identity,
1637
1858
  buyerCookie: cookie,
1638
1859
  payloadId: makePayloadId(host2(), (/* @__PURE__ */ new Date()).toISOString()),
1639
1860
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1642,27 +1863,26 @@ simRoute.post("/:id/checkout", async (c) => {
1642
1863
  });
1643
1864
  appendLog({
1644
1865
  sessionId: cookie,
1645
- connectionId: conn.id,
1866
+ connectionId: supplier.id,
1646
1867
  direction: "out",
1647
1868
  docType: "PunchOutOrderMessage",
1648
1869
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
1649
1870
  body: xml,
1650
- validation: validateDocument(xml, { connection: conn, forceDocType: "PunchOutOrderMessage" })
1871
+ validation: validateDocument(xml, { forceDocType: "PunchOutOrderMessage" })
1651
1872
  });
1652
1873
  return c.html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
1653
1874
  <title>Returning cart\u2026</title></head>
1654
1875
  <body onload="document.forms[0].submit()" style="font-family:system-ui;background:#0f172a;color:#e2e8f0">
1655
1876
  <p style="padding:2rem">Returning cart to the buyer\u2026</p>
1656
- <form method="post" action="${escapeHtml(formpost)}">
1657
- <input type="hidden" name="cxml-urlencoded" value="${escapeHtml(xml)}">
1877
+ <form method="post" action="${escapeXml(formpost)}">
1878
+ <input type="hidden" name="cxml-urlencoded" value="${escapeXml(xml)}">
1658
1879
  <noscript><button type="submit">Continue</button></noscript>
1659
1880
  </form>
1660
1881
  </body></html>`);
1661
1882
  });
1662
1883
  simRoute.post("/:id/order", async (c) => {
1663
- const conn = getConnection(c.req.param("id"));
1664
- const err = requireSupplier(conn);
1665
- if (err) return c.text(err, 400);
1884
+ const supplier = getSupplier(c.req.param("id"));
1885
+ if (!supplier) return c.text("supplier not found", 404);
1666
1886
  const ct = c.req.header("content-type") ?? "";
1667
1887
  const raw = Buffer.from(await c.req.arrayBuffer());
1668
1888
  let xml;
@@ -1675,26 +1895,22 @@ simRoute.post("/:id/order", async (c) => {
1675
1895
  if (part === mp.root) continue;
1676
1896
  const cid = normalizeContentId(part.contentId);
1677
1897
  if (cid) availableContentIds.add(cid);
1678
- savedRefs.push(
1679
- saveAttachment(part.body, {
1680
- contentId: cid,
1681
- contentType: part.contentType ?? "application/octet-stream"
1682
- })
1683
- );
1898
+ savedRefs.push(saveAttachment(part.body, { contentId: cid, contentType: part.contentType ?? "application/octet-stream" }));
1684
1899
  }
1685
1900
  } else {
1686
1901
  xml = raw.toString("utf8");
1687
1902
  }
1688
1903
  const doc = parseXml(xml);
1904
+ const from = getHeaderCredentials(doc).from;
1689
1905
  const sessionId = findSessionForOrder(doc) ?? `order-${nanoid6(8)}`;
1690
1906
  const validation = validateDocument(xml, {
1691
- connection: conn,
1907
+ expected: expectedFor(supplier.id, from),
1692
1908
  forceDocType: "OrderRequest",
1693
1909
  availableContentIds: isMultipart(ct) ? availableContentIds : void 0
1694
1910
  });
1695
1911
  appendLog({
1696
1912
  sessionId,
1697
- connectionId: conn.id,
1913
+ connectionId: supplier.id,
1698
1914
  direction: "in",
1699
1915
  docType: "OrderRequest",
1700
1916
  headers: { "Content-Type": ct },
@@ -1709,13 +1925,13 @@ simRoute.post("/:id/order", async (c) => {
1709
1925
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1710
1926
  statusCode: ok ? "200" : "400",
1711
1927
  statusText: ok ? "OK" : "Bad Request",
1712
- from: conn.from,
1713
- to: conn.to,
1714
- sender: conn.sender
1928
+ from: supplier.identity,
1929
+ to: from ?? { domain: "", identity: "" },
1930
+ sender: supplier.identity
1715
1931
  });
1716
1932
  appendLog({
1717
1933
  sessionId,
1718
- connectionId: conn.id,
1934
+ connectionId: supplier.id,
1719
1935
  direction: "out",
1720
1936
  docType: "OrderResponse",
1721
1937
  headers: { "Content-Type": "text/xml; charset=UTF-8" },
@@ -1727,22 +1943,17 @@ simRoute.post("/:id/order", async (c) => {
1727
1943
  });
1728
1944
  function findSessionForOrder(doc) {
1729
1945
  const header2 = root(doc)?.Request?.OrderRequest?.OrderRequestHeader;
1730
- const orderId = header2 ? attrOf(header2, "orderID") : void 0;
1731
- return orderId ? `order-${orderId}` : void 0;
1732
- }
1733
- function attrOf(node, name) {
1734
- const v = node?.[`@_${name}`];
1735
- return v == null ? void 0 : String(v);
1736
- }
1737
- function escapeHtml(s) {
1738
- return escapeXml(s);
1946
+ const orderId = header2?.["@_orderID"];
1947
+ return orderId ? `order-${String(orderId)}` : void 0;
1739
1948
  }
1740
1949
 
1741
1950
  // src/server/app.ts
1742
1951
  import { relative } from "path";
1743
1952
  function createApp(opts = {}) {
1744
- const app = new Hono7();
1953
+ const app = new Hono8();
1745
1954
  if (!opts.quiet) app.use("*", logger());
1955
+ app.route("/api/buyers", buyersRoute);
1956
+ app.route("/api/suppliers", suppliersRoute);
1746
1957
  app.route("/api/connections", connectionsRoute);
1747
1958
  app.route("/api/connections", flowRoute);
1748
1959
  app.route("/api", dataRoute);
@@ -1770,34 +1981,28 @@ function relativeToCwd(abs) {
1770
1981
  // src/server/seed.ts
1771
1982
  async function seedDemoIfEmpty() {
1772
1983
  if (listConnections().length > 0) return;
1773
- const supplier = await createConnection({
1984
+ const buyer = await createBuyer({
1985
+ id: "demo-buyer",
1986
+ name: "Demo Buyer",
1987
+ identity: { domain: "DUNS", identity: "123456789" }
1988
+ });
1989
+ const supplier = await createSupplier({
1774
1990
  id: "demo-supplier",
1775
1991
  name: "Demo Supplier (built-in mock)",
1776
- mode: "virtual-supplier",
1777
- from: { domain: "DUNS", identity: "987654321" },
1778
- // supplier identity (the tool)
1779
- to: { domain: "DUNS", identity: "123456789" },
1780
- // buyer identity (counterparty)
1781
- sender: { domain: "DUNS", identity: "987654321" },
1782
- sharedSecret: "demo-secret",
1783
- deploymentMode: "test",
1784
- authStyle: "SharedSecret",
1992
+ identity: { domain: "DUNS", identity: "987654321" },
1993
+ punchoutUrl: `${getPublicUrl()}/sim/demo-supplier/punchout`,
1994
+ orderUrl: `${getPublicUrl()}/sim/demo-supplier/order`,
1785
1995
  catalog: []
1786
1996
  });
1787
1997
  await createConnection({
1788
- id: "demo-buyer",
1789
- name: "Demo Buyer \u2192 built-in supplier",
1998
+ id: "demo",
1999
+ name: "Demo Buyer \u2192 Demo Supplier",
2000
+ buyerId: buyer.id,
2001
+ supplierId: supplier.id,
1790
2002
  mode: "virtual-buyer",
1791
- from: { domain: "DUNS", identity: "123456789" },
1792
- // buyer identity (the tool)
1793
- to: { domain: "DUNS", identity: "987654321" },
1794
- // supplier identity (counterparty)
1795
- sender: { domain: "DUNS", identity: "123456789" },
1796
2003
  sharedSecret: "demo-secret",
1797
2004
  deploymentMode: "test",
1798
- authStyle: "SharedSecret",
1799
- punchoutUrl: `${getPublicUrl()}/sim/${supplier.id}/punchout`,
1800
- orderUrl: `${getPublicUrl()}/sim/${supplier.id}/order`
2005
+ authStyle: "SharedSecret"
1801
2006
  });
1802
2007
  }
1803
2008