@abstractframework/continuum 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/bin/cli.js +206 -0
- package/bin/hub_proxy.js +549 -0
- package/dist/assets/abnfDiagram-VRR7QNED-DHZzhKO_.js +2 -0
- package/dist/assets/arc-9iPl5OLd.js +2 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-BVRc0NRl.js +37 -0
- package/dist/assets/blockDiagram-677ZJIJ3-BTIe50OR.js +133 -0
- package/dist/assets/c4Diagram-LMCZKHZV-D5P-4VLr.js +11 -0
- package/dist/assets/channel-CEGeSBNz.js +2 -0
- package/dist/assets/chunk-2Q5K7J3B-WcmxYPWP.js +2 -0
- package/dist/assets/chunk-32BRIVSS-BPArWk_S.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-DplW9bbf.js +16 -0
- package/dist/assets/chunk-EX3LRPZG-B8O0P99j.js +232 -0
- package/dist/assets/chunk-JWPE2WC7-DZoiE5m4.js +2 -0
- package/dist/assets/chunk-MOJQB5TN-FqMZZAe1.js +89 -0
- package/dist/assets/chunk-RYQCIY6F-D4I_kub_.js +2 -0
- package/dist/assets/chunk-V7JOEXUC-CwJZa6s3.js +207 -0
- package/dist/assets/chunk-VR4S4FIN-CdF7hCRc.js +2 -0
- package/dist/assets/chunk-XXDRQBXY-C2vm2DXm.js +2 -0
- package/dist/assets/classDiagram-OUVF2IWQ-D11Ru5dS.js +2 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-D11Ru5dS.js +2 -0
- package/dist/assets/cose-bilkent-JH36ORCC-BEoqSVoj.js +2 -0
- package/dist/assets/cynefin-VYW2F7L2-DzsDaKjB.js +179 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-qVt2ETEm.js +63 -0
- package/dist/assets/cytoscape.esm-DTSO7Bv0.js +332 -0
- package/dist/assets/dagre-VKFMJZFB-BszpuHqM.js +5 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/dist/assets/diagram-FQU43EPY-DkSwN897.js +4 -0
- package/dist/assets/diagram-G47NLZAW-DmmkniMR.js +25 -0
- package/dist/assets/diagram-NH7WQ7WH-PkbaZ3fb.js +25 -0
- package/dist/assets/diagram-OA4YK3LP-D5O-VGkx.js +31 -0
- package/dist/assets/diagram-WEI45ONY-CtS1f8zZ.js +42 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-2ydDo0Sq.js +2 -0
- package/dist/assets/erDiagram-Q63AITRT-Dc-2Dmcc.js +86 -0
- package/dist/assets/flowDiagram-23GEKE2U-DKKOHSrp.js +157 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-B9Zi-Zd_.js +293 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-lXzgHn-s.js +107 -0
- package/dist/assets/graph-C9eacEi8.js +2 -0
- package/dist/assets/index-CCcufdYK.css +1 -0
- package/dist/assets/index-DmnZnFTc.js +124 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-BHV_H3bL.js +3 -0
- package/dist/assets/init-Gi6I4Gst.js +2 -0
- package/dist/assets/ishikawaDiagram-FXEZZL3T-BX74rKth.js +71 -0
- package/dist/assets/journeyDiagram-5HDEW3XC-BByysGFJ.js +140 -0
- package/dist/assets/kanban-definition-HUTT4EX6-DatfLmCp.js +90 -0
- package/dist/assets/katex-C5jXJg4s.js +258 -0
- package/dist/assets/layout-DEXfKzaS.js +2 -0
- package/dist/assets/linear-BgTPNh4i.js +2 -0
- package/dist/assets/map-Czzmt4hB.js +2 -0
- package/dist/assets/mermaid.core-CrABX5SH.js +315 -0
- package/dist/assets/mindmap-definition-LN4V7U3C-Bz1XgdXA.js +97 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +2 -0
- package/dist/assets/pegDiagram-2B236MQR-D5B8Egll.js +2 -0
- package/dist/assets/pieDiagram-ENE6RG2P-BaDg4f41.js +40 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-jr1V5VXJ.js +8 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-F13Ho8ra.js +2 -0
- package/dist/assets/requirementDiagram-TGXJPOKE-DXb_w8Ww.js +85 -0
- package/dist/assets/sankeyDiagram-HTMAVEWB-BN42kL5b.js +41 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-DI7-dEyi.js +163 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-Dv-15ctr.js +2 -0
- package/dist/assets/stateDiagram-2N3HPSRC-De5ObM42.js +2 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-D9AwhP3S.js +2 -0
- package/dist/assets/swimlanes-5IMT3BWC-DzZwgXNi.js +3 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-DyzT80_E.js +9 -0
- package/dist/assets/timeline-definition-FHXFAJF6-BO8KkL_w.js +121 -0
- package/dist/assets/vennDiagram-L72KCM5P-D6LUv_CX.js +35 -0
- package/dist/assets/wardleyDiagram-EHGQE667-DWJp0Lmi.js +79 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-B5IA4jNf.js +8 -0
- package/dist/index.html +14 -0
- package/package.json +68 -0
package/bin/hub_proxy.js
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agora hub proxy — the Team page's transport (proposal c1692, agency's
|
|
3
|
+
* contract c1696).
|
|
4
|
+
*
|
|
5
|
+
* Serves /api/hub/* by forwarding an ALLOWLISTED subset of the hub HTTP
|
|
6
|
+
* API with the OPERATOR seat's key attached server-side. The key comes
|
|
7
|
+
* from the operator's standard key store (~/.agora/keys.json) or env; it
|
|
8
|
+
* never appears in the browser, a repo, or a URL (the c1556 rule
|
|
9
|
+
* generalized from charter reads to the whole chat surface).
|
|
10
|
+
*
|
|
11
|
+
* Contract rules honored here (agency c1696):
|
|
12
|
+
* - Only allowlisted paths forward — the proxy is a door, not a tunnel.
|
|
13
|
+
* - GET /api/hub/meta names the seat so the UI can render authorship
|
|
14
|
+
* honestly ("posting as laurent").
|
|
15
|
+
* - Ack/read_message are ordinary POSTs the UI calls on EXPLICIT read
|
|
16
|
+
* only (never on render) — enforced UI-side, but the proxy exposes
|
|
17
|
+
* them as distinct paths so an audit can see which fired.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from 'fs';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import { dirname, join } from 'path';
|
|
23
|
+
|
|
24
|
+
/** Console version for the hub's X-Agora-Client handshake (read once at
|
|
25
|
+
* boot; "0.0.0" only if package.json is unreadable — the header's job is
|
|
26
|
+
* identifying a CURRENT client, so presence matters more than the number). */
|
|
27
|
+
const CONSOLE_VERSION = (() => {
|
|
28
|
+
try {
|
|
29
|
+
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
30
|
+
return String(pkg.version || '0.0.0');
|
|
31
|
+
} catch {
|
|
32
|
+
return '0.0.0';
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
|
|
36
|
+
/** Allowlisted hub routes (method + path regex against the hub path).
|
|
37
|
+
* `keyless: true` marks routes the hub itself serves unauthenticated
|
|
38
|
+
* (healthz) — they forward even when no seat key is provisioned, so the
|
|
39
|
+
* protocol pin works on a read-only deployment.
|
|
40
|
+
*
|
|
41
|
+
* DM EXCLUSION (c1696 rule 4, adversary find): channel names are simple
|
|
42
|
+
* slugs — the only hub channels containing ":" are `dm:*`, so the
|
|
43
|
+
* channel character class deliberately omits ":" and the former /dms/*
|
|
44
|
+
* routes are gone. DMs carry blind-vote ballots and stay OFF this
|
|
45
|
+
* surface structurally, not by UI convention. */
|
|
46
|
+
const HUB_ROUTES = [
|
|
47
|
+
{ method: 'GET', re: /^\/healthz$/, keyless: true },
|
|
48
|
+
{ method: 'GET', re: /^\/whoami$/ },
|
|
49
|
+
// Hub-wide search (agora-0132, hub ≥ 0.12.44 `search-grouped`): read-only
|
|
50
|
+
// grouped report, membership-scoped SERVER-side to the seat key's own
|
|
51
|
+
// channels — the proxy adds no scoping because the hub already refuses
|
|
52
|
+
// non-member hits structurally (golden vector pins it). Query params ride
|
|
53
|
+
// through untouched (q/channel/sender/kind/since/until/ref/sort/limit/
|
|
54
|
+
// cursor). Admin ops (rebuild/drift) stay OFF this surface deliberately.
|
|
55
|
+
{ method: 'GET', re: /^\/search$/ },
|
|
56
|
+
{ method: 'GET', re: /^\/channels$/ },
|
|
57
|
+
// OPERATOR DIRECTIVE 2026-07-15 (c2240): channel management + DMs are
|
|
58
|
+
// now first-class on this surface — the channel class includes ":" so
|
|
59
|
+
// dm:* channels READ here (the operator's own seat, his own ballots;
|
|
60
|
+
// the earlier dm-exclusion protected agents' surfaces, his call
|
|
61
|
+
// supersedes it for his console). Writes stay allowlisted per route.
|
|
62
|
+
{ method: 'POST', re: /^\/channels$/ },
|
|
63
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/messages$/ },
|
|
64
|
+
{ method: 'POST', re: /^\/channels\/[A-Za-z0-9_.:-]+\/messages$/ },
|
|
65
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/messages\/[A-Za-z0-9_-]+$/ },
|
|
66
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/info$/ },
|
|
67
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/members$/ },
|
|
68
|
+
// Leave a channel (operator dm 14: a DM trash icon). Removes the seat's
|
|
69
|
+
// OWN membership — the channel drops off the member-scoped list. Non-
|
|
70
|
+
// destructive: history persists hub-side; a DM reopens on next contact.
|
|
71
|
+
{ method: 'POST', re: /^\/channels\/[A-Za-z0-9_.:-]+\/leave$/ },
|
|
72
|
+
// Virtual filesystem: LIST (table of contents, no content) + READ one
|
|
73
|
+
// file (operator dm 35 — a per-channel /fs browser). Read-only; any
|
|
74
|
+
// member key serves it. Writes/deletes stay off this surface.
|
|
75
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/fs$/ },
|
|
76
|
+
// fs READ charset mirrors what the hub itself accepts for paths (it
|
|
77
|
+
// rejects only backslash/control chars/dot-segments) — the old
|
|
78
|
+
// [A-Za-z0-9_./-] class 403'd LISTED files with spaces, unicode, or
|
|
79
|
+
// '+' in their names: a click-to-error surface (adversarial find,
|
|
80
|
+
// operator dm 93 class). Dot-segment refusal stays proxy-side too.
|
|
81
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/fs\/(?!(?:.*\/)?\.\.(?:\/|$))[^\x00-\x1f\\]+$/ },
|
|
82
|
+
// Verbatim transcript for INDEPENDENT client-side chain verification
|
|
83
|
+
// (Team page "Verify transcript"; hub docs/protocol.md "Verbatim
|
|
84
|
+
// ledger"). Read-only; any member key serves it.
|
|
85
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/ledger$/ },
|
|
86
|
+
// Actionable-state digest (open questions / decided) — feeds the Team
|
|
87
|
+
// page's per-channel vigilance badges. Read-only.
|
|
88
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/digest$/ },
|
|
89
|
+
// Channel lifecycle: closed-state lives in the channel:meta store key
|
|
90
|
+
// (owner/operator writable hub-side). channel:meta stays the ONLY
|
|
91
|
+
// writable key; the unification vote (Option A, c3010 + S3) opened the
|
|
92
|
+
// general store READ-ONLY — claim:/decision: rows are the board's
|
|
93
|
+
// annotation lane (pointer claims join backlog files by work id).
|
|
94
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/store$/ },
|
|
95
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/store\/[A-Za-z0-9_.:-]+$/ },
|
|
96
|
+
{ method: 'PUT', re: /^\/channels\/[A-Za-z0-9_.:-]+\/store\/channel:meta$/ },
|
|
97
|
+
// reactions:* store WRITES are CLOSED (operator dm 150 "one reputation
|
|
98
|
+
// score system"): the store convention stranded 26 operator votes where
|
|
99
|
+
// reputation could not see them. Votes go through the rating verbs
|
|
100
|
+
// below; this route's removal makes re-stranding structurally
|
|
101
|
+
// impossible from this surface (reads stay open for audits).
|
|
102
|
+
// Work-id activity index (agora 0093, S2): every claim/decision/message
|
|
103
|
+
// citing one work id across the seat's channels — the card drawer's
|
|
104
|
+
// one-call render source. Read-only; activates when the hub reaches
|
|
105
|
+
// 0.12.12 (feature-detected client-side until then).
|
|
106
|
+
{ method: 'GET', re: /^\/work\/[A-Za-z0-9_.-]+$/ },
|
|
107
|
+
// Unified-backlog list (agora blessing c3345, ships 0.12.19): all
|
|
108
|
+
// work:* rows of a channel in ONE call — replaces the store-paging
|
|
109
|
+
// read when live (feature-detected).
|
|
110
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/work$/ },
|
|
111
|
+
// Operator desk (dm:agora--continuum contract, hub half in flight):
|
|
112
|
+
// everything blocked on the operator, one read, operator-only
|
|
113
|
+
// hub-side. Feature-detected (404 until agora ships).
|
|
114
|
+
{ method: 'GET', re: /^\/desk$/ },
|
|
115
|
+
// Owed report (comms-audit ask 2, dm:agora--continuum#102): the viewer's
|
|
116
|
+
// own debts + to_consume (answers to their OWN asks not yet used). The
|
|
117
|
+
// console renders to_consume as a sticky "answers waiting on you" rail so
|
|
118
|
+
// an answer to the operator's question never hides behind an unopened
|
|
119
|
+
// surface (the #155-never-read incident).
|
|
120
|
+
{ method: 'GET', re: /^\/owed$/ },
|
|
121
|
+
// Message retraction (agora 0097, operator dm 88): author-only +
|
|
122
|
+
// operator override; the hub redacts at every read and clears the
|
|
123
|
+
// obligation. Live at hub >= 0.12.16.
|
|
124
|
+
{ method: 'POST', re: /^\/channels\/[A-Za-z0-9_.:-]+\/messages\/[A-Za-z0-9]+\/retract$/ },
|
|
125
|
+
// Moderation (kick/ban): channel-scope blocks (owner/operator) and
|
|
126
|
+
// hub-wide blocks (operator-only — the hub enforces authority; the
|
|
127
|
+
// proxy only carries the operator seat's own key).
|
|
128
|
+
{ method: 'POST', re: /^\/channels\/[A-Za-z0-9_.:-]+\/blocks$/ },
|
|
129
|
+
{ method: 'DELETE', re: /^\/channels\/[A-Za-z0-9_.:-]+\/blocks\/[A-Za-z0-9_.-]+$/ },
|
|
130
|
+
{ method: 'POST', re: /^\/hub\/blocks$/ },
|
|
131
|
+
{ method: 'DELETE', re: /^\/hub\/blocks\/[A-Za-z0-9_.-]+$/ },
|
|
132
|
+
{ method: 'GET', re: /^\/blocks$/ },
|
|
133
|
+
// Channel archive (operator dm 19/29 — safe delete with member eviction;
|
|
134
|
+
// agora backlog 0090). Owner/operator; hub enforces. Forwards today even
|
|
135
|
+
// where the hub lacks the route (404 handled client-side as "ships with
|
|
136
|
+
// the next hub update") so the UI is wired ahead of the hub verb.
|
|
137
|
+
{ method: 'POST', re: /^\/channels\/[A-Za-z0-9_.:-]+\/archive$/ },
|
|
138
|
+
{ method: 'DELETE', re: /^\/channels\/[A-Za-z0-9_.:-]+\/archive$/ },
|
|
139
|
+
// Agent retire (operator dm 15 — neutral decommission, NOT a ban; agora
|
|
140
|
+
// backlog 0089). Operator only; hub enforces + reserves the id forever.
|
|
141
|
+
// GET /agents/retired enumerates the retired list (the un-retire
|
|
142
|
+
// candidate source, agora 0.12.0) — MUST precede the {id}/retire routes
|
|
143
|
+
// conceptually but regexes are exact so order is irrelevant.
|
|
144
|
+
{ method: 'GET', re: /^\/agents\/retired$/ },
|
|
145
|
+
{ method: 'POST', re: /^\/agents\/[A-Za-z0-9_.-]+\/retire$/ },
|
|
146
|
+
{ method: 'DELETE', re: /^\/agents\/[A-Za-z0-9_.-]+\/retire$/ },
|
|
147
|
+
// Hard-delete a retired agent (operator dm 164b, hub 0.12.41): the
|
|
148
|
+
// irreversible cleanup step. MUST come after the /retire routes so the
|
|
149
|
+
// more specific {id}/retire pattern matches first (this bare {id} is the
|
|
150
|
+
// fallthrough). The hub 409s while the agent is still active.
|
|
151
|
+
{ method: 'DELETE', re: /^\/agents\/[A-Za-z0-9_.-]+$/ },
|
|
152
|
+
// Message attachments (operator dm 21; agora backlog 0091). UPLOAD is a
|
|
153
|
+
// RAW-BYTES body (not JSON) — carved out of the JSON-only write gate
|
|
154
|
+
// below; FETCH returns BINARY — served through a binary passthrough (not
|
|
155
|
+
// r.text(), which corrupts bytes). Both membership-gated hub-side.
|
|
156
|
+
{ method: 'POST', re: /^\/channels\/[A-Za-z0-9_.:-]+\/attachments$/, upload: true },
|
|
157
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/attachments\/[A-Za-z0-9_-]+$/, binary: true },
|
|
158
|
+
// Direct messages (send side; reading rides the dm:* channel routes).
|
|
159
|
+
{ method: 'POST', re: /^\/dms\/[A-Za-z0-9_.-]+\/messages$/ },
|
|
160
|
+
// Invite mint (operator /group, agora 0.12.10 parity): owner-only
|
|
161
|
+
// hub-side; the token travels to the invitee via a DM, never a URL.
|
|
162
|
+
{ method: 'POST', re: /^\/channels\/[A-Za-z0-9_.:-]+\/invites$/ },
|
|
163
|
+
// One-call group creation (agora dm#43, hub 0.12.29): replaces the
|
|
164
|
+
// 4-call /group macro; older consoles keep the macro as fallback.
|
|
165
|
+
{ method: 'POST', re: /^\/groups$/ },
|
|
166
|
+
// Message ratings (agora-0122, hub 0.12.31 — one reputation system):
|
|
167
|
+
// PUT casts/flips the seat's standing ±1, DELETE withdraws. Reads ride
|
|
168
|
+
// the history-row tally decoration, so no GET here.
|
|
169
|
+
{ method: 'PUT', re: /^\/channels\/[A-Za-z0-9_.:-]+\/messages\/[A-Za-z0-9-]+\/rating$/ },
|
|
170
|
+
{ method: 'DELETE', re: /^\/channels\/[A-Za-z0-9_.:-]+\/messages\/[A-Za-z0-9-]+\/rating$/ },
|
|
171
|
+
// Hub delegation (operator dm 154; hub ADR-0004 separable powers): the
|
|
172
|
+
// Members drawer assigns/resigns the operator's delegate. The grant is
|
|
173
|
+
// an OPERATOR act — the hub authorizes by the seat key (this proxy
|
|
174
|
+
// signs as the operator); the public list lets any viewer verify.
|
|
175
|
+
{ method: 'GET', re: /^\/delegations$/ },
|
|
176
|
+
{ method: 'PUT', re: /^\/admin\/delegation$/ },
|
|
177
|
+
{ method: 'DELETE', re: /^\/admin\/delegation\/[A-Za-z0-9_.-]+$/ },
|
|
178
|
+
{ method: 'GET', re: /^\/inbox$/ },
|
|
179
|
+
{ method: 'POST', re: /^\/inbox\/ack$/ },
|
|
180
|
+
{ method: 'GET', re: /^\/presence$/ },
|
|
181
|
+
// Reputation (operator dm 12; agora 0094): per-channel + hub leaderboards,
|
|
182
|
+
// the attributed votes behind one score, and the operator seat's own vote
|
|
183
|
+
// (PUT casts/revises, DELETE withdraws — the hub enforces one live vote
|
|
184
|
+
// per rater/target/axis and refuses self-votes). Read-only surfaces are
|
|
185
|
+
// membership-gated hub-side like every channel read.
|
|
186
|
+
{ method: 'GET', re: /^\/reputation$/ },
|
|
187
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/reputation$/ },
|
|
188
|
+
{ method: 'GET', re: /^\/channels\/[A-Za-z0-9_.:-]+\/reputation\/[A-Za-z0-9_.-]+\/votes$/ },
|
|
189
|
+
{ method: 'PUT', re: /^\/channels\/[A-Za-z0-9_.:-]+\/reputation\/[A-Za-z0-9_.-]+$/ },
|
|
190
|
+
{ method: 'DELETE', re: /^\/channels\/[A-Za-z0-9_.:-]+\/reputation\/[A-Za-z0-9_.-]+$/ },
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Cross-origin refusal (adversary P0, 2026-07-15): the loopback peer gate
|
|
195
|
+
* defends against REMOTE peers, but the browser itself is a loopback peer —
|
|
196
|
+
* a hostile page the operator visits can fire cross-origin fetches and
|
|
197
|
+
* WebSocket handshakes at this proxy (WS handshakes skip CORS preflight
|
|
198
|
+
* entirely), and the seat key rides server-side. Rule: when the browser
|
|
199
|
+
* declares an Origin, it must match the Host the request was addressed to;
|
|
200
|
+
* absent Origin (curl, scripts, same-origin GET navigations) passes — a
|
|
201
|
+
* browser NEVER omits Origin on cross-origin fetch/WS, so the vector this
|
|
202
|
+
* closes cannot dodge the check.
|
|
203
|
+
*/
|
|
204
|
+
function origin_allowed(req) {
|
|
205
|
+
const origin = String((req.headers && req.headers.origin) || '').trim();
|
|
206
|
+
if (!origin) return true;
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = new URL(origin);
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const host = String((req.headers && req.headers.host) || '').trim().toLowerCase();
|
|
214
|
+
return Boolean(host) && parsed.host.toLowerCase() === host;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function read_seat_key(keys_path, hub_url, seat) {
|
|
218
|
+
// Env override wins (deployments without the operator key store).
|
|
219
|
+
const env_key = String(process.env.ABSTRACTCONTINUUM_HUB_KEY || '').trim();
|
|
220
|
+
if (env_key) return env_key;
|
|
221
|
+
try {
|
|
222
|
+
const store = JSON.parse(readFileSync(keys_path, 'utf8'));
|
|
223
|
+
const entry = store[`${hub_url}::${seat}`];
|
|
224
|
+
if (!entry) return '';
|
|
225
|
+
if (typeof entry === 'string') return entry;
|
|
226
|
+
return String(entry.api_key || entry.key || '');
|
|
227
|
+
} catch {
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function createHubProxy(opts) {
|
|
233
|
+
const hub_url = String(opts.hubUrl || '').replace(/\/+$/, '');
|
|
234
|
+
const seat = String(opts.seat || '').trim();
|
|
235
|
+
const keys_path = String(opts.keysPath || '');
|
|
236
|
+
|
|
237
|
+
/** Lazily re-read per request: key rotation must not need a restart. */
|
|
238
|
+
function key() {
|
|
239
|
+
return read_seat_key(keys_path, hub_url, seat);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function forward(req, res, hub_path, search) {
|
|
243
|
+
// Allowlist matching runs on the DECODED path: the browser client
|
|
244
|
+
// percent-encodes channel names (dm%3Acontinuum--laurent), and the
|
|
245
|
+
// raw-path match refused every dm read with a misleading
|
|
246
|
+
// "outside the allowlist" (operator-hit 2026-07-15). The RAW path is
|
|
247
|
+
// what forwards — decoding is for the gate only, so an encoded "/"
|
|
248
|
+
// or ".." can never smuggle a different route past it.
|
|
249
|
+
let decoded_path = hub_path;
|
|
250
|
+
try {
|
|
251
|
+
decoded_path = decodeURIComponent(hub_path);
|
|
252
|
+
} catch {
|
|
253
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
254
|
+
res.end(JSON.stringify({ error: 'bad_path_encoding' }));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// eslint-disable-next-line no-control-regex
|
|
258
|
+
if (/[\u0000-\u001f]/.test(decoded_path) || decoded_path.includes('..')) {
|
|
259
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
260
|
+
res.end(JSON.stringify({ error: 'bad_path' }));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const route = HUB_ROUTES.find((r) => r.method === req.method && r.re.test(decoded_path));
|
|
264
|
+
if (!route) {
|
|
265
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
266
|
+
res.end(JSON.stringify({ error: 'hub_route_not_allowed', detail: `${req.method} ${decoded_path} is outside the Team page allowlist.` }));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Subresource-fetch gate (adversary: zero-click read-receipt forgery).
|
|
270
|
+
// A GET route may reach the hub with SIDE EFFECTS under the operator
|
|
271
|
+
// seat — GET /messages/{id} is the hub's read_message (records a read,
|
|
272
|
+
// unpins criticals). Because CSP is img-src 'self' and the proxy is
|
|
273
|
+
// same-origin, an untrusted message body rendering
|
|
274
|
+
// `` would auto-fire that GET as an <img>
|
|
275
|
+
// the instant it's viewed. The client sanitizer (neutralize_unsafe_embeds)
|
|
276
|
+
// is the primary defense; this is the server belt: browsers stamp
|
|
277
|
+
// Sec-Fetch-Dest on subresource loads (image/audio/video/…/object/embed),
|
|
278
|
+
// and NONE of our routes are legitimately loaded that way EXCEPT the
|
|
279
|
+
// attachment blob (an actual <img src>). Refuse any other route fired as
|
|
280
|
+
// a non-document/non-empty destination. Absent header (older browsers,
|
|
281
|
+
// curl) passes — the client sanitizer still covers the browser case.
|
|
282
|
+
const dest = String((req.headers && req.headers['sec-fetch-dest']) || '').toLowerCase();
|
|
283
|
+
const is_subresource = dest && dest !== 'empty' && dest !== 'document';
|
|
284
|
+
const is_attachment_blob = /^\/channels\/[A-Za-z0-9_.:-]+\/attachments\/[A-Za-z0-9_-]+$/.test(decoded_path);
|
|
285
|
+
if (is_subresource && !is_attachment_blob) {
|
|
286
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
287
|
+
res.end(JSON.stringify({ error: 'hub_subresource_blocked', detail: `Route ${decoded_path} may not be loaded as a "${dest}" subresource (only attachment blobs may).` }));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const seat_key = key();
|
|
291
|
+
if (!seat_key && !route.keyless) {
|
|
292
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
293
|
+
res.end(JSON.stringify({ error: 'hub_seat_unavailable', detail: `No hub key for seat "${seat}" (${keys_path}); set ABSTRACTCONTINUUM_HUB_KEY or provision the seat.` }));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Buffer the body with a hard cap (adversary #6): the hub caps
|
|
298
|
+
// attachments at 16 MiB and streams with its own running cap; this
|
|
299
|
+
// bounds the proxy's memory too (a same-origin client or a local
|
|
300
|
+
// non-browser caller can't OOM us with an unbounded body). 24 MiB
|
|
301
|
+
// leaves headroom over the 16 MiB file cap for form overhead.
|
|
302
|
+
const MAX_BODY_BYTES = 24 * 1024 * 1024;
|
|
303
|
+
const chunks = [];
|
|
304
|
+
let received = 0;
|
|
305
|
+
for await (const c of req) {
|
|
306
|
+
received += c.length;
|
|
307
|
+
if (received > MAX_BODY_BYTES) {
|
|
308
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
309
|
+
res.end(JSON.stringify({ error: 'body_too_large', detail: `Request body exceeds ${MAX_BODY_BYTES} bytes.` }));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
chunks.push(c);
|
|
313
|
+
}
|
|
314
|
+
const body = chunks.length ? Buffer.concat(chunks) : null;
|
|
315
|
+
|
|
316
|
+
// Writes must arrive as declared JSON — EXCEPT the attachment upload,
|
|
317
|
+
// which is a raw-bytes body by contract (agora 0091). The upload route
|
|
318
|
+
// is carved out here and forwards the declared Content-Type verbatim so
|
|
319
|
+
// the hub records the true type; every other write stays JSON-only (the
|
|
320
|
+
// CSRF/simple-request hardening).
|
|
321
|
+
if (body && req.method !== 'GET' && req.method !== 'HEAD' && !route.upload) {
|
|
322
|
+
const ctype = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
323
|
+
if (ctype !== 'application/json') {
|
|
324
|
+
res.writeHead(415, { 'Content-Type': 'application/json' });
|
|
325
|
+
res.end(JSON.stringify({ error: 'hub_body_not_json', detail: `Write bodies must be application/json (got "${ctype || 'none'}").` }));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
// Version handshake (hub 0.12.3 contract; operator dm 147 root fix):
|
|
332
|
+
// a client that identifies itself owns its staleness detection, and
|
|
333
|
+
// the hub stops appending the synthetic stale-client notice to /inbox
|
|
334
|
+
// — that phantom envelope rode a real channel+seq and inflated the
|
|
335
|
+
// console's unread badges (+1 per delivery, "2 unread, there is only
|
|
336
|
+
// one"). The console renders attachments and typed decorations, so it
|
|
337
|
+
// IS a current client; say so on every forwarded request.
|
|
338
|
+
const upstream_headers = {
|
|
339
|
+
'X-Agora-Client': `abstractcontinuum-console/${CONSOLE_VERSION}`,
|
|
340
|
+
...(seat_key ? { Authorization: `Bearer ${seat_key}` } : {}),
|
|
341
|
+
};
|
|
342
|
+
if (body) {
|
|
343
|
+
// Upload: forward the declared type verbatim (the hub stores it as
|
|
344
|
+
// metadata). Every other write: application/json.
|
|
345
|
+
upstream_headers['Content-Type'] = route.upload ? String(req.headers['content-type'] || 'application/octet-stream') : 'application/json';
|
|
346
|
+
}
|
|
347
|
+
const r = await fetch(`${hub_url}${hub_path}${search || ''}`, {
|
|
348
|
+
method: req.method,
|
|
349
|
+
headers: upstream_headers,
|
|
350
|
+
body: body && req.method !== 'GET' ? body : undefined,
|
|
351
|
+
// Loopback hub: 15s is generous. Without a signal, undici hangs
|
|
352
|
+
// ~300s per request against a wedged hub and the browser's
|
|
353
|
+
// connection pool starves (adversarial F3, dm 99).
|
|
354
|
+
signal: AbortSignal.timeout(15_000),
|
|
355
|
+
// Never follow a hub redirect (adversary b22b19ed P2, SSRF-adjacent):
|
|
356
|
+
// a 3xx on the attachment route would otherwise be chased by undici's
|
|
357
|
+
// default redirect:follow. The proxy is a fixed-target door.
|
|
358
|
+
redirect: 'manual',
|
|
359
|
+
});
|
|
360
|
+
if (route.binary) {
|
|
361
|
+
// Attachment FETCH: pass the bytes through unmodified (r.text()
|
|
362
|
+
// would corrupt binary), and forward the hub's serve-hardening
|
|
363
|
+
// headers verbatim (Content-Disposition:attachment, nosniff,
|
|
364
|
+
// octet-stream-for-active-types) so the browser honors them.
|
|
365
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
366
|
+
const ctype = r.headers.get('content-type') || 'application/octet-stream';
|
|
367
|
+
const out_headers = { 'Content-Type': ctype };
|
|
368
|
+
// Belt (adversary b22b19ed): nosniff prevents SNIFFING but not
|
|
369
|
+
// HONORING a declared active type, and forwarding a hub-sent
|
|
370
|
+
// `Content-Disposition: inline` on an active type would let it render
|
|
371
|
+
// as HTML in our origin. Force `attachment` for ANY active type
|
|
372
|
+
// regardless of what the hub sent, and default to attachment when
|
|
373
|
+
// absent — the disposition is only trusted (verbatim) for inert types.
|
|
374
|
+
const base_type = ctype.split(';')[0].trim().toLowerCase();
|
|
375
|
+
const is_active = /html|xml|svg/.test(base_type);
|
|
376
|
+
const cd = r.headers.get('content-disposition');
|
|
377
|
+
out_headers['Content-Disposition'] = is_active ? 'attachment' : cd || 'attachment';
|
|
378
|
+
out_headers['X-Content-Type-Options'] = r.headers.get('x-content-type-options') || 'nosniff';
|
|
379
|
+
res.writeHead(r.status, out_headers);
|
|
380
|
+
res.end(buf);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const text = await r.text();
|
|
384
|
+
res.writeHead(r.status, { 'Content-Type': r.headers.get('content-type') || 'application/json' });
|
|
385
|
+
res.end(text);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
388
|
+
res.end(JSON.stringify({ error: 'hub_unreachable', detail: String(e && e.message ? e.message : e) }));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
/**
|
|
394
|
+
* WebSocket relay for live updates (operator c2240: "is everything
|
|
395
|
+
* updated in realtime? they need to."). Mount on the server's
|
|
396
|
+
* 'upgrade' event: browser ⇄ this relay ⇄ hub /ws, with the seat key
|
|
397
|
+
* attached SERVER-side (the browser never sees it; EventSource-style
|
|
398
|
+
* token-in-URL is forbidden by the connection contract). Requires
|
|
399
|
+
* the `ws` package; absent, callers keep the 5s polling lane.
|
|
400
|
+
*/
|
|
401
|
+
async handleUpgrade(req, socket, head) {
|
|
402
|
+
const url_path = String(req.url || '').split('?')[0];
|
|
403
|
+
if (url_path !== '/api/hub/ws') return false;
|
|
404
|
+
// F7 (dm-99 tail): a raw upgrade socket with NO 'error' listener is a
|
|
405
|
+
// process-killer — a browser abort during the async window below
|
|
406
|
+
// (dynamic import, upstream connect) raises an uncaught exception.
|
|
407
|
+
// Attach the guard BEFORE the first await.
|
|
408
|
+
socket.on('error', () => {
|
|
409
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
410
|
+
});
|
|
411
|
+
try {
|
|
412
|
+
// Same unforgeable peer gate as the HTTP surface, PLUS the origin
|
|
413
|
+
// gate — WS handshakes have no CORS preflight, so without it any
|
|
414
|
+
// page the operator visits could open this relay and read/write
|
|
415
|
+
// as the operator seat (adversary P0).
|
|
416
|
+
const peer = String(req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : '');
|
|
417
|
+
const loopback = peer === '127.0.0.1' || peer === '::1' || peer === '::ffff:127.0.0.1' || peer === '';
|
|
418
|
+
const allow_remote = String(process.env.ABSTRACTCONTINUUM_HUB_ALLOW_REMOTE || '').trim() === '1';
|
|
419
|
+
const seat_key = key();
|
|
420
|
+
let WebSocketServer, WebSocket;
|
|
421
|
+
try {
|
|
422
|
+
({ WebSocketServer, WebSocket } = await import('ws'));
|
|
423
|
+
} catch {
|
|
424
|
+
socket.destroy();
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
if ((!loopback && !allow_remote) || !seat_key || !origin_allowed(req)) {
|
|
428
|
+
socket.destroy();
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
// Compression OFF on both legs: a negotiated permessage-deflate on
|
|
432
|
+
// one side of a string-pipe relay ships RSV1 frames the other side
|
|
433
|
+
// never negotiated (live find: browsers/ws clients offer it by
|
|
434
|
+
// default and the pipe corrupts).
|
|
435
|
+
if (!this._wss) this._wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
436
|
+
const hub_ws_url = hub_url.replace(/^http/, 'ws') + '/ws';
|
|
437
|
+
const upstream = new WebSocket(hub_ws_url, {
|
|
438
|
+
headers: { Authorization: `Bearer ${seat_key}` },
|
|
439
|
+
perMessageDeflate: false,
|
|
440
|
+
});
|
|
441
|
+
// Handshake deadline (F7): a hub that ACCEPTS TCP but never
|
|
442
|
+
// completes the WS handshake would strand the browser socket
|
|
443
|
+
// forever (no 'open', no 'error'). 15s is generous for loopback;
|
|
444
|
+
// on expiry both legs die and the client falls back to polling.
|
|
445
|
+
const handshake_timer = setTimeout(() => {
|
|
446
|
+
try { upstream.terminate(); } catch { /* already gone */ }
|
|
447
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
448
|
+
}, 15_000);
|
|
449
|
+
// Browser leg dying during the connect window must reap the
|
|
450
|
+
// upstream (wave adversary P1-1): ws's handleUpgrade returns
|
|
451
|
+
// WITHOUT invoking the callback on a non-readable socket, so an
|
|
452
|
+
// upstream that opens after a browser abort would otherwise live
|
|
453
|
+
// on listener-less, holding a seat-key-authenticated hub
|
|
454
|
+
// connection until hub restart. 'close' fires for orderly aborts
|
|
455
|
+
// too ('error' alone misses them).
|
|
456
|
+
socket.on('close', () => {
|
|
457
|
+
try { upstream.terminate(); } catch { /* already gone */ }
|
|
458
|
+
});
|
|
459
|
+
// Accept the browser only AFTER the hub leg is up: an onopen-lit
|
|
460
|
+
// "live" dot over a dead upstream was a lie (adversary find), and
|
|
461
|
+
// the pending-frame queue becomes unnecessary by construction.
|
|
462
|
+
upstream.on('open', () => {
|
|
463
|
+
clearTimeout(handshake_timer);
|
|
464
|
+
if (socket.destroyed) {
|
|
465
|
+
// Browser gone while the hub leg connected — reap, never
|
|
466
|
+
// hand a dead socket to handleUpgrade (its callback would
|
|
467
|
+
// silently not run and the upstream would leak).
|
|
468
|
+
try { upstream.terminate(); } catch { /* already gone */ }
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this._wss.handleUpgrade(req, socket, head, (client) => {
|
|
472
|
+
const close_both = () => {
|
|
473
|
+
try { client.close(); } catch { /* already closed */ }
|
|
474
|
+
try { upstream.close(); } catch { /* already closed */ }
|
|
475
|
+
};
|
|
476
|
+
client.on('message', (data) => {
|
|
477
|
+
// Frame-type allowlist (the WS twin of the HTTP route
|
|
478
|
+
// allowlist): the relay is a door, not a tunnel. subscribe/
|
|
479
|
+
// ping are the only frames this UI sends; ack must ride the
|
|
480
|
+
// distinct audited HTTP path (c1696), and post/presence have
|
|
481
|
+
// HTTP routes with their own gates.
|
|
482
|
+
let kind = '';
|
|
483
|
+
try {
|
|
484
|
+
kind = String(JSON.parse(data.toString())?.type || '');
|
|
485
|
+
} catch {
|
|
486
|
+
return; // non-JSON frame — drop
|
|
487
|
+
}
|
|
488
|
+
if (kind !== 'subscribe' && kind !== 'ping') return;
|
|
489
|
+
try { upstream.send(data.toString()); } catch { close_both(); }
|
|
490
|
+
});
|
|
491
|
+
upstream.on('message', (data) => {
|
|
492
|
+
try { client.send(data.toString()); } catch { close_both(); }
|
|
493
|
+
});
|
|
494
|
+
upstream.on('close', close_both);
|
|
495
|
+
upstream.on('error', close_both);
|
|
496
|
+
client.on('close', close_both);
|
|
497
|
+
client.on('error', close_both);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
upstream.on('error', () => {
|
|
501
|
+
// Hub leg failed before accept: refuse the upgrade so the
|
|
502
|
+
// browser falls back to polling honestly.
|
|
503
|
+
clearTimeout(handshake_timer);
|
|
504
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
505
|
+
});
|
|
506
|
+
} catch {
|
|
507
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
508
|
+
}
|
|
509
|
+
return true;
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
/** Returns true when the request was handled. */
|
|
513
|
+
handle(req, res, pathname, search) {
|
|
514
|
+
if (!pathname.startsWith('/api/hub/') && pathname !== '/api/hub/meta') return false;
|
|
515
|
+
|
|
516
|
+
// Socket-peer gate (entity's c1768 class, applied at the mount): this
|
|
517
|
+
// proxy authors hub messages AS THE OPERATOR, so a non-loopback peer
|
|
518
|
+
// reaching it could forge the operator's authorship. Gate on the
|
|
519
|
+
// connection's REAL peer (unforgeable), never a client-controlled
|
|
520
|
+
// header. The default 127.0.0.1 bind already prevents remote peers;
|
|
521
|
+
// this is defense-in-depth if HOST is widened deliberately.
|
|
522
|
+
const peer = String(req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : '');
|
|
523
|
+
const loopback = peer === '127.0.0.1' || peer === '::1' || peer === '::ffff:127.0.0.1' || peer === '';
|
|
524
|
+
const allow_remote = String(process.env.ABSTRACTCONTINUUM_HUB_ALLOW_REMOTE || '').trim() === '1';
|
|
525
|
+
if (!loopback && !allow_remote) {
|
|
526
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
527
|
+
res.end(JSON.stringify({ error: 'hub_proxy_non_loopback', detail: `Hub proxy refuses non-loopback peer ${peer} (it authors as the operator seat). Set ABSTRACTCONTINUUM_HUB_ALLOW_REMOTE=1 only behind a trusted front.` }));
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
// Cross-origin browser requests are refused even from loopback: the
|
|
531
|
+
// browser is a loopback peer, and a hostile page can fire simple
|
|
532
|
+
// POSTs (no preflight) at this surface. Origin-vs-Host is the gate.
|
|
533
|
+
if (!origin_allowed(req)) {
|
|
534
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
535
|
+
res.end(JSON.stringify({ error: 'hub_proxy_cross_origin', detail: 'Cross-origin requests to the hub proxy are refused (the proxy authors as the operator seat).' }));
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (pathname === '/api/hub/meta') {
|
|
540
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
541
|
+
res.end(JSON.stringify({ ok: true, hub_url, seat, seat_key_present: Boolean(key()) }));
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
const hub_path = pathname.slice('/api/hub'.length);
|
|
545
|
+
void forward(req, res, hub_path, search);
|
|
546
|
+
return true;
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-FqMZZAe1.js";import{p as f}from"./chunk-JWPE2WC7-DZoiE5m4.js";import{_ as n,l as o}from"./mermaid.core-CrABX5SH.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-DzsDaKjB.js";import"./index-DmnZnFTc.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};
|
|
2
|
+
//# sourceMappingURL=abnfDiagram-VRR7QNED-DHZzhKO_.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Y as ln,$ as an,a0 as F,a1 as q,a2 as j,a3 as un,a4 as y,a5 as tn,a6 as J,a7 as _,a8 as rn,a9 as o,aa as on,ab as sn,ac as fn}from"./mermaid.core-CrABX5SH.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,I,D,v,A,z,a){var O=I-l,i=D-h,n=z-v,d=a-A,u=d*O-n*i;if(!(u*u<y))return u=(n*(h-A)-d*(l-v))/u,[l+u*O,h+u*i]}function U(l,h,I,D,v,A,z){var a=l-I,O=h-D,i=(z?A:-A)/J(a*a+O*O),n=i*O,d=-i*a,u=l+n,s=h+d,f=I+n,c=D+d,B=(u+f)/2,t=(s+c)/2,m=f-u,g=c-s,R=m*m+g*g,T=v-A,P=u*c-f*s,S=(g<0?-1:1)*J(on(0,T*T*R-P*P)),Y=(P*g-m*S)/R,$=(-P*m-g*S)/R,w=(P*g+m*S)/R,p=(-P*m+g*S)/R,x=Y-B,e=$-t,r=w-B,C=p-t;return x*x+e*e>r*r+C*C&&(Y=w,$=p),{cx:Y,cy:$,x01:-n,y01:-d,x11:Y*(v/T-1),y11:$*(v/T-1)}}function hn(){var l=cn,h=yn,I=j(0),D=null,v=gn,A=dn,z=mn,a=null,O=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-un,c=A.apply(this,arguments)-un,B=rn(c-f),t=c>f;if(a||(a=n=O()),s<u&&(d=s,s=u,u=d),!(s>y))a.moveTo(0,0);else if(B>tn-y)a.moveTo(s*F(f),s*q(f)),a.arc(0,0,s,f,c,!t),u>y&&(a.moveTo(u*F(c),u*q(c)),a.arc(0,0,u,c,f,t));else{var m=f,g=c,R=f,T=c,P=B,S=B,Y=z.apply(this,arguments)/2,$=Y>y&&(D?+D.apply(this,arguments):J(u*u+s*s)),w=_(rn(s-u)/2,+I.apply(this,arguments)),p=w,x=w,e,r;if($>y){var C=sn($/u*q(Y)),K=sn($/s*q(Y));(P-=C*2)>y?(C*=t?1:-1,R+=C,T-=C):(P=0,R=T=(f+c)/2),(S-=K*2)>y?(K*=t?1:-1,m+=K,g-=K):(S=0,m=g=(f+c)/2)}var G=s*F(m),H=s*q(m),L=u*F(T),M=u*q(T);if(w>y){var N=s*F(g),Q=s*q(g),V=u*F(R),W=u*q(R),E;if(B<an)if(E=pn(G,H,V,W,N,Q,L,M)){var X=G-E[0],Z=H-E[1],b=N-E[0],k=Q-E[1],nn=1/q(fn((X*b+Z*k)/(J(X*X+Z*Z)*J(b*b+k*k)))/2),en=J(E[0]*E[0]+E[1]*E[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}S>y?x>y?(e=U(V,W,G,H,s,x,t),r=U(N,Q,L,M,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo(G,H),a.arc(0,0,s,m,g,!t)):a.moveTo(G,H),!(u>y)||!(P>y)?a.lineTo(L,M):p>y?(e=U(L,M,N,Q,u,-p,t),r=U(G,H,V,W,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,R,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +A.apply(this,arguments))/2-an/2;return[F(d)*n,q(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:j(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:j(+n),i):h},i.cornerRadius=function(n){return arguments.length?(I=typeof n=="function"?n:j(+n),i):I},i.padRadius=function(n){return arguments.length?(D=n==null?null:typeof n=="function"?n:j(+n),i):D},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:j(+n),i):v},i.endAngle=function(n){return arguments.length?(A=typeof n=="function"?n:j(+n),i):A},i.padAngle=function(n){return arguments.length?(z=typeof n=="function"?n:j(+n),i):z},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};
|
|
2
|
+
//# sourceMappingURL=arc-9iPl5OLd.js.map
|