punchout-simulator 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/server/cli.js +447 -242
- package/dist/server/cli.js.map +1 -1
- package/dist/web/assets/{cssMode-DLffFuN6.js → cssMode-B-got2hv.js} +1 -1
- package/dist/web/assets/{freemarker2-zfr9jUGh.js → freemarker2-RnkeBrE4.js} +1 -1
- package/dist/web/assets/{handlebars-M_QseyaG.js → handlebars-54KQc4Ip.js} +1 -1
- package/dist/web/assets/{html-BRMBHmCq.js → html-BHfOj4V7.js} +1 -1
- package/dist/web/assets/{htmlMode-CicmEJMR.js → htmlMode-Chd2dG7N.js} +1 -1
- package/dist/web/assets/{index-sxaLM6ld.css → index-9LlIENcD.css} +1 -1
- package/dist/web/assets/{index-Cm4lvUlh.js → index-CdJNNMRn.js} +206 -206
- package/dist/web/assets/{javascript-D6_vZEGx.js → javascript-DfxvokuB.js} +1 -1
- package/dist/web/assets/{jsonMode-CTxq7vAq.js → jsonMode-VBut9FUK.js} +1 -1
- package/dist/web/assets/{liquid-BUpPE-Wb.js → liquid-BnObGKeE.js} +1 -1
- package/dist/web/assets/{mdx-ymOE7fzM.js → mdx-DugOiDtO.js} +1 -1
- package/dist/web/assets/{python-Db74sog5.js → python-BctDjJVU.js} +1 -1
- package/dist/web/assets/{razor-BxQoOni_.js → razor-wtD6sxeJ.js} +1 -1
- package/dist/web/assets/{tsMode-C9sm5xMG.js → tsMode-DcIWDCPt.js} +1 -1
- package/dist/web/assets/{typescript-Ctr2X0xG.js → typescript-amfHwAlg.js} +1 -1
- package/dist/web/assets/{xml-DGpUXjuk.js → xml-CRuxB8WJ.js} +1 -1
- package/dist/web/assets/{yaml-6Dpo9WGU.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;
|
|
83
|
+
}
|
|
84
|
+
async function deleteBuyer(id) {
|
|
85
|
+
const d = requireDb();
|
|
86
|
+
if (d.data.connections.some((c) => c.buyerId === id)) {
|
|
87
|
+
throw new Error("buyer is referenced by a connection");
|
|
88
|
+
}
|
|
89
|
+
const before = d.data.buyers.length;
|
|
90
|
+
d.data.buyers = d.data.buyers.filter((b) => b.id !== id);
|
|
91
|
+
const removed = d.data.buyers.length < before;
|
|
92
|
+
if (removed) await d.write();
|
|
93
|
+
return removed;
|
|
94
|
+
}
|
|
95
|
+
var listSuppliers = () => requireDb().data.suppliers;
|
|
96
|
+
var getSupplier = (id) => requireDb().data.suppliers.find((s) => s.id === id);
|
|
97
|
+
async function createSupplier(input) {
|
|
98
|
+
const supplier = { ...input, id: input.id ?? nanoid(8), createdAt: now(), updatedAt: now() };
|
|
99
|
+
const d = requireDb();
|
|
100
|
+
d.data.suppliers.push(supplier);
|
|
101
|
+
await d.write();
|
|
102
|
+
return supplier;
|
|
103
|
+
}
|
|
104
|
+
async function updateSupplier(id, patch) {
|
|
105
|
+
const d = requireDb();
|
|
106
|
+
const existing = d.data.suppliers.find((s) => s.id === id);
|
|
107
|
+
if (!existing) return void 0;
|
|
108
|
+
Object.assign(existing, patch, { id, updatedAt: now() });
|
|
109
|
+
await d.write();
|
|
110
|
+
return existing;
|
|
64
111
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
112
|
+
async function deleteSupplier(id) {
|
|
113
|
+
const d = requireDb();
|
|
114
|
+
if (d.data.connections.some((c) => c.supplierId === id)) {
|
|
115
|
+
throw new Error("supplier is referenced by a connection");
|
|
116
|
+
}
|
|
117
|
+
const before = d.data.suppliers.length;
|
|
118
|
+
d.data.suppliers = d.data.suppliers.filter((s) => s.id !== id);
|
|
119
|
+
const removed = d.data.suppliers.length < before;
|
|
120
|
+
if (removed) await d.write();
|
|
121
|
+
return removed;
|
|
67
122
|
}
|
|
123
|
+
var listConnections = () => requireDb().data.connections;
|
|
124
|
+
var getConnection = (id) => requireDb().data.connections.find((c) => c.id === id);
|
|
68
125
|
async function createConnection(input) {
|
|
69
|
-
const
|
|
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,32 +1467,25 @@ flowRoute.post("/:id/setup", async (c) => {
|
|
|
1237
1467
|
response: respLog
|
|
1238
1468
|
});
|
|
1239
1469
|
});
|
|
1240
|
-
|
|
1241
|
-
const conn = getConnection(c.req.param("id"));
|
|
1242
|
-
const err = requireVirtualBuyer(conn);
|
|
1243
|
-
if (err) return c.json({ error: err }, 400);
|
|
1244
|
-
const body = await c.req.json().catch(() => ({}));
|
|
1245
|
-
const sessionId = body.sessionId || `pos-${nanoid5(16)}`;
|
|
1470
|
+
function buildOrderXml(ctx, body) {
|
|
1246
1471
|
const items = body.items ?? [];
|
|
1247
1472
|
const currency = body.currency || items[0]?.currency || "USD";
|
|
1248
1473
|
const total = body.total ?? items.reduce((s, it) => s + (it.unitPriceAmount ?? 0) * it.quantity, 0);
|
|
1249
1474
|
const orderId = body.orderId || `PO-${nanoid5(8)}`;
|
|
1250
|
-
const
|
|
1251
|
-
const inputAtts = body.attachments ?? [];
|
|
1252
|
-
const attMeta = inputAtts.map((a) => ({
|
|
1475
|
+
const attMeta = (body.attachments ?? []).map((a) => ({
|
|
1253
1476
|
contentId: a.contentId,
|
|
1254
1477
|
scope: a.scope === "order" || a.scope == null ? "order" : { itemIndex: Number(a.scope) }
|
|
1255
1478
|
}));
|
|
1256
|
-
const xml =
|
|
1257
|
-
from:
|
|
1258
|
-
to:
|
|
1259
|
-
sender:
|
|
1260
|
-
sharedSecret:
|
|
1479
|
+
const xml = buildOrderRequest({
|
|
1480
|
+
from: ctx.from,
|
|
1481
|
+
to: ctx.to,
|
|
1482
|
+
sender: ctx.sender,
|
|
1483
|
+
sharedSecret: ctx.sharedSecret,
|
|
1261
1484
|
orderId,
|
|
1262
1485
|
orderDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1263
1486
|
payloadId: makePayloadId(host(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
1264
1487
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1265
|
-
deploymentMode:
|
|
1488
|
+
deploymentMode: ctx.deploymentMode,
|
|
1266
1489
|
currency,
|
|
1267
1490
|
total,
|
|
1268
1491
|
items,
|
|
@@ -1270,6 +1493,24 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1270
1493
|
billTo: body.billTo,
|
|
1271
1494
|
attachments: attMeta
|
|
1272
1495
|
});
|
|
1496
|
+
return { xml, orderId };
|
|
1497
|
+
}
|
|
1498
|
+
flowRoute.post("/:id/order/preview", async (c) => {
|
|
1499
|
+
const r = resolveVirtualBuyer(c.req.param("id"));
|
|
1500
|
+
if ("error" in r) return c.json({ error: r.error }, 400);
|
|
1501
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1502
|
+
const { xml, orderId } = buildOrderXml(r.ctx, body);
|
|
1503
|
+
return c.json({ xml, orderId });
|
|
1504
|
+
});
|
|
1505
|
+
flowRoute.post("/:id/order", async (c) => {
|
|
1506
|
+
const r = resolveVirtualBuyer(c.req.param("id"));
|
|
1507
|
+
if ("error" in r) return c.json({ error: r.error }, 400);
|
|
1508
|
+
const { ctx } = r;
|
|
1509
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1510
|
+
const sessionId = body.sessionId || `pos-${nanoid5(16)}`;
|
|
1511
|
+
const dangling = !!body.danglingCid;
|
|
1512
|
+
const inputAtts = body.attachments ?? [];
|
|
1513
|
+
const xml = body.xml || buildOrderXml(ctx, body).xml;
|
|
1273
1514
|
let wireBody = xml;
|
|
1274
1515
|
let wireContentType = "text/xml; charset=UTF-8";
|
|
1275
1516
|
const availableContentIds = /* @__PURE__ */ new Set();
|
|
@@ -1298,13 +1539,13 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1298
1539
|
wireContentType = built.contentType;
|
|
1299
1540
|
}
|
|
1300
1541
|
const reqValidation = validateDocument(xml, {
|
|
1301
|
-
|
|
1542
|
+
expected: ctx.expected,
|
|
1302
1543
|
forceDocType: "OrderRequest",
|
|
1303
1544
|
availableContentIds: inputAtts.length > 0 ? availableContentIds : void 0
|
|
1304
1545
|
});
|
|
1305
1546
|
const reqLog = appendLog({
|
|
1306
1547
|
sessionId,
|
|
1307
|
-
connectionId:
|
|
1548
|
+
connectionId: ctx.connectionId,
|
|
1308
1549
|
direction: "out",
|
|
1309
1550
|
docType: "OrderRequest",
|
|
1310
1551
|
headers: { "Content-Type": wireContentType },
|
|
@@ -1314,11 +1555,12 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1314
1555
|
attachments: savedRefs,
|
|
1315
1556
|
note: dangling ? "dangling-cid test" : void 0
|
|
1316
1557
|
});
|
|
1317
|
-
|
|
1318
|
-
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" });
|
|
1319
1561
|
const respLog = appendLog({
|
|
1320
1562
|
sessionId,
|
|
1321
|
-
connectionId:
|
|
1563
|
+
connectionId: ctx.connectionId,
|
|
1322
1564
|
direction: "in",
|
|
1323
1565
|
docType: "OrderResponse",
|
|
1324
1566
|
status: res.status,
|
|
@@ -1339,8 +1581,8 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1339
1581
|
});
|
|
1340
1582
|
|
|
1341
1583
|
// src/server/routes/punchout-return.ts
|
|
1342
|
-
import { Hono as
|
|
1343
|
-
var punchoutReturnRoute = new
|
|
1584
|
+
import { Hono as Hono4 } from "hono";
|
|
1585
|
+
var punchoutReturnRoute = new Hono4();
|
|
1344
1586
|
async function extractCxml(c) {
|
|
1345
1587
|
const ct = (c.req.header("content-type") ?? "").toLowerCase();
|
|
1346
1588
|
if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) {
|
|
@@ -1380,9 +1622,15 @@ punchoutReturnRoute.post("/return", async (c) => {
|
|
|
1380
1622
|
const cart = parseCart(doc);
|
|
1381
1623
|
const sessionId = cart.sessionId || text(root(doc)?.Message?.PunchOutOrderMessage?.BuyerCookie) || "unknown";
|
|
1382
1624
|
const connectionId = connectionForSession(sessionId);
|
|
1383
|
-
const
|
|
1625
|
+
const resolved = connectionId ? resolveConnection(connectionId) : void 0;
|
|
1626
|
+
const expected = resolved ? {
|
|
1627
|
+
from: resolved.buyer.identity,
|
|
1628
|
+
to: resolved.supplier.identity,
|
|
1629
|
+
sender: resolved.connection.senderIdentity ?? resolved.supplier.identity,
|
|
1630
|
+
authStyle: resolved.connection.authStyle
|
|
1631
|
+
} : void 0;
|
|
1384
1632
|
const validation = validateDocument(xml, {
|
|
1385
|
-
|
|
1633
|
+
expected,
|
|
1386
1634
|
expectedBuyerCookie: sessionId,
|
|
1387
1635
|
forceDocType: "PunchOutOrderMessage"
|
|
1388
1636
|
});
|
|
@@ -1403,9 +1651,9 @@ punchoutReturnRoute.post("/return", async (c) => {
|
|
|
1403
1651
|
});
|
|
1404
1652
|
|
|
1405
1653
|
// src/server/routes/stream.ts
|
|
1406
|
-
import { Hono as
|
|
1654
|
+
import { Hono as Hono5 } from "hono";
|
|
1407
1655
|
import { streamSSE } from "hono/streaming";
|
|
1408
|
-
var streamRoute = new
|
|
1656
|
+
var streamRoute = new Hono5();
|
|
1409
1657
|
streamRoute.get("/stream", (c) => {
|
|
1410
1658
|
return streamSSE(c, async (stream) => {
|
|
1411
1659
|
let open = true;
|
|
@@ -1432,8 +1680,8 @@ streamRoute.get("/stream", (c) => {
|
|
|
1432
1680
|
});
|
|
1433
1681
|
|
|
1434
1682
|
// src/server/routes/data.ts
|
|
1435
|
-
import { Hono as
|
|
1436
|
-
var dataRoute = new
|
|
1683
|
+
import { Hono as Hono6 } from "hono";
|
|
1684
|
+
var dataRoute = new Hono6();
|
|
1437
1685
|
dataRoute.get("/health", (c) => c.json({ ok: true }));
|
|
1438
1686
|
dataRoute.get(
|
|
1439
1687
|
"/runtime",
|
|
@@ -1457,38 +1705,13 @@ dataRoute.get("/attachments/:hash", (c) => {
|
|
|
1457
1705
|
});
|
|
1458
1706
|
|
|
1459
1707
|
// src/server/routes/sim.ts
|
|
1460
|
-
import { Hono as
|
|
1708
|
+
import { Hono as Hono7 } from "hono";
|
|
1461
1709
|
import { nanoid as nanoid6 } from "nanoid";
|
|
1462
|
-
var simRoute = new
|
|
1710
|
+
var simRoute = new Hono7();
|
|
1463
1711
|
var DEMO_CATALOG = [
|
|
1464
|
-
{
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
unitPrice: 12.5,
|
|
1468
|
-
currency: "USD",
|
|
1469
|
-
uom: "EA",
|
|
1470
|
-
unspsc: "31161500",
|
|
1471
|
-
manufacturerPartId: "MFR-W001",
|
|
1472
|
-
manufacturerName: "Acme Manufacturing"
|
|
1473
|
-
},
|
|
1474
|
-
{
|
|
1475
|
-
supplierPartId: "BOLT-250",
|
|
1476
|
-
description: "M8 Hex Bolt (pack of 250)",
|
|
1477
|
-
unitPrice: 34,
|
|
1478
|
-
currency: "USD",
|
|
1479
|
-
uom: "PK",
|
|
1480
|
-
unspsc: "31161600",
|
|
1481
|
-
manufacturerPartId: "MFR-B250",
|
|
1482
|
-
manufacturerName: "Acme Manufacturing"
|
|
1483
|
-
},
|
|
1484
|
-
{
|
|
1485
|
-
supplierPartId: "TAPE-RED",
|
|
1486
|
-
description: "Industrial Marking Tape, Red",
|
|
1487
|
-
unitPrice: 5.75,
|
|
1488
|
-
currency: "USD",
|
|
1489
|
-
uom: "RL",
|
|
1490
|
-
unspsc: "31201500"
|
|
1491
|
-
}
|
|
1712
|
+
{ supplierPartId: "WIDGET-001", description: "Premium Steel Widget", unitPrice: 12.5, currency: "USD", uom: "EA", unspsc: "31161500", manufacturerPartId: "MFR-W001", manufacturerName: "Acme Manufacturing" },
|
|
1713
|
+
{ supplierPartId: "BOLT-250", description: "M8 Hex Bolt (pack of 250)", unitPrice: 34, currency: "USD", uom: "PK", unspsc: "31161600", manufacturerPartId: "MFR-B250", manufacturerName: "Acme Manufacturing" },
|
|
1714
|
+
{ supplierPartId: "TAPE-RED", description: "Industrial Marking Tape, Red", unitPrice: 5.75, currency: "USD", uom: "RL", unspsc: "31201500" }
|
|
1492
1715
|
];
|
|
1493
1716
|
function host2() {
|
|
1494
1717
|
try {
|
|
@@ -1497,59 +1720,58 @@ function host2() {
|
|
|
1497
1720
|
return "punchout-simulator";
|
|
1498
1721
|
}
|
|
1499
1722
|
}
|
|
1723
|
+
var catalogOf = (s) => s.catalog && s.catalog.length > 0 ? s.catalog : DEMO_CATALOG;
|
|
1500
1724
|
function safeHttpUrl(u) {
|
|
1501
1725
|
if (!u) return "";
|
|
1502
1726
|
try {
|
|
1503
|
-
const
|
|
1504
|
-
return
|
|
1727
|
+
const p = new URL(u.trim());
|
|
1728
|
+
return p.protocol === "http:" || p.protocol === "https:" ? u.trim() : "";
|
|
1505
1729
|
} catch {
|
|
1506
1730
|
return "";
|
|
1507
1731
|
}
|
|
1508
1732
|
}
|
|
1509
|
-
function
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1733
|
+
function expectedFor(supplierId, from) {
|
|
1734
|
+
const r = findConnectionBySupplierAndBuyerIdentity(supplierId, from);
|
|
1735
|
+
if (!r) return void 0;
|
|
1736
|
+
return {
|
|
1737
|
+
from: r.buyer.identity,
|
|
1738
|
+
to: r.supplier.identity,
|
|
1739
|
+
sender: r.connection.senderIdentity ?? r.buyer.identity,
|
|
1740
|
+
sharedSecret: r.connection.sharedSecret,
|
|
1741
|
+
authStyle: r.connection.authStyle
|
|
1742
|
+
};
|
|
1516
1743
|
}
|
|
1517
1744
|
simRoute.post("/:id/punchout", async (c) => {
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
if (err) return c.text(err, 400);
|
|
1745
|
+
const supplier = getSupplier(c.req.param("id"));
|
|
1746
|
+
if (!supplier) return c.text("supplier not found", 404);
|
|
1521
1747
|
const reqXml = await c.req.text();
|
|
1522
1748
|
const doc = parseXml(reqXml);
|
|
1523
1749
|
const reqRoot = root(doc)?.Request?.PunchOutSetupRequest;
|
|
1524
1750
|
const buyerCookie = text(reqRoot?.BuyerCookie) || `pos-${nanoid6(16)}`;
|
|
1525
1751
|
const formPost = safeHttpUrl(text(reqRoot?.BrowserFormPost?.URL));
|
|
1526
|
-
const
|
|
1527
|
-
connection: conn,
|
|
1528
|
-
forceDocType: "SetupRequest"
|
|
1529
|
-
});
|
|
1752
|
+
const from = getHeaderCredentials(doc).from;
|
|
1530
1753
|
appendLog({
|
|
1531
1754
|
sessionId: buyerCookie,
|
|
1532
|
-
connectionId:
|
|
1755
|
+
connectionId: supplier.id,
|
|
1533
1756
|
direction: "in",
|
|
1534
1757
|
docType: "SetupRequest",
|
|
1535
1758
|
headers: { "Content-Type": c.req.header("content-type") ?? "" },
|
|
1536
1759
|
body: reqXml,
|
|
1537
|
-
validation:
|
|
1760
|
+
validation: validateDocument(reqXml, { expected: expectedFor(supplier.id, from), forceDocType: "SetupRequest" })
|
|
1538
1761
|
});
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
)}&formpost=${encodeURIComponent(formPost)}`;
|
|
1762
|
+
const buyerCred = from ?? { domain: "", identity: "" };
|
|
1763
|
+
const startPageUrl = `${getPublicUrl()}/sim/${supplier.id}/catalog?cookie=${encodeURIComponent(buyerCookie)}&formpost=${encodeURIComponent(formPost)}&bd=${encodeURIComponent(buyerCred.domain)}&bi=${encodeURIComponent(buyerCred.identity)}`;
|
|
1542
1764
|
const respXml = buildSetupResponse({
|
|
1543
1765
|
payloadId: makePayloadId(host2(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
1544
1766
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1545
1767
|
startPageUrl,
|
|
1546
|
-
from:
|
|
1547
|
-
to:
|
|
1548
|
-
sender:
|
|
1768
|
+
from: supplier.identity,
|
|
1769
|
+
to: buyerCred,
|
|
1770
|
+
sender: supplier.identity
|
|
1549
1771
|
});
|
|
1550
1772
|
appendLog({
|
|
1551
1773
|
sessionId: buyerCookie,
|
|
1552
|
-
connectionId:
|
|
1774
|
+
connectionId: supplier.id,
|
|
1553
1775
|
direction: "out",
|
|
1554
1776
|
docType: "SetupResponse",
|
|
1555
1777
|
headers: { "Content-Type": "text/xml; charset=UTF-8" },
|
|
@@ -1560,24 +1782,23 @@ simRoute.post("/:id/punchout", async (c) => {
|
|
|
1560
1782
|
return c.body(respXml);
|
|
1561
1783
|
});
|
|
1562
1784
|
simRoute.get("/:id/catalog", (c) => {
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
if (err) return c.text(err, 400);
|
|
1785
|
+
const supplier = getSupplier(c.req.param("id"));
|
|
1786
|
+
if (!supplier) return c.text("supplier not found", 404);
|
|
1566
1787
|
const cookie = c.req.query("cookie") ?? "";
|
|
1567
|
-
const formpost = c.req.query("formpost") ?? "";
|
|
1568
|
-
const
|
|
1788
|
+
const formpost = safeHttpUrl(c.req.query("formpost") ?? "");
|
|
1789
|
+
const bd = c.req.query("bd") ?? "";
|
|
1790
|
+
const bi = c.req.query("bi") ?? "";
|
|
1791
|
+
const items = catalogOf(supplier);
|
|
1569
1792
|
const rows = items.map(
|
|
1570
1793
|
(it, i) => `<tr>
|
|
1571
|
-
<td><strong>${
|
|
1572
|
-
it.
|
|
1573
|
-
)} \xB7 ${escapeHtml(it.uom)} \xB7 UNSPSC ${escapeHtml(it.unspsc)}</small></td>
|
|
1574
|
-
<td class="price">${it.currency} ${it.unitPrice.toFixed(2)}</td>
|
|
1794
|
+
<td><strong>${escapeXml(it.description)}</strong><br><small>${escapeXml(it.supplierPartId)} \xB7 ${escapeXml(it.uom)} \xB7 UNSPSC ${escapeXml(it.unspsc)}</small></td>
|
|
1795
|
+
<td class="price">${escapeXml(it.currency)} ${it.unitPrice.toFixed(2)}</td>
|
|
1575
1796
|
<td><input type="number" name="q_${i}" value="0" min="0" step="1" inputmode="numeric"></td>
|
|
1576
1797
|
</tr>`
|
|
1577
1798
|
).join("\n");
|
|
1578
1799
|
return c.html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
1579
1800
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1580
|
-
<title>${
|
|
1801
|
+
<title>${escapeXml(supplier.name)} \u2014 mock catalog</title>
|
|
1581
1802
|
<style>
|
|
1582
1803
|
body{font-family:system-ui,sans-serif;background:#0f172a;color:#e2e8f0;margin:0;padding:2rem}
|
|
1583
1804
|
.wrap{max-width:720px;margin:0 auto}
|
|
@@ -1586,17 +1807,17 @@ simRoute.get("/:id/catalog", (c) => {
|
|
|
1586
1807
|
th,td{padding:.75rem 1rem;text-align:left;border-bottom:1px solid #334155}
|
|
1587
1808
|
th{background:#0b1220;font-size:.8rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8}
|
|
1588
1809
|
.price{white-space:nowrap;color:#fbbf24}
|
|
1589
|
-
input[type=number]{width:5rem;background:#0b1220;border:1px solid #334155;color:#e2e8f0;
|
|
1590
|
-
|
|
1591
|
-
button{margin-top:1.5rem;background:#6366f1;color:#fff;border:0;border-radius:8px;
|
|
1592
|
-
padding:.7rem 1.4rem;font-size:1rem;cursor:pointer}
|
|
1810
|
+
input[type=number]{width:5rem;background:#0b1220;border:1px solid #334155;color:#e2e8f0;border-radius:6px;padding:.35rem .5rem}
|
|
1811
|
+
button{margin-top:1.5rem;background:#6366f1;color:#fff;border:0;border-radius:8px;padding:.7rem 1.4rem;font-size:1rem;cursor:pointer}
|
|
1593
1812
|
button:hover{background:#4f46e5}
|
|
1594
1813
|
</style></head><body><div class="wrap">
|
|
1595
|
-
<h1>${
|
|
1814
|
+
<h1>${escapeXml(supplier.name)} <small>(virtual supplier)</small></h1>
|
|
1596
1815
|
<div class="sub">Mock catalog served by punchout-simulator. Set quantities and return the cart.</div>
|
|
1597
|
-
<form method="post" action="${getPublicUrl()}/sim/${
|
|
1598
|
-
<input type="hidden" name="cookie" value="${
|
|
1599
|
-
<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)}">
|
|
1600
1821
|
<table><thead><tr><th>Item</th><th>Price</th><th>Qty</th></tr></thead>
|
|
1601
1822
|
<tbody>${rows}</tbody></table>
|
|
1602
1823
|
<button type="submit">Return cart to buyer \u2192</button>
|
|
@@ -1604,13 +1825,13 @@ simRoute.get("/:id/catalog", (c) => {
|
|
|
1604
1825
|
</div></body></html>`);
|
|
1605
1826
|
});
|
|
1606
1827
|
simRoute.post("/:id/checkout", async (c) => {
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
if (err) return c.text(err, 400);
|
|
1828
|
+
const supplier = getSupplier(c.req.param("id"));
|
|
1829
|
+
if (!supplier) return c.text("supplier not found", 404);
|
|
1610
1830
|
const form = await c.req.parseBody();
|
|
1611
1831
|
const cookie = String(form.cookie ?? `pos-${nanoid6(16)}`);
|
|
1612
1832
|
const formpost = safeHttpUrl(String(form.formpost ?? ""));
|
|
1613
|
-
const
|
|
1833
|
+
const buyerCred = { domain: String(form.bd ?? ""), identity: String(form.bi ?? "") };
|
|
1834
|
+
const catalog = catalogOf(supplier);
|
|
1614
1835
|
const items = [];
|
|
1615
1836
|
catalog.forEach((it, i) => {
|
|
1616
1837
|
const qty = Number(form[`q_${i}`] ?? 0);
|
|
@@ -1631,9 +1852,9 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
1631
1852
|
});
|
|
1632
1853
|
const currency = items[0]?.currency ?? "USD";
|
|
1633
1854
|
const xml = buildPunchOutOrderMessage({
|
|
1634
|
-
from:
|
|
1635
|
-
to:
|
|
1636
|
-
sender:
|
|
1855
|
+
from: supplier.identity,
|
|
1856
|
+
to: buyerCred,
|
|
1857
|
+
sender: supplier.identity,
|
|
1637
1858
|
buyerCookie: cookie,
|
|
1638
1859
|
payloadId: makePayloadId(host2(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
1639
1860
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1642,27 +1863,26 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
1642
1863
|
});
|
|
1643
1864
|
appendLog({
|
|
1644
1865
|
sessionId: cookie,
|
|
1645
|
-
connectionId:
|
|
1866
|
+
connectionId: supplier.id,
|
|
1646
1867
|
direction: "out",
|
|
1647
1868
|
docType: "PunchOutOrderMessage",
|
|
1648
1869
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1649
1870
|
body: xml,
|
|
1650
|
-
validation: validateDocument(xml, {
|
|
1871
|
+
validation: validateDocument(xml, { forceDocType: "PunchOutOrderMessage" })
|
|
1651
1872
|
});
|
|
1652
1873
|
return c.html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
1653
1874
|
<title>Returning cart\u2026</title></head>
|
|
1654
1875
|
<body onload="document.forms[0].submit()" style="font-family:system-ui;background:#0f172a;color:#e2e8f0">
|
|
1655
1876
|
<p style="padding:2rem">Returning cart to the buyer\u2026</p>
|
|
1656
|
-
<form method="post" action="${
|
|
1657
|
-
<input type="hidden" name="cxml-urlencoded" value="${
|
|
1877
|
+
<form method="post" action="${escapeXml(formpost)}">
|
|
1878
|
+
<input type="hidden" name="cxml-urlencoded" value="${escapeXml(xml)}">
|
|
1658
1879
|
<noscript><button type="submit">Continue</button></noscript>
|
|
1659
1880
|
</form>
|
|
1660
1881
|
</body></html>`);
|
|
1661
1882
|
});
|
|
1662
1883
|
simRoute.post("/:id/order", async (c) => {
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1665
|
-
if (err) return c.text(err, 400);
|
|
1884
|
+
const supplier = getSupplier(c.req.param("id"));
|
|
1885
|
+
if (!supplier) return c.text("supplier not found", 404);
|
|
1666
1886
|
const ct = c.req.header("content-type") ?? "";
|
|
1667
1887
|
const raw = Buffer.from(await c.req.arrayBuffer());
|
|
1668
1888
|
let xml;
|
|
@@ -1675,26 +1895,22 @@ simRoute.post("/:id/order", async (c) => {
|
|
|
1675
1895
|
if (part === mp.root) continue;
|
|
1676
1896
|
const cid = normalizeContentId(part.contentId);
|
|
1677
1897
|
if (cid) availableContentIds.add(cid);
|
|
1678
|
-
savedRefs.push(
|
|
1679
|
-
saveAttachment(part.body, {
|
|
1680
|
-
contentId: cid,
|
|
1681
|
-
contentType: part.contentType ?? "application/octet-stream"
|
|
1682
|
-
})
|
|
1683
|
-
);
|
|
1898
|
+
savedRefs.push(saveAttachment(part.body, { contentId: cid, contentType: part.contentType ?? "application/octet-stream" }));
|
|
1684
1899
|
}
|
|
1685
1900
|
} else {
|
|
1686
1901
|
xml = raw.toString("utf8");
|
|
1687
1902
|
}
|
|
1688
1903
|
const doc = parseXml(xml);
|
|
1904
|
+
const from = getHeaderCredentials(doc).from;
|
|
1689
1905
|
const sessionId = findSessionForOrder(doc) ?? `order-${nanoid6(8)}`;
|
|
1690
1906
|
const validation = validateDocument(xml, {
|
|
1691
|
-
|
|
1907
|
+
expected: expectedFor(supplier.id, from),
|
|
1692
1908
|
forceDocType: "OrderRequest",
|
|
1693
1909
|
availableContentIds: isMultipart(ct) ? availableContentIds : void 0
|
|
1694
1910
|
});
|
|
1695
1911
|
appendLog({
|
|
1696
1912
|
sessionId,
|
|
1697
|
-
connectionId:
|
|
1913
|
+
connectionId: supplier.id,
|
|
1698
1914
|
direction: "in",
|
|
1699
1915
|
docType: "OrderRequest",
|
|
1700
1916
|
headers: { "Content-Type": ct },
|
|
@@ -1709,13 +1925,13 @@ simRoute.post("/:id/order", async (c) => {
|
|
|
1709
1925
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1710
1926
|
statusCode: ok ? "200" : "400",
|
|
1711
1927
|
statusText: ok ? "OK" : "Bad Request",
|
|
1712
|
-
from:
|
|
1713
|
-
to:
|
|
1714
|
-
sender:
|
|
1928
|
+
from: supplier.identity,
|
|
1929
|
+
to: from ?? { domain: "", identity: "" },
|
|
1930
|
+
sender: supplier.identity
|
|
1715
1931
|
});
|
|
1716
1932
|
appendLog({
|
|
1717
1933
|
sessionId,
|
|
1718
|
-
connectionId:
|
|
1934
|
+
connectionId: supplier.id,
|
|
1719
1935
|
direction: "out",
|
|
1720
1936
|
docType: "OrderResponse",
|
|
1721
1937
|
headers: { "Content-Type": "text/xml; charset=UTF-8" },
|
|
@@ -1727,22 +1943,17 @@ simRoute.post("/:id/order", async (c) => {
|
|
|
1727
1943
|
});
|
|
1728
1944
|
function findSessionForOrder(doc) {
|
|
1729
1945
|
const header2 = root(doc)?.Request?.OrderRequest?.OrderRequestHeader;
|
|
1730
|
-
const orderId = header2
|
|
1731
|
-
return orderId ? `order-${orderId}` : void 0;
|
|
1732
|
-
}
|
|
1733
|
-
function attrOf(node, name) {
|
|
1734
|
-
const v = node?.[`@_${name}`];
|
|
1735
|
-
return v == null ? void 0 : String(v);
|
|
1736
|
-
}
|
|
1737
|
-
function escapeHtml(s) {
|
|
1738
|
-
return escapeXml(s);
|
|
1946
|
+
const orderId = header2?.["@_orderID"];
|
|
1947
|
+
return orderId ? `order-${String(orderId)}` : void 0;
|
|
1739
1948
|
}
|
|
1740
1949
|
|
|
1741
1950
|
// src/server/app.ts
|
|
1742
1951
|
import { relative } from "path";
|
|
1743
1952
|
function createApp(opts = {}) {
|
|
1744
|
-
const app = new
|
|
1953
|
+
const app = new Hono8();
|
|
1745
1954
|
if (!opts.quiet) app.use("*", logger());
|
|
1955
|
+
app.route("/api/buyers", buyersRoute);
|
|
1956
|
+
app.route("/api/suppliers", suppliersRoute);
|
|
1746
1957
|
app.route("/api/connections", connectionsRoute);
|
|
1747
1958
|
app.route("/api/connections", flowRoute);
|
|
1748
1959
|
app.route("/api", dataRoute);
|
|
@@ -1770,34 +1981,28 @@ function relativeToCwd(abs) {
|
|
|
1770
1981
|
// src/server/seed.ts
|
|
1771
1982
|
async function seedDemoIfEmpty() {
|
|
1772
1983
|
if (listConnections().length > 0) return;
|
|
1773
|
-
const
|
|
1984
|
+
const buyer = await createBuyer({
|
|
1985
|
+
id: "demo-buyer",
|
|
1986
|
+
name: "Demo Buyer",
|
|
1987
|
+
identity: { domain: "DUNS", identity: "123456789" }
|
|
1988
|
+
});
|
|
1989
|
+
const supplier = await createSupplier({
|
|
1774
1990
|
id: "demo-supplier",
|
|
1775
1991
|
name: "Demo Supplier (built-in mock)",
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
to: { domain: "DUNS", identity: "123456789" },
|
|
1780
|
-
// buyer identity (counterparty)
|
|
1781
|
-
sender: { domain: "DUNS", identity: "987654321" },
|
|
1782
|
-
sharedSecret: "demo-secret",
|
|
1783
|
-
deploymentMode: "test",
|
|
1784
|
-
authStyle: "SharedSecret",
|
|
1992
|
+
identity: { domain: "DUNS", identity: "987654321" },
|
|
1993
|
+
punchoutUrl: `${getPublicUrl()}/sim/demo-supplier/punchout`,
|
|
1994
|
+
orderUrl: `${getPublicUrl()}/sim/demo-supplier/order`,
|
|
1785
1995
|
catalog: []
|
|
1786
1996
|
});
|
|
1787
1997
|
await createConnection({
|
|
1788
|
-
id: "demo
|
|
1789
|
-
name: "Demo Buyer \u2192
|
|
1998
|
+
id: "demo",
|
|
1999
|
+
name: "Demo Buyer \u2192 Demo Supplier",
|
|
2000
|
+
buyerId: buyer.id,
|
|
2001
|
+
supplierId: supplier.id,
|
|
1790
2002
|
mode: "virtual-buyer",
|
|
1791
|
-
from: { domain: "DUNS", identity: "123456789" },
|
|
1792
|
-
// buyer identity (the tool)
|
|
1793
|
-
to: { domain: "DUNS", identity: "987654321" },
|
|
1794
|
-
// supplier identity (counterparty)
|
|
1795
|
-
sender: { domain: "DUNS", identity: "123456789" },
|
|
1796
2003
|
sharedSecret: "demo-secret",
|
|
1797
2004
|
deploymentMode: "test",
|
|
1798
|
-
authStyle: "SharedSecret"
|
|
1799
|
-
punchoutUrl: `${getPublicUrl()}/sim/${supplier.id}/punchout`,
|
|
1800
|
-
orderUrl: `${getPublicUrl()}/sim/${supplier.id}/order`
|
|
2005
|
+
authStyle: "SharedSecret"
|
|
1801
2006
|
});
|
|
1802
2007
|
}
|
|
1803
2008
|
|