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