@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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/bin/cli.js +206 -0
  4. package/bin/hub_proxy.js +549 -0
  5. package/dist/assets/abnfDiagram-VRR7QNED-DHZzhKO_.js +2 -0
  6. package/dist/assets/arc-9iPl5OLd.js +2 -0
  7. package/dist/assets/architectureDiagram-ZJ3FMSHR-BVRc0NRl.js +37 -0
  8. package/dist/assets/blockDiagram-677ZJIJ3-BTIe50OR.js +133 -0
  9. package/dist/assets/c4Diagram-LMCZKHZV-D5P-4VLr.js +11 -0
  10. package/dist/assets/channel-CEGeSBNz.js +2 -0
  11. package/dist/assets/chunk-2Q5K7J3B-WcmxYPWP.js +2 -0
  12. package/dist/assets/chunk-32BRIVSS-BPArWk_S.js +2 -0
  13. package/dist/assets/chunk-5VM5RSS4-DplW9bbf.js +16 -0
  14. package/dist/assets/chunk-EX3LRPZG-B8O0P99j.js +232 -0
  15. package/dist/assets/chunk-JWPE2WC7-DZoiE5m4.js +2 -0
  16. package/dist/assets/chunk-MOJQB5TN-FqMZZAe1.js +89 -0
  17. package/dist/assets/chunk-RYQCIY6F-D4I_kub_.js +2 -0
  18. package/dist/assets/chunk-V7JOEXUC-CwJZa6s3.js +207 -0
  19. package/dist/assets/chunk-VR4S4FIN-CdF7hCRc.js +2 -0
  20. package/dist/assets/chunk-XXDRQBXY-C2vm2DXm.js +2 -0
  21. package/dist/assets/classDiagram-OUVF2IWQ-D11Ru5dS.js +2 -0
  22. package/dist/assets/classDiagram-v2-EOCWNBFH-D11Ru5dS.js +2 -0
  23. package/dist/assets/cose-bilkent-JH36ORCC-BEoqSVoj.js +2 -0
  24. package/dist/assets/cynefin-VYW2F7L2-DzsDaKjB.js +179 -0
  25. package/dist/assets/cynefinDiagram-TSTJHNR4-qVt2ETEm.js +63 -0
  26. package/dist/assets/cytoscape.esm-DTSO7Bv0.js +332 -0
  27. package/dist/assets/dagre-VKFMJZFB-BszpuHqM.js +5 -0
  28. package/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  29. package/dist/assets/diagram-FQU43EPY-DkSwN897.js +4 -0
  30. package/dist/assets/diagram-G47NLZAW-DmmkniMR.js +25 -0
  31. package/dist/assets/diagram-NH7WQ7WH-PkbaZ3fb.js +25 -0
  32. package/dist/assets/diagram-OA4YK3LP-D5O-VGkx.js +31 -0
  33. package/dist/assets/diagram-WEI45ONY-CtS1f8zZ.js +42 -0
  34. package/dist/assets/ebnfDiagram-CCIWWBDH-2ydDo0Sq.js +2 -0
  35. package/dist/assets/erDiagram-Q63AITRT-Dc-2Dmcc.js +86 -0
  36. package/dist/assets/flowDiagram-23GEKE2U-DKKOHSrp.js +157 -0
  37. package/dist/assets/ganttDiagram-NO4QXBWP-B9Zi-Zd_.js +293 -0
  38. package/dist/assets/gitGraphDiagram-IHSO6WYX-lXzgHn-s.js +107 -0
  39. package/dist/assets/graph-C9eacEi8.js +2 -0
  40. package/dist/assets/index-CCcufdYK.css +1 -0
  41. package/dist/assets/index-DmnZnFTc.js +124 -0
  42. package/dist/assets/infoDiagram-FWYZ7A6U-BHV_H3bL.js +3 -0
  43. package/dist/assets/init-Gi6I4Gst.js +2 -0
  44. package/dist/assets/ishikawaDiagram-FXEZZL3T-BX74rKth.js +71 -0
  45. package/dist/assets/journeyDiagram-5HDEW3XC-BByysGFJ.js +140 -0
  46. package/dist/assets/kanban-definition-HUTT4EX6-DatfLmCp.js +90 -0
  47. package/dist/assets/katex-C5jXJg4s.js +258 -0
  48. package/dist/assets/layout-DEXfKzaS.js +2 -0
  49. package/dist/assets/linear-BgTPNh4i.js +2 -0
  50. package/dist/assets/map-Czzmt4hB.js +2 -0
  51. package/dist/assets/mermaid.core-CrABX5SH.js +315 -0
  52. package/dist/assets/mindmap-definition-LN4V7U3C-Bz1XgdXA.js +97 -0
  53. package/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  54. package/dist/assets/pegDiagram-2B236MQR-D5B8Egll.js +2 -0
  55. package/dist/assets/pieDiagram-ENE6RG2P-BaDg4f41.js +40 -0
  56. package/dist/assets/quadrantDiagram-ABIIQ3AL-jr1V5VXJ.js +8 -0
  57. package/dist/assets/railroadDiagram-RFXS5EU6-F13Ho8ra.js +2 -0
  58. package/dist/assets/requirementDiagram-TGXJPOKE-DXb_w8Ww.js +85 -0
  59. package/dist/assets/sankeyDiagram-HTMAVEWB-BN42kL5b.js +41 -0
  60. package/dist/assets/sequenceDiagram-DBY2YBRQ-DI7-dEyi.js +163 -0
  61. package/dist/assets/sizeCapture-X5ZJPWSS-Dv-15ctr.js +2 -0
  62. package/dist/assets/stateDiagram-2N3HPSRC-De5ObM42.js +2 -0
  63. package/dist/assets/stateDiagram-v2-6OUMAXLB-D9AwhP3S.js +2 -0
  64. package/dist/assets/swimlanes-5IMT3BWC-DzZwgXNi.js +3 -0
  65. package/dist/assets/swimlanesDiagram-G3AALYLV-DyzT80_E.js +9 -0
  66. package/dist/assets/timeline-definition-FHXFAJF6-BO8KkL_w.js +121 -0
  67. package/dist/assets/vennDiagram-L72KCM5P-D6LUv_CX.js +35 -0
  68. package/dist/assets/wardleyDiagram-EHGQE667-DWJp0Lmi.js +79 -0
  69. package/dist/assets/xychartDiagram-FW5EYKEG-B5IA4jNf.js +8 -0
  70. package/dist/index.html +14 -0
  71. package/package.json +68 -0
@@ -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
+ // `![x](/api/hub/.../messages/id)` 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