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.
- package/README.md +13 -2
- package/dist/server/cli.js +434 -241
- package/dist/server/cli.js.map +1 -1
- package/dist/web/assets/{cssMode-WA7HerFP.js → cssMode-B-got2hv.js} +1 -1
- package/dist/web/assets/{freemarker2-BTOdUHiQ.js → freemarker2-RnkeBrE4.js} +1 -1
- package/dist/web/assets/{handlebars-DdTiW10M.js → handlebars-54KQc4Ip.js} +1 -1
- package/dist/web/assets/{html-CP7qpuUD.js → html-BHfOj4V7.js} +1 -1
- package/dist/web/assets/{htmlMode-BG3HQ1Sh.js → htmlMode-Chd2dG7N.js} +1 -1
- package/dist/web/assets/{index-CRWJsM13.css → index-9LlIENcD.css} +1 -1
- package/dist/web/assets/{index-DQ_OSoxK.js → index-CdJNNMRn.js} +194 -194
- package/dist/web/assets/{javascript-IQKq-LZB.js → javascript-DfxvokuB.js} +1 -1
- package/dist/web/assets/{jsonMode-B1p3z-UY.js → jsonMode-VBut9FUK.js} +1 -1
- package/dist/web/assets/{liquid-4Fxw7q97.js → liquid-BnObGKeE.js} +1 -1
- package/dist/web/assets/{mdx-00dxYvhf.js → mdx-DugOiDtO.js} +1 -1
- package/dist/web/assets/{python-BI6CPDlf.js → python-BctDjJVU.js} +1 -1
- package/dist/web/assets/{razor-DGRBM6nC.js → razor-wtD6sxeJ.js} +1 -1
- package/dist/web/assets/{tsMode-BVtwLDYC.js → tsMode-DcIWDCPt.js} +1 -1
- package/dist/web/assets/{typescript-Dgh1yWP4.js → typescript-amfHwAlg.js} +1 -1
- package/dist/web/assets/{xml-BOb15_Cq.js → xml-CRuxB8WJ.js} +1 -1
- package/dist/web/assets/{yaml-Dv2aUhkS.js → yaml-D_zXW3Py.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/dist/server/cli.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
66
|
-
|
|
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
|
|
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: (
|
|
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
|
|
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 ?? "
|
|
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
|
|
245
|
+
function validate(input) {
|
|
118
246
|
const errors = [];
|
|
119
|
-
if (!input.
|
|
120
|
-
if (input.
|
|
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
|
-
|
|
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 =
|
|
269
|
+
const errors = validate(input);
|
|
130
270
|
if (errors.length) return c.json({ errors }, 400);
|
|
131
|
-
|
|
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
|
|
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
|
|
143
|
-
const
|
|
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
|
-
|
|
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/
|
|
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
|
|
722
|
-
if (!
|
|
723
|
-
const first = Array.isArray(
|
|
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,
|
|
1077
|
+
function credKnown(c, exp) {
|
|
863
1078
|
if (!c) return false;
|
|
864
|
-
return [
|
|
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.
|
|
881
|
-
const
|
|
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,
|
|
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
|
-
|
|
909
|
-
|
|
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 (
|
|
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
|
|
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
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
|
1167
|
-
|
|
1168
|
-
|
|
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:
|
|
1172
|
-
to:
|
|
1173
|
-
sender:
|
|
1174
|
-
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:
|
|
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
|
|
1185
|
-
|
|
1186
|
-
|
|
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:
|
|
1191
|
-
to:
|
|
1192
|
-
sender:
|
|
1193
|
-
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:
|
|
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:
|
|
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
|
-
|
|
1216
|
-
const
|
|
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:
|
|
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(
|
|
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:
|
|
1251
|
-
to:
|
|
1252
|
-
sender:
|
|
1253
|
-
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:
|
|
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
|
|
1270
|
-
|
|
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(
|
|
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
|
|
1278
|
-
|
|
1279
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1330
|
-
const
|
|
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:
|
|
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
|
|
1355
|
-
var punchoutReturnRoute = new
|
|
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
|
|
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
|
-
|
|
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
|
|
1654
|
+
import { Hono as Hono5 } from "hono";
|
|
1419
1655
|
import { streamSSE } from "hono/streaming";
|
|
1420
|
-
var streamRoute = new
|
|
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
|
|
1448
|
-
var dataRoute = new
|
|
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
|
|
1708
|
+
import { Hono as Hono7 } from "hono";
|
|
1473
1709
|
import { nanoid as nanoid6 } from "nanoid";
|
|
1474
|
-
var simRoute = new
|
|
1710
|
+
var simRoute = new Hono7();
|
|
1475
1711
|
var DEMO_CATALOG = [
|
|
1476
|
-
{
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
|
1516
|
-
return
|
|
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
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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
|
|
1531
|
-
|
|
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
|
|
1539
|
-
connection: conn,
|
|
1540
|
-
forceDocType: "SetupRequest"
|
|
1541
|
-
});
|
|
1752
|
+
const from = getHeaderCredentials(doc).from;
|
|
1542
1753
|
appendLog({
|
|
1543
1754
|
sessionId: buyerCookie,
|
|
1544
|
-
connectionId:
|
|
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:
|
|
1760
|
+
validation: validateDocument(reqXml, { expected: expectedFor(supplier.id, from), forceDocType: "SetupRequest" })
|
|
1550
1761
|
});
|
|
1551
|
-
const
|
|
1552
|
-
|
|
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:
|
|
1559
|
-
to:
|
|
1560
|
-
sender:
|
|
1768
|
+
from: supplier.identity,
|
|
1769
|
+
to: buyerCred,
|
|
1770
|
+
sender: supplier.identity
|
|
1561
1771
|
});
|
|
1562
1772
|
appendLog({
|
|
1563
1773
|
sessionId: buyerCookie,
|
|
1564
|
-
connectionId:
|
|
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
|
|
1576
|
-
|
|
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
|
|
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>${
|
|
1584
|
-
it.
|
|
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>${
|
|
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
|
-
|
|
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>${
|
|
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/${
|
|
1610
|
-
<input type="hidden" name="cookie" value="${
|
|
1611
|
-
<input type="hidden" name="formpost" value="${
|
|
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
|
|
1620
|
-
|
|
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
|
|
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:
|
|
1647
|
-
to:
|
|
1648
|
-
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:
|
|
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, {
|
|
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="${
|
|
1669
|
-
<input type="hidden" name="cxml-urlencoded" value="${
|
|
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
|
|
1676
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1725
|
-
to:
|
|
1726
|
-
sender:
|
|
1928
|
+
from: supplier.identity,
|
|
1929
|
+
to: from ?? { domain: "", identity: "" },
|
|
1930
|
+
sender: supplier.identity
|
|
1727
1931
|
});
|
|
1728
1932
|
appendLog({
|
|
1729
1933
|
sessionId,
|
|
1730
|
-
connectionId:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
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
|
|
1801
|
-
name: "Demo Buyer \u2192
|
|
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
|
|