punchout-simulator 0.2.0 → 0.3.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 +86 -14
- package/dist/server/cli.js +534 -93
- package/dist/web/assets/{cssMode-B-got2hv.js → cssMode-UKotGf5X.js} +1 -1
- package/dist/web/assets/{freemarker2-RnkeBrE4.js → freemarker2-0DRATxLT.js} +1 -1
- package/dist/web/assets/{handlebars-54KQc4Ip.js → handlebars-U7ip3Hjn.js} +1 -1
- package/dist/web/assets/{html-BHfOj4V7.js → html-pMwW-Db1.js} +1 -1
- package/dist/web/assets/{htmlMode-Chd2dG7N.js → htmlMode-BZiR_DER.js} +1 -1
- package/dist/web/assets/{index-CdJNNMRn.js → index-fwAo3vG0.js} +197 -197
- package/dist/web/assets/{index-9LlIENcD.css → index-sN4D-IAg.css} +1 -1
- package/dist/web/assets/{javascript-DfxvokuB.js → javascript-BNQzFVRF.js} +1 -1
- package/dist/web/assets/{jsonMode-VBut9FUK.js → jsonMode-CSAIHDlB.js} +1 -1
- package/dist/web/assets/{liquid-BnObGKeE.js → liquid--vvS9GUJ.js} +1 -1
- package/dist/web/assets/{mdx-DugOiDtO.js → mdx-BJDQflEF.js} +1 -1
- package/dist/web/assets/{python-BctDjJVU.js → python-CepodNht.js} +1 -1
- package/dist/web/assets/{razor-wtD6sxeJ.js → razor-BQksENSs.js} +1 -1
- package/dist/web/assets/{tsMode-DcIWDCPt.js → tsMode-BRKq1Rx4.js} +1 -1
- package/dist/web/assets/{typescript-amfHwAlg.js → typescript-V-bP8GlZ.js} +1 -1
- package/dist/web/assets/{xml-CRuxB8WJ.js → xml-Bgme81_M.js} +1 -1
- package/dist/web/assets/{yaml-D_zXW3Py.js → yaml-CkpLYd3y.js} +1 -1
- package/dist/web/favicon.svg +8 -0
- package/dist/web/index.html +12 -2
- package/package.json +1 -1
- package/dist/server/cli.js.map +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -3,23 +3,149 @@
|
|
|
3
3
|
// src/server/cli.ts
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { serve } from "@hono/node-server";
|
|
6
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
6
7
|
|
|
7
8
|
// src/server/app.ts
|
|
8
9
|
import { existsSync as existsSync3 } from "fs";
|
|
9
|
-
import { Hono as
|
|
10
|
+
import { Hono as Hono9 } from "hono";
|
|
10
11
|
import { logger } from "hono/logger";
|
|
12
|
+
import { bodyLimit } from "hono/body-limit";
|
|
13
|
+
import { getCookie } from "hono/cookie";
|
|
11
14
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
12
15
|
|
|
16
|
+
// src/server/runtime.ts
|
|
17
|
+
var runtime = {
|
|
18
|
+
port: 8080,
|
|
19
|
+
publicUrl: "http://localhost:8080"
|
|
20
|
+
};
|
|
21
|
+
function setRuntime(r) {
|
|
22
|
+
Object.assign(runtime, r);
|
|
23
|
+
}
|
|
24
|
+
function getToken() {
|
|
25
|
+
return runtime.token;
|
|
26
|
+
}
|
|
27
|
+
function getPublicUrl() {
|
|
28
|
+
return runtime.publicUrl.replace(/\/$/, "");
|
|
29
|
+
}
|
|
30
|
+
function browserFormPostUrl() {
|
|
31
|
+
return `${getPublicUrl()}/punchout/return`;
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
// src/server/routes/connections.ts
|
|
14
35
|
import { Hono } from "hono";
|
|
15
36
|
|
|
16
37
|
// src/server/store/config.ts
|
|
38
|
+
import { chmodSync as chmodSync2 } from "fs";
|
|
17
39
|
import { Low } from "lowdb";
|
|
18
40
|
import { JSONFile } from "lowdb/node";
|
|
19
41
|
import { nanoid } from "nanoid";
|
|
20
42
|
|
|
43
|
+
// src/server/cxml/profile-presets.ts
|
|
44
|
+
var PROFILE_PRESETS = [
|
|
45
|
+
{
|
|
46
|
+
id: "generic",
|
|
47
|
+
name: "Generic cXML",
|
|
48
|
+
platform: "Generic",
|
|
49
|
+
builtin: true,
|
|
50
|
+
dtdVersions: { default: "1.2.045" },
|
|
51
|
+
userAgent: "punchout-simulator",
|
|
52
|
+
setupOperation: "create",
|
|
53
|
+
attachmentEncoding: "binary",
|
|
54
|
+
cartReturnTransport: "cxml-urlencoded",
|
|
55
|
+
extrinsics: []
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "ariba",
|
|
59
|
+
name: "SAP Ariba",
|
|
60
|
+
platform: "Ariba",
|
|
61
|
+
builtin: true,
|
|
62
|
+
dtdVersions: { default: "1.2.045", PunchOutOrderMessage: "1.2.045" },
|
|
63
|
+
userAgent: "Ariba Network/1.0",
|
|
64
|
+
setupOperation: "create",
|
|
65
|
+
attachmentEncoding: "base64",
|
|
66
|
+
cartReturnTransport: "cxml-urlencoded",
|
|
67
|
+
extrinsics: [{ name: "User", value: "${buyerCookie}", scope: "setup" }]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "coupa",
|
|
71
|
+
name: "Coupa",
|
|
72
|
+
platform: "Coupa",
|
|
73
|
+
builtin: true,
|
|
74
|
+
// Coupa publishes specific DTD versions per document type.
|
|
75
|
+
dtdVersions: { default: "1.2.014", PunchOutOrderMessage: "1.2.023" },
|
|
76
|
+
userAgent: "Coupa Procurement",
|
|
77
|
+
setupOperation: "create",
|
|
78
|
+
attachmentEncoding: "base64",
|
|
79
|
+
cartReturnTransport: "cxml-urlencoded",
|
|
80
|
+
extrinsics: []
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "jaggaer",
|
|
84
|
+
name: "JAGGAER",
|
|
85
|
+
platform: "Jaggaer",
|
|
86
|
+
builtin: true,
|
|
87
|
+
dtdVersions: { default: "1.2.021" },
|
|
88
|
+
userAgent: "JAGGAER Procurement",
|
|
89
|
+
setupOperation: "create",
|
|
90
|
+
attachmentEncoding: "binary",
|
|
91
|
+
cartReturnTransport: "cxml-urlencoded",
|
|
92
|
+
extrinsics: []
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "oracle",
|
|
96
|
+
name: "Oracle iProcurement",
|
|
97
|
+
platform: "Oracle",
|
|
98
|
+
builtin: true,
|
|
99
|
+
dtdVersions: { default: "1.2.008" },
|
|
100
|
+
userAgent: "Oracle iProcurement",
|
|
101
|
+
setupOperation: "create",
|
|
102
|
+
attachmentEncoding: "binary",
|
|
103
|
+
cartReturnTransport: "cxml-urlencoded",
|
|
104
|
+
extrinsics: []
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "sap-srm",
|
|
108
|
+
name: "SAP SRM / Business Network",
|
|
109
|
+
platform: "SAP",
|
|
110
|
+
builtin: true,
|
|
111
|
+
// NOTE: real SAP SRM speaks OCI (form parameters), which this cXML-only tool
|
|
112
|
+
// cannot emit. This profile models the cXML/Business-Network side: base64
|
|
113
|
+
// attachments and a base64 cart return.
|
|
114
|
+
dtdVersions: { default: "1.2.040" },
|
|
115
|
+
userAgent: "SAP Business Network",
|
|
116
|
+
setupOperation: "create",
|
|
117
|
+
attachmentEncoding: "base64",
|
|
118
|
+
cartReturnTransport: "cxml-base64",
|
|
119
|
+
extrinsics: []
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "workday",
|
|
123
|
+
name: "Workday",
|
|
124
|
+
platform: "Workday",
|
|
125
|
+
builtin: true,
|
|
126
|
+
dtdVersions: { default: "1.2.045" },
|
|
127
|
+
userAgent: "Workday Strategic Sourcing",
|
|
128
|
+
setupOperation: "create",
|
|
129
|
+
attachmentEncoding: "base64",
|
|
130
|
+
cartReturnTransport: "cxml-urlencoded",
|
|
131
|
+
extrinsics: []
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
var GENERIC_PROFILE = {
|
|
135
|
+
...PROFILE_PRESETS[0],
|
|
136
|
+
createdAt: "",
|
|
137
|
+
updatedAt: ""
|
|
138
|
+
};
|
|
139
|
+
function seedBuiltinProfiles(data, now2) {
|
|
140
|
+
for (const preset of PROFILE_PRESETS) {
|
|
141
|
+
if (!data.profiles.some((p) => p.id === preset.id)) {
|
|
142
|
+
data.profiles.push({ ...preset, createdAt: now2, updatedAt: now2 });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
21
147
|
// src/server/store/paths.ts
|
|
22
|
-
import { mkdirSync } from "fs";
|
|
148
|
+
import { chmodSync, mkdirSync } from "fs";
|
|
23
149
|
import { resolve } from "path";
|
|
24
150
|
var dataDir = resolve(process.cwd(), "data");
|
|
25
151
|
function setDataDir(dir) {
|
|
@@ -40,9 +166,13 @@ function sessionFile(sessionId) {
|
|
|
40
166
|
return resolve(sessionsDir(), `${safe}.jsonl`);
|
|
41
167
|
}
|
|
42
168
|
function ensureDirs() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
169
|
+
for (const d of [dataDir, sessionsDir(), attachmentsDir()]) {
|
|
170
|
+
mkdirSync(d, { recursive: true, mode: 448 });
|
|
171
|
+
try {
|
|
172
|
+
chmodSync(d, 448);
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
46
176
|
}
|
|
47
177
|
|
|
48
178
|
// src/server/store/config.ts
|
|
@@ -50,14 +180,20 @@ var db = null;
|
|
|
50
180
|
async function initConfig() {
|
|
51
181
|
ensureDirs();
|
|
52
182
|
const adapter = new JSONFile(configPath());
|
|
53
|
-
db = new Low(adapter, { buyers: [], suppliers: [], connections: [] });
|
|
183
|
+
db = new Low(adapter, { buyers: [], suppliers: [], connections: [], profiles: [] });
|
|
54
184
|
await db.read();
|
|
55
|
-
db.data ||= { buyers: [], suppliers: [], connections: [] };
|
|
185
|
+
db.data ||= { buyers: [], suppliers: [], connections: [], profiles: [] };
|
|
56
186
|
db.data.buyers ||= [];
|
|
57
187
|
db.data.suppliers ||= [];
|
|
58
188
|
db.data.connections ||= [];
|
|
189
|
+
db.data.profiles ||= [];
|
|
190
|
+
seedBuiltinProfiles(db.data, now());
|
|
59
191
|
migrateLegacy(db.data);
|
|
60
192
|
await db.write();
|
|
193
|
+
try {
|
|
194
|
+
chmodSync2(configPath(), 384);
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
61
197
|
}
|
|
62
198
|
function requireDb() {
|
|
63
199
|
if (!db) throw new Error("config store not initialized \u2014 call initConfig() first");
|
|
@@ -166,6 +302,53 @@ function findConnectionBySupplierAndBuyerIdentity(supplierId, from) {
|
|
|
166
302
|
}
|
|
167
303
|
return void 0;
|
|
168
304
|
}
|
|
305
|
+
var listProfiles = () => requireDb().data.profiles;
|
|
306
|
+
var getProfile = (id) => requireDb().data.profiles.find((p) => p.id === id);
|
|
307
|
+
async function createProfile(input) {
|
|
308
|
+
const profile = { ...input, id: input.id ?? nanoid(8), createdAt: now(), updatedAt: now() };
|
|
309
|
+
const d = requireDb();
|
|
310
|
+
d.data.profiles.push(profile);
|
|
311
|
+
await d.write();
|
|
312
|
+
return profile;
|
|
313
|
+
}
|
|
314
|
+
async function updateProfile(id, patch) {
|
|
315
|
+
const d = requireDb();
|
|
316
|
+
const existing = d.data.profiles.find((p) => p.id === id);
|
|
317
|
+
if (!existing) return void 0;
|
|
318
|
+
Object.assign(existing, patch, { id, updatedAt: now() });
|
|
319
|
+
await d.write();
|
|
320
|
+
return existing;
|
|
321
|
+
}
|
|
322
|
+
async function deleteProfile(id) {
|
|
323
|
+
const d = requireDb();
|
|
324
|
+
if (d.data.buyers.some((b) => b.profileId === id)) {
|
|
325
|
+
throw new Error("profile is referenced by a buyer");
|
|
326
|
+
}
|
|
327
|
+
const before = d.data.profiles.length;
|
|
328
|
+
d.data.profiles = d.data.profiles.filter((p) => p.id !== id);
|
|
329
|
+
const removed = d.data.profiles.length < before;
|
|
330
|
+
if (removed) await d.write();
|
|
331
|
+
return removed;
|
|
332
|
+
}
|
|
333
|
+
function profileForBuyer(buyer) {
|
|
334
|
+
const p = buyer.profileId ? getProfile(buyer.profileId) : void 0;
|
|
335
|
+
return p ?? getProfile("generic") ?? GENERIC_PROFILE;
|
|
336
|
+
}
|
|
337
|
+
function effectiveProfile(connection, buyer) {
|
|
338
|
+
const p = profileForBuyer(buyer);
|
|
339
|
+
return {
|
|
340
|
+
dtdVersions: p.dtdVersions,
|
|
341
|
+
userAgent: p.userAgent,
|
|
342
|
+
setupOperation: p.setupOperation,
|
|
343
|
+
// Connection attachmentEncoding is concrete by construction → explicit override.
|
|
344
|
+
attachmentEncoding: connection.attachmentEncoding ?? p.attachmentEncoding,
|
|
345
|
+
cartReturnTransport: p.cartReturnTransport,
|
|
346
|
+
extrinsics: p.extrinsics
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function dtdVersionFor(eff, docType) {
|
|
350
|
+
return eff.dtdVersions[docType] ?? eff.dtdVersions.default;
|
|
351
|
+
}
|
|
169
352
|
function migrateLegacy(data) {
|
|
170
353
|
const legacy = data.connections.filter((c) => "from" in c || "to" in c);
|
|
171
354
|
if (legacy.length === 0) return;
|
|
@@ -219,7 +402,6 @@ function migrateLegacy(data) {
|
|
|
219
402
|
sharedSecret: c.sharedSecret ?? "",
|
|
220
403
|
senderIdentity: c.sender,
|
|
221
404
|
deploymentMode: c.deploymentMode ?? "test",
|
|
222
|
-
authStyle: c.authStyle ?? "SharedSecret",
|
|
223
405
|
createdAt: c.createdAt ?? now(),
|
|
224
406
|
updatedAt: now()
|
|
225
407
|
});
|
|
@@ -239,7 +421,7 @@ function normalize(body) {
|
|
|
239
421
|
sharedSecret: String(body?.sharedSecret ?? ""),
|
|
240
422
|
senderIdentity: sender,
|
|
241
423
|
deploymentMode: body?.deploymentMode === "production" ? "production" : "test",
|
|
242
|
-
|
|
424
|
+
attachmentEncoding: body?.attachmentEncoding === "base64" ? "base64" : "binary"
|
|
243
425
|
};
|
|
244
426
|
}
|
|
245
427
|
function validate(input) {
|
|
@@ -254,11 +436,14 @@ function withLabel(input) {
|
|
|
254
436
|
const supplier = getSupplier(input.supplierId);
|
|
255
437
|
return { ...input, name: `${buyer?.name ?? "Buyer"} \u2192 ${supplier?.name ?? "Supplier"}` };
|
|
256
438
|
}
|
|
439
|
+
function maskSecret(conn) {
|
|
440
|
+
return { ...conn, sharedSecret: "", hasSharedSecret: !!conn.sharedSecret };
|
|
441
|
+
}
|
|
257
442
|
connectionsRoute.get(
|
|
258
443
|
"/",
|
|
259
444
|
(c) => c.json(
|
|
260
445
|
listConnections().map((conn) => ({
|
|
261
|
-
...conn,
|
|
446
|
+
...maskSecret(conn),
|
|
262
447
|
buyer: getBuyer(conn.buyerId),
|
|
263
448
|
supplier: getSupplier(conn.supplierId)
|
|
264
449
|
}))
|
|
@@ -268,21 +453,23 @@ connectionsRoute.post("/", async (c) => {
|
|
|
268
453
|
const input = normalize(await c.req.json().catch(() => ({})));
|
|
269
454
|
const errors = validate(input);
|
|
270
455
|
if (errors.length) return c.json({ errors }, 400);
|
|
271
|
-
return c.json(await createConnection(withLabel(input)), 201);
|
|
456
|
+
return c.json(maskSecret(await createConnection(withLabel(input))), 201);
|
|
272
457
|
});
|
|
273
458
|
connectionsRoute.get("/:id", (c) => {
|
|
274
459
|
const resolved = resolveConnection(c.req.param("id"));
|
|
275
|
-
if (resolved) return c.json({ ...resolved.connection, buyer: resolved.buyer, supplier: resolved.supplier });
|
|
460
|
+
if (resolved) return c.json({ ...maskSecret(resolved.connection), buyer: resolved.buyer, supplier: resolved.supplier });
|
|
276
461
|
const conn = getConnection(c.req.param("id"));
|
|
277
|
-
return conn ? c.json(conn) : c.json({ error: "not found" }, 404);
|
|
462
|
+
return conn ? c.json(maskSecret(conn)) : c.json({ error: "not found" }, 404);
|
|
278
463
|
});
|
|
279
464
|
connectionsRoute.put("/:id", async (c) => {
|
|
280
465
|
const existing = getConnection(c.req.param("id"));
|
|
281
466
|
if (!existing) return c.json({ error: "not found" }, 404);
|
|
282
|
-
const
|
|
467
|
+
const body = await c.req.json().catch(() => ({}));
|
|
468
|
+
if (!body || !body.sharedSecret) delete body.sharedSecret;
|
|
469
|
+
const input = normalize({ ...existing, ...body });
|
|
283
470
|
const errors = validate(input);
|
|
284
471
|
if (errors.length) return c.json({ errors }, 400);
|
|
285
|
-
return c.json(await updateConnection(c.req.param("id"), withLabel(input)));
|
|
472
|
+
return c.json(maskSecret(await updateConnection(c.req.param("id"), withLabel(input))));
|
|
286
473
|
});
|
|
287
474
|
connectionsRoute.delete("/:id", async (c) => {
|
|
288
475
|
const ok = await deleteConnection(c.req.param("id"));
|
|
@@ -297,7 +484,11 @@ var cred = (c) => ({
|
|
|
297
484
|
});
|
|
298
485
|
var buyersRoute = new Hono2();
|
|
299
486
|
function normalizeBuyer(body) {
|
|
300
|
-
return {
|
|
487
|
+
return {
|
|
488
|
+
name: String(body?.name ?? "Untitled buyer"),
|
|
489
|
+
identity: cred(body?.identity),
|
|
490
|
+
profileId: body?.profileId ? String(body.profileId) : void 0
|
|
491
|
+
};
|
|
301
492
|
}
|
|
302
493
|
buyersRoute.get("/", (c) => c.json(listBuyers()));
|
|
303
494
|
buyersRoute.post("/", async (c) => {
|
|
@@ -366,8 +557,77 @@ suppliersRoute.delete("/:id", async (c) => {
|
|
|
366
557
|
}
|
|
367
558
|
});
|
|
368
559
|
|
|
369
|
-
// src/server/routes/
|
|
560
|
+
// src/server/routes/profiles.ts
|
|
370
561
|
import { Hono as Hono3 } from "hono";
|
|
562
|
+
var profilesRoute = new Hono3();
|
|
563
|
+
var profilePresetsRoute = new Hono3();
|
|
564
|
+
var VERSION_KEYS = [
|
|
565
|
+
"SetupRequest",
|
|
566
|
+
"SetupResponse",
|
|
567
|
+
"PunchOutOrderMessage",
|
|
568
|
+
"OrderRequest",
|
|
569
|
+
"OrderResponse"
|
|
570
|
+
];
|
|
571
|
+
function normalizeDtdVersions(v) {
|
|
572
|
+
const out = { default: String(v?.default ?? "1.2.045") };
|
|
573
|
+
for (const k of VERSION_KEYS) {
|
|
574
|
+
if (v?.[k]) out[k] = String(v[k]);
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
function normalizeExtrinsics(v) {
|
|
579
|
+
if (!Array.isArray(v)) return [];
|
|
580
|
+
return v.map((e) => ({
|
|
581
|
+
name: String(e?.name ?? ""),
|
|
582
|
+
value: String(e?.value ?? ""),
|
|
583
|
+
scope: e?.scope === "order" ? "order" : "setup"
|
|
584
|
+
})).filter((e) => e.name.trim().length > 0);
|
|
585
|
+
}
|
|
586
|
+
function normalizeProfile(body) {
|
|
587
|
+
const setupOperation = ["create", "edit", "inspect"].includes(body?.setupOperation) ? body.setupOperation : "create";
|
|
588
|
+
const attachmentEncoding = body?.attachmentEncoding === "base64" ? "base64" : "binary";
|
|
589
|
+
const cartReturnTransport = ["cxml-urlencoded", "cxml-base64", "raw"].includes(
|
|
590
|
+
body?.cartReturnTransport
|
|
591
|
+
) ? body.cartReturnTransport : "cxml-urlencoded";
|
|
592
|
+
return {
|
|
593
|
+
name: String(body?.name ?? "Untitled profile"),
|
|
594
|
+
platform: body?.platform ? String(body.platform) : void 0,
|
|
595
|
+
dtdVersions: normalizeDtdVersions(body?.dtdVersions),
|
|
596
|
+
userAgent: String(body?.userAgent ?? "punchout-simulator"),
|
|
597
|
+
setupOperation,
|
|
598
|
+
attachmentEncoding,
|
|
599
|
+
cartReturnTransport,
|
|
600
|
+
extrinsics: normalizeExtrinsics(body?.extrinsics)
|
|
601
|
+
// `builtin` is never set from the wire — only code-seeded presets carry it.
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
profilesRoute.get("/", (c) => c.json(listProfiles()));
|
|
605
|
+
profilesRoute.post("/", async (c) => {
|
|
606
|
+
const input = normalizeProfile(await c.req.json().catch(() => ({})));
|
|
607
|
+
if (!input.name.trim()) return c.json({ errors: ["name is required"] }, 400);
|
|
608
|
+
return c.json(await createProfile(input), 201);
|
|
609
|
+
});
|
|
610
|
+
profilesRoute.get("/:id", (c) => {
|
|
611
|
+
const p = getProfile(c.req.param("id"));
|
|
612
|
+
return p ? c.json(p) : c.json({ error: "not found" }, 404);
|
|
613
|
+
});
|
|
614
|
+
profilesRoute.put("/:id", async (c) => {
|
|
615
|
+
if (!getProfile(c.req.param("id"))) return c.json({ error: "not found" }, 404);
|
|
616
|
+
const input = normalizeProfile(await c.req.json().catch(() => ({})));
|
|
617
|
+
return c.json(await updateProfile(c.req.param("id"), input));
|
|
618
|
+
});
|
|
619
|
+
profilesRoute.delete("/:id", async (c) => {
|
|
620
|
+
try {
|
|
621
|
+
const ok = await deleteProfile(c.req.param("id"));
|
|
622
|
+
return ok ? c.json({ ok: true }) : c.json({ error: "not found" }, 404);
|
|
623
|
+
} catch (e) {
|
|
624
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 409);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
profilePresetsRoute.get("/", (c) => c.json(PROFILE_PRESETS));
|
|
628
|
+
|
|
629
|
+
// src/server/routes/flow.ts
|
|
630
|
+
import { Hono as Hono4 } from "hono";
|
|
371
631
|
import { nanoid as nanoid5 } from "nanoid";
|
|
372
632
|
|
|
373
633
|
// src/server/store/log.ts
|
|
@@ -392,14 +652,19 @@ var bus = new Bus();
|
|
|
392
652
|
bus.setMaxListeners(0);
|
|
393
653
|
|
|
394
654
|
// src/server/store/log.ts
|
|
655
|
+
function redactSecrets(body) {
|
|
656
|
+
if (!body) return body ?? "";
|
|
657
|
+
return body.replace(/(<SharedSecret>)[\s\S]*?(<\/SharedSecret>)/g, "$1***$2");
|
|
658
|
+
}
|
|
395
659
|
function appendLog(input) {
|
|
396
660
|
ensureDirs();
|
|
397
661
|
const record = {
|
|
398
662
|
...input,
|
|
663
|
+
body: redactSecrets(input.body),
|
|
399
664
|
id: input.id ?? nanoid2(12),
|
|
400
665
|
ts: input.ts ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
401
666
|
};
|
|
402
|
-
appendFileSync(sessionFile(record.sessionId), JSON.stringify(record) + "\n", "utf8");
|
|
667
|
+
appendFileSync(sessionFile(record.sessionId), JSON.stringify(record) + "\n", { encoding: "utf8", mode: 384 });
|
|
403
668
|
bus.emitLog(record);
|
|
404
669
|
return record;
|
|
405
670
|
}
|
|
@@ -449,8 +714,13 @@ function normalizeContentId(cid) {
|
|
|
449
714
|
if (!cid) return "";
|
|
450
715
|
return cid.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
451
716
|
}
|
|
717
|
+
function base64Wrapped(data) {
|
|
718
|
+
const lines = data.toString("base64").match(/.{1,76}/g) ?? [];
|
|
719
|
+
return Buffer.from(lines.join("\r\n"), "utf8");
|
|
720
|
+
}
|
|
452
721
|
function buildMultipartRelated(cxml, attachments, opts = {}) {
|
|
453
|
-
const boundary = `cxml-${nanoid3(20)}`;
|
|
722
|
+
const boundary = opts.boundary || `cxml-${nanoid3(20)}`;
|
|
723
|
+
const useBase64 = opts.attachmentEncoding === "base64";
|
|
454
724
|
const mainCid = opts.mainContentId ?? `cxml-main@punchout-simulator`;
|
|
455
725
|
const CRLF = "\r\n";
|
|
456
726
|
const parts = [];
|
|
@@ -473,11 +743,11 @@ function buildMultipartRelated(cxml, attachments, opts = {}) {
|
|
|
473
743
|
pushPart(
|
|
474
744
|
[
|
|
475
745
|
`Content-Type: ${att.contentType}`,
|
|
476
|
-
`Content-Transfer-Encoding: binary`,
|
|
746
|
+
`Content-Transfer-Encoding: ${useBase64 ? "base64" : "binary"}`,
|
|
477
747
|
`Content-ID: <${normalizeContentId(att.contentId)}>`,
|
|
478
748
|
disposition
|
|
479
749
|
],
|
|
480
|
-
att.data
|
|
750
|
+
useBase64 ? base64Wrapped(att.data) : att.data
|
|
481
751
|
);
|
|
482
752
|
}
|
|
483
753
|
parts.push(Buffer.from(`--${boundary}--${CRLF}`, "utf8"));
|
|
@@ -491,6 +761,10 @@ function getBoundary(contentType) {
|
|
|
491
761
|
const m = /boundary="?([^";]+)"?/i.exec(contentType);
|
|
492
762
|
return m?.[1];
|
|
493
763
|
}
|
|
764
|
+
function getStartCid(contentType) {
|
|
765
|
+
const m = /start="?<?([^">]+)>?"?/i.exec(contentType);
|
|
766
|
+
return m ? normalizeContentId(m[1]) : void 0;
|
|
767
|
+
}
|
|
494
768
|
function isMultipart(contentType) {
|
|
495
769
|
return !!contentType && /^multipart\//i.test(contentType.trim());
|
|
496
770
|
}
|
|
@@ -520,6 +794,9 @@ function parseMultipartRelated(body, contentType) {
|
|
|
520
794
|
headers[line.slice(0, idx).trim().toLowerCase()] = line.slice(idx + 1).trim();
|
|
521
795
|
}
|
|
522
796
|
}
|
|
797
|
+
if (headers["content-transfer-encoding"]?.toLowerCase() === "base64") {
|
|
798
|
+
bodyBuf = Buffer.from(bodyBuf.toString("utf8"), "base64");
|
|
799
|
+
}
|
|
523
800
|
parts.push({
|
|
524
801
|
headers,
|
|
525
802
|
contentId: normalizeContentId(headers["content-id"]),
|
|
@@ -568,7 +845,7 @@ function saveAttachment(data, meta) {
|
|
|
568
845
|
ensureDirs();
|
|
569
846
|
const hash = createHash("sha256").update(data).digest("hex");
|
|
570
847
|
const path = resolve2(attachmentsDir(), hash);
|
|
571
|
-
if (!existsSync2(path)) writeFileSync(path, data);
|
|
848
|
+
if (!existsSync2(path)) writeFileSync(path, data, { mode: 384 });
|
|
572
849
|
return {
|
|
573
850
|
contentId: normalizeContentId(meta.contentId),
|
|
574
851
|
filename: meta.filename,
|
|
@@ -603,14 +880,32 @@ function connectionForSession(sessionId) {
|
|
|
603
880
|
}
|
|
604
881
|
|
|
605
882
|
// src/server/http.ts
|
|
883
|
+
function assertSafeOutboundUrl(url) {
|
|
884
|
+
let u;
|
|
885
|
+
try {
|
|
886
|
+
u = new URL(url);
|
|
887
|
+
} catch {
|
|
888
|
+
throw new Error(`invalid URL: ${url}`);
|
|
889
|
+
}
|
|
890
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
891
|
+
throw new Error(`unsupported URL scheme "${u.protocol}" (only http/https allowed)`);
|
|
892
|
+
}
|
|
893
|
+
if (u.username || u.password) {
|
|
894
|
+
throw new Error("credentials embedded in the URL are not allowed");
|
|
895
|
+
}
|
|
896
|
+
}
|
|
606
897
|
async function sendCxml(url, body, contentType = "text/xml; charset=UTF-8", timeoutMs = 3e4) {
|
|
607
898
|
const controller = new AbortController();
|
|
608
899
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
609
900
|
try {
|
|
901
|
+
assertSafeOutboundUrl(url);
|
|
610
902
|
const res = await fetch(url, {
|
|
611
903
|
method: "POST",
|
|
612
904
|
headers: { "Content-Type": contentType },
|
|
613
905
|
body,
|
|
906
|
+
// Do not transparently follow redirects to a different host (SSRF pivot);
|
|
907
|
+
// a 3xx is surfaced to the caller as the response instead.
|
|
908
|
+
redirect: "manual",
|
|
614
909
|
signal: controller.signal
|
|
615
910
|
});
|
|
616
911
|
const headers = {};
|
|
@@ -636,21 +931,6 @@ async function sendCxml(url, body, contentType = "text/xml; charset=UTF-8", time
|
|
|
636
931
|
}
|
|
637
932
|
}
|
|
638
933
|
|
|
639
|
-
// src/server/runtime.ts
|
|
640
|
-
var runtime = {
|
|
641
|
-
port: 8080,
|
|
642
|
-
publicUrl: "http://localhost:8080"
|
|
643
|
-
};
|
|
644
|
-
function setRuntime(r) {
|
|
645
|
-
Object.assign(runtime, r);
|
|
646
|
-
}
|
|
647
|
-
function getPublicUrl() {
|
|
648
|
-
return runtime.publicUrl.replace(/\/$/, "");
|
|
649
|
-
}
|
|
650
|
-
function browserFormPostUrl() {
|
|
651
|
-
return `${getPublicUrl()}/punchout/return`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
934
|
// src/server/cxml/build.ts
|
|
655
935
|
import { nanoid as nanoid4 } from "nanoid";
|
|
656
936
|
function escapeXml(value) {
|
|
@@ -684,14 +964,24 @@ ${credentialBlock("To", p.to)}
|
|
|
684
964
|
${senderBlock(p.sender, p.sharedSecret, p.userAgent)}
|
|
685
965
|
</Header>`;
|
|
686
966
|
}
|
|
687
|
-
var
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
967
|
+
var DEFAULT_DTD_VERSION = "1.2.045";
|
|
968
|
+
function doctype(version) {
|
|
969
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
970
|
+
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/${escapeXml(version)}/cXML.dtd">`;
|
|
971
|
+
}
|
|
972
|
+
function envelope(payloadId, timestamp, lang, inner, dtdVersion = DEFAULT_DTD_VERSION) {
|
|
973
|
+
return `${doctype(dtdVersion)}
|
|
691
974
|
<cXML payloadID="${escapeXml(payloadId)}" timestamp="${escapeXml(timestamp)}" xml:lang="${escapeXml(lang)}">
|
|
692
975
|
${inner}
|
|
693
976
|
</cXML>`;
|
|
694
977
|
}
|
|
978
|
+
function applyExtrinsicTokens(value, tokens) {
|
|
979
|
+
return value.replace(/\$\{(\w+)\}/g, (_, k) => tokens[k] ?? "");
|
|
980
|
+
}
|
|
981
|
+
function extrinsicBlock(items, indent) {
|
|
982
|
+
if (!items || items.length === 0) return "";
|
|
983
|
+
return "\n" + items.map((e) => `${indent}<Extrinsic name="${escapeXml(e.name)}">${escapeXml(e.value)}</Extrinsic>`).join("\n");
|
|
984
|
+
}
|
|
695
985
|
function buildSetupRequest(o) {
|
|
696
986
|
const lang = o.lang ?? "en-US";
|
|
697
987
|
const deployment = o.deploymentMode ? ` deploymentMode="${escapeXml(o.deploymentMode)}"` : "";
|
|
@@ -699,17 +989,18 @@ function buildSetupRequest(o) {
|
|
|
699
989
|
from: o.from,
|
|
700
990
|
to: o.to,
|
|
701
991
|
sender: o.sender,
|
|
702
|
-
sharedSecret: o.sharedSecret
|
|
992
|
+
sharedSecret: o.sharedSecret,
|
|
993
|
+
userAgent: o.userAgent
|
|
703
994
|
})}
|
|
704
995
|
<Request${deployment}>
|
|
705
996
|
<PunchOutSetupRequest operation="${escapeXml(o.operation ?? "create")}">
|
|
706
997
|
<BuyerCookie>${escapeXml(o.buyerCookie)}</BuyerCookie>
|
|
707
998
|
<BrowserFormPost>
|
|
708
999
|
<URL>${escapeXml(o.browserFormPostUrl)}</URL>
|
|
709
|
-
</BrowserFormPost
|
|
1000
|
+
</BrowserFormPost>${extrinsicBlock(o.extrinsics, " ")}
|
|
710
1001
|
</PunchOutSetupRequest>
|
|
711
1002
|
</Request>`;
|
|
712
|
-
return envelope(o.payloadId, o.timestamp, lang, inner);
|
|
1003
|
+
return envelope(o.payloadId, o.timestamp, lang, inner, o.dtdVersion);
|
|
713
1004
|
}
|
|
714
1005
|
function addressBlock(tag, a) {
|
|
715
1006
|
return ` <${tag}>
|
|
@@ -771,7 +1062,8 @@ function buildOrderRequest(o) {
|
|
|
771
1062
|
from: o.from,
|
|
772
1063
|
to: o.to,
|
|
773
1064
|
sender: o.sender,
|
|
774
|
-
sharedSecret: o.sharedSecret
|
|
1065
|
+
sharedSecret: o.sharedSecret,
|
|
1066
|
+
userAgent: o.userAgent
|
|
775
1067
|
})}
|
|
776
1068
|
<Request${deployment}>
|
|
777
1069
|
<OrderRequest>
|
|
@@ -782,12 +1074,15 @@ function buildOrderRequest(o) {
|
|
|
782
1074
|
<Money currency="${escapeXml(o.currency)}">${escapeXml(o.total)}</Money>
|
|
783
1075
|
</Total>
|
|
784
1076
|
${addressBlock("ShipTo", o.shipTo ?? {})}
|
|
785
|
-
${addressBlock("BillTo", o.billTo ?? {})}${commentsWithAttachments(orderLevelCids, " ")}
|
|
1077
|
+
${addressBlock("BillTo", o.billTo ?? {})}${commentsWithAttachments(orderLevelCids, " ")}${extrinsicBlock(
|
|
1078
|
+
o.extrinsics,
|
|
1079
|
+
" "
|
|
1080
|
+
)}
|
|
786
1081
|
</OrderRequestHeader>
|
|
787
1082
|
${items}
|
|
788
1083
|
</OrderRequest>
|
|
789
1084
|
</Request>`;
|
|
790
|
-
return envelope(o.payloadId, o.timestamp, lang, inner);
|
|
1085
|
+
return envelope(o.payloadId, o.timestamp, lang, inner, o.dtdVersion);
|
|
791
1086
|
}
|
|
792
1087
|
function optionalHeader(p) {
|
|
793
1088
|
return p ? `${header({ from: p.from, to: p.to, sender: p.sender })}
|
|
@@ -805,7 +1100,7 @@ function buildSetupResponse(o) {
|
|
|
805
1100
|
</StartPage>
|
|
806
1101
|
</PunchOutSetupResponse>
|
|
807
1102
|
</Response>`;
|
|
808
|
-
return envelope(o.payloadId, o.timestamp, o.lang ?? "en-US", inner);
|
|
1103
|
+
return envelope(o.payloadId, o.timestamp, o.lang ?? "en-US", inner, o.dtdVersion);
|
|
809
1104
|
}
|
|
810
1105
|
function buildResponseStatus(o) {
|
|
811
1106
|
const head = o.from && o.to && o.sender ? optionalHeader({ from: o.from, to: o.to, sender: o.sender }) : "";
|
|
@@ -814,7 +1109,7 @@ function buildResponseStatus(o) {
|
|
|
814
1109
|
o.statusText ?? "OK"
|
|
815
1110
|
)}">${escapeXml(o.statusText === "OK" || !o.statusText ? "" : o.statusText)}</Status>
|
|
816
1111
|
</Response>`;
|
|
817
|
-
return envelope(o.payloadId, o.timestamp, o.lang ?? "en-US", inner);
|
|
1112
|
+
return envelope(o.payloadId, o.timestamp, o.lang ?? "en-US", inner, o.dtdVersion);
|
|
818
1113
|
}
|
|
819
1114
|
function buildPunchOutOrderMessage(o) {
|
|
820
1115
|
const total = o.items.reduce(
|
|
@@ -846,7 +1141,7 @@ function buildPunchOutOrderMessage(o) {
|
|
|
846
1141
|
<Message>
|
|
847
1142
|
<PunchOutOrderMessage>
|
|
848
1143
|
<BuyerCookie>${escapeXml(o.buyerCookie)}</BuyerCookie>
|
|
849
|
-
<PunchOutOrderMessageHeader operationAllowed="create">
|
|
1144
|
+
<PunchOutOrderMessageHeader operationAllowed="${escapeXml(o.operationAllowed ?? "create")}">
|
|
850
1145
|
<Total>
|
|
851
1146
|
<Money currency="${escapeXml(o.currency)}">${escapeXml(total.toFixed(2))}</Money>
|
|
852
1147
|
</Total>
|
|
@@ -854,7 +1149,7 @@ function buildPunchOutOrderMessage(o) {
|
|
|
854
1149
|
${items}
|
|
855
1150
|
</PunchOutOrderMessage>
|
|
856
1151
|
</Message>`;
|
|
857
|
-
return envelope(o.payloadId, o.timestamp, o.lang ?? "en-US", inner);
|
|
1152
|
+
return envelope(o.payloadId, o.timestamp, o.lang ?? "en-US", inner, o.dtdVersion);
|
|
858
1153
|
}
|
|
859
1154
|
|
|
860
1155
|
// src/server/cxml/parse.ts
|
|
@@ -871,6 +1166,9 @@ var parser = new XMLParser({
|
|
|
871
1166
|
isArray: (name) => ["ItemIn", "ItemOut", "Attachment", "Comments", "Extrinsic"].includes(name)
|
|
872
1167
|
});
|
|
873
1168
|
function parseXml(raw) {
|
|
1169
|
+
if (/<!ENTITY/i.test(raw)) {
|
|
1170
|
+
return { raw, tree: null, wellFormed: false, wellFormedError: "DTD entity definitions are not allowed" };
|
|
1171
|
+
}
|
|
874
1172
|
const check = XMLValidator.validate(raw, { allowBooleanAttributes: true });
|
|
875
1173
|
if (check !== true) {
|
|
876
1174
|
return {
|
|
@@ -1121,7 +1419,7 @@ function checkGeneral(doc, ctx, issues) {
|
|
|
1121
1419
|
}
|
|
1122
1420
|
function checkSharedSecret(doc, ctx, issues) {
|
|
1123
1421
|
const exp = ctx.expected;
|
|
1124
|
-
if (!exp
|
|
1422
|
+
if (!exp) return;
|
|
1125
1423
|
const creds = getHeaderCredentials(doc);
|
|
1126
1424
|
if (!creds.sharedSecret) {
|
|
1127
1425
|
issues.warn(
|
|
@@ -1363,7 +1661,7 @@ function validateDocument(raw, ctx = {}) {
|
|
|
1363
1661
|
}
|
|
1364
1662
|
|
|
1365
1663
|
// src/server/routes/flow.ts
|
|
1366
|
-
var flowRoute = new
|
|
1664
|
+
var flowRoute = new Hono4();
|
|
1367
1665
|
function host() {
|
|
1368
1666
|
try {
|
|
1369
1667
|
return new URL(getPublicUrl()).host;
|
|
@@ -1376,6 +1674,7 @@ function buyerContext(r) {
|
|
|
1376
1674
|
const from = buyer.identity;
|
|
1377
1675
|
const to = supplier.identity;
|
|
1378
1676
|
const sender = connection.senderIdentity ?? buyer.identity;
|
|
1677
|
+
const eff = effectiveProfile(connection, buyer);
|
|
1379
1678
|
return {
|
|
1380
1679
|
from,
|
|
1381
1680
|
to,
|
|
@@ -1385,9 +1684,17 @@ function buyerContext(r) {
|
|
|
1385
1684
|
orderUrl: supplier.orderUrl,
|
|
1386
1685
|
deploymentMode: connection.deploymentMode,
|
|
1387
1686
|
connectionId: connection.id,
|
|
1388
|
-
|
|
1687
|
+
attachmentEncoding: eff.attachmentEncoding,
|
|
1688
|
+
eff,
|
|
1689
|
+
expected: { from, to, sender, sharedSecret: connection.sharedSecret }
|
|
1389
1690
|
};
|
|
1390
1691
|
}
|
|
1692
|
+
function setupExtrinsics(ctx, buyerCookie) {
|
|
1693
|
+
return ctx.eff.extrinsics.filter((e) => e.scope === "setup").map((e) => ({ name: e.name, value: applyExtrinsicTokens(e.value, { buyerCookie }) }));
|
|
1694
|
+
}
|
|
1695
|
+
function orderExtrinsics(ctx, orderId) {
|
|
1696
|
+
return ctx.eff.extrinsics.filter((e) => e.scope === "order").map((e) => ({ name: e.name, value: applyExtrinsicTokens(e.value, { orderId }) }));
|
|
1697
|
+
}
|
|
1391
1698
|
function resolveVirtualBuyer(id) {
|
|
1392
1699
|
const r = resolveConnection(id);
|
|
1393
1700
|
if (!r) return { error: "connection not found (or its buyer/supplier is missing)" };
|
|
@@ -1408,7 +1715,11 @@ flowRoute.get("/:id/setup/preview", (c) => {
|
|
|
1408
1715
|
browserFormPostUrl: browserFormPostUrl(),
|
|
1409
1716
|
payloadId: makePayloadId(host(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
1410
1717
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1411
|
-
deploymentMode: ctx.deploymentMode
|
|
1718
|
+
deploymentMode: ctx.deploymentMode,
|
|
1719
|
+
operation: ctx.eff.setupOperation,
|
|
1720
|
+
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
1721
|
+
userAgent: ctx.eff.userAgent,
|
|
1722
|
+
extrinsics: setupExtrinsics(ctx, buyerCookie)
|
|
1412
1723
|
});
|
|
1413
1724
|
return c.json({ buyerCookie, xml, browserFormPostUrl: browserFormPostUrl() });
|
|
1414
1725
|
});
|
|
@@ -1427,7 +1738,11 @@ flowRoute.post("/:id/setup", async (c) => {
|
|
|
1427
1738
|
browserFormPostUrl: browserFormPostUrl(),
|
|
1428
1739
|
payloadId: makePayloadId(host(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
1429
1740
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1430
|
-
deploymentMode: ctx.deploymentMode
|
|
1741
|
+
deploymentMode: ctx.deploymentMode,
|
|
1742
|
+
operation: ctx.eff.setupOperation,
|
|
1743
|
+
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
1744
|
+
userAgent: ctx.eff.userAgent,
|
|
1745
|
+
extrinsics: setupExtrinsics(ctx, buyerCookie)
|
|
1431
1746
|
});
|
|
1432
1747
|
rememberSessionConnection(buyerCookie, ctx.connectionId);
|
|
1433
1748
|
const reqValidation = validateDocument(xml, { expected: ctx.expected, forceDocType: "SetupRequest" });
|
|
@@ -1491,7 +1806,10 @@ function buildOrderXml(ctx, body) {
|
|
|
1491
1806
|
items,
|
|
1492
1807
|
shipTo: body.shipTo,
|
|
1493
1808
|
billTo: body.billTo,
|
|
1494
|
-
attachments: attMeta
|
|
1809
|
+
attachments: attMeta,
|
|
1810
|
+
dtdVersion: dtdVersionFor(ctx.eff, "OrderRequest"),
|
|
1811
|
+
userAgent: ctx.eff.userAgent,
|
|
1812
|
+
extrinsics: orderExtrinsics(ctx, orderId)
|
|
1495
1813
|
});
|
|
1496
1814
|
return { xml, orderId };
|
|
1497
1815
|
}
|
|
@@ -1534,7 +1852,7 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1534
1852
|
data
|
|
1535
1853
|
};
|
|
1536
1854
|
});
|
|
1537
|
-
const built = buildMultipartRelated(xml, parts);
|
|
1855
|
+
const built = buildMultipartRelated(xml, parts, { attachmentEncoding: ctx.attachmentEncoding });
|
|
1538
1856
|
wireBody = built.body;
|
|
1539
1857
|
wireContentType = built.contentType;
|
|
1540
1858
|
}
|
|
@@ -1553,6 +1871,7 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1553
1871
|
contentType: wireContentType,
|
|
1554
1872
|
validation: reqValidation,
|
|
1555
1873
|
attachments: savedRefs,
|
|
1874
|
+
attachmentEncoding: inputAtts.length > 0 ? ctx.attachmentEncoding : void 0,
|
|
1556
1875
|
note: dangling ? "dangling-cid test" : void 0
|
|
1557
1876
|
});
|
|
1558
1877
|
if (!ctx.orderUrl) return c.json({ error: "supplier has no orderUrl configured" }, 400);
|
|
@@ -1581,8 +1900,8 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1581
1900
|
});
|
|
1582
1901
|
|
|
1583
1902
|
// src/server/routes/punchout-return.ts
|
|
1584
|
-
import { Hono as
|
|
1585
|
-
var punchoutReturnRoute = new
|
|
1903
|
+
import { Hono as Hono5 } from "hono";
|
|
1904
|
+
var punchoutReturnRoute = new Hono5();
|
|
1586
1905
|
async function extractCxml(c) {
|
|
1587
1906
|
const ct = (c.req.header("content-type") ?? "").toLowerCase();
|
|
1588
1907
|
if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) {
|
|
@@ -1626,8 +1945,7 @@ punchoutReturnRoute.post("/return", async (c) => {
|
|
|
1626
1945
|
const expected = resolved ? {
|
|
1627
1946
|
from: resolved.buyer.identity,
|
|
1628
1947
|
to: resolved.supplier.identity,
|
|
1629
|
-
sender: resolved.connection.senderIdentity ?? resolved.supplier.identity
|
|
1630
|
-
authStyle: resolved.connection.authStyle
|
|
1948
|
+
sender: resolved.connection.senderIdentity ?? resolved.supplier.identity
|
|
1631
1949
|
} : void 0;
|
|
1632
1950
|
const validation = validateDocument(xml, {
|
|
1633
1951
|
expected,
|
|
@@ -1651,10 +1969,14 @@ punchoutReturnRoute.post("/return", async (c) => {
|
|
|
1651
1969
|
});
|
|
1652
1970
|
|
|
1653
1971
|
// src/server/routes/stream.ts
|
|
1654
|
-
import { Hono as
|
|
1972
|
+
import { Hono as Hono6 } from "hono";
|
|
1655
1973
|
import { streamSSE } from "hono/streaming";
|
|
1656
|
-
var streamRoute = new
|
|
1974
|
+
var streamRoute = new Hono6();
|
|
1975
|
+
var MAX_STREAMS = 64;
|
|
1976
|
+
var activeStreams = 0;
|
|
1657
1977
|
streamRoute.get("/stream", (c) => {
|
|
1978
|
+
if (activeStreams >= MAX_STREAMS) return c.text("too many live-log connections", 503);
|
|
1979
|
+
activeStreams++;
|
|
1658
1980
|
return streamSSE(c, async (stream) => {
|
|
1659
1981
|
let open = true;
|
|
1660
1982
|
let id = 0;
|
|
@@ -1668,6 +1990,7 @@ streamRoute.get("/stream", (c) => {
|
|
|
1668
1990
|
});
|
|
1669
1991
|
stream.onAbort(() => {
|
|
1670
1992
|
open = false;
|
|
1993
|
+
activeStreams = Math.max(0, activeStreams - 1);
|
|
1671
1994
|
unsubscribe();
|
|
1672
1995
|
});
|
|
1673
1996
|
await stream.writeSSE({ event: "ready", data: JSON.stringify({ ts: Date.now() }) });
|
|
@@ -1680,8 +2003,30 @@ streamRoute.get("/stream", (c) => {
|
|
|
1680
2003
|
});
|
|
1681
2004
|
|
|
1682
2005
|
// src/server/routes/data.ts
|
|
1683
|
-
import { Hono as
|
|
1684
|
-
var dataRoute = new
|
|
2006
|
+
import { Hono as Hono7 } from "hono";
|
|
2007
|
+
var dataRoute = new Hono7();
|
|
2008
|
+
function rawMessage(record) {
|
|
2009
|
+
const ct = record.contentType ?? record.headers?.["Content-Type"] ?? record.headers?.["content-type"];
|
|
2010
|
+
let body = record.body;
|
|
2011
|
+
if (ct && isMultipart(ct) && record.attachments && record.attachments.length > 0) {
|
|
2012
|
+
const parts = record.attachments.map((a) => ({
|
|
2013
|
+
contentId: a.contentId,
|
|
2014
|
+
filename: a.filename,
|
|
2015
|
+
contentType: a.contentType,
|
|
2016
|
+
data: readAttachment(a.hash) ?? Buffer.alloc(0)
|
|
2017
|
+
}));
|
|
2018
|
+
const built = buildMultipartRelated(record.body, parts, {
|
|
2019
|
+
boundary: getBoundary(ct),
|
|
2020
|
+
mainContentId: getStartCid(ct),
|
|
2021
|
+
attachmentEncoding: record.attachmentEncoding
|
|
2022
|
+
});
|
|
2023
|
+
body = built.body.toString("utf8");
|
|
2024
|
+
}
|
|
2025
|
+
const headerLines = Object.entries(record.headers ?? {}).map(([k, v]) => `${k}: ${v}`);
|
|
2026
|
+
return headerLines.length > 0 ? `${headerLines.join("\r\n")}\r
|
|
2027
|
+
\r
|
|
2028
|
+
${body}` : body;
|
|
2029
|
+
}
|
|
1685
2030
|
dataRoute.get("/health", (c) => c.json({ ok: true }));
|
|
1686
2031
|
dataRoute.get(
|
|
1687
2032
|
"/runtime",
|
|
@@ -1689,6 +2034,12 @@ dataRoute.get(
|
|
|
1689
2034
|
);
|
|
1690
2035
|
dataRoute.get("/sessions", (c) => c.json(listSessions()));
|
|
1691
2036
|
dataRoute.get("/sessions/:id", (c) => c.json(readSession(c.req.param("id"))));
|
|
2037
|
+
dataRoute.get("/sessions/:sessionId/records/:recordId/raw", (c) => {
|
|
2038
|
+
const record = readSession(c.req.param("sessionId")).find((r) => r.id === c.req.param("recordId"));
|
|
2039
|
+
if (!record) return c.json({ error: "not found" }, 404);
|
|
2040
|
+
c.header("Content-Type", "text/plain; charset=utf-8");
|
|
2041
|
+
return c.body(rawMessage(record));
|
|
2042
|
+
});
|
|
1692
2043
|
dataRoute.get("/recent", (c) => {
|
|
1693
2044
|
const limit = Number(c.req.query("limit") ?? "200");
|
|
1694
2045
|
return c.json(readAllRecent(Number.isFinite(limit) ? limit : 200));
|
|
@@ -1705,9 +2056,9 @@ dataRoute.get("/attachments/:hash", (c) => {
|
|
|
1705
2056
|
});
|
|
1706
2057
|
|
|
1707
2058
|
// src/server/routes/sim.ts
|
|
1708
|
-
import { Hono as
|
|
2059
|
+
import { Hono as Hono8 } from "hono";
|
|
1709
2060
|
import { nanoid as nanoid6 } from "nanoid";
|
|
1710
|
-
var simRoute = new
|
|
2061
|
+
var simRoute = new Hono8();
|
|
1711
2062
|
var DEMO_CATALOG = [
|
|
1712
2063
|
{ supplierPartId: "WIDGET-001", description: "Premium Steel Widget", unitPrice: 12.5, currency: "USD", uom: "EA", unspsc: "31161500", manufacturerPartId: "MFR-W001", manufacturerName: "Acme Manufacturing" },
|
|
1713
2064
|
{ supplierPartId: "BOLT-250", description: "M8 Hex Bolt (pack of 250)", unitPrice: 34, currency: "USD", uom: "PK", unspsc: "31161600", manufacturerPartId: "MFR-B250", manufacturerName: "Acme Manufacturing" },
|
|
@@ -1725,11 +2076,14 @@ function safeHttpUrl(u) {
|
|
|
1725
2076
|
if (!u) return "";
|
|
1726
2077
|
try {
|
|
1727
2078
|
const p = new URL(u.trim());
|
|
1728
|
-
return p.protocol === "http:" || p.protocol === "https:" ?
|
|
2079
|
+
return p.protocol === "http:" || p.protocol === "https:" ? p.href : "";
|
|
1729
2080
|
} catch {
|
|
1730
2081
|
return "";
|
|
1731
2082
|
}
|
|
1732
2083
|
}
|
|
2084
|
+
function jsonForScript(value) {
|
|
2085
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2086
|
+
}
|
|
1733
2087
|
function expectedFor(supplierId, from) {
|
|
1734
2088
|
const r = findConnectionBySupplierAndBuyerIdentity(supplierId, from);
|
|
1735
2089
|
if (!r) return void 0;
|
|
@@ -1737,10 +2091,13 @@ function expectedFor(supplierId, from) {
|
|
|
1737
2091
|
from: r.buyer.identity,
|
|
1738
2092
|
to: r.supplier.identity,
|
|
1739
2093
|
sender: r.connection.senderIdentity ?? r.buyer.identity,
|
|
1740
|
-
sharedSecret: r.connection.sharedSecret
|
|
1741
|
-
authStyle: r.connection.authStyle
|
|
2094
|
+
sharedSecret: r.connection.sharedSecret
|
|
1742
2095
|
};
|
|
1743
2096
|
}
|
|
2097
|
+
function effFor(supplierId, from) {
|
|
2098
|
+
const r = findConnectionBySupplierAndBuyerIdentity(supplierId, from);
|
|
2099
|
+
return r ? effectiveProfile(r.connection, r.buyer) : void 0;
|
|
2100
|
+
}
|
|
1744
2101
|
simRoute.post("/:id/punchout", async (c) => {
|
|
1745
2102
|
const supplier = getSupplier(c.req.param("id"));
|
|
1746
2103
|
if (!supplier) return c.text("supplier not found", 404);
|
|
@@ -1761,13 +2118,15 @@ simRoute.post("/:id/punchout", async (c) => {
|
|
|
1761
2118
|
});
|
|
1762
2119
|
const buyerCred = from ?? { domain: "", identity: "" };
|
|
1763
2120
|
const startPageUrl = `${getPublicUrl()}/sim/${supplier.id}/catalog?cookie=${encodeURIComponent(buyerCookie)}&formpost=${encodeURIComponent(formPost)}&bd=${encodeURIComponent(buyerCred.domain)}&bi=${encodeURIComponent(buyerCred.identity)}`;
|
|
2121
|
+
const eff = effFor(supplier.id, from);
|
|
1764
2122
|
const respXml = buildSetupResponse({
|
|
1765
2123
|
payloadId: makePayloadId(host2(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
1766
2124
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1767
2125
|
startPageUrl,
|
|
1768
2126
|
from: supplier.identity,
|
|
1769
2127
|
to: buyerCred,
|
|
1770
|
-
sender: supplier.identity
|
|
2128
|
+
sender: supplier.identity,
|
|
2129
|
+
dtdVersion: eff ? dtdVersionFor(eff, "SetupResponse") : void 0
|
|
1771
2130
|
});
|
|
1772
2131
|
appendLog({
|
|
1773
2132
|
sessionId: buyerCookie,
|
|
@@ -1851,6 +2210,7 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
1851
2210
|
}
|
|
1852
2211
|
});
|
|
1853
2212
|
const currency = items[0]?.currency ?? "USD";
|
|
2213
|
+
const eff = effFor(supplier.id, buyerCred);
|
|
1854
2214
|
const xml = buildPunchOutOrderMessage({
|
|
1855
2215
|
from: supplier.identity,
|
|
1856
2216
|
to: buyerCred,
|
|
@@ -1859,7 +2219,8 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
1859
2219
|
payloadId: makePayloadId(host2(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
1860
2220
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1861
2221
|
currency,
|
|
1862
|
-
items
|
|
2222
|
+
items,
|
|
2223
|
+
dtdVersion: eff ? dtdVersionFor(eff, "PunchOutOrderMessage") : void 0
|
|
1863
2224
|
});
|
|
1864
2225
|
appendLog({
|
|
1865
2226
|
sessionId: cookie,
|
|
@@ -1870,16 +2231,43 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
1870
2231
|
body: xml,
|
|
1871
2232
|
validation: validateDocument(xml, { forceDocType: "PunchOutOrderMessage" })
|
|
1872
2233
|
});
|
|
1873
|
-
|
|
2234
|
+
const transport = eff?.cartReturnTransport ?? "cxml-urlencoded";
|
|
2235
|
+
return c.html(cartReturnPage(formpost, xml, transport));
|
|
2236
|
+
});
|
|
2237
|
+
function cartReturnPage(formpost, xml, transport) {
|
|
2238
|
+
const shell = (inner) => `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
1874
2239
|
<title>Returning cart\u2026</title></head>
|
|
1875
|
-
<body
|
|
2240
|
+
<body style="font-family:system-ui;background:#0f172a;color:#e2e8f0">
|
|
1876
2241
|
<p style="padding:2rem">Returning cart to the buyer\u2026</p>
|
|
1877
|
-
|
|
2242
|
+
${inner}
|
|
2243
|
+
</body></html>`;
|
|
2244
|
+
if (transport === "raw") {
|
|
2245
|
+
return shell(` <form method="post" action="${escapeXml(formpost)}">
|
|
1878
2246
|
<input type="hidden" name="cxml-urlencoded" value="${escapeXml(xml)}">
|
|
1879
2247
|
<noscript><button type="submit">Continue</button></noscript>
|
|
1880
2248
|
</form>
|
|
1881
|
-
|
|
1882
|
-
}
|
|
2249
|
+
<script>
|
|
2250
|
+
fetch(${jsonForScript(formpost)}, { method: "POST",
|
|
2251
|
+
headers: { "Content-Type": "text/xml; charset=UTF-8" },
|
|
2252
|
+
body: ${jsonForScript(xml)} })
|
|
2253
|
+
.then(function () {
|
|
2254
|
+
document.body.replaceChildren();
|
|
2255
|
+
var p = document.createElement("p");
|
|
2256
|
+
p.style.padding = "2rem";
|
|
2257
|
+
p.textContent = "Cart returned to the buyer. You can close this tab.";
|
|
2258
|
+
document.body.appendChild(p);
|
|
2259
|
+
})
|
|
2260
|
+
.catch(function () { document.forms[0].submit(); });
|
|
2261
|
+
</script>`);
|
|
2262
|
+
}
|
|
2263
|
+
const field = transport === "cxml-base64" ? "cxml-base64" : "cxml-urlencoded";
|
|
2264
|
+
const value = transport === "cxml-base64" ? Buffer.from(xml, "utf8").toString("base64") : xml;
|
|
2265
|
+
return shell(` <form method="post" action="${escapeXml(formpost)}">
|
|
2266
|
+
<input type="hidden" name="${field}" value="${escapeXml(value)}">
|
|
2267
|
+
<noscript><button type="submit">Continue</button></noscript>
|
|
2268
|
+
</form>
|
|
2269
|
+
<script>document.forms[0].submit()</script>`);
|
|
2270
|
+
}
|
|
1883
2271
|
simRoute.post("/:id/order", async (c) => {
|
|
1884
2272
|
const supplier = getSupplier(c.req.param("id"));
|
|
1885
2273
|
if (!supplier) return c.text("supplier not found", 404);
|
|
@@ -1888,6 +2276,7 @@ simRoute.post("/:id/order", async (c) => {
|
|
|
1888
2276
|
let xml;
|
|
1889
2277
|
const availableContentIds = /* @__PURE__ */ new Set();
|
|
1890
2278
|
const savedRefs = [];
|
|
2279
|
+
let attachmentEncoding;
|
|
1891
2280
|
if (isMultipart(ct)) {
|
|
1892
2281
|
const mp = parseMultipartRelated(raw, ct);
|
|
1893
2282
|
xml = mp.root?.body.toString("utf8") ?? "";
|
|
@@ -1895,8 +2284,12 @@ simRoute.post("/:id/order", async (c) => {
|
|
|
1895
2284
|
if (part === mp.root) continue;
|
|
1896
2285
|
const cid = normalizeContentId(part.contentId);
|
|
1897
2286
|
if (cid) availableContentIds.add(cid);
|
|
2287
|
+
if ((part.headers["content-transfer-encoding"] ?? "").toLowerCase() === "base64") {
|
|
2288
|
+
attachmentEncoding = "base64";
|
|
2289
|
+
}
|
|
1898
2290
|
savedRefs.push(saveAttachment(part.body, { contentId: cid, contentType: part.contentType ?? "application/octet-stream" }));
|
|
1899
2291
|
}
|
|
2292
|
+
if (savedRefs.length > 0 && !attachmentEncoding) attachmentEncoding = "binary";
|
|
1900
2293
|
} else {
|
|
1901
2294
|
xml = raw.toString("utf8");
|
|
1902
2295
|
}
|
|
@@ -1917,9 +2310,11 @@ simRoute.post("/:id/order", async (c) => {
|
|
|
1917
2310
|
body: xml,
|
|
1918
2311
|
contentType: ct,
|
|
1919
2312
|
validation,
|
|
1920
|
-
attachments: savedRefs
|
|
2313
|
+
attachments: savedRefs,
|
|
2314
|
+
attachmentEncoding
|
|
1921
2315
|
});
|
|
1922
2316
|
const ok = validation.ok;
|
|
2317
|
+
const eff = effFor(supplier.id, from);
|
|
1923
2318
|
const respXml = buildResponseStatus({
|
|
1924
2319
|
payloadId: makePayloadId(host2(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
1925
2320
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1927,7 +2322,8 @@ simRoute.post("/:id/order", async (c) => {
|
|
|
1927
2322
|
statusText: ok ? "OK" : "Bad Request",
|
|
1928
2323
|
from: supplier.identity,
|
|
1929
2324
|
to: from ?? { domain: "", identity: "" },
|
|
1930
|
-
sender: supplier.identity
|
|
2325
|
+
sender: supplier.identity,
|
|
2326
|
+
dtdVersion: eff ? dtdVersionFor(eff, "OrderResponse") : void 0
|
|
1931
2327
|
});
|
|
1932
2328
|
appendLog({
|
|
1933
2329
|
sessionId,
|
|
@@ -1950,10 +2346,25 @@ function findSessionForOrder(doc) {
|
|
|
1950
2346
|
// src/server/app.ts
|
|
1951
2347
|
import { relative } from "path";
|
|
1952
2348
|
function createApp(opts = {}) {
|
|
1953
|
-
const app = new
|
|
2349
|
+
const app = new Hono9();
|
|
1954
2350
|
if (!opts.quiet) app.use("*", logger());
|
|
2351
|
+
app.use(
|
|
2352
|
+
"*",
|
|
2353
|
+
bodyLimit({ maxSize: 24 * 1024 * 1024, onError: (c) => c.json({ error: "payload too large" }, 413) })
|
|
2354
|
+
);
|
|
2355
|
+
app.use("/api/*", async (c, next) => {
|
|
2356
|
+
const token = getToken();
|
|
2357
|
+
if (!token) return next();
|
|
2358
|
+
if (c.req.path === "/api/health") return next();
|
|
2359
|
+
const auth = c.req.header("authorization") ?? "";
|
|
2360
|
+
const provided = (auth.toLowerCase().startsWith("bearer ") ? auth.slice(7).trim() : "") || c.req.header("x-pos-token") || c.req.query("token") || getCookie(c, "pos-api-token") || "";
|
|
2361
|
+
if (provided === token) return next();
|
|
2362
|
+
return c.json({ error: "unauthorized" }, 401);
|
|
2363
|
+
});
|
|
1955
2364
|
app.route("/api/buyers", buyersRoute);
|
|
1956
2365
|
app.route("/api/suppliers", suppliersRoute);
|
|
2366
|
+
app.route("/api/profiles", profilesRoute);
|
|
2367
|
+
app.route("/api/profile-presets", profilePresetsRoute);
|
|
1957
2368
|
app.route("/api/connections", connectionsRoute);
|
|
1958
2369
|
app.route("/api/connections", flowRoute);
|
|
1959
2370
|
app.route("/api", dataRoute);
|
|
@@ -1984,7 +2395,10 @@ async function seedDemoIfEmpty() {
|
|
|
1984
2395
|
const buyer = await createBuyer({
|
|
1985
2396
|
id: "demo-buyer",
|
|
1986
2397
|
name: "Demo Buyer",
|
|
1987
|
-
identity: { domain: "DUNS", identity: "123456789" }
|
|
2398
|
+
identity: { domain: "DUNS", identity: "123456789" },
|
|
2399
|
+
// Exercise a non-default platform profile end-to-end (Coupa: per-doc-type
|
|
2400
|
+
// DTD versions, base64 attachments). Built-in profiles are seeded by initConfig.
|
|
2401
|
+
profileId: "coupa"
|
|
1988
2402
|
});
|
|
1989
2403
|
const supplier = await createSupplier({
|
|
1990
2404
|
id: "demo-supplier",
|
|
@@ -2001,8 +2415,7 @@ async function seedDemoIfEmpty() {
|
|
|
2001
2415
|
supplierId: supplier.id,
|
|
2002
2416
|
mode: "virtual-buyer",
|
|
2003
2417
|
sharedSecret: "demo-secret",
|
|
2004
|
-
deploymentMode: "test"
|
|
2005
|
-
authStyle: "SharedSecret"
|
|
2418
|
+
deploymentMode: "test"
|
|
2006
2419
|
});
|
|
2007
2420
|
}
|
|
2008
2421
|
|
|
@@ -2011,6 +2424,8 @@ function parseFlags(argv) {
|
|
|
2011
2424
|
const flags = {
|
|
2012
2425
|
port: Number(process.env.PORT ?? 8080),
|
|
2013
2426
|
dataDir: process.env.DATA_DIR ?? "./data",
|
|
2427
|
+
host: process.env.HOST,
|
|
2428
|
+
token: process.env.POS_TOKEN,
|
|
2014
2429
|
open: true,
|
|
2015
2430
|
dev: false,
|
|
2016
2431
|
seed: true
|
|
@@ -2030,6 +2445,12 @@ function parseFlags(argv) {
|
|
|
2030
2445
|
case "--public-url":
|
|
2031
2446
|
flags.publicUrl = next();
|
|
2032
2447
|
break;
|
|
2448
|
+
case "--host":
|
|
2449
|
+
flags.host = next();
|
|
2450
|
+
break;
|
|
2451
|
+
case "--token":
|
|
2452
|
+
flags.token = next();
|
|
2453
|
+
break;
|
|
2033
2454
|
case "--no-open":
|
|
2034
2455
|
flags.open = false;
|
|
2035
2456
|
break;
|
|
@@ -2048,6 +2469,14 @@ function parseFlags(argv) {
|
|
|
2048
2469
|
}
|
|
2049
2470
|
return flags;
|
|
2050
2471
|
}
|
|
2472
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", ""]);
|
|
2473
|
+
function hostnameOf(url) {
|
|
2474
|
+
try {
|
|
2475
|
+
return new URL(url).hostname;
|
|
2476
|
+
} catch {
|
|
2477
|
+
return "";
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2051
2480
|
function printHelp() {
|
|
2052
2481
|
console.log(`punchout-simulator \u2014 test cXML PunchOut integrations as a virtual counterparty
|
|
2053
2482
|
|
|
@@ -2058,6 +2487,9 @@ Options:
|
|
|
2058
2487
|
-d, --data-dir <path> Where to store config + logs (default ./data)
|
|
2059
2488
|
--public-url <url> Externally reachable base URL (default http://localhost:<port>)
|
|
2060
2489
|
Set this when fronting the tool with ngrok/cloudflared.
|
|
2490
|
+
--host <addr> Bind address (default 127.0.0.1; use 0.0.0.0 for LAN)
|
|
2491
|
+
--token <secret> Require this token on /api (auto-generated when exposed
|
|
2492
|
+
and not provided; or set POS_TOKEN)
|
|
2061
2493
|
--no-open Do not open a browser on start
|
|
2062
2494
|
--no-seed Do not seed the built-in demo connections on first run
|
|
2063
2495
|
--dev Dev mode (do not serve SPA, do not open browser)
|
|
@@ -2067,22 +2499,32 @@ Options:
|
|
|
2067
2499
|
async function main() {
|
|
2068
2500
|
const flags = parseFlags(process.argv.slice(2));
|
|
2069
2501
|
const publicUrl = flags.publicUrl ?? `http://localhost:${flags.port}`;
|
|
2502
|
+
const bindHost = flags.host ?? "127.0.0.1";
|
|
2503
|
+
const exposed = !LOOPBACK.has(hostnameOf(publicUrl)) || !LOOPBACK.has(bindHost);
|
|
2504
|
+
const token = flags.token || (exposed ? nanoid7(24) : void 0);
|
|
2070
2505
|
setDataDir(flags.dataDir);
|
|
2071
|
-
setRuntime({ port: flags.port, publicUrl });
|
|
2506
|
+
setRuntime({ port: flags.port, publicUrl, token });
|
|
2072
2507
|
await initConfig();
|
|
2073
2508
|
if (flags.seed) await seedDemoIfEmpty();
|
|
2074
2509
|
const webRoot = flags.dev ? void 0 : fileURLToPath(new URL("../web", import.meta.url));
|
|
2075
2510
|
const app = createApp({ webRoot, quiet: false });
|
|
2076
|
-
serve({ fetch: app.fetch, port: flags.port }, (info) => {
|
|
2077
|
-
const
|
|
2511
|
+
serve({ fetch: app.fetch, port: flags.port, hostname: bindHost }, (info) => {
|
|
2512
|
+
const local = `http://${bindHost}:${info.port}`;
|
|
2078
2513
|
console.log(`
|
|
2079
|
-
punchout-simulator listening on ${
|
|
2080
|
-
if (getPublicUrl() !==
|
|
2514
|
+
punchout-simulator listening on ${local}`);
|
|
2515
|
+
if (getPublicUrl() !== local) console.log(` public URL: ${getPublicUrl()}`);
|
|
2081
2516
|
console.log(` data dir: ${flags.dataDir}`);
|
|
2082
|
-
console.log(` callback: ${getPublicUrl()}/punchout/return
|
|
2083
|
-
`
|
|
2517
|
+
console.log(` callback: ${getPublicUrl()}/punchout/return`);
|
|
2518
|
+
const openUrl = token ? `${getPublicUrl()}/?token=${token}` : local;
|
|
2519
|
+
if (token) {
|
|
2520
|
+
console.log(`
|
|
2521
|
+
\u26A0 EXPOSED: /api requires a token. Open the UI with the token:`);
|
|
2522
|
+
console.log(` ${openUrl}`);
|
|
2523
|
+
console.log(` (inbound /sim and /punchout stay open for buyer traffic)`);
|
|
2524
|
+
}
|
|
2525
|
+
console.log("");
|
|
2084
2526
|
if (flags.open) {
|
|
2085
|
-
import("open").then((m) => m.default(
|
|
2527
|
+
import("open").then((m) => m.default(token ? `http://localhost:${info.port}/?token=${token}` : local)).catch(() => {
|
|
2086
2528
|
});
|
|
2087
2529
|
}
|
|
2088
2530
|
});
|
|
@@ -2091,4 +2533,3 @@ main().catch((e) => {
|
|
|
2091
2533
|
console.error(e);
|
|
2092
2534
|
process.exit(1);
|
|
2093
2535
|
});
|
|
2094
|
-
//# sourceMappingURL=cli.js.map
|