@rubytech/create-sitedesk-code 0.1.451 → 0.1.453
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/dist/__tests__/preserve-house-cloudflare.test.js +81 -2
- package/dist/index.js +10 -1
- package/dist/preserve-house-cloudflare.js +49 -2
- package/package.json +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +24 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/house-scoped-token.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-scoped-token.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-scoped-token.test.js +48 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-scoped-token.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.js +6 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.js +14 -4
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +18 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.d.ts +9 -0
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.js +60 -0
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.js +1 -0
- package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/remediate.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/remediate.js +14 -1
- package/payload/platform/lib/storage-broker/dist/remediate.js.map +1 -1
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +28 -1
- package/payload/platform/lib/storage-broker/src/__tests__/house-scoped-token.test.ts +48 -0
- package/payload/platform/lib/storage-broker/src/__tests__/remediate.test.ts +8 -0
- package/payload/platform/lib/storage-broker/src/bin/remediate-run.ts +17 -5
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +22 -0
- package/payload/platform/lib/storage-broker/src/house-scoped-token.ts +72 -0
- package/payload/platform/lib/storage-broker/src/index.ts +1 -0
- package/payload/platform/lib/storage-broker/src/remediate.ts +14 -1
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/quote-render-gate.test.sh +107 -0
- package/payload/platform/plugins/admin/hooks/quote-render-gate.sh +74 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +4 -4
- package/payload/platform/plugins/cloudflare/bin/__tests__/cf-token.test.sh +25 -1
- package/payload/platform/plugins/cloudflare/bin/cf-token.sh +9 -1
- package/payload/platform/plugins/cloudflare/references/api.md +3 -1
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/outlook-guide.md +3 -3
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +3 -3
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/complete-registration.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/complete-registration.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/complete-registration.test.js +131 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/complete-registration.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.js +55 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.js +120 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js +66 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-scan.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-scan.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-scan.test.js +48 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-scan.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/complete-registration.d.ts +31 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/complete-registration.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/complete-registration.js +47 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/complete-registration.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pending-scan.d.ts +11 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pending-scan.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pending-scan.js +31 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pending-scan.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js +13 -9
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +19 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +14 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/scripts/complete-registration.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/complete-registration.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/complete-registration.js +111 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/complete-registration.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts +16 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js +37 -7
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts +12 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js +16 -12
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js.map +1 -1
- package/payload/platform/plugins/outlook/references/auth.md +2 -0
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +20 -9
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +7 -7
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/house-scoped-token.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/house-scoped-token.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/house-scoped-token.test.js +36 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/house-scoped-token.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/house-scoped-token.d.ts +9 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/house-scoped-token.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/house-scoped-token.js +51 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/house-scoped-token.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +16 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +16 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/scripts/lib/provision-account-dir.sh +6 -0
- package/payload/platform/scripts/smoke-boot-services.sh +1 -0
- package/payload/platform/scripts/voice-mirror-audit.sh +70 -0
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +7 -1
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +15 -0
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +38 -3
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +21 -10
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +20 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/plugin-enum.d.ts +34 -0
- package/payload/platform/services/claude-session-manager/dist/plugin-enum.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/plugin-enum.js +66 -0
- package/payload/platform/services/claude-session-manager/dist/plugin-enum.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +8 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +7 -0
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/premium-plugins/sitedesk/agents/sitedesk--quoter.md +2 -2
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/PLUGIN.md +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-receipt.d.ts +17 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-receipt.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-receipt.js +27 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-receipt.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.d.ts +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.d.ts.map +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.js +34 -3
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.js.map +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-render.js +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-render.js.map +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/__tests__/render-run.test.ts +68 -2
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/render-receipt.ts +36 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/render-run.ts +38 -4
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/tools/quote-render.ts +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/skills/quotation/references/quote-generation.md +52 -24
- package/payload/server/{chunk-TTKYHVA7.js → chunk-SDYRKIYY.js} +70 -3
- package/payload/server/server.js +59 -10
- package/payload/server/{src-FBUXV2FV.js → src-554BYJMN.js} +5 -1
|
@@ -10,6 +10,7 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
12
|
import { gateReconcile, gateMarginLeak, gateSignLink } from "./gates.js";
|
|
13
|
+
import { writeRenderReceipt } from "./render-receipt.js";
|
|
13
14
|
export async function runRender(a) {
|
|
14
15
|
if (!existsSync(a.renderScriptPath)) {
|
|
15
16
|
return { ok: false, reason: "render-absent", detail: `no render script at ${a.renderScriptPath}` };
|
|
@@ -62,10 +63,30 @@ export async function runRender(a) {
|
|
|
62
63
|
const recQuote = gateReconcile(html.quote, engineTotal);
|
|
63
64
|
const leakBreakdown = gateMarginLeak(html.breakdown);
|
|
64
65
|
const leakQuote = gateMarginLeak(html.quote);
|
|
65
|
-
|
|
66
|
+
// E-sign is opt-in. The Sign-Online acceptance block and its link-liveness
|
|
67
|
+
// gate apply only when the operator supplied a signUrl for this job. On the
|
|
68
|
+
// default (no-signer) path the client quote must carry no acceptance anchor;
|
|
69
|
+
// a stray one is a dead/placeholder button and fails closed the same way a
|
|
70
|
+
// margin leak does. gateSignLink returns reason "no-anchor" exactly when the
|
|
71
|
+
// marked anchor is absent, so anchor-present is `reason !== "no-anchor"`.
|
|
72
|
+
const esignRequested = typeof a.signUrl === "string" && a.signUrl.length > 0;
|
|
73
|
+
const signVerdict = gateSignLink(html.quote);
|
|
74
|
+
const hasAnchor = signVerdict.reason !== "no-anchor";
|
|
75
|
+
let signOk;
|
|
76
|
+
let signDetail;
|
|
77
|
+
if (esignRequested) {
|
|
78
|
+
signOk = signVerdict.pass;
|
|
79
|
+
signDetail = `quote Sign-Online link ${signVerdict.reason} (href=${signVerdict.href ?? ""})`;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
signOk = !hasAnchor;
|
|
83
|
+
signDetail = "quote carries a Sign-Online acceptance anchor but no signer was requested (no signUrl) — e-sign is opt-in";
|
|
84
|
+
}
|
|
85
|
+
// The verdict is reported only when a signer was requested; null otherwise.
|
|
86
|
+
const signLink = esignRequested ? signVerdict : null;
|
|
66
87
|
const internalOk = recInternal.pass;
|
|
67
88
|
const breakdownOk = recBreakdown.pass && leakBreakdown.pass;
|
|
68
|
-
const quoteOk = recQuote.pass && leakQuote.pass &&
|
|
89
|
+
const quoteOk = recQuote.pass && leakQuote.pass && signOk;
|
|
69
90
|
if (!internalOk || !breakdownOk || !quoteOk) {
|
|
70
91
|
// Reason names the first failing gate, in document then gate order.
|
|
71
92
|
let reason;
|
|
@@ -92,7 +113,7 @@ export async function runRender(a) {
|
|
|
92
113
|
}
|
|
93
114
|
else {
|
|
94
115
|
reason = "sign-link";
|
|
95
|
-
detail =
|
|
116
|
+
detail = signDetail;
|
|
96
117
|
}
|
|
97
118
|
return { ok: false, reason, detail };
|
|
98
119
|
}
|
|
@@ -103,6 +124,16 @@ export async function runRender(a) {
|
|
|
103
124
|
mkdirSync(dirname(p), { recursive: true });
|
|
104
125
|
writeFileSync(p, body);
|
|
105
126
|
}
|
|
127
|
+
// Only now — every gate passed and all three docs filed — record the durable
|
|
128
|
+
// receipt. The delivery gate reads it to prove a real render produced this
|
|
129
|
+
// job's client quote; a fail-closed render above returned before this point,
|
|
130
|
+
// so an un-rendered job has no receipt.
|
|
131
|
+
writeRenderReceipt(a.accountDir, {
|
|
132
|
+
jobId: a.jobId,
|
|
133
|
+
engineTotal,
|
|
134
|
+
filed: { internal: internalPath, breakdown: breakdownPath, quote: quotePath },
|
|
135
|
+
renderedAt: new Date().toISOString(),
|
|
136
|
+
});
|
|
106
137
|
return {
|
|
107
138
|
ok: true,
|
|
108
139
|
docs: { internal: internalPath, breakdown: breakdownPath, quote: quotePath },
|
package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-run.js","sourceRoot":"","sources":["../../src/lib/render-run.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,uEAAuE;AACvE,EAAE;AACF,wEAAwE;AACxE,iFAAiF;AAEjF,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAwB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"render-run.js","sourceRoot":"","sources":["../../src/lib/render-run.ts"],"names":[],"mappings":"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"}
|
package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-render.js
CHANGED
|
@@ -11,7 +11,7 @@ import { resolveAccountDir } from "../lib/account.js";
|
|
|
11
11
|
import { runRender } from "../lib/render-run.js";
|
|
12
12
|
import { logLine } from "../lib/log.js";
|
|
13
13
|
export function registerQuoteRender(server) {
|
|
14
|
-
lifelineTool(server, "quote-render", "Turn engine figures into the three reconciling HTML documents (internal margin view, client breakdown, client quote) via the operator's persisted quoting/render.mjs, enforcing
|
|
14
|
+
lifelineTool(server, "quote-render", "Turn engine figures into the three reconciling HTML documents (internal margin view, client breakdown, client quote) via the operator's persisted quoting/render.mjs, enforcing reconcile (displayed total == engine total) and margin-leak (no cost/margin in client docs) always, plus link-liveness only when a signUrl is supplied (online acceptance is opt-in: with signUrl the client quote's Sign-Online href must be absolute https; without it the quote must carry no acceptance anchor). Files each doc by scope and returns the HTML paths. HTML only; print the PDFs with the browser plugin's browser-pdf-save. A gate failure files nothing.", {
|
|
15
15
|
jobId: z.string().describe("Correlation id; figures load from quoting/jobs/<jobId>/figures.json unless figures is supplied inline."),
|
|
16
16
|
figures: z.record(z.any()).optional().describe("Inline figures (overrides the persisted figures.json) — for fixed-figure render parity."),
|
|
17
17
|
signUrl: z.string().optional().describe("The deployed e-sign signing URL injected into the client quote's acceptance anchor. Absent or non-https fails the link-liveness gate."),
|
package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-render.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quote-render.js","sourceRoot":"","sources":["../../src/tools/quote-render.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,uEAAuE;AACvE,8EAA8E;AAC9E,iFAAiF;AACjF,wDAAwD;AAExD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,8DAA8D,CAAC;AAC5F,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,MAAM,UAAU,mBAAmB,CAAC,MAAiB;IACnD,YAAY,CACV,MAAM,EACN,cAAc,EACd,
|
|
1
|
+
{"version":3,"file":"quote-render.js","sourceRoot":"","sources":["../../src/tools/quote-render.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, readdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { runRender } from "../render-run.js";
|
|
6
|
+
import { receiptPath } from "../render-receipt.js";
|
|
6
7
|
|
|
7
8
|
// Fixture render scripts simulating the operator's own quoting/render.mjs: read
|
|
8
9
|
// {figures, signUrl}, emit the three HTML docs. Brand/layout is theirs; the tool
|
|
@@ -41,6 +42,23 @@ process.stdin.on("end", () => {
|
|
|
41
42
|
// is sub-penny → the doc shows the penny total, the engine total is sub-penny
|
|
42
43
|
const SUBPENNY = GOOD.replace("const t = figures.total;", "const t = figures.total.toFixed(2);");
|
|
43
44
|
const THROW = `process.stdin.resume(); process.exit(1);`;
|
|
45
|
+
// A conforming render script for the default (no-signer) path: it emits the
|
|
46
|
+
// Sign-Online acceptance anchor only when a signUrl is supplied, and omits it
|
|
47
|
+
// entirely otherwise. e-sign is opt-in.
|
|
48
|
+
const NOSIGNER = `
|
|
49
|
+
let input = "";
|
|
50
|
+
process.stdin.on("data", (d) => (input += d));
|
|
51
|
+
process.stdin.on("end", () => {
|
|
52
|
+
const { figures, signUrl } = JSON.parse(input);
|
|
53
|
+
const t = figures.total;
|
|
54
|
+
const accept = signUrl ? ('<a data-esign-accept href="' + signUrl + '">Sign online</a>') : '';
|
|
55
|
+
process.stdout.write(JSON.stringify({
|
|
56
|
+
internal: '<html><body><td data-internal-only>cost 900</td><p data-quote-total="' + t + '">Total ' + t + '</p></body></html>',
|
|
57
|
+
breakdown: '<html><body><p data-quote-total="' + t + '">Total ' + t + '</p></body></html>',
|
|
58
|
+
quote: '<html><body><p data-quote-total="' + t + '">Total ' + t + '</p>' + accept + '</body></html>',
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
`;
|
|
44
62
|
|
|
45
63
|
let dir: string;
|
|
46
64
|
const figures = { total: 1250, header: { phone: "44700900000" }, lines: [] };
|
|
@@ -56,6 +74,7 @@ beforeAll(() => {
|
|
|
56
74
|
writeFileSync(join(dir, "quoting", "render-wrongtotal.mjs"), WRONGTOTAL);
|
|
57
75
|
writeFileSync(join(dir, "quoting", "render-subpenny.mjs"), SUBPENNY);
|
|
58
76
|
writeFileSync(join(dir, "quoting", "render-throw.mjs"), THROW);
|
|
77
|
+
writeFileSync(join(dir, "quoting", "render-nosigner.mjs"), NOSIGNER);
|
|
59
78
|
});
|
|
60
79
|
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
61
80
|
const rs = (n: string) => join(dir, "quoting", n);
|
|
@@ -66,12 +85,57 @@ describe("runRender — happy path files all three docs to their scopes", () =>
|
|
|
66
85
|
expect(r.ok).toBe(true);
|
|
67
86
|
if (!r.ok) return;
|
|
68
87
|
expect(r.reconcile).toEqual({ internal: true, breakdown: true, quote: true });
|
|
69
|
-
expect(r.signLink
|
|
88
|
+
expect(r.signLink?.pass).toBe(true);
|
|
70
89
|
expect(r.docs.internal).toBe(join(dir, "memory/admin/customers/44700900000/quote-job-1-internal.html"));
|
|
71
90
|
expect(r.docs.breakdown).toBe(join(dir, "memory/users/44700900000/documents/quote-job-1-breakdown.html"));
|
|
72
91
|
expect(r.docs.quote).toBe(join(dir, "memory/users/44700900000/documents/quote-job-1.html"));
|
|
73
92
|
expect(existsSync(r.docs.internal) && existsSync(r.docs.breakdown) && existsSync(r.docs.quote)).toBe(true);
|
|
74
93
|
});
|
|
94
|
+
|
|
95
|
+
it("writes a durable render receipt keyed on the jobId, recording the engine total and the filed client-doc paths", async () => {
|
|
96
|
+
const r = await runRender({ accountDir: dir, renderScriptPath: rs("render.mjs"), figures, signUrl, jobId: "job-receipt" });
|
|
97
|
+
expect(r.ok).toBe(true);
|
|
98
|
+
if (!r.ok) return;
|
|
99
|
+
const rp = receiptPath(dir, "job-receipt");
|
|
100
|
+
expect(rp).toBe(join(dir, "quoting/jobs/job-receipt/render-receipt.json"));
|
|
101
|
+
expect(existsSync(rp)).toBe(true);
|
|
102
|
+
const receipt = JSON.parse(readFileSync(rp, "utf8"));
|
|
103
|
+
expect(receipt.jobId).toBe("job-receipt");
|
|
104
|
+
expect(receipt.engineTotal).toBe(1250);
|
|
105
|
+
expect(receipt.filed.breakdown).toBe(r.docs.breakdown);
|
|
106
|
+
expect(receipt.filed.quote).toBe(r.docs.quote);
|
|
107
|
+
expect(typeof receipt.renderedAt).toBe("string");
|
|
108
|
+
expect(Number.isNaN(Date.parse(receipt.renderedAt))).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("runRender — e-sign is opt-in; the sign-link gate applies only when a signer is requested", () => {
|
|
113
|
+
it("files all three docs when no signer is requested and the quote carries no acceptance anchor", async () => {
|
|
114
|
+
const r = await runRender({ accountDir: dir, renderScriptPath: rs("render-nosigner.mjs"), figures, jobId: "job-nosigner" });
|
|
115
|
+
expect(r.ok).toBe(true);
|
|
116
|
+
if (!r.ok) return;
|
|
117
|
+
expect(r.reconcile).toEqual({ internal: true, breakdown: true, quote: true });
|
|
118
|
+
// No signer requested → no sign-link verdict on the result.
|
|
119
|
+
expect(r.signLink).toBeNull();
|
|
120
|
+
expect(existsSync(join(dir, "memory/users/44700900000/documents/quote-job-nosigner.html"))).toBe(true);
|
|
121
|
+
expect(existsSync(receiptPath(dir, "job-nosigner"))).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
it("still enforces the sign-link gate on the opt-in path when a signUrl is supplied", async () => {
|
|
124
|
+
const r = await runRender({ accountDir: dir, renderScriptPath: rs("render-nosigner.mjs"), figures, signUrl, jobId: "job-optin" });
|
|
125
|
+
expect(r.ok).toBe(true);
|
|
126
|
+
if (!r.ok) return;
|
|
127
|
+
expect(r.signLink?.pass).toBe(true);
|
|
128
|
+
expect(r.signLink?.href).toBe(signUrl);
|
|
129
|
+
});
|
|
130
|
+
it("rejects a stray acceptance anchor when no signer was requested (fail-closed, nothing filed)", async () => {
|
|
131
|
+
// GOOD always injects the anchor; with no signUrl that is a stray/placeholder signer.
|
|
132
|
+
const r = await runRender({ accountDir: dir, renderScriptPath: rs("render.mjs"), figures, jobId: "job-stray" });
|
|
133
|
+
expect(r).toMatchObject({ ok: false, reason: "sign-link" });
|
|
134
|
+
if (r.ok) return;
|
|
135
|
+
expect(r.detail).toMatch(/no signer|opt-in/i);
|
|
136
|
+
expect(existsSync(join(dir, "memory/users/44700900000/documents/quote-job-stray.html"))).toBe(false);
|
|
137
|
+
expect(existsSync(receiptPath(dir, "job-stray"))).toBe(false);
|
|
138
|
+
});
|
|
75
139
|
});
|
|
76
140
|
|
|
77
141
|
describe("runRender — a failing gate is fail-closed and atomic (no doc filed)", () => {
|
|
@@ -79,6 +143,8 @@ describe("runRender — a failing gate is fail-closed and atomic (no doc filed)"
|
|
|
79
143
|
const r = await runRender({ accountDir: dir, renderScriptPath: rs("render-leak.mjs"), figures, signUrl, jobId: "job-leak" });
|
|
80
144
|
expect(r).toMatchObject({ ok: false, reason: "margin-leak" });
|
|
81
145
|
expect(existsSync(join(dir, "memory/users/44700900000/documents/quote-job-leak.html"))).toBe(false);
|
|
146
|
+
// A fail-closed render files no receipt — the gate must treat this job as un-rendered.
|
|
147
|
+
expect(existsSync(receiptPath(dir, "job-leak"))).toBe(false);
|
|
82
148
|
});
|
|
83
149
|
it("sign-link when the client quote's acceptance href is dead", async () => {
|
|
84
150
|
const r = await runRender({ accountDir: dir, renderScriptPath: rs("render-deadlink.mjs"), figures, signUrl, jobId: "job-dead" });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The durable render receipt — the queryable proof that quote-render actually
|
|
2
|
+
// ran the deterministic path for a job. quote-render logs `op=render` to stderr
|
|
3
|
+
// (journald), which no deterministic gate can read; the receipt is the on-disk
|
|
4
|
+
// record the browser-pdf-save bypass gate checks before it lets a client-quote
|
|
5
|
+
// PDF ship. One receipt per jobId, written only after every gate passed and all
|
|
6
|
+
// three documents filed — a fail-closed render writes none, so the gate treats
|
|
7
|
+
// that job as un-rendered.
|
|
8
|
+
//
|
|
9
|
+
// The path convention (quoting/jobs/<jobId>/render-receipt.json, next to the
|
|
10
|
+
// job's figures.json) is mirrored verbatim in the gate hook
|
|
11
|
+
// (platform/plugins/admin/hooks/quote-render-gate.sh); the two must agree.
|
|
12
|
+
|
|
13
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
export interface RenderReceipt {
|
|
17
|
+
jobId: string;
|
|
18
|
+
engineTotal: number;
|
|
19
|
+
filed: { internal: string; breakdown: string; quote: string };
|
|
20
|
+
renderedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The receipt path for a job, relative to the account workspace root. */
|
|
24
|
+
export function receiptPath(accountDir: string, jobId: string): string {
|
|
25
|
+
return join(accountDir, "quoting", "jobs", jobId, "render-receipt.json");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Write the receipt for a successful render. Called only after all gates pass
|
|
29
|
+
* and the three documents are filed, so its presence is the "a real render ran
|
|
30
|
+
* for this job" signal the delivery gate keys on. */
|
|
31
|
+
export function writeRenderReceipt(accountDir: string, receipt: RenderReceipt): string {
|
|
32
|
+
const p = receiptPath(accountDir, receipt.jobId);
|
|
33
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
34
|
+
writeFileSync(p, JSON.stringify(receipt, null, 2));
|
|
35
|
+
return p;
|
|
36
|
+
}
|
|
@@ -11,6 +11,7 @@ import { spawn } from "node:child_process";
|
|
|
11
11
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { gateReconcile, gateMarginLeak, gateSignLink, type SignLinkVerdict } from "./gates.js";
|
|
14
|
+
import { writeRenderReceipt } from "./render-receipt.js";
|
|
14
15
|
|
|
15
16
|
export type RenderFault = "render-absent" | "render-error" | "reconcile-fail" | "margin-leak" | "sign-link";
|
|
16
17
|
|
|
@@ -27,7 +28,9 @@ export type RenderResult =
|
|
|
27
28
|
ok: true;
|
|
28
29
|
docs: { internal: string; breakdown: string; quote: string };
|
|
29
30
|
reconcile: { internal: boolean; breakdown: boolean; quote: boolean };
|
|
30
|
-
|
|
31
|
+
// The link-liveness verdict, or null when e-sign was not requested for
|
|
32
|
+
// this job (no signUrl) — the default, signer-less quote.
|
|
33
|
+
signLink: SignLinkVerdict | null;
|
|
31
34
|
}
|
|
32
35
|
| { ok: false; reason: RenderFault; detail: string };
|
|
33
36
|
|
|
@@ -89,11 +92,31 @@ export async function runRender(a: RunRenderArgs): Promise<RenderResult> {
|
|
|
89
92
|
const recQuote = gateReconcile(html.quote, engineTotal);
|
|
90
93
|
const leakBreakdown = gateMarginLeak(html.breakdown);
|
|
91
94
|
const leakQuote = gateMarginLeak(html.quote);
|
|
92
|
-
|
|
95
|
+
|
|
96
|
+
// E-sign is opt-in. The Sign-Online acceptance block and its link-liveness
|
|
97
|
+
// gate apply only when the operator supplied a signUrl for this job. On the
|
|
98
|
+
// default (no-signer) path the client quote must carry no acceptance anchor;
|
|
99
|
+
// a stray one is a dead/placeholder button and fails closed the same way a
|
|
100
|
+
// margin leak does. gateSignLink returns reason "no-anchor" exactly when the
|
|
101
|
+
// marked anchor is absent, so anchor-present is `reason !== "no-anchor"`.
|
|
102
|
+
const esignRequested = typeof a.signUrl === "string" && a.signUrl.length > 0;
|
|
103
|
+
const signVerdict = gateSignLink(html.quote);
|
|
104
|
+
const hasAnchor = signVerdict.reason !== "no-anchor";
|
|
105
|
+
let signOk: boolean;
|
|
106
|
+
let signDetail: string;
|
|
107
|
+
if (esignRequested) {
|
|
108
|
+
signOk = signVerdict.pass;
|
|
109
|
+
signDetail = `quote Sign-Online link ${signVerdict.reason} (href=${signVerdict.href ?? ""})`;
|
|
110
|
+
} else {
|
|
111
|
+
signOk = !hasAnchor;
|
|
112
|
+
signDetail = "quote carries a Sign-Online acceptance anchor but no signer was requested (no signUrl) — e-sign is opt-in";
|
|
113
|
+
}
|
|
114
|
+
// The verdict is reported only when a signer was requested; null otherwise.
|
|
115
|
+
const signLink: SignLinkVerdict | null = esignRequested ? signVerdict : null;
|
|
93
116
|
|
|
94
117
|
const internalOk = recInternal.pass;
|
|
95
118
|
const breakdownOk = recBreakdown.pass && leakBreakdown.pass;
|
|
96
|
-
const quoteOk = recQuote.pass && leakQuote.pass &&
|
|
119
|
+
const quoteOk = recQuote.pass && leakQuote.pass && signOk;
|
|
97
120
|
|
|
98
121
|
if (!internalOk || !breakdownOk || !quoteOk) {
|
|
99
122
|
// Reason names the first failing gate, in document then gate order.
|
|
@@ -104,7 +127,7 @@ export async function runRender(a: RunRenderArgs): Promise<RenderResult> {
|
|
|
104
127
|
else if (!leakBreakdown.pass) { reason = "margin-leak"; detail = "breakdown carries data-internal-only"; }
|
|
105
128
|
else if (!recQuote.pass) { reason = "reconcile-fail"; detail = `quote total ${recQuote.shown} != engine ${engineTotal}`; }
|
|
106
129
|
else if (!leakQuote.pass) { reason = "margin-leak"; detail = "quote carries data-internal-only"; }
|
|
107
|
-
else { reason = "sign-link"; detail =
|
|
130
|
+
else { reason = "sign-link"; detail = signDetail; }
|
|
108
131
|
return { ok: false, reason, detail };
|
|
109
132
|
}
|
|
110
133
|
|
|
@@ -116,6 +139,17 @@ export async function runRender(a: RunRenderArgs): Promise<RenderResult> {
|
|
|
116
139
|
writeFileSync(p, body);
|
|
117
140
|
}
|
|
118
141
|
|
|
142
|
+
// Only now — every gate passed and all three docs filed — record the durable
|
|
143
|
+
// receipt. The delivery gate reads it to prove a real render produced this
|
|
144
|
+
// job's client quote; a fail-closed render above returned before this point,
|
|
145
|
+
// so an un-rendered job has no receipt.
|
|
146
|
+
writeRenderReceipt(a.accountDir, {
|
|
147
|
+
jobId: a.jobId,
|
|
148
|
+
engineTotal,
|
|
149
|
+
filed: { internal: internalPath, breakdown: breakdownPath, quote: quotePath },
|
|
150
|
+
renderedAt: new Date().toISOString(),
|
|
151
|
+
});
|
|
152
|
+
|
|
119
153
|
return {
|
|
120
154
|
ok: true,
|
|
121
155
|
docs: { internal: internalPath, breakdown: breakdownPath, quote: quotePath },
|
|
@@ -17,7 +17,7 @@ export function registerQuoteRender(server: McpServer): void {
|
|
|
17
17
|
lifelineTool(
|
|
18
18
|
server,
|
|
19
19
|
"quote-render",
|
|
20
|
-
"Turn engine figures into the three reconciling HTML documents (internal margin view, client breakdown, client quote) via the operator's persisted quoting/render.mjs, enforcing
|
|
20
|
+
"Turn engine figures into the three reconciling HTML documents (internal margin view, client breakdown, client quote) via the operator's persisted quoting/render.mjs, enforcing reconcile (displayed total == engine total) and margin-leak (no cost/margin in client docs) always, plus link-liveness only when a signUrl is supplied (online acceptance is opt-in: with signUrl the client quote's Sign-Online href must be absolute https; without it the quote must carry no acceptance anchor). Files each doc by scope and returns the HTML paths. HTML only; print the PDFs with the browser plugin's browser-pdf-save. A gate failure files nothing.",
|
|
21
21
|
{
|
|
22
22
|
jobId: z.string().describe("Correlation id; figures load from quoting/jobs/<jobId>/figures.json unless figures is supplied inline."),
|
|
23
23
|
figures: z.record(z.any()).optional().describe("Inline figures (overrides the persisted figures.json) — for fixed-figure render parity."),
|
|
@@ -49,15 +49,18 @@ whatever the operator's brand or language:
|
|
|
49
49
|
tool reconciles it against the engine total;
|
|
50
50
|
- **`data-internal-only`** on every cost, margin, or internal-rate figure in the internal view, so the
|
|
51
51
|
tool asserts the two client documents carry none;
|
|
52
|
-
- **`data-esign-accept`** on the client quote's Sign-Online anchor (one line, double-quoted `href`)
|
|
53
|
-
the
|
|
52
|
+
- **`data-esign-accept`** on the client quote's Sign-Online anchor (one line, double-quoted `href`) —
|
|
53
|
+
present **only when the operator opted the quote into online acceptance** — so the tool asserts the
|
|
54
|
+
acceptance link is live.
|
|
54
55
|
|
|
55
56
|
`quote-render` enforces three fail-closed gates before it files anything: **reconcile** (each
|
|
56
57
|
document's `data-quote-total` equals the engine total), **margin-leak** (no `data-internal-only` in the
|
|
57
58
|
client breakdown or client quote), and **link-liveness** (the client quote's `data-esign-accept` href
|
|
58
|
-
is an absolute `https://` URL).
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
is an absolute `https://` URL). Link-liveness runs **only on the opt-in path** — when a `signUrl` was
|
|
60
|
+
passed; on the default, signer-less path the quote must carry no acceptance anchor, and a stray one
|
|
61
|
+
fails closed. If any gate fails the tool files nothing and returns a cause-naming fault
|
|
62
|
+
(`reconcile-fail`, `margin-leak`, `sign-link`, `render-absent`, `render-error`). On success it files
|
|
63
|
+
each document to its scope (below) and returns the three HTML paths plus the gate verdicts.
|
|
61
64
|
|
|
62
65
|
The tool produces **HTML, not PDF**. Print each returned path to PDF with the `browser` plugin as the
|
|
63
66
|
final step: `browser-navigate` to its `file://` URL, then `browser-pdf-save` to a `.pdf` alongside it,
|
|
@@ -80,19 +83,28 @@ expose cost and margin freely. The two client documents **never** surface a cost
|
|
|
80
83
|
internal-rate figure. A column or line that shows the price to the client is fine; a figure that
|
|
81
84
|
reveals what the work cost the operator, or the markup applied, is a leak.
|
|
82
85
|
|
|
83
|
-
###
|
|
86
|
+
### Online acceptance is optional and off by default
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
Most quotes carry **no** online-acceptance link. Online signing is an opt-in the operator asks for per
|
|
89
|
+
quote, not a default. **By default the client quote has no acceptance block and no Sign-Online anchor,
|
|
90
|
+
and it renders and ships with none.** The render script emits the acceptance block **only** when a
|
|
91
|
+
`signUrl` is passed to `quote-render`; with no `signUrl` it omits the block entirely. Do not add an
|
|
92
|
+
online-acceptance button to a quote the operator did not ask to be signable — a stray anchor on the
|
|
93
|
+
default path fails the render closed (`sign-link`), because a signer link with no signing page behind
|
|
94
|
+
it is a dead button.
|
|
95
|
+
|
|
96
|
+
The **acceptance block**, when the operator does opt in, is not free-form prose — it is the signing
|
|
97
|
+
form of the `e-sign` skill (`platform/plugins/business-assistant/skills/e-sign/SKILL.md`). That skill
|
|
98
|
+
owns how the quote is accepted: the `name` / `email` / intent-and-consent form, the capture to D1, the
|
|
88
99
|
stamped acceptance certificate, and the email dispatch. This contract produces the quote document;
|
|
89
100
|
e-sign turns its acceptance block into a recordable signature. This contract does not restate those
|
|
90
101
|
mechanics.
|
|
91
102
|
|
|
92
|
-
**
|
|
93
|
-
render.** The acceptance block carries a **Sign-Online
|
|
94
|
-
the signing page. That page does not exist until e-sign
|
|
95
|
-
rendered until the deploy has run**. The order
|
|
103
|
+
**When the operator opts into online acceptance, the Sign-Online link must be live before the quote is
|
|
104
|
+
finalised — deploy first, then inject, then render.** The acceptance block carries a **Sign-Online
|
|
105
|
+
anchor** — the button the client taps to reach the signing page. That page does not exist until e-sign
|
|
106
|
+
deploys it, so a **signable** client quote **cannot be rendered until the deploy has run**. The order
|
|
107
|
+
is fixed and load-bearing:
|
|
96
108
|
|
|
97
109
|
1. run **e-sign § 5** to deploy the signing page and take its returned absolute `https://` URL (e-sign
|
|
98
110
|
§ 5 names it as an explicit hand-back);
|
|
@@ -104,11 +116,13 @@ rendered until the deploy has run**. The order is fixed and load-bearing:
|
|
|
104
116
|
|
|
105
117
|
The render script marks that one anchor with the stable attribute `data-esign-accept` so
|
|
106
118
|
`quote-render`'s link-liveness gate finds it deterministically, whatever the operator's brand,
|
|
107
|
-
wording, or language. The gate
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
wording, or language. The gate applies **only on the opt-in path** (a `signUrl` was passed); on the
|
|
120
|
+
default path it does not run and the quote must carry no anchor. When it does apply, the gate matches
|
|
121
|
+
the anchor's opening tag, so the render script emits that opening tag on a **single line** with a
|
|
122
|
+
**double-quoted `href`** (the default for these self-contained documents); a split tag or
|
|
123
|
+
single-quoted `href` reads as no anchor and fails the gate closed. The anchor's `href` is **never** a
|
|
124
|
+
placeholder, a bare `#`, or a `file://`/relative path: it is the deployed signing URL or the quote
|
|
125
|
+
does not ship.
|
|
112
126
|
|
|
113
127
|
When the quote is **confidential to a named client**, the acceptance is gated per e-sign § 1a. That
|
|
114
128
|
section's **same-origin invariant** is load-bearing: the `/api/accept` and `/api/status` capture
|
|
@@ -166,14 +180,28 @@ these checks are enforced deterministically at render time, not as a separate ha
|
|
|
166
180
|
- **margin-leak.** The client breakdown and client quote must carry no `data-internal-only` marker. A
|
|
167
181
|
leak files nothing and returns `margin-leak`. The margin boundary is structural, not a matter of
|
|
168
182
|
remembering to omit a column.
|
|
169
|
-
- **link-liveness.**
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
- **link-liveness (opt-in only).** This gate runs only when the operator opted the quote into online
|
|
184
|
+
acceptance — a `signUrl` was passed. Then the client quote's `data-esign-accept` href must be an
|
|
185
|
+
absolute `https://` URL, not empty, a bare fragment (`#…`), relative, `file://`, `about:`, or any
|
|
186
|
+
non-https target; a dead link files nothing and returns `sign-link`, because a dead link emits no
|
|
187
|
+
other signal. On the default, signer-less path (no `signUrl`) the quote must carry **no** acceptance
|
|
188
|
+
anchor at all; a stray anchor also returns `sign-link` (a signer link with no signing page is a dead
|
|
189
|
+
button).
|
|
172
190
|
|
|
173
191
|
The tool emits one `[quote-render] op=render jobId=… docs=3 internal=… breakdown=… quote=…` line on
|
|
174
|
-
success and `[quote-render] op=fault jobId=… reason=… detail=…` on any gate failure.
|
|
175
|
-
|
|
176
|
-
|
|
192
|
+
success and `[quote-render] op=fault jobId=… reason=… detail=…` on any gate failure. On success it also
|
|
193
|
+
writes a durable **render receipt** at `quoting/jobs/{jobId}/render-receipt.json` (recording the engine
|
|
194
|
+
total and the filed client-doc paths) — the on-disk proof, next to the job's figures, that the
|
|
195
|
+
deterministic path ran for this job.
|
|
196
|
+
|
|
197
|
+
**The receipt is what makes the deterministic path the only route.** Printing a client quote to PDF is
|
|
198
|
+
gated on it: a `browser-pdf-save` of a client-facing quote document
|
|
199
|
+
(`memory/users/{phone}/documents/quote-{jobId}.pdf` or `…-breakdown.pdf`) with no render receipt for
|
|
200
|
+
that job is **refused** — the print is blocked and the bypass surfaces as `[quote-render] op=bypass
|
|
201
|
+
jobId=… detail=…`, not delivered. Hand-authoring the quote HTML and printing it is therefore not a
|
|
202
|
+
route a customer document can reach: without a receipt the PDF does not ship. Produce every client
|
|
203
|
+
quote through `quote-render` and print the HTML path it returns; there is no supported hand-authored
|
|
204
|
+
path.
|
|
177
205
|
|
|
178
206
|
**No-event standing check — filed quotes against live signing pages.** The gate fires only while a
|
|
179
207
|
quote is being produced; it cannot catch a quote that shipped a bad link before the gate existed.
|
|
@@ -67,8 +67,12 @@ function readHouseCredential(platformRoot) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// ../lib/storage-broker/src/cf-exec.ts
|
|
70
|
+
import { execFile as execFile2 } from "child_process";
|
|
71
|
+
import { join as join2 } from "path";
|
|
72
|
+
|
|
73
|
+
// ../lib/storage-broker/src/house-scoped-token.ts
|
|
74
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
70
75
|
import { execFile } from "child_process";
|
|
71
|
-
var defaultFetch = (url, init) => fetch(url, init);
|
|
72
76
|
var defaultRun = (cmd, args, env) => new Promise((resolve, reject) => {
|
|
73
77
|
execFile(cmd, args, { env }, (err, stdout, stderr) => {
|
|
74
78
|
if (err) {
|
|
@@ -79,6 +83,54 @@ ${stderr}`));
|
|
|
79
83
|
resolve({ stdout: stdout.toString() });
|
|
80
84
|
});
|
|
81
85
|
});
|
|
86
|
+
function readKey(path, key) {
|
|
87
|
+
for (const line of readFileSync2(path, "utf8").split("\n")) {
|
|
88
|
+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
89
|
+
if (m && m[1] === key) return m[2].trim();
|
|
90
|
+
}
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
async function resolveHouseScopedToken(scope, houseEnvPath, cfTokenShPath, run = defaultRun) {
|
|
94
|
+
const preexisting = readKey(houseEnvPath, keyForScope(scope)) !== void 0;
|
|
95
|
+
const { stdout } = await run(
|
|
96
|
+
"bash",
|
|
97
|
+
[cfTokenShPath, scope, houseEnvPath],
|
|
98
|
+
{ ...process.env }
|
|
99
|
+
);
|
|
100
|
+
const keyName = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
101
|
+
if (!keyName) throw new Error(`storage-broker: cf-token.sh ${scope} returned no key name`);
|
|
102
|
+
const value = readKey(houseEnvPath, keyName);
|
|
103
|
+
if (!value) {
|
|
104
|
+
throw new Error(`storage-broker: scoped token ${keyName} absent in ${houseEnvPath} after mint`);
|
|
105
|
+
}
|
|
106
|
+
console.error(
|
|
107
|
+
`[storage-broker] op=house-scoped-token src=house scope=${scope} action=${preexisting ? "reuse" : "mint"} result=active`
|
|
108
|
+
);
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
function keyForScope(scope) {
|
|
112
|
+
switch (scope) {
|
|
113
|
+
case "storage":
|
|
114
|
+
return "CF_STORAGE_TOKEN";
|
|
115
|
+
case "calendar-d1":
|
|
116
|
+
return "CF_CALENDAR_D1_TOKEN";
|
|
117
|
+
default:
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ../lib/storage-broker/src/cf-exec.ts
|
|
123
|
+
var defaultFetch = (url, init) => fetch(url, init);
|
|
124
|
+
var defaultRun2 = (cmd, args, env) => new Promise((resolve, reject) => {
|
|
125
|
+
execFile2(cmd, args, { env }, (err, stdout, stderr) => {
|
|
126
|
+
if (err) {
|
|
127
|
+
reject(new Error(`${cmd} ${args.join(" ")} failed: ${err.message}
|
|
128
|
+
${stderr}`));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
resolve({ stdout: stdout.toString() });
|
|
132
|
+
});
|
|
133
|
+
});
|
|
82
134
|
function parseJson(raw, context) {
|
|
83
135
|
try {
|
|
84
136
|
return JSON.parse(raw);
|
|
@@ -86,7 +138,7 @@ function parseJson(raw, context) {
|
|
|
86
138
|
throw new Error(`storage-broker: ${context} returned non-JSON output: ${raw}`);
|
|
87
139
|
}
|
|
88
140
|
}
|
|
89
|
-
function makeCfExec(cred, run =
|
|
141
|
+
function makeCfExec(cred, run = defaultRun2, fetchFn = defaultFetch) {
|
|
90
142
|
const env = {
|
|
91
143
|
...process.env,
|
|
92
144
|
CLOUDFLARE_API_TOKEN: cred.apiToken,
|
|
@@ -148,6 +200,13 @@ ${body}`);
|
|
|
148
200
|
}
|
|
149
201
|
};
|
|
150
202
|
}
|
|
203
|
+
async function makeHouseCfExec(platformRoot, run = defaultRun2, fetchFn = defaultFetch) {
|
|
204
|
+
const cred = readHouseCredential(platformRoot);
|
|
205
|
+
const houseEnvPath = join2(platformRoot, "config", "cloudflare-house.env");
|
|
206
|
+
const cfTokenSh = join2(platformRoot, "plugins/cloudflare/bin/cf-token.sh");
|
|
207
|
+
const scoped = await resolveHouseScopedToken("storage", houseEnvPath, cfTokenSh, run);
|
|
208
|
+
return makeCfExec({ apiToken: scoped, accountId: cred.accountId }, run, fetchFn);
|
|
209
|
+
}
|
|
151
210
|
|
|
152
211
|
// ../lib/storage-broker/src/audit.ts
|
|
153
212
|
var ACCOUNT_WIDE_KEYS = ["CLOUDFLARE_API_TOKEN", "CF_PAGES_D1_TOKEN", "CF_PAGES_TOKEN"];
|
|
@@ -180,7 +239,13 @@ async function reconcileStorage(deps) {
|
|
|
180
239
|
}
|
|
181
240
|
|
|
182
241
|
// ../lib/storage-broker/src/remediate.ts
|
|
183
|
-
var STRIP_KEYS = [
|
|
242
|
+
var STRIP_KEYS = [
|
|
243
|
+
"CLOUDFLARE_API_TOKEN",
|
|
244
|
+
"CF_PAGES_D1_TOKEN",
|
|
245
|
+
"CF_PAGES_TOKEN",
|
|
246
|
+
"CF_STORAGE_TOKEN",
|
|
247
|
+
"CF_CALENDAR_D1_TOKEN"
|
|
248
|
+
];
|
|
184
249
|
function validateMapping(enumerated, mapping) {
|
|
185
250
|
const missing = enumerated.filter((name) => !(name in mapping));
|
|
186
251
|
return { ok: missing.length === 0, missing };
|
|
@@ -199,7 +264,9 @@ export {
|
|
|
199
264
|
resolveOwner,
|
|
200
265
|
listResources,
|
|
201
266
|
readHouseCredential,
|
|
267
|
+
resolveHouseScopedToken,
|
|
202
268
|
makeCfExec,
|
|
269
|
+
makeHouseCfExec,
|
|
203
270
|
reconcileStorage,
|
|
204
271
|
validateMapping,
|
|
205
272
|
stripAccountWideTokens
|