punchout-simulator 0.1.5 → 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;
64
83
  }
65
- function getConnection(id) {
66
- return requireDb().data.connections.find((c) => c.id === id);
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;
67
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;
111
+ }
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;
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,7 +1467,7 @@ flowRoute.post("/:id/setup", async (c) => {
1237
1467
  response: respLog
1238
1468
  });
1239
1469
  });
1240
- function buildOrderXml(conn, body) {
1470
+ function buildOrderXml(ctx, body) {
1241
1471
  const items = body.items ?? [];
1242
1472
  const currency = body.currency || items[0]?.currency || "USD";
1243
1473
  const total = body.total ?? items.reduce((s, it) => s + (it.unitPriceAmount ?? 0) * it.quantity, 0);
@@ -1247,15 +1477,15 @@ function buildOrderXml(conn, body) {
1247
1477
  scope: a.scope === "order" || a.scope == null ? "order" : { itemIndex: Number(a.scope) }
1248
1478
  }));
1249
1479
  const xml = buildOrderRequest({
1250
- from: conn.from,
1251
- to: conn.to,
1252
- sender: conn.sender,
1253
- sharedSecret: conn.sharedSecret,
1480
+ from: ctx.from,
1481
+ to: ctx.to,
1482
+ sender: ctx.sender,
1483
+ sharedSecret: ctx.sharedSecret,
1254
1484
  orderId,
1255
1485
  orderDate: (/* @__PURE__ */ new Date()).toISOString(),
1256
1486
  payloadId: makePayloadId(host(), (/* @__PURE__ */ new Date()).toISOString()),
1257
1487
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1258
- deploymentMode: conn.deploymentMode,
1488
+ deploymentMode: ctx.deploymentMode,
1259
1489
  currency,
1260
1490
  total,
1261
1491
  items,
@@ -1266,22 +1496,21 @@ function buildOrderXml(conn, body) {
1266
1496
  return { xml, orderId };
1267
1497
  }
1268
1498
  flowRoute.post("/:id/order/preview", async (c) => {
1269
- const conn = getConnection(c.req.param("id"));
1270
- const err = requireVirtualBuyer(conn);
1271
- if (err) return c.json({ error: err }, 400);
1499
+ const r = resolveVirtualBuyer(c.req.param("id"));
1500
+ if ("error" in r) return c.json({ error: r.error }, 400);
1272
1501
  const body = await c.req.json().catch(() => ({}));
1273
- const { xml, orderId } = buildOrderXml(conn, body);
1502
+ const { xml, orderId } = buildOrderXml(r.ctx, body);
1274
1503
  return c.json({ xml, orderId });
1275
1504
  });
1276
1505
  flowRoute.post("/:id/order", async (c) => {
1277
- const conn = getConnection(c.req.param("id"));
1278
- const err = requireVirtualBuyer(conn);
1279
- if (err) return c.json({ error: err }, 400);
1506
+ const r = resolveVirtualBuyer(c.req.param("id"));
1507
+ if ("error" in r) return c.json({ error: r.error }, 400);
1508
+ const { ctx } = r;
1280
1509
  const body = await c.req.json().catch(() => ({}));
1281
1510
  const sessionId = body.sessionId || `pos-${nanoid5(16)}`;
1282
1511
  const dangling = !!body.danglingCid;
1283
1512
  const inputAtts = body.attachments ?? [];
1284
- const xml = body.xml || buildOrderXml(conn, body).xml;
1513
+ const xml = body.xml || buildOrderXml(ctx, body).xml;
1285
1514
  let wireBody = xml;
1286
1515
  let wireContentType = "text/xml; charset=UTF-8";
1287
1516
  const availableContentIds = /* @__PURE__ */ new Set();
@@ -1310,13 +1539,13 @@ flowRoute.post("/:id/order", async (c) => {
1310
1539
  wireContentType = built.contentType;
1311
1540
  }
1312
1541
  const reqValidation = validateDocument(xml, {
1313
- connection: conn,
1542
+ expected: ctx.expected,
1314
1543
  forceDocType: "OrderRequest",
1315
1544
  availableContentIds: inputAtts.length > 0 ? availableContentIds : void 0
1316
1545
  });
1317
1546
  const reqLog = appendLog({
1318
1547
  sessionId,
1319
- connectionId: conn.id,
1548
+ connectionId: ctx.connectionId,
1320
1549
  direction: "out",
1321
1550
  docType: "OrderRequest",
1322
1551
  headers: { "Content-Type": wireContentType },
@@ -1326,11 +1555,12 @@ flowRoute.post("/:id/order", async (c) => {
1326
1555
  attachments: savedRefs,
1327
1556
  note: dangling ? "dangling-cid test" : void 0
1328
1557
  });
1329
- const res = await sendCxml(conn.orderUrl, wireBody, wireContentType);
1330
- 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" });
1331
1561
  const respLog = appendLog({
1332
1562
  sessionId,
1333
- connectionId: conn.id,
1563
+ connectionId: ctx.connectionId,
1334
1564
  direction: "in",
1335
1565
  docType: "OrderResponse",
1336
1566
  status: res.status,
@@ -1351,8 +1581,8 @@ flowRoute.post("/:id/order", async (c) => {
1351
1581
  });
1352
1582
 
1353
1583
  // src/server/routes/punchout-return.ts
1354
- import { Hono as Hono3 } from "hono";
1355
- var punchoutReturnRoute = new Hono3();
1584
+ import { Hono as Hono4 } from "hono";
1585
+ var punchoutReturnRoute = new Hono4();
1356
1586
  async function extractCxml(c) {
1357
1587
  const ct = (c.req.header("content-type") ?? "").toLowerCase();
1358
1588
  if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) {
@@ -1392,9 +1622,15 @@ punchoutReturnRoute.post("/return", async (c) => {
1392
1622
  const cart = parseCart(doc);
1393
1623
  const sessionId = cart.sessionId || text(root(doc)?.Message?.PunchOutOrderMessage?.BuyerCookie) || "unknown";
1394
1624
  const connectionId = connectionForSession(sessionId);
1395
- 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;
1396
1632
  const validation = validateDocument(xml, {
1397
- connection: conn,
1633
+ expected,
1398
1634
  expectedBuyerCookie: sessionId,
1399
1635
  forceDocType: "PunchOutOrderMessage"
1400
1636
  });
@@ -1415,9 +1651,9 @@ punchoutReturnRoute.post("/return", async (c) => {
1415
1651
  });
1416
1652
 
1417
1653
  // src/server/routes/stream.ts
1418
- import { Hono as Hono4 } from "hono";
1654
+ import { Hono as Hono5 } from "hono";
1419
1655
  import { streamSSE } from "hono/streaming";
1420
- var streamRoute = new Hono4();
1656
+ var streamRoute = new Hono5();
1421
1657
  streamRoute.get("/stream", (c) => {
1422
1658
  return streamSSE(c, async (stream) => {
1423
1659
  let open = true;
@@ -1444,8 +1680,8 @@ streamRoute.get("/stream", (c) => {
1444
1680
  });
1445
1681
 
1446
1682
  // src/server/routes/data.ts
1447
- import { Hono as Hono5 } from "hono";
1448
- var dataRoute = new Hono5();
1683
+ import { Hono as Hono6 } from "hono";
1684
+ var dataRoute = new Hono6();
1449
1685
  dataRoute.get("/health", (c) => c.json({ ok: true }));
1450
1686
  dataRoute.get(
1451
1687
  "/runtime",
@@ -1469,38 +1705,13 @@ dataRoute.get("/attachments/:hash", (c) => {
1469
1705
  });
1470
1706
 
1471
1707
  // src/server/routes/sim.ts
1472
- import { Hono as Hono6 } from "hono";
1708
+ import { Hono as Hono7 } from "hono";
1473
1709
  import { nanoid as nanoid6 } from "nanoid";
1474
- var simRoute = new Hono6();
1710
+ var simRoute = new Hono7();
1475
1711
  var DEMO_CATALOG = [
1476
- {
1477
- supplierPartId: "WIDGET-001",
1478
- description: "Premium Steel Widget",
1479
- unitPrice: 12.5,
1480
- currency: "USD",
1481
- uom: "EA",
1482
- unspsc: "31161500",
1483
- manufacturerPartId: "MFR-W001",
1484
- manufacturerName: "Acme Manufacturing"
1485
- },
1486
- {
1487
- supplierPartId: "BOLT-250",
1488
- description: "M8 Hex Bolt (pack of 250)",
1489
- unitPrice: 34,
1490
- currency: "USD",
1491
- uom: "PK",
1492
- unspsc: "31161600",
1493
- manufacturerPartId: "MFR-B250",
1494
- manufacturerName: "Acme Manufacturing"
1495
- },
1496
- {
1497
- supplierPartId: "TAPE-RED",
1498
- description: "Industrial Marking Tape, Red",
1499
- unitPrice: 5.75,
1500
- currency: "USD",
1501
- uom: "RL",
1502
- unspsc: "31201500"
1503
- }
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" }
1504
1715
  ];
1505
1716
  function host2() {
1506
1717
  try {
@@ -1509,59 +1720,58 @@ function host2() {
1509
1720
  return "punchout-simulator";
1510
1721
  }
1511
1722
  }
1723
+ var catalogOf = (s) => s.catalog && s.catalog.length > 0 ? s.catalog : DEMO_CATALOG;
1512
1724
  function safeHttpUrl(u) {
1513
1725
  if (!u) return "";
1514
1726
  try {
1515
- const parsed = new URL(u.trim());
1516
- 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() : "";
1517
1729
  } catch {
1518
1730
  return "";
1519
1731
  }
1520
1732
  }
1521
- function catalogOf(conn) {
1522
- return conn.catalog && conn.catalog.length > 0 ? conn.catalog : DEMO_CATALOG;
1523
- }
1524
- function requireSupplier(conn) {
1525
- if (!conn) return "connection not found";
1526
- if (conn.mode !== "virtual-supplier") return "connection is not in virtual-supplier mode";
1527
- 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
+ };
1528
1743
  }
1529
1744
  simRoute.post("/:id/punchout", async (c) => {
1530
- const conn = getConnection(c.req.param("id"));
1531
- const err = requireSupplier(conn);
1532
- 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);
1533
1747
  const reqXml = await c.req.text();
1534
1748
  const doc = parseXml(reqXml);
1535
1749
  const reqRoot = root(doc)?.Request?.PunchOutSetupRequest;
1536
1750
  const buyerCookie = text(reqRoot?.BuyerCookie) || `pos-${nanoid6(16)}`;
1537
1751
  const formPost = safeHttpUrl(text(reqRoot?.BrowserFormPost?.URL));
1538
- const reqValidation = validateDocument(reqXml, {
1539
- connection: conn,
1540
- forceDocType: "SetupRequest"
1541
- });
1752
+ const from = getHeaderCredentials(doc).from;
1542
1753
  appendLog({
1543
1754
  sessionId: buyerCookie,
1544
- connectionId: conn.id,
1755
+ connectionId: supplier.id,
1545
1756
  direction: "in",
1546
1757
  docType: "SetupRequest",
1547
1758
  headers: { "Content-Type": c.req.header("content-type") ?? "" },
1548
1759
  body: reqXml,
1549
- validation: reqValidation
1760
+ validation: validateDocument(reqXml, { expected: expectedFor(supplier.id, from), forceDocType: "SetupRequest" })
1550
1761
  });
1551
- const startPageUrl = `${getPublicUrl()}/sim/${conn.id}/catalog?cookie=${encodeURIComponent(
1552
- buyerCookie
1553
- )}&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)}`;
1554
1764
  const respXml = buildSetupResponse({
1555
1765
  payloadId: makePayloadId(host2(), (/* @__PURE__ */ new Date()).toISOString()),
1556
1766
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1557
1767
  startPageUrl,
1558
- from: conn.from,
1559
- to: conn.to,
1560
- sender: conn.sender
1768
+ from: supplier.identity,
1769
+ to: buyerCred,
1770
+ sender: supplier.identity
1561
1771
  });
1562
1772
  appendLog({
1563
1773
  sessionId: buyerCookie,
1564
- connectionId: conn.id,
1774
+ connectionId: supplier.id,
1565
1775
  direction: "out",
1566
1776
  docType: "SetupResponse",
1567
1777
  headers: { "Content-Type": "text/xml; charset=UTF-8" },
@@ -1572,24 +1782,23 @@ simRoute.post("/:id/punchout", async (c) => {
1572
1782
  return c.body(respXml);
1573
1783
  });
1574
1784
  simRoute.get("/:id/catalog", (c) => {
1575
- const conn = getConnection(c.req.param("id"));
1576
- const err = requireSupplier(conn);
1577
- 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);
1578
1787
  const cookie = c.req.query("cookie") ?? "";
1579
- const formpost = c.req.query("formpost") ?? "";
1580
- 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);
1581
1792
  const rows = items.map(
1582
1793
  (it, i) => `<tr>
1583
- <td><strong>${escapeHtml(it.description)}</strong><br><small>${escapeHtml(
1584
- it.supplierPartId
1585
- )} \xB7 ${escapeHtml(it.uom)} \xB7 UNSPSC ${escapeHtml(it.unspsc)}</small></td>
1586
- <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>
1587
1796
  <td><input type="number" name="q_${i}" value="0" min="0" step="1" inputmode="numeric"></td>
1588
1797
  </tr>`
1589
1798
  ).join("\n");
1590
1799
  return c.html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
1591
1800
  <meta name="viewport" content="width=device-width, initial-scale=1">
1592
- <title>${escapeHtml(conn.name)} \u2014 mock catalog</title>
1801
+ <title>${escapeXml(supplier.name)} \u2014 mock catalog</title>
1593
1802
  <style>
1594
1803
  body{font-family:system-ui,sans-serif;background:#0f172a;color:#e2e8f0;margin:0;padding:2rem}
1595
1804
  .wrap{max-width:720px;margin:0 auto}
@@ -1598,17 +1807,17 @@ simRoute.get("/:id/catalog", (c) => {
1598
1807
  th,td{padding:.75rem 1rem;text-align:left;border-bottom:1px solid #334155}
1599
1808
  th{background:#0b1220;font-size:.8rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8}
1600
1809
  .price{white-space:nowrap;color:#fbbf24}
1601
- input[type=number]{width:5rem;background:#0b1220;border:1px solid #334155;color:#e2e8f0;
1602
- border-radius:6px;padding:.35rem .5rem}
1603
- button{margin-top:1.5rem;background:#6366f1;color:#fff;border:0;border-radius:8px;
1604
- 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}
1605
1812
  button:hover{background:#4f46e5}
1606
1813
  </style></head><body><div class="wrap">
1607
- <h1>${escapeHtml(conn.name)} <small>(virtual supplier)</small></h1>
1814
+ <h1>${escapeXml(supplier.name)} <small>(virtual supplier)</small></h1>
1608
1815
  <div class="sub">Mock catalog served by punchout-simulator. Set quantities and return the cart.</div>
1609
- <form method="post" action="${getPublicUrl()}/sim/${conn.id}/checkout">
1610
- <input type="hidden" name="cookie" value="${escapeHtml(cookie)}">
1611
- <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)}">
1612
1821
  <table><thead><tr><th>Item</th><th>Price</th><th>Qty</th></tr></thead>
1613
1822
  <tbody>${rows}</tbody></table>
1614
1823
  <button type="submit">Return cart to buyer \u2192</button>
@@ -1616,13 +1825,13 @@ simRoute.get("/:id/catalog", (c) => {
1616
1825
  </div></body></html>`);
1617
1826
  });
1618
1827
  simRoute.post("/:id/checkout", async (c) => {
1619
- const conn = getConnection(c.req.param("id"));
1620
- const err = requireSupplier(conn);
1621
- 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);
1622
1830
  const form = await c.req.parseBody();
1623
1831
  const cookie = String(form.cookie ?? `pos-${nanoid6(16)}`);
1624
1832
  const formpost = safeHttpUrl(String(form.formpost ?? ""));
1625
- const catalog = catalogOf(conn);
1833
+ const buyerCred = { domain: String(form.bd ?? ""), identity: String(form.bi ?? "") };
1834
+ const catalog = catalogOf(supplier);
1626
1835
  const items = [];
1627
1836
  catalog.forEach((it, i) => {
1628
1837
  const qty = Number(form[`q_${i}`] ?? 0);
@@ -1643,9 +1852,9 @@ simRoute.post("/:id/checkout", async (c) => {
1643
1852
  });
1644
1853
  const currency = items[0]?.currency ?? "USD";
1645
1854
  const xml = buildPunchOutOrderMessage({
1646
- from: conn.from,
1647
- to: conn.to,
1648
- sender: conn.sender,
1855
+ from: supplier.identity,
1856
+ to: buyerCred,
1857
+ sender: supplier.identity,
1649
1858
  buyerCookie: cookie,
1650
1859
  payloadId: makePayloadId(host2(), (/* @__PURE__ */ new Date()).toISOString()),
1651
1860
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1654,27 +1863,26 @@ simRoute.post("/:id/checkout", async (c) => {
1654
1863
  });
1655
1864
  appendLog({
1656
1865
  sessionId: cookie,
1657
- connectionId: conn.id,
1866
+ connectionId: supplier.id,
1658
1867
  direction: "out",
1659
1868
  docType: "PunchOutOrderMessage",
1660
1869
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
1661
1870
  body: xml,
1662
- validation: validateDocument(xml, { connection: conn, forceDocType: "PunchOutOrderMessage" })
1871
+ validation: validateDocument(xml, { forceDocType: "PunchOutOrderMessage" })
1663
1872
  });
1664
1873
  return c.html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
1665
1874
  <title>Returning cart\u2026</title></head>
1666
1875
  <body onload="document.forms[0].submit()" style="font-family:system-ui;background:#0f172a;color:#e2e8f0">
1667
1876
  <p style="padding:2rem">Returning cart to the buyer\u2026</p>
1668
- <form method="post" action="${escapeHtml(formpost)}">
1669
- <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)}">
1670
1879
  <noscript><button type="submit">Continue</button></noscript>
1671
1880
  </form>
1672
1881
  </body></html>`);
1673
1882
  });
1674
1883
  simRoute.post("/:id/order", async (c) => {
1675
- const conn = getConnection(c.req.param("id"));
1676
- const err = requireSupplier(conn);
1677
- 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);
1678
1886
  const ct = c.req.header("content-type") ?? "";
1679
1887
  const raw = Buffer.from(await c.req.arrayBuffer());
1680
1888
  let xml;
@@ -1687,26 +1895,22 @@ simRoute.post("/:id/order", async (c) => {
1687
1895
  if (part === mp.root) continue;
1688
1896
  const cid = normalizeContentId(part.contentId);
1689
1897
  if (cid) availableContentIds.add(cid);
1690
- savedRefs.push(
1691
- saveAttachment(part.body, {
1692
- contentId: cid,
1693
- contentType: part.contentType ?? "application/octet-stream"
1694
- })
1695
- );
1898
+ savedRefs.push(saveAttachment(part.body, { contentId: cid, contentType: part.contentType ?? "application/octet-stream" }));
1696
1899
  }
1697
1900
  } else {
1698
1901
  xml = raw.toString("utf8");
1699
1902
  }
1700
1903
  const doc = parseXml(xml);
1904
+ const from = getHeaderCredentials(doc).from;
1701
1905
  const sessionId = findSessionForOrder(doc) ?? `order-${nanoid6(8)}`;
1702
1906
  const validation = validateDocument(xml, {
1703
- connection: conn,
1907
+ expected: expectedFor(supplier.id, from),
1704
1908
  forceDocType: "OrderRequest",
1705
1909
  availableContentIds: isMultipart(ct) ? availableContentIds : void 0
1706
1910
  });
1707
1911
  appendLog({
1708
1912
  sessionId,
1709
- connectionId: conn.id,
1913
+ connectionId: supplier.id,
1710
1914
  direction: "in",
1711
1915
  docType: "OrderRequest",
1712
1916
  headers: { "Content-Type": ct },
@@ -1721,13 +1925,13 @@ simRoute.post("/:id/order", async (c) => {
1721
1925
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1722
1926
  statusCode: ok ? "200" : "400",
1723
1927
  statusText: ok ? "OK" : "Bad Request",
1724
- from: conn.from,
1725
- to: conn.to,
1726
- sender: conn.sender
1928
+ from: supplier.identity,
1929
+ to: from ?? { domain: "", identity: "" },
1930
+ sender: supplier.identity
1727
1931
  });
1728
1932
  appendLog({
1729
1933
  sessionId,
1730
- connectionId: conn.id,
1934
+ connectionId: supplier.id,
1731
1935
  direction: "out",
1732
1936
  docType: "OrderResponse",
1733
1937
  headers: { "Content-Type": "text/xml; charset=UTF-8" },
@@ -1739,22 +1943,17 @@ simRoute.post("/:id/order", async (c) => {
1739
1943
  });
1740
1944
  function findSessionForOrder(doc) {
1741
1945
  const header2 = root(doc)?.Request?.OrderRequest?.OrderRequestHeader;
1742
- const orderId = header2 ? attrOf(header2, "orderID") : void 0;
1743
- return orderId ? `order-${orderId}` : void 0;
1744
- }
1745
- function attrOf(node, name) {
1746
- const v = node?.[`@_${name}`];
1747
- return v == null ? void 0 : String(v);
1748
- }
1749
- function escapeHtml(s) {
1750
- return escapeXml(s);
1946
+ const orderId = header2?.["@_orderID"];
1947
+ return orderId ? `order-${String(orderId)}` : void 0;
1751
1948
  }
1752
1949
 
1753
1950
  // src/server/app.ts
1754
1951
  import { relative } from "path";
1755
1952
  function createApp(opts = {}) {
1756
- const app = new Hono7();
1953
+ const app = new Hono8();
1757
1954
  if (!opts.quiet) app.use("*", logger());
1955
+ app.route("/api/buyers", buyersRoute);
1956
+ app.route("/api/suppliers", suppliersRoute);
1758
1957
  app.route("/api/connections", connectionsRoute);
1759
1958
  app.route("/api/connections", flowRoute);
1760
1959
  app.route("/api", dataRoute);
@@ -1782,34 +1981,28 @@ function relativeToCwd(abs) {
1782
1981
  // src/server/seed.ts
1783
1982
  async function seedDemoIfEmpty() {
1784
1983
  if (listConnections().length > 0) return;
1785
- 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({
1786
1990
  id: "demo-supplier",
1787
1991
  name: "Demo Supplier (built-in mock)",
1788
- mode: "virtual-supplier",
1789
- from: { domain: "DUNS", identity: "987654321" },
1790
- // supplier identity (the tool)
1791
- to: { domain: "DUNS", identity: "123456789" },
1792
- // buyer identity (counterparty)
1793
- sender: { domain: "DUNS", identity: "987654321" },
1794
- sharedSecret: "demo-secret",
1795
- deploymentMode: "test",
1796
- authStyle: "SharedSecret",
1992
+ identity: { domain: "DUNS", identity: "987654321" },
1993
+ punchoutUrl: `${getPublicUrl()}/sim/demo-supplier/punchout`,
1994
+ orderUrl: `${getPublicUrl()}/sim/demo-supplier/order`,
1797
1995
  catalog: []
1798
1996
  });
1799
1997
  await createConnection({
1800
- id: "demo-buyer",
1801
- 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,
1802
2002
  mode: "virtual-buyer",
1803
- from: { domain: "DUNS", identity: "123456789" },
1804
- // buyer identity (the tool)
1805
- to: { domain: "DUNS", identity: "987654321" },
1806
- // supplier identity (counterparty)
1807
- sender: { domain: "DUNS", identity: "123456789" },
1808
2003
  sharedSecret: "demo-secret",
1809
2004
  deploymentMode: "test",
1810
- authStyle: "SharedSecret",
1811
- punchoutUrl: `${getPublicUrl()}/sim/${supplier.id}/punchout`,
1812
- orderUrl: `${getPublicUrl()}/sim/${supplier.id}/order`
2005
+ authStyle: "SharedSecret"
1813
2006
  });
1814
2007
  }
1815
2008