@dszp/netsapiens-lib 0.1.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -1
- package/dist/eligibility.d.ts.map +1 -0
- package/dist/eligibility.js.map +1 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +151 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +0 -0
- package/dist/inventory.js.map +1 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js.map +1 -0
- package/dist/mermaid.d.ts.map +1 -0
- package/dist/mermaid.js.map +1 -0
- package/dist/model.d.ts +18 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js.map +1 -0
- package/dist/nsAuthClient.d.ts.map +1 -0
- package/dist/nsAuthClient.js.map +1 -0
- package/dist/nsClient.d.ts +19 -0
- package/dist/nsClient.d.ts.map +1 -0
- package/dist/nsClient.js +40 -3
- package/dist/nsClient.js.map +1 -0
- package/dist/nsDevice.d.ts.map +1 -0
- package/dist/nsDevice.js.map +1 -0
- package/dist/nsSubscriptions.d.ts.map +1 -0
- package/dist/nsSubscriptions.js.map +1 -0
- package/dist/nsSynchronous.d.ts.map +1 -0
- package/dist/nsSynchronous.js.map +1 -0
- package/dist/nsWriteClient.d.ts.map +1 -0
- package/dist/nsWriteClient.js.map +1 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js.map +1 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js.map +1 -0
- package/dist/raster.d.ts.map +1 -0
- package/dist/raster.js.map +1 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js.map +1 -0
- package/dist/sensitivity.d.ts.map +1 -0
- package/dist/sensitivity.js.map +1 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js.map +1 -0
- package/package.json +7 -3
- package/src/eligibility.selftest.ts +95 -0
- package/src/eligibility.ts +118 -0
- package/src/html.ts +407 -0
- package/src/index.ts +120 -0
- package/src/inventory.selftest.ts +198 -0
- package/src/inventory.ts +314 -0
- package/src/jwt.selftest.ts +145 -0
- package/src/jwt.ts +491 -0
- package/src/mermaid.ts +169 -0
- package/src/model.ts +130 -0
- package/src/nsAuthClient.selftest.ts +60 -0
- package/src/nsAuthClient.ts +102 -0
- package/src/nsClient.selftest.ts +173 -0
- package/src/nsClient.ts +323 -0
- package/src/nsDevice.selftest.ts +190 -0
- package/src/nsDevice.ts +167 -0
- package/src/nsSubscriptions.selftest.ts +486 -0
- package/src/nsSubscriptions.ts +638 -0
- package/src/nsSynchronous.selftest.ts +63 -0
- package/src/nsSynchronous.ts +98 -0
- package/src/nsWriteClient.selftest.ts +104 -0
- package/src/nsWriteClient.ts +157 -0
- package/src/policy.ts +123 -0
- package/src/principal.selftest.ts +118 -0
- package/src/principal.ts +101 -0
- package/src/raster.selftest.ts +42 -0
- package/src/raster.ts +79 -0
- package/src/resolver.selftest.ts +225 -0
- package/src/resolver.ts +1115 -0
- package/src/sensitivity.ts +40 -0
- package/src/themes.ts +142 -0
package/src/html.ts
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowGraph[] -> a self-contained gallery HTML string. Portable (no Node deps): the caller
|
|
3
|
+
* decides where the string goes — a file (CLI), an HTTP response (Worker), or embedded in
|
|
4
|
+
* a host review page / build-preview. Mermaid renders client-side from a CDN.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FlowGraph } from './model.js';
|
|
8
|
+
import { toMermaid, type FlowTheme } from './mermaid.js';
|
|
9
|
+
|
|
10
|
+
// Pinned Mermaid build + Subresource Integrity. A floating `mermaid@11` tag lets jsDelivr serve whatever
|
|
11
|
+
// the latest 11.x is with NO integrity guarantee — a compromised/substituted CDN response would execute in
|
|
12
|
+
// the gallery (and, before the modal iframe was sandboxed, could reach the portal's ns_t). Pin an exact
|
|
13
|
+
// version + SRI hash so the browser refuses any bytes that don't match. Bump BOTH together — recompute:
|
|
14
|
+
// curl -s https://cdn.jsdelivr.net/npm/mermaid@<v>/dist/mermaid.min.js | openssl dgst -sha384 -binary | openssl base64 -A
|
|
15
|
+
const MERMAID_VERSION = '11.16.0';
|
|
16
|
+
const MERMAID_CDN = `https://cdn.jsdelivr.net/npm/mermaid@${MERMAID_VERSION}/dist/mermaid.min.js`;
|
|
17
|
+
const MERMAID_SRI = 'sha384-T/0lMUdJpd2S1ZHtRiofG3htU3xPCrFVeAQ1UUE2TJwlEJSV5NUwn30kP28n238E';
|
|
18
|
+
|
|
19
|
+
/** Build the Mermaid `<script>` tag. For the pinned jsDelivr build (the default) attach `integrity` +
|
|
20
|
+
* `crossorigin` so the browser rejects a substituted CDN payload. A caller-supplied self-hosted `src`
|
|
21
|
+
* is emitted WITHOUT SRI — the caller owns that origin's integrity, and its bytes won't match this hash. */
|
|
22
|
+
function mermaidScriptTag(src?: string): string {
|
|
23
|
+
const url = src ?? MERMAID_CDN;
|
|
24
|
+
const sri = url === MERMAID_CDN ? ` integrity="${MERMAID_SRI}" crossorigin="anonymous"` : '';
|
|
25
|
+
return `<script src="${escapeHtml(url)}"${sri}></script>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Left-align multi-line node content (agent / device / queue lists) + edge labels so they read as
|
|
29
|
+
* lists, not centered blobs. Hosts include this wherever `.mermaid` diagrams render. */
|
|
30
|
+
const FLOW_LABEL_CSS = `.mermaid g.agents div, .mermaid g.agents span, .mermaid g.agents p,
|
|
31
|
+
.mermaid g.devices div, .mermaid g.devices span, .mermaid g.devices p,
|
|
32
|
+
.mermaid g.queue div, .mermaid g.queue span, .mermaid g.queue p { text-align:left !important; }
|
|
33
|
+
.mermaid .edgeLabel div, .mermaid .edgeLabel span, .mermaid .edgeLabel p { text-align:left !important; }
|
|
34
|
+
/* Edge-label chips — keep them TIGHT. Mermaid gives the label foreignObject a tall line-height and a
|
|
35
|
+
semi-transparent .labelBkg; once overflow:visible stops the right-edge clip (it under-measures width
|
|
36
|
+
under look:neo, so "press 2" got cut), that block showed as an oversized blocky highlight and short
|
|
37
|
+
labels even wrapped. So: drop the block bg, force single-line (explicit <br/> in multi-option labels
|
|
38
|
+
still breaks), and render the text as a compact rounded chip with a little space before/after.
|
|
39
|
+
mermaid.ts drops the old trailing-nbsp slack in favor of this. */
|
|
40
|
+
.mermaid g.edgeLabel foreignObject { overflow:visible; }
|
|
41
|
+
.mermaid .edgeLabel .labelBkg { background:transparent !important; }
|
|
42
|
+
.mermaid .edgeLabel foreignObject > div { white-space:nowrap !important; line-height:1.35 !important; max-width:none !important; }
|
|
43
|
+
.mermaid span.edgeLabel { display:inline-block; padding:1px 7px; border-radius:4px; line-height:1.35; white-space:nowrap; }`;
|
|
44
|
+
|
|
45
|
+
/** Shared flowchart layout config. Node clipping is prevented by nbsp label slack in mermaid.ts,
|
|
46
|
+
* not by padding — so this keeps Mermaid's default node padding (a larger override made wide nodes
|
|
47
|
+
* like AAs balloon). */
|
|
48
|
+
const FLOWCHART_CFG = `htmlLabels:true, curve:'basis', nodeSpacing:45, rankSpacing:55`;
|
|
49
|
+
|
|
50
|
+
/** Escapes for BOTH text and quoted-attribute contexts — `mermaidScriptTag` interpolates into
|
|
51
|
+
* `src="…"`, so omitting the quote entities made `escapeHtml(url)` look safe while allowing
|
|
52
|
+
* `x.js" onload="…`. Over-escaping in text position is inert, so one function covers both. */
|
|
53
|
+
function escapeHtml(t: string): string {
|
|
54
|
+
return String(t)
|
|
55
|
+
.replace(/&/g, '&')
|
|
56
|
+
.replace(/</g, '<')
|
|
57
|
+
.replace(/>/g, '>')
|
|
58
|
+
.replace(/"/g, '"')
|
|
59
|
+
.replace(/'/g, ''');
|
|
60
|
+
}
|
|
61
|
+
function cap(t: string): string {
|
|
62
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
63
|
+
}
|
|
64
|
+
/** Stable, collision-safe DOM id for a flow card so a host page can deep-link one diagram.
|
|
65
|
+
* `kind` is sanitized alongside `ref`: resolveFlow only ever emits the four literals, but
|
|
66
|
+
* FlowGraph.entity.kind is typed `string` and hand-built graphs are a supported use, so an
|
|
67
|
+
* unsanitized kind would land unescaped in `id="…"`/`href="#…"`. */
|
|
68
|
+
export function flowAnchorId(g: FlowGraph): string {
|
|
69
|
+
const safe = (s: unknown) => String(s).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
70
|
+
return `flow-${safe(g.entity.kind)}-${safe(g.entity.ref)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A CSS color safe to interpolate into a `<style>` block. Anything else is refused rather than
|
|
74
|
+
* escaped: `accent` is documented as host-supplied config, and a white-label host that sources it
|
|
75
|
+
* per-tenant would otherwise hand any tenant a `</style><script>` breakout. Callers that were
|
|
76
|
+
* already passing hex (the documented contract) see no change. */
|
|
77
|
+
function safeAccent(accent: string | undefined, fallback: string): string {
|
|
78
|
+
return accent && /^#[0-9a-fA-F]{3,8}$/.test(accent) ? accent : fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CardOptions {
|
|
82
|
+
/** Themed rendering (light/dark palette + `look: neo` + a card anchor id). OMIT for the
|
|
83
|
+
* legacy dark card with no frontmatter and no id — the Worker relies on that being unchanged. */
|
|
84
|
+
theme?: FlowTheme;
|
|
85
|
+
/** Render the card as a collapsible `<details>` (gallery navigation). */
|
|
86
|
+
collapsible?: boolean;
|
|
87
|
+
/** When collapsible, start expanded. */
|
|
88
|
+
open?: boolean;
|
|
89
|
+
/** When collapsible, add a "↑ top" link in the summary. */
|
|
90
|
+
backToTop?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface GalleryOptions extends CardOptions {
|
|
94
|
+
/** Load Mermaid from this URL (CDN by default). Override to self-host / inline for CSP. */
|
|
95
|
+
mermaidSrc?: string;
|
|
96
|
+
/** Extra note shown under the title. */
|
|
97
|
+
subtitle?: string;
|
|
98
|
+
/** Brand accent for subtle highlights — links, the "Legend:" lead, the card's top rule, and the
|
|
99
|
+
* pan/zoom controls' hover. Themed path only; defaults to the theme's link color. Pass your own
|
|
100
|
+
* brand color here (from your host's config) rather than baking one into a theme, e.g.
|
|
101
|
+
* `accent: '#1a6bb0'`.
|
|
102
|
+
*
|
|
103
|
+
* MUST be a CSS hex color (`#rgb` … `#rrggbbaa`); this value is interpolated into a `<style>`
|
|
104
|
+
* block, so anything else is IGNORED in favour of the theme's link color rather than escaped.
|
|
105
|
+
* If you source it per-tenant, that rejection is the only thing between a hostile value and a
|
|
106
|
+
* `</style><script>` breakout — don't defeat it by pre-formatting the string. */
|
|
107
|
+
accent?: string;
|
|
108
|
+
/** Themed galleries only: anchor ids ({@link flowAnchorId}) to render pre-expanded. Cards not in
|
|
109
|
+
* the set render collapsed. A themed gallery is always collapsible with a table-of-contents. */
|
|
110
|
+
expand?: Set<string>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Render one flow as a gallery `<section>` card. Themed cards carry an `id` anchor
|
|
114
|
+
* ({@link flowAnchorId}); the legacy (no-theme) card is byte-identical to the original. */
|
|
115
|
+
export function renderFlowCard(g: FlowGraph, opts: CardOptions = {}): string {
|
|
116
|
+
const notes = g.notes.length ? `<ul class="cf-notes">${g.notes.map((n) => `<li>${escapeHtml(n)}</li>`).join('')}</ul>` : '';
|
|
117
|
+
const mermaid = `<div class="mermaid">${escapeHtml(toMermaid(g, opts.theme ? { theme: opts.theme } : undefined))}</div>`;
|
|
118
|
+
const title = `${escapeHtml(g.entity.kind === 'did' ? 'DID' : cap(g.entity.kind))}: ${escapeHtml(g.entity.label)}`;
|
|
119
|
+
if (opts.collapsible) {
|
|
120
|
+
const back = opts.backToTop ? ` <a class="cf-top" href="#cf-top" onclick="event.stopPropagation()">↑ top</a>` : '';
|
|
121
|
+
return `<details class="cf-card"${opts.theme ? ` id="${flowAnchorId(g)}"` : ''}${opts.open ? ' open' : ''}>
|
|
122
|
+
<summary><span class="cf-title">${title}</span><span class="cf-meta"> · ${g.nodes.length} nodes · ${g.edges.length} edges</span>${back}</summary>
|
|
123
|
+
${mermaid}
|
|
124
|
+
${notes}
|
|
125
|
+
</details>`;
|
|
126
|
+
}
|
|
127
|
+
const idAttr = opts.theme ? ` id="${flowAnchorId(g)}"` : '';
|
|
128
|
+
return `<section class="cf-card"${idAttr}>
|
|
129
|
+
<h2>${title}</h2>
|
|
130
|
+
<div class="cf-meta">${g.nodes.length} nodes · ${g.edges.length} edges</div>
|
|
131
|
+
${mermaid}
|
|
132
|
+
${notes}
|
|
133
|
+
</section>`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Render the gallery `<section>` cards only (no page chrome) — for embedding in another page. */
|
|
137
|
+
export function renderFlowCards(graphs: FlowGraph[], opts: CardOptions = {}): string {
|
|
138
|
+
return graphs.map((g) => renderFlowCard(g, opts)).join('\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The `<script>` tags a host page needs to render embedded `.mermaid` blocks itself
|
|
143
|
+
* (e.g. a review report). `securityLevel: 'strict'` is MANDATORY — it is the
|
|
144
|
+
* second escaping layer `mermaid.ts` depends on; do not make it caller-configurable.
|
|
145
|
+
*/
|
|
146
|
+
export function mermaidBootstrap(opts: { theme?: FlowTheme; mermaidSrc?: string } = {}): string {
|
|
147
|
+
const mermaidTheme = opts.theme === 'light' ? 'base' : 'dark';
|
|
148
|
+
return `<style>${FLOW_LABEL_CSS}</style>
|
|
149
|
+
${mermaidScriptTag(opts.mermaidSrc)}
|
|
150
|
+
<script>mermaid.initialize({ startOnLoad:true, theme:'${mermaidTheme}', securityLevel:'strict', flowchart:{ ${FLOWCHART_CFG} } });</script>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Full standalone HTML document for a set of flows. */
|
|
154
|
+
export function renderGalleryHtml(domain: string, graphs: FlowGraph[], opts: GalleryOptions = {}): string {
|
|
155
|
+
const mermaidSrc = opts.mermaidSrc ?? MERMAID_CDN;
|
|
156
|
+
const subtitle = opts.subtitle ?? `resolved from snapshot · ${graphs.length} flows`;
|
|
157
|
+
// Themed path (light for a review context, or explicit dark-neo). No theme → the original dark
|
|
158
|
+
// document below, byte-identical (the Worker depends on this).
|
|
159
|
+
if (opts.theme) return themedGalleryHtml(domain, graphs, mermaidSrc, subtitle, opts.theme, opts.expand, opts.accent);
|
|
160
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
161
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
162
|
+
<title>Call Flows — ${escapeHtml(domain)}</title>
|
|
163
|
+
${mermaidScriptTag(mermaidSrc)}
|
|
164
|
+
<style>
|
|
165
|
+
:root { color-scheme: dark; }
|
|
166
|
+
body { background:#0f1115; color:#e6e6e6; font:15px/1.5 system-ui,sans-serif; margin:0; padding:24px; }
|
|
167
|
+
h1 { font-size:14px; font-weight:600; color:#8a94a6; margin:0 0 2px; }
|
|
168
|
+
.cf-sub { color:#e6e6e6; margin:0 0 24px; font-size:18px; font-weight:700; }
|
|
169
|
+
.cf-card { background:#161a22; border:1px solid #232a36; border-radius:12px; padding:18px 20px; margin:0 0 22px; }
|
|
170
|
+
.cf-card h2 { font-size:16px; margin:0 0 2px; }
|
|
171
|
+
.cf-meta { color:#7b8494; font-size:12px; margin-bottom:12px; }
|
|
172
|
+
.mermaid { background:#0c0e12; border-radius:8px; padding:14px; overflow:auto; text-align:center; }
|
|
173
|
+
${FLOW_LABEL_CSS}
|
|
174
|
+
.cf-notes { margin:12px 0 0; padding-left:18px; color:#c9a24a; font-size:12.5px; }
|
|
175
|
+
.cf-notes li { margin:2px 0; }
|
|
176
|
+
.cf-legend { display:flex; align-items:center; flex-wrap:wrap; gap:10px; margin:0 0 22px; font-size:12px; color:#aab; }
|
|
177
|
+
.cf-legend span { background:#161a22; border:1px solid #232a36; border-radius:6px; padding:3px 8px; }
|
|
178
|
+
.cf-legend-lead { font-weight:700; }
|
|
179
|
+
.mermaid { cursor:zoom-in; }
|
|
180
|
+
.cf-lightbox { position:fixed; inset:0; background:rgba(6,8,12,.95); display:none; z-index:9999; padding:20px; cursor:zoom-out; }
|
|
181
|
+
.cf-lightbox.open { display:block; }
|
|
182
|
+
.cf-lightbox-inner { width:100%; height:100%; overflow:auto; display:flex; align-items:flex-start; justify-content:center; }
|
|
183
|
+
.cf-lightbox-inner svg { max-width:none !important; height:auto; }
|
|
184
|
+
.cf-lightbox-hint { position:fixed; top:10px; right:16px; color:#8a94a6; font-size:12px; pointer-events:none; }
|
|
185
|
+
</style></head>
|
|
186
|
+
<body>
|
|
187
|
+
<h1>Call Flow Diagram — ${escapeHtml(domain)}</h1>
|
|
188
|
+
<p class="cf-sub">${escapeHtml(subtitle)}</p>
|
|
189
|
+
<div class="cf-legend">
|
|
190
|
+
<b class="cf-legend-lead">Legend:</b><span>📞 DID</span><span>🕒 time-of-day</span><span>👤 user</span><span>📱 ring devices</span>
|
|
191
|
+
<span>📋 queue</span><span>👥 agents</span><span>🔀 auto attendant</span><span>🔊 prompt</span>
|
|
192
|
+
<span>📭 voicemail</span><span>☎️ external</span>
|
|
193
|
+
</div>
|
|
194
|
+
${renderFlowCards(graphs)}
|
|
195
|
+
<div id="cf-lightbox" class="cf-lightbox"><span class="cf-lightbox-hint">click / Esc to close</span><div class="cf-lightbox-inner"></div></div>
|
|
196
|
+
<script>
|
|
197
|
+
mermaid.initialize({ startOnLoad:true, theme:'dark', securityLevel:'strict', flowchart:{ ${FLOWCHART_CFG} } });
|
|
198
|
+
// Click any diagram to enlarge it in a scrollable overlay; click anywhere / Esc to close.
|
|
199
|
+
(function(){
|
|
200
|
+
var box = document.getElementById('cf-lightbox');
|
|
201
|
+
var inner = box.querySelector('.cf-lightbox-inner');
|
|
202
|
+
function close(){ box.classList.remove('open'); inner.replaceChildren(); }
|
|
203
|
+
document.addEventListener('click', function(e){
|
|
204
|
+
var card = e.target.closest ? e.target.closest('.mermaid') : null;
|
|
205
|
+
if (card && !box.contains(card)) {
|
|
206
|
+
var svg = card.querySelector('svg');
|
|
207
|
+
if (!svg) return;
|
|
208
|
+
var clone = svg.cloneNode(true);
|
|
209
|
+
clone.removeAttribute('height'); clone.removeAttribute('style');
|
|
210
|
+
inner.replaceChildren(clone);
|
|
211
|
+
box.classList.add('open');
|
|
212
|
+
} else if (box.classList.contains('open')) {
|
|
213
|
+
close();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') close(); });
|
|
217
|
+
})();
|
|
218
|
+
</script>
|
|
219
|
+
</body></html>`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Themed gallery document — light (review context) or explicit dark-neo. Parallel to
|
|
223
|
+
* the legacy dark document in {@link renderGalleryHtml}; kept separate so that path stays byte-identical. */
|
|
224
|
+
function themedGalleryHtml(
|
|
225
|
+
domain: string,
|
|
226
|
+
graphs: FlowGraph[],
|
|
227
|
+
mermaidSrc: string,
|
|
228
|
+
subtitle: string,
|
|
229
|
+
theme: FlowTheme,
|
|
230
|
+
expand?: Set<string>,
|
|
231
|
+
accent?: string,
|
|
232
|
+
): string {
|
|
233
|
+
const light = theme === 'light';
|
|
234
|
+
const c = light
|
|
235
|
+
? { scheme: 'light', pageBg: '#fafafa', text: '#1e293b', sub: '#64748b', cardBg: '#ffffff', cardBorder: '#e2e8f0', meta: '#64748b', mermaidBg: '#f8fafc', notes: '#b45309', legendText: '#475569', legendBg: '#ffffff', lightboxBg: 'rgba(248,250,252,.96)', hint: '#64748b', shadow: 'box-shadow:0 1px 2px rgba(0,0,0,.04);', link: '#21618c' }
|
|
236
|
+
: { scheme: 'dark', pageBg: '#0f1115', text: '#e6e6e6', sub: '#8a94a6', cardBg: '#161a22', cardBorder: '#232a36', meta: '#7b8494', mermaidBg: '#0c0e12', notes: '#c9a24a', legendText: '#aab', legendBg: '#161a22', lightboxBg: 'rgba(6,8,12,.95)', hint: '#8a94a6', shadow: '', link: '#7db3e6' };
|
|
237
|
+
const initTheme = light ? 'base' : 'dark';
|
|
238
|
+
const brandAccent = safeAccent(accent, c.link);
|
|
239
|
+
const single = graphs.length === 1; // a one-flow gallery (e.g. the portal modal) needs no contents nav
|
|
240
|
+
const isOpen = (g: FlowGraph) => single || (!!expand && expand.has(flowAnchorId(g)));
|
|
241
|
+
|
|
242
|
+
// Table of contents grouped by entity kind; ● marks the pre-expanded (notable) flows.
|
|
243
|
+
const KIND_LABEL: Record<string, string> = { did: '📞 DIDs', user: '👤 Users', queue: '📋 Queues', attendant: '🔀 Auto Attendants' };
|
|
244
|
+
const byKind = new Map<string, FlowGraph[]>();
|
|
245
|
+
for (const g of graphs) {
|
|
246
|
+
const k = g.entity.kind;
|
|
247
|
+
if (!byKind.has(k)) byKind.set(k, []);
|
|
248
|
+
byKind.get(k)!.push(g);
|
|
249
|
+
}
|
|
250
|
+
const toc = [...byKind.entries()]
|
|
251
|
+
.map(
|
|
252
|
+
([kind, gs]) => `<div class="cf-toc-group"><div class="cf-toc-h">${KIND_LABEL[kind] ?? escapeHtml(cap(kind))}</div>
|
|
253
|
+
<ul>${gs
|
|
254
|
+
.map((g) => `<li>${isOpen(g) ? '<b>●</b> ' : ''}<a href="#${flowAnchorId(g)}">${escapeHtml(g.entity.label)}</a></li>`)
|
|
255
|
+
.join('')}</ul></div>`,
|
|
256
|
+
)
|
|
257
|
+
.join('\n');
|
|
258
|
+
|
|
259
|
+
// A single-flow gallery (portal modal) renders as a fixed, non-collapsible card; multi-flow keeps the
|
|
260
|
+
// collapsible <details> for navigation.
|
|
261
|
+
const cards = graphs.map((g) => renderFlowCard(g, { theme, collapsible: !single, open: isOpen(g), backToTop: !single })).join('\n');
|
|
262
|
+
|
|
263
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
264
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
265
|
+
<title>Call Flows — ${escapeHtml(domain)}</title>
|
|
266
|
+
${mermaidScriptTag(mermaidSrc)}
|
|
267
|
+
<style>
|
|
268
|
+
:root { color-scheme: ${c.scheme}; scroll-behavior:smooth; }
|
|
269
|
+
body { background:${c.pageBg}; color:${c.text}; font:15px/1.5 system-ui,sans-serif; margin:0; padding:24px; }
|
|
270
|
+
a { color:${brandAccent}; }
|
|
271
|
+
h1 { font-size:14px; font-weight:600; color:${c.sub}; margin:0 0 2px; }
|
|
272
|
+
.cf-sub { color:${c.text}; margin:0 0 18px; font-size:18px; font-weight:700; }
|
|
273
|
+
.cf-legend { display:flex; align-items:center; flex-wrap:wrap; gap:10px; margin:0 0 18px; font-size:12px; color:${c.legendText}; }
|
|
274
|
+
.cf-legend span { background:${c.legendBg}; border:1px solid ${c.cardBorder}; border-radius:6px; padding:3px 8px; }
|
|
275
|
+
.cf-legend-lead { font-weight:700; color:${brandAccent}; }
|
|
276
|
+
.cf-toc { background:${c.cardBg}; border:1px solid ${c.cardBorder}; border-radius:12px; padding:14px 18px; margin:0 0 22px; ${c.shadow} }
|
|
277
|
+
.cf-toc-top { font-weight:700; font-size:13px; margin:0 0 8px; }
|
|
278
|
+
.cf-toc-cols { display:flex; flex-wrap:wrap; gap:8px 28px; }
|
|
279
|
+
.cf-toc-group { min-width:160px; }
|
|
280
|
+
.cf-toc-h { font-size:12px; font-weight:700; color:${c.sub}; margin:4px 0 2px; }
|
|
281
|
+
.cf-toc ul { margin:0 0 6px; padding-left:16px; font-size:13px; }
|
|
282
|
+
.cf-toc li { margin:1px 0; }
|
|
283
|
+
.cf-card { background:${c.cardBg}; border:1px solid ${c.cardBorder}; border-radius:12px; padding:0; margin:0 0 14px; ${c.shadow} scroll-margin-top:12px; }
|
|
284
|
+
section.cf-card { padding:12px 18px; border-top:3px solid ${brandAccent}; }
|
|
285
|
+
section.cf-card > h2 { font-size:16px; margin:0 0 2px; }
|
|
286
|
+
details.cf-card > summary { cursor:pointer; padding:12px 18px; font-size:15px; font-weight:700; list-style:none; user-select:none; }
|
|
287
|
+
details.cf-card > summary::-webkit-details-marker { display:none; }
|
|
288
|
+
details.cf-card > summary::before { content:'▸ '; color:${c.sub}; font-weight:400; }
|
|
289
|
+
details.cf-card[open] > summary::before { content:'▾ '; }
|
|
290
|
+
details.cf-card[open] > summary { border-bottom:1px solid ${c.cardBorder}; }
|
|
291
|
+
.cf-title { }
|
|
292
|
+
.cf-meta { color:${c.meta}; font-size:12px; font-weight:400; }
|
|
293
|
+
.cf-top { float:right; font-size:11px; font-weight:400; }
|
|
294
|
+
details.cf-card > .mermaid, details.cf-card > .cf-notes { margin:0 18px; }
|
|
295
|
+
.mermaid { background:${c.mermaidBg}; border-radius:8px; padding:14px; margin:14px 0; overflow:auto; text-align:center; cursor:zoom-in; }
|
|
296
|
+
.mermaid.cf-pz { overflow:hidden; cursor:grab; position:relative; text-align:left; padding:0; height:70vh; touch-action:none; }
|
|
297
|
+
.mermaid.cf-pz svg { position:absolute; top:0; left:0; max-width:none !important; height:auto; }
|
|
298
|
+
.cf-pz-ctl { position:absolute; top:10px; right:10px; display:flex; flex-direction:column; gap:5px; z-index:5; }
|
|
299
|
+
.cf-pz-ctl button { width:30px; height:30px; border:1px solid ${c.cardBorder}; background:${c.cardBg}; color:${c.text}; border-radius:6px; cursor:pointer; font:16px/1 system-ui,sans-serif; ${c.shadow} }
|
|
300
|
+
.cf-pz-ctl button:hover { border-color:${brandAccent}; color:${brandAccent}; }
|
|
301
|
+
${FLOW_LABEL_CSS}
|
|
302
|
+
.cf-notes { padding:0 0 14px 34px; color:${c.notes}; font-size:12.5px; }
|
|
303
|
+
.cf-notes li { margin:2px 0; }
|
|
304
|
+
.cf-lightbox { position:fixed; inset:0; background:${c.lightboxBg}; display:none; z-index:9999; padding:20px; cursor:zoom-out; }
|
|
305
|
+
.cf-lightbox.open { display:block; }
|
|
306
|
+
.cf-lightbox-inner { width:100%; height:100%; overflow:auto; display:flex; align-items:flex-start; justify-content:center; }
|
|
307
|
+
.cf-lightbox-inner svg { max-width:none !important; height:auto; }
|
|
308
|
+
.cf-lightbox-hint { position:fixed; top:10px; right:16px; color:${c.hint}; font-size:12px; pointer-events:none; }
|
|
309
|
+
</style></head>
|
|
310
|
+
<body>
|
|
311
|
+
<span id="cf-top"></span>
|
|
312
|
+
<h1>Call Flow Diagram — ${escapeHtml(domain)}</h1>
|
|
313
|
+
<p class="cf-sub">${escapeHtml(subtitle)}</p>
|
|
314
|
+
<div class="cf-legend">
|
|
315
|
+
<b class="cf-legend-lead">Legend:</b><span>📞 DID</span><span>🕒 time-of-day</span><span>👤 user</span><span>📱 ring devices</span>
|
|
316
|
+
<span>📋 queue</span><span>👥 agents</span><span>🔀 auto attendant</span><span>🔊 prompt</span>
|
|
317
|
+
<span>📭 voicemail</span><span>☎️ external</span>
|
|
318
|
+
</div>
|
|
319
|
+
${single ? '' : `<nav class="cf-toc">
|
|
320
|
+
<div class="cf-toc-top">Contents <span class="cf-meta">(● = notable / pre-expanded)</span></div>
|
|
321
|
+
<div class="cf-toc-cols">${toc}</div>
|
|
322
|
+
</nav>`}
|
|
323
|
+
${cards}
|
|
324
|
+
<div id="cf-lightbox" class="cf-lightbox"><span class="cf-lightbox-hint">click / Esc to close</span><div class="cf-lightbox-inner"></div></div>
|
|
325
|
+
${mermaidBootstrapInline(initTheme)}
|
|
326
|
+
</body></html>`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The gallery's own Mermaid init + lightbox script (self-contained; not the host-page bootstrap). */
|
|
330
|
+
function mermaidBootstrapInline(initTheme: string): string {
|
|
331
|
+
return `<script>
|
|
332
|
+
mermaid.initialize({ startOnLoad:false, theme:'${initTheme}', securityLevel:'strict', flowchart:{ ${FLOWCHART_CFG} } });
|
|
333
|
+
document.querySelectorAll('.mermaid').forEach(function(el){ if(!el.getAttribute('data-src')){ el.setAttribute('data-src', el.textContent); } });
|
|
334
|
+
mermaid.run({ querySelector:'.mermaid' }).then(function(){
|
|
335
|
+
var els = document.querySelectorAll('.mermaid');
|
|
336
|
+
// Single-flow (the portal modal): scroll/drag pan, Shift+scroll or +/- buttons zoom, flip layout.
|
|
337
|
+
if (els.length === 1) { panZoom(els[0]); } else { lightbox(); }
|
|
338
|
+
});
|
|
339
|
+
function panZoom(el){
|
|
340
|
+
var svg = el.querySelector('svg'); if(!svg) return;
|
|
341
|
+
el.classList.add('cf-pz');
|
|
342
|
+
// Pin the SVG to its natural viewBox pixel size so our transform (not mermaid's width:100% +
|
|
343
|
+
// viewBox auto-scaling) fully controls the displayed size — otherwise fit measurements are wrong.
|
|
344
|
+
var vb=(svg.viewBox&&svg.viewBox.baseVal)||{};
|
|
345
|
+
var natW=vb.width||svg.getBoundingClientRect().width, natH=vb.height||svg.getBoundingClientRect().height;
|
|
346
|
+
svg.setAttribute('width', natW); svg.setAttribute('height', natH); svg.style.maxWidth='none'; svg.style.transformOrigin='0 0';
|
|
347
|
+
var st = el._pz || (el._pz = {}); st.svg=svg; st.natW=natW; st.natH=natH; st.k=1; st.tx=0; st.ty=0; st.drag=false;
|
|
348
|
+
st.apply = function(){ svg.style.transform='translate('+st.tx+'px,'+st.ty+'px) scale('+st.k+')'; };
|
|
349
|
+
st.fit = function(){
|
|
350
|
+
var r=el.getBoundingClientRect(), w=st.natW||r.width, h=st.natH||r.height;
|
|
351
|
+
var pad=16, lr=/flowchart\\s+(LR|RL)/.test(el.getAttribute('data-src')||'');
|
|
352
|
+
// Fill the primary axis (TD → width, LR → height); never upscale past natural (1x) so small
|
|
353
|
+
// few-node diagrams don't balloon. Center the cross-axis; start-align the overflowing one.
|
|
354
|
+
st.k=Math.min(lr?(r.height-pad)/h:(r.width-pad)/w, 1); if(!isFinite(st.k)||st.k<=0){ st.k=1; }
|
|
355
|
+
if(lr){ st.ty=(r.height-h*st.k)/2; st.tx=(w*st.k>r.width)?pad:(r.width-w*st.k)/2; }
|
|
356
|
+
else { st.tx=(r.width-w*st.k)/2; st.ty=(h*st.k>r.height)?pad:(r.height-h*st.k)/2; }
|
|
357
|
+
st.apply();
|
|
358
|
+
};
|
|
359
|
+
st.zoomAt = function(f, cx, cy){ var nk=Math.max(0.2,Math.min(6,st.k*f)), r=nk/st.k; st.tx=cx-(cx-st.tx)*r; st.ty=cy-(cy-st.ty)*r; st.k=nk; st.apply(); };
|
|
360
|
+
function flip(){
|
|
361
|
+
var src=el.getAttribute('data-src')||'';
|
|
362
|
+
var cur=(src.match(/flowchart\\s+(TB|TD|LR|RL|BT)/)||[])[1]||'TD';
|
|
363
|
+
var next=(cur==='LR'||cur==='RL')?'TD':'LR';
|
|
364
|
+
el.setAttribute('data-src', src.replace(/flowchart\\s+(TB|TD|LR|RL|BT)/, 'flowchart '+next));
|
|
365
|
+
var oc=el.querySelector('.cf-pz-ctl'); if(oc){ oc.remove(); }
|
|
366
|
+
el.classList.remove('cf-pz'); el.removeAttribute('data-processed'); el.textContent=el.getAttribute('data-src');
|
|
367
|
+
mermaid.run({ nodes:[el] }).then(function(){ panZoom(el); });
|
|
368
|
+
}
|
|
369
|
+
var oldc=el.querySelector('.cf-pz-ctl'); if(oldc){ oldc.remove(); }
|
|
370
|
+
var ctl=document.createElement('div'); ctl.className='cf-pz-ctl';
|
|
371
|
+
function mk(t,tip,f){ var b=document.createElement('button'); b.type='button'; b.textContent=t; b.title=tip; b.addEventListener('click', function(e){ e.stopPropagation(); f(); }); ctl.appendChild(b); }
|
|
372
|
+
mk('+','Zoom in', function(){ st.zoomAt(1.2, el.clientWidth/2, 0); });
|
|
373
|
+
mk('−','Zoom out', function(){ st.zoomAt(1/1.2, el.clientWidth/2, 0); });
|
|
374
|
+
mk('↺','Reset view', function(){ st.fit(); });
|
|
375
|
+
mk('⇄','Flip layout (horizontal / vertical)', flip);
|
|
376
|
+
el.appendChild(ctl);
|
|
377
|
+
if(!el._pzBound){
|
|
378
|
+
el._pzBound=true;
|
|
379
|
+
el.addEventListener('wheel', function(e){
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
if(e.shiftKey){ var d=e.deltaY||e.deltaX, r=el.getBoundingClientRect(); st.zoomAt(d<0?1.1:1/1.1, e.clientX-r.left, e.clientY-r.top); }
|
|
382
|
+
else { st.tx-=e.deltaX; st.ty-=e.deltaY; st.apply(); }
|
|
383
|
+
}, {passive:false});
|
|
384
|
+
el.addEventListener('pointerdown', function(e){ if(e.target.closest && e.target.closest('.cf-pz-ctl')){ return; } st.drag=true; st.px=e.clientX; st.py=e.clientY; try{ el.setPointerCapture(e.pointerId); }catch(_){} el.style.cursor='grabbing'; });
|
|
385
|
+
el.addEventListener('pointermove', function(e){ if(!st.drag){ return; } st.tx+=e.clientX-st.px; st.ty+=e.clientY-st.py; st.px=e.clientX; st.py=e.clientY; st.apply(); });
|
|
386
|
+
var end=function(){ st.drag=false; el.style.cursor='grab'; };
|
|
387
|
+
el.addEventListener('pointerup', end); el.addEventListener('pointercancel', end);
|
|
388
|
+
window.addEventListener('resize', function(){ if(st.fit){ st.fit(); } });
|
|
389
|
+
}
|
|
390
|
+
requestAnimationFrame(function(){ st.fit(); }); setTimeout(function(){ st.fit(); }, 80);
|
|
391
|
+
}
|
|
392
|
+
function lightbox(){
|
|
393
|
+
var box = document.getElementById('cf-lightbox'); if(!box){ return; }
|
|
394
|
+
var inner = box.querySelector('.cf-lightbox-inner');
|
|
395
|
+
function close(){ box.classList.remove('open'); inner.replaceChildren(); }
|
|
396
|
+
document.addEventListener('click', function(e){
|
|
397
|
+
var card = e.target.closest ? e.target.closest('.mermaid') : null;
|
|
398
|
+
if (card && !box.contains(card)) {
|
|
399
|
+
var svg = card.querySelector('svg'); if (!svg) return;
|
|
400
|
+
var clone = svg.cloneNode(true); clone.removeAttribute('height'); clone.removeAttribute('style');
|
|
401
|
+
inner.replaceChildren(clone); box.classList.add('open');
|
|
402
|
+
} else if (box.classList.contains('open')) { close(); }
|
|
403
|
+
});
|
|
404
|
+
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') close(); });
|
|
405
|
+
}
|
|
406
|
+
</script>`;
|
|
407
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API of the portable call-flow library — the surface any host imports (Cloudflare
|
|
3
|
+
* Worker, an onboarding CLI / review page / build-preview, the portal viewer). Everything
|
|
4
|
+
* re-exported here is Node-free and runtime-portable; any Node-only host code (e.g. a CLI) lives
|
|
5
|
+
* outside this surface.
|
|
6
|
+
*
|
|
7
|
+
* Typical use in another project:
|
|
8
|
+
* import { resolveFlow, toMermaid, renderGalleryHtml, verify } from '@dszp/netsapiens-lib';
|
|
9
|
+
* const graph = resolveFlow(snapshot, { kind: 'did', ref: '13175550100' });
|
|
10
|
+
* const html = renderGalleryHtml(snapshot.meta.domain, [graph]);
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type { FlowGraph, FlowNode, FlowEdge, NodeKind, EdgeKind, Snapshot, Rec } from './model.js';
|
|
14
|
+
export { resolveFlow, listEntities, type EntityRef } from './resolver.js';
|
|
15
|
+
export { toMermaid, type FlowTheme, type MermaidOptions } from './mermaid.js';
|
|
16
|
+
export {
|
|
17
|
+
THEMES,
|
|
18
|
+
DEFAULT_LIGHT_THEME,
|
|
19
|
+
DEFAULT_DARK_THEME,
|
|
20
|
+
NODE_LIGHT,
|
|
21
|
+
NODE_DARK,
|
|
22
|
+
NODE_SLATE,
|
|
23
|
+
NODE_A11Y,
|
|
24
|
+
type ThemeDef,
|
|
25
|
+
type ThemeChrome,
|
|
26
|
+
type ThemeMode,
|
|
27
|
+
type NodePalette,
|
|
28
|
+
} from './themes.js';
|
|
29
|
+
export {
|
|
30
|
+
renderGalleryHtml,
|
|
31
|
+
renderFlowCards,
|
|
32
|
+
renderFlowCard,
|
|
33
|
+
mermaidBootstrap,
|
|
34
|
+
flowAnchorId,
|
|
35
|
+
type GalleryOptions,
|
|
36
|
+
type CardOptions,
|
|
37
|
+
} from './html.js';
|
|
38
|
+
export { resolveSvgSize, rasterizerScript } from './raster.js';
|
|
39
|
+
export { NsClient, NsApiError, assertBareServer, fetchDomainSnapshot, listDomains, asArray, type NsClientConfig, type FetchSnapshotOptions } from './nsClient.js';
|
|
40
|
+
export { countDomainInventory, listDomainInventory, itemsFor, itemLabel, type DomainInventory, type DomainInventoryDetail, type ExtensionItem, type NumberItem, type AddressItem, type SmsItem, type InventoryItem } from './inventory.js';
|
|
41
|
+
export { NsWriteClient, type NsWriteClientConfig } from './nsWriteClient.js';
|
|
42
|
+
export {
|
|
43
|
+
supportsSynchronous,
|
|
44
|
+
SYNCHRONOUS_OPERATIONS,
|
|
45
|
+
type SynchronousMethod,
|
|
46
|
+
type SynchronousOperation,
|
|
47
|
+
} from './nsSynchronous.js';
|
|
48
|
+
export {
|
|
49
|
+
ensureNsDevice,
|
|
50
|
+
generateSipPassword,
|
|
51
|
+
SIP_PW_FIELD,
|
|
52
|
+
type NsDeviceWriter,
|
|
53
|
+
type EnsureNsDeviceOptions,
|
|
54
|
+
type EnsureNsDeviceResult,
|
|
55
|
+
} from './nsDevice.js';
|
|
56
|
+
export { NsAuthClient, NsAuthError, type NsAuthClientConfig, type NsTokenResponse } from './nsAuthClient.js';
|
|
57
|
+
export {
|
|
58
|
+
NsSubscriptionsClient,
|
|
59
|
+
NsSubscriptionConflictError,
|
|
60
|
+
SUBSCRIPTION_MODELS,
|
|
61
|
+
isSubscriptionModel,
|
|
62
|
+
nsDatetime,
|
|
63
|
+
parseNsDatetime,
|
|
64
|
+
subscriptionFromWire,
|
|
65
|
+
createInputToWire,
|
|
66
|
+
updateInputToWire,
|
|
67
|
+
planSubscriptions,
|
|
68
|
+
type SubscriptionModel,
|
|
69
|
+
type SubscriptionStatus,
|
|
70
|
+
type Subscription,
|
|
71
|
+
type CreateSubscriptionInput,
|
|
72
|
+
type UpdateSubscriptionInput,
|
|
73
|
+
type NsSubscriptionsClientConfig,
|
|
74
|
+
type DesiredSubscription,
|
|
75
|
+
type SubscriptionAction,
|
|
76
|
+
type PlanSubscriptionsOptions,
|
|
77
|
+
} from './nsSubscriptions.js';
|
|
78
|
+
export {
|
|
79
|
+
verify,
|
|
80
|
+
validateJwtFormat,
|
|
81
|
+
extractContext,
|
|
82
|
+
assertClaims,
|
|
83
|
+
verifyHs256Signature,
|
|
84
|
+
normalizeToken,
|
|
85
|
+
tokenKey,
|
|
86
|
+
MemoryVerdictCache,
|
|
87
|
+
type JwtVerdict,
|
|
88
|
+
type JwtContext,
|
|
89
|
+
type ClaimExpectations,
|
|
90
|
+
type VerdictCache,
|
|
91
|
+
type VerifyOptions,
|
|
92
|
+
type FormatResult,
|
|
93
|
+
} from './jwt.js';
|
|
94
|
+
export { type CallSensitivity, needsFreshAuth, SENSITIVITY_NOTE } from './sensitivity.js';
|
|
95
|
+
export {
|
|
96
|
+
toPrincipal,
|
|
97
|
+
parseOperator,
|
|
98
|
+
isResellerScope,
|
|
99
|
+
isAdminScope,
|
|
100
|
+
type Principal,
|
|
101
|
+
type Operator,
|
|
102
|
+
type Scope,
|
|
103
|
+
} from './principal.js';
|
|
104
|
+
export {
|
|
105
|
+
ruleMatches,
|
|
106
|
+
isAllowed,
|
|
107
|
+
can,
|
|
108
|
+
type PolicyRule,
|
|
109
|
+
type Policy,
|
|
110
|
+
type FeaturePolicies,
|
|
111
|
+
} from './policy.js';
|
|
112
|
+
export {
|
|
113
|
+
evaluateEligibility,
|
|
114
|
+
type SoftCategory,
|
|
115
|
+
type EligibilityConfig,
|
|
116
|
+
type EligUser,
|
|
117
|
+
type EligContext,
|
|
118
|
+
type EligTier,
|
|
119
|
+
type EligResult,
|
|
120
|
+
} from './eligibility.js';
|