@czap/astro 0.1.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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/dist/Satellite.d.ts +42 -0
  4. package/dist/Satellite.d.ts.map +1 -0
  5. package/dist/Satellite.js +55 -0
  6. package/dist/Satellite.js.map +1 -0
  7. package/dist/client-directives/gpu.d.ts +3 -0
  8. package/dist/client-directives/gpu.d.ts.map +1 -0
  9. package/dist/client-directives/gpu.js +5 -0
  10. package/dist/client-directives/gpu.js.map +1 -0
  11. package/dist/client-directives/llm.d.ts +3 -0
  12. package/dist/client-directives/llm.d.ts.map +1 -0
  13. package/dist/client-directives/llm.js +5 -0
  14. package/dist/client-directives/llm.js.map +1 -0
  15. package/dist/client-directives/satellite.d.ts +3 -0
  16. package/dist/client-directives/satellite.d.ts.map +1 -0
  17. package/dist/client-directives/satellite.js +5 -0
  18. package/dist/client-directives/satellite.js.map +1 -0
  19. package/dist/client-directives/stream.d.ts +3 -0
  20. package/dist/client-directives/stream.d.ts.map +1 -0
  21. package/dist/client-directives/stream.js +5 -0
  22. package/dist/client-directives/stream.js.map +1 -0
  23. package/dist/client-directives/wasm.d.ts +3 -0
  24. package/dist/client-directives/wasm.d.ts.map +1 -0
  25. package/dist/client-directives/wasm.js +6 -0
  26. package/dist/client-directives/wasm.js.map +1 -0
  27. package/dist/client-directives/worker.d.ts +3 -0
  28. package/dist/client-directives/worker.d.ts.map +1 -0
  29. package/dist/client-directives/worker.js +5 -0
  30. package/dist/client-directives/worker.js.map +1 -0
  31. package/dist/detect-upgrade.d.ts +16 -0
  32. package/dist/detect-upgrade.d.ts.map +1 -0
  33. package/dist/detect-upgrade.js +105 -0
  34. package/dist/detect-upgrade.js.map +1 -0
  35. package/dist/headers.d.ts +45 -0
  36. package/dist/headers.d.ts.map +1 -0
  37. package/dist/headers.js +64 -0
  38. package/dist/headers.js.map +1 -0
  39. package/dist/index.d.ts +30 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +26 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/integration.d.ts +76 -0
  44. package/dist/integration.d.ts.map +1 -0
  45. package/dist/integration.js +240 -0
  46. package/dist/integration.js.map +1 -0
  47. package/dist/middleware.d.ts +69 -0
  48. package/dist/middleware.d.ts.map +1 -0
  49. package/dist/middleware.js +75 -0
  50. package/dist/middleware.js.map +1 -0
  51. package/dist/quantize.d.ts +50 -0
  52. package/dist/quantize.d.ts.map +1 -0
  53. package/dist/quantize.js +122 -0
  54. package/dist/quantize.js.map +1 -0
  55. package/dist/runtime/boundary.d.ts +123 -0
  56. package/dist/runtime/boundary.d.ts.map +1 -0
  57. package/dist/runtime/boundary.js +164 -0
  58. package/dist/runtime/boundary.js.map +1 -0
  59. package/dist/runtime/globals.d.ts +32 -0
  60. package/dist/runtime/globals.d.ts.map +1 -0
  61. package/dist/runtime/globals.js +45 -0
  62. package/dist/runtime/globals.js.map +1 -0
  63. package/dist/runtime/gpu.d.ts +15 -0
  64. package/dist/runtime/gpu.d.ts.map +1 -0
  65. package/dist/runtime/gpu.js +266 -0
  66. package/dist/runtime/gpu.js.map +1 -0
  67. package/dist/runtime/index.d.ts +7 -0
  68. package/dist/runtime/index.d.ts.map +1 -0
  69. package/dist/runtime/index.js +5 -0
  70. package/dist/runtime/index.js.map +1 -0
  71. package/dist/runtime/llm-receipt-tracker.d.ts +21 -0
  72. package/dist/runtime/llm-receipt-tracker.d.ts.map +1 -0
  73. package/dist/runtime/llm-receipt-tracker.js +60 -0
  74. package/dist/runtime/llm-receipt-tracker.js.map +1 -0
  75. package/dist/runtime/llm-render-pipeline.d.ts +89 -0
  76. package/dist/runtime/llm-render-pipeline.d.ts.map +1 -0
  77. package/dist/runtime/llm-render-pipeline.js +241 -0
  78. package/dist/runtime/llm-render-pipeline.js.map +1 -0
  79. package/dist/runtime/llm-session.d.ts +126 -0
  80. package/dist/runtime/llm-session.d.ts.map +1 -0
  81. package/dist/runtime/llm-session.js +385 -0
  82. package/dist/runtime/llm-session.js.map +1 -0
  83. package/dist/runtime/llm.d.ts +16 -0
  84. package/dist/runtime/llm.d.ts.map +1 -0
  85. package/dist/runtime/llm.js +273 -0
  86. package/dist/runtime/llm.js.map +1 -0
  87. package/dist/runtime/policy.d.ts +100 -0
  88. package/dist/runtime/policy.d.ts.map +1 -0
  89. package/dist/runtime/policy.js +147 -0
  90. package/dist/runtime/policy.js.map +1 -0
  91. package/dist/runtime/receipt-chain.d.ts +22 -0
  92. package/dist/runtime/receipt-chain.d.ts.map +1 -0
  93. package/dist/runtime/receipt-chain.js +80 -0
  94. package/dist/runtime/receipt-chain.js.map +1 -0
  95. package/dist/runtime/runtime-session.d.ts +34 -0
  96. package/dist/runtime/runtime-session.d.ts.map +1 -0
  97. package/dist/runtime/runtime-session.js +102 -0
  98. package/dist/runtime/runtime-session.js.map +1 -0
  99. package/dist/runtime/satellite.d.ts +13 -0
  100. package/dist/runtime/satellite.d.ts.map +1 -0
  101. package/dist/runtime/satellite.js +59 -0
  102. package/dist/runtime/satellite.js.map +1 -0
  103. package/dist/runtime/slots.d.ts +34 -0
  104. package/dist/runtime/slots.d.ts.map +1 -0
  105. package/dist/runtime/slots.js +108 -0
  106. package/dist/runtime/slots.js.map +1 -0
  107. package/dist/runtime/stream-session.d.ts +47 -0
  108. package/dist/runtime/stream-session.d.ts.map +1 -0
  109. package/dist/runtime/stream-session.js +82 -0
  110. package/dist/runtime/stream-session.js.map +1 -0
  111. package/dist/runtime/stream.d.ts +9 -0
  112. package/dist/runtime/stream.d.ts.map +1 -0
  113. package/dist/runtime/stream.js +308 -0
  114. package/dist/runtime/stream.js.map +1 -0
  115. package/dist/runtime/url-policy.d.ts +28 -0
  116. package/dist/runtime/url-policy.d.ts.map +1 -0
  117. package/dist/runtime/url-policy.js +87 -0
  118. package/dist/runtime/url-policy.js.map +1 -0
  119. package/dist/runtime/wasm.d.ts +20 -0
  120. package/dist/runtime/wasm.d.ts.map +1 -0
  121. package/dist/runtime/wasm.js +70 -0
  122. package/dist/runtime/wasm.js.map +1 -0
  123. package/dist/runtime/worker.d.ts +11 -0
  124. package/dist/runtime/worker.d.ts.map +1 -0
  125. package/dist/runtime/worker.js +249 -0
  126. package/dist/runtime/worker.js.map +1 -0
  127. package/package.json +106 -0
  128. package/src/Satellite.astro +39 -0
  129. package/src/Satellite.ts +84 -0
  130. package/src/client-directives/gpu.ts +5 -0
  131. package/src/client-directives/llm.ts +5 -0
  132. package/src/client-directives/satellite.ts +5 -0
  133. package/src/client-directives/stream.ts +5 -0
  134. package/src/client-directives/wasm.ts +6 -0
  135. package/src/client-directives/worker.ts +5 -0
  136. package/src/detect-upgrade.ts +105 -0
  137. package/src/headers.ts +84 -0
  138. package/src/index.ts +30 -0
  139. package/src/integration.ts +309 -0
  140. package/src/middleware.ts +133 -0
  141. package/src/quantize.ts +173 -0
  142. package/src/runtime/boundary.ts +263 -0
  143. package/src/runtime/globals.ts +57 -0
  144. package/src/runtime/gpu.ts +291 -0
  145. package/src/runtime/index.ts +12 -0
  146. package/src/runtime/llm-receipt-tracker.ts +88 -0
  147. package/src/runtime/llm-render-pipeline.ts +366 -0
  148. package/src/runtime/llm-session.ts +548 -0
  149. package/src/runtime/llm.ts +344 -0
  150. package/src/runtime/policy.ts +229 -0
  151. package/src/runtime/receipt-chain.ts +106 -0
  152. package/src/runtime/runtime-session.ts +139 -0
  153. package/src/runtime/satellite.ts +80 -0
  154. package/src/runtime/slots.ts +136 -0
  155. package/src/runtime/stream-session.ts +125 -0
  156. package/src/runtime/stream.ts +407 -0
  157. package/src/runtime/url-policy.ts +107 -0
  158. package/src/runtime/wasm.ts +85 -0
  159. package/src/runtime/worker.ts +307 -0
@@ -0,0 +1,407 @@
1
+ import { Effect } from 'effect';
2
+ import { Millis, SSE_RECONNECT_INITIAL_MS, SSE_RECONNECT_MAX_MS } from '@czap/core';
3
+ import { Morph, Resumption, SSE, SlotAddressing, SlotRegistry, resolveHtmlString } from '@czap/web';
4
+ import type { ResumeResponse, SSEMessage } from '@czap/web';
5
+ import { bootstrapSlots, rescanSlots } from './slots.js';
6
+ import { readRuntimeHtmlPolicy, readRuntimeEndpointPolicy } from './policy.js';
7
+ import { createStreamScheduler } from './stream-session.js';
8
+ import { allowRuntimeEndpointUrl } from './url-policy.js';
9
+
10
+ type Locator =
11
+ | { readonly type: 'slot'; readonly value: string }
12
+ | { readonly type: 'id'; readonly value: string }
13
+ | { readonly type: 'semantic-id'; readonly value: string };
14
+
15
+ function targetLocator(element: HTMLElement): Locator | null {
16
+ const slot = element.getAttribute('data-czap-slot');
17
+ if (slot) {
18
+ return { type: 'slot', value: slot };
19
+ }
20
+
21
+ if (element.id) {
22
+ return { type: 'id', value: element.id };
23
+ }
24
+
25
+ const semanticId = element.getAttribute('data-czap-id');
26
+ if (semanticId) {
27
+ return { type: 'semantic-id', value: semanticId };
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ function findTarget(locator: Locator | null): HTMLElement | null {
34
+ if (!locator) {
35
+ return null;
36
+ }
37
+
38
+ switch (locator.type) {
39
+ case 'slot': {
40
+ const el = SlotRegistry.findElement(SlotAddressing.brand(locator.value));
41
+ /* v8 ignore next — slot elements are always HTML host elements (divs/sections/etc.);
42
+ this narrows SlotRegistry.findElement's generic `Element | null` return so SVG-like
43
+ non-HTML descendants are rejected if they ever leak into the slot registry. */
44
+ return el instanceof HTMLElement ? el : null;
45
+ }
46
+ case 'id':
47
+ return document.getElementById(locator.value);
48
+ case 'semantic-id': {
49
+ const root = document.documentElement;
50
+ if (root.getAttribute('data-czap-id') === locator.value) {
51
+ return root;
52
+ }
53
+
54
+ for (const candidate of Array.from(root.querySelectorAll('[data-czap-id]'))) {
55
+ if (candidate.getAttribute('data-czap-id') === locator.value && candidate instanceof HTMLElement) {
56
+ return candidate;
57
+ }
58
+ }
59
+
60
+ return null;
61
+ }
62
+ }
63
+ }
64
+
65
+ function messageHtml(message: SSEMessage): string | null {
66
+ if ((message.type === 'patch' || message.type === 'batch') && typeof message.data === 'string') {
67
+ return message.data;
68
+ }
69
+
70
+ if (message.type === 'snapshot' && message.data !== null && typeof message.data === 'object') {
71
+ if ('html' in message.data && typeof message.data.html === 'string') {
72
+ return message.data.html;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ return null;
78
+ }
79
+
80
+ function replayHtml(patch: unknown): string | null {
81
+ if (typeof patch === 'string') {
82
+ return patch;
83
+ }
84
+
85
+ if (patch !== null && typeof patch === 'object') {
86
+ if ('html' in patch && typeof patch.html === 'string') {
87
+ return patch.html;
88
+ }
89
+ if ('data' in patch && typeof patch.data === 'string') {
90
+ return patch.data;
91
+ }
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ function patchCouldInvalidateSlots(
98
+ locator: Locator | null,
99
+ morphStyle: 'innerHTML' | 'outerHTML',
100
+ html: string,
101
+ ): boolean {
102
+ if (morphStyle === 'outerHTML') {
103
+ return true;
104
+ }
105
+
106
+ if (locator?.type === 'slot') {
107
+ return true;
108
+ }
109
+
110
+ return (
111
+ html.includes('data-czap-slot') ||
112
+ html.includes('data-czap-id') ||
113
+ html.includes(' id=') ||
114
+ html.includes(' id="') ||
115
+ html.includes(" id='")
116
+ );
117
+ }
118
+
119
+ function saveResumptionState(artifactId: string | undefined, lastEventId: string): void {
120
+ if (!artifactId || !lastEventId) {
121
+ return;
122
+ }
123
+
124
+ const parsed = Resumption.parseEventId(lastEventId);
125
+ Effect.runSync(
126
+ Resumption.saveState({
127
+ artifactId,
128
+ lastEventId,
129
+ lastSequence: parsed.sequence,
130
+ timestamp: Date.now(),
131
+ }),
132
+ );
133
+ }
134
+
135
+ function hasCustomEndpointPolicy(policy: ReturnType<typeof readRuntimeEndpointPolicy>): boolean {
136
+ return (
137
+ policy.mode !== 'same-origin' ||
138
+ policy.allowOrigins.length > 0 ||
139
+ Object.values(policy.byKind).some((allowlist) => allowlist.length > 0)
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Entry point for the `client:stream` directive. Opens an SSE client
145
+ * to the `data-czap-stream-url` endpoint, funnels incoming HTML
146
+ * patches through a {@link createStreamScheduler}, and triggers slot
147
+ * rescans when necessary. Honors `czap:reinit` / `czap:dispose` to
148
+ * survive Astro view transitions.
149
+ */
150
+ export function initStreamDirective(load: () => Promise<unknown>, element: HTMLElement): void {
151
+ bootstrapSlots();
152
+ const endpointPolicy = readRuntimeEndpointPolicy();
153
+ const htmlPolicy = readRuntimeHtmlPolicy();
154
+ const prepareHtml = (html: string): string =>
155
+ resolveHtmlString(html, {
156
+ policy: htmlPolicy.streamDefault,
157
+ allowTrustedHtml: htmlPolicy.allowTrustedHtml,
158
+ });
159
+
160
+ let target = element;
161
+ let reinitTarget: HTMLElement | null = null;
162
+ const streamUrl = allowRuntimeEndpointUrl(
163
+ target.getAttribute('data-czap-stream-url'),
164
+ 'stream',
165
+ 'czap/astro.stream',
166
+ {
167
+ crossOriginRejected: 'stream-cross-origin-url-rejected',
168
+ malformedUrl: 'stream-malformed-url-rejected',
169
+ originNotAllowed: 'stream-origin-not-allowed',
170
+ endpointKindNotPermitted: 'stream-endpoint-kind-not-permitted',
171
+ },
172
+ endpointPolicy,
173
+ );
174
+ if (!streamUrl) {
175
+ return;
176
+ }
177
+
178
+ const artifactId = target.getAttribute('data-czap-stream-artifact') ?? undefined;
179
+ const morphStyle = (target.getAttribute('data-czap-stream-morph') ?? 'innerHTML') as 'innerHTML' | 'outerHTML';
180
+ const snapshotUrl =
181
+ allowRuntimeEndpointUrl(
182
+ target.getAttribute('data-czap-snapshot-url'),
183
+ 'snapshot',
184
+ 'czap/astro.stream',
185
+ {
186
+ crossOriginRejected: 'snapshot-cross-origin-url-rejected',
187
+ malformedUrl: 'snapshot-malformed-url-rejected',
188
+ originNotAllowed: 'snapshot-origin-not-allowed',
189
+ endpointKindNotPermitted: 'snapshot-endpoint-kind-not-permitted',
190
+ },
191
+ endpointPolicy,
192
+ ) ?? undefined;
193
+ const replayUrl =
194
+ allowRuntimeEndpointUrl(
195
+ target.getAttribute('data-czap-replay-url'),
196
+ 'replay',
197
+ 'czap/astro.stream',
198
+ {
199
+ crossOriginRejected: 'replay-cross-origin-url-rejected',
200
+ malformedUrl: 'replay-malformed-url-rejected',
201
+ originNotAllowed: 'replay-origin-not-allowed',
202
+ endpointKindNotPermitted: 'replay-endpoint-kind-not-permitted',
203
+ },
204
+ endpointPolicy,
205
+ ) ?? undefined;
206
+
207
+ let source: EventSource | null = null;
208
+ let reconnectAttempt = 0;
209
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
210
+ let lastEventId: string | null = null;
211
+ let recoveryPending = false;
212
+ let pendingLocator: Locator | null = null;
213
+
214
+ const reconnectConfig = {
215
+ maxAttempts: 10,
216
+ initialDelay: Millis(SSE_RECONNECT_INITIAL_MS),
217
+ maxDelay: Millis(SSE_RECONNECT_MAX_MS),
218
+ factor: 2,
219
+ } as const;
220
+
221
+ const bindReinit = (nextTarget: HTMLElement): void => {
222
+ if (reinitTarget === nextTarget) {
223
+ return;
224
+ }
225
+
226
+ reinitTarget?.removeEventListener('czap:reinit', handleReinit);
227
+ reinitTarget = nextTarget;
228
+ reinitTarget.addEventListener('czap:reinit', handleReinit);
229
+ };
230
+
231
+ const patchScheduler = createStreamScheduler({
232
+ applyHtml: (html) => {
233
+ const locator = targetLocator(target);
234
+ pendingLocator = locator;
235
+ Effect.runSync(
236
+ Morph.morphWithState(target, html, {
237
+ morphStyle,
238
+ preserveFocus: true,
239
+ preserveScroll: true,
240
+ preserveSelection: true,
241
+ }),
242
+ );
243
+
244
+ if (locator && locator.type !== 'slot') {
245
+ target = findTarget(locator) ?? target;
246
+ }
247
+ },
248
+ onFlush: ({ patchCount, requiresRescan }) => {
249
+ if (requiresRescan) {
250
+ rescanSlots(document.documentElement);
251
+ }
252
+
253
+ target = findTarget(pendingLocator) ?? target;
254
+ bindReinit(target);
255
+ for (let index = 0; index < patchCount; index++) {
256
+ target.dispatchEvent(
257
+ new CustomEvent('czap:stream-morph', {
258
+ bubbles: true,
259
+ }),
260
+ );
261
+ }
262
+ pendingLocator = null;
263
+ },
264
+ });
265
+
266
+ const enqueueHtml = (html: string): Promise<void> => {
267
+ const normalizedHtml = prepareHtml(html);
268
+ return patchScheduler.enqueue({
269
+ html: normalizedHtml,
270
+ requiresRescan: patchCouldInvalidateSlots(targetLocator(target), morphStyle, normalizedHtml),
271
+ });
272
+ };
273
+
274
+ const applyResumeResponse = async (response: ResumeResponse): Promise<void> => {
275
+ if (response.type === 'snapshot') {
276
+ await enqueueHtml(response.html);
277
+ return;
278
+ }
279
+
280
+ const patches = response.patches
281
+ .map((patch) => replayHtml(patch))
282
+ .filter((html): html is string => html !== null)
283
+ .map((html) => ({
284
+ html,
285
+ requiresRescan: patchCouldInvalidateSlots(targetLocator(target), morphStyle, html),
286
+ }));
287
+
288
+ await patchScheduler.enqueueBatch(patches);
289
+ };
290
+
291
+ const reconcileResumption = async (currentEventId: string): Promise<void> => {
292
+ const resolvedArtifactId = artifactId!;
293
+ try {
294
+ const response = await Effect.runPromise(
295
+ Resumption.resume(resolvedArtifactId, currentEventId, {
296
+ ...(snapshotUrl ? { snapshotUrl } : {}),
297
+ ...(replayUrl ? { replayUrl } : {}),
298
+ ...(hasCustomEndpointPolicy(endpointPolicy) ? { endpointPolicy } : {}),
299
+ }),
300
+ );
301
+ await applyResumeResponse(response);
302
+ } catch (error) {
303
+ target.dispatchEvent(
304
+ new CustomEvent('czap:stream-error', {
305
+ detail: {
306
+ reason: 'resume-failed',
307
+ message: error instanceof Error ? error.message : String(error),
308
+ },
309
+ bubbles: true,
310
+ }),
311
+ );
312
+ }
313
+ };
314
+
315
+ const buildUrl = (): string => SSE.buildUrl(streamUrl, artifactId, lastEventId ?? undefined);
316
+
317
+ const connect = (): void => {
318
+ source = new EventSource(buildUrl());
319
+
320
+ source.onopen = () => {
321
+ reconnectAttempt = 0;
322
+ patchScheduler.activate();
323
+ target.dispatchEvent(new CustomEvent('czap:stream-connected', { bubbles: true }));
324
+ };
325
+
326
+ source.onmessage = (event: MessageEvent) => {
327
+ if (event.lastEventId) {
328
+ lastEventId = event.lastEventId;
329
+ saveResumptionState(artifactId, event.lastEventId);
330
+ }
331
+
332
+ if (recoveryPending && artifactId && event.lastEventId) {
333
+ recoveryPending = false;
334
+ void reconcileResumption(event.lastEventId);
335
+ }
336
+
337
+ const message = SSE.parseMessage(event);
338
+ if (!message) {
339
+ return;
340
+ }
341
+
342
+ if (message.type === 'signal') {
343
+ target.dispatchEvent(
344
+ new CustomEvent('czap:signal', {
345
+ detail: message.data,
346
+ bubbles: true,
347
+ }),
348
+ );
349
+ return;
350
+ }
351
+
352
+ if (message.type === 'heartbeat' || message.type === 'receipt') {
353
+ return;
354
+ }
355
+
356
+ const html = messageHtml(message);
357
+ if (html) {
358
+ void enqueueHtml(html);
359
+ }
360
+ };
361
+
362
+ source.onerror = () => {
363
+ source?.close();
364
+ source = null;
365
+ recoveryPending = artifactId !== undefined && lastEventId !== null;
366
+ patchScheduler.beginReconnect();
367
+
368
+ target.dispatchEvent(new CustomEvent('czap:stream-disconnected', { bubbles: true }));
369
+
370
+ if (reconnectAttempt < reconnectConfig.maxAttempts) {
371
+ const delay = SSE.calculateDelay(reconnectAttempt, reconnectConfig);
372
+ reconnectAttempt += 1;
373
+ reconnectTimer = patchScheduler.setReconnectTimer(connect, delay);
374
+ return;
375
+ }
376
+
377
+ target.dispatchEvent(
378
+ new CustomEvent('czap:stream-error', {
379
+ detail: { reason: 'max-reconnect-attempts' },
380
+ bubbles: true,
381
+ }),
382
+ );
383
+ };
384
+ };
385
+
386
+ const cleanup = (): void => {
387
+ reconnectTimer = patchScheduler.clearReconnectTimer(reconnectTimer);
388
+
389
+ source?.close();
390
+ source = null;
391
+ };
392
+
393
+ const handleReinit = (): void => {
394
+ cleanup();
395
+ reconnectAttempt = 0;
396
+ recoveryPending = false;
397
+ connect();
398
+ };
399
+
400
+ bindReinit(target);
401
+ connect();
402
+ element.addEventListener('czap:dispose', () => {
403
+ cleanup();
404
+ patchScheduler.dispose();
405
+ });
406
+ load();
407
+ }
@@ -0,0 +1,107 @@
1
+ import { Diagnostics } from '@czap/core';
2
+ import type { RuntimeEndpointKind, RuntimeEndpointPolicy } from '@czap/web';
3
+ import { resolveRuntimeUrl } from '@czap/web';
4
+ import { readRuntimeEndpointPolicy } from './policy.js';
5
+
6
+ interface RuntimeEndpointDiagnosticCodes {
7
+ readonly malformedUrl: string;
8
+ readonly crossOriginRejected: string;
9
+ readonly originNotAllowed: string;
10
+ readonly endpointKindNotPermitted: string;
11
+ }
12
+
13
+ /**
14
+ * Fast boolean check -- does `rawUrl` resolve under a `same-origin`
15
+ * stream policy? Handy for runtime code that only needs a guard and
16
+ * does not want to emit diagnostics.
17
+ */
18
+ export function isSameOriginRuntimeUrl(rawUrl: string): boolean {
19
+ return (
20
+ resolveRuntimeUrl(rawUrl, {
21
+ kind: 'stream',
22
+ policy: { mode: 'same-origin' },
23
+ }).type === 'allowed'
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Convenience wrapper around {@link allowRuntimeEndpointUrl} that
29
+ * collapses every diagnostic code into a single `code`. Used by
30
+ * directives that only care whether a URL is same-origin-safe.
31
+ */
32
+ export function allowSameOriginRuntimeUrl(rawUrl: string | null, source: string, code: string): string | null {
33
+ return allowRuntimeEndpointUrl(rawUrl, 'stream', source, {
34
+ malformedUrl: code,
35
+ crossOriginRejected: code,
36
+ originNotAllowed: code,
37
+ endpointKindNotPermitted: code,
38
+ });
39
+ }
40
+
41
+ function defaultDiagnosticCodes(kind: RuntimeEndpointKind): RuntimeEndpointDiagnosticCodes {
42
+ return {
43
+ malformedUrl: `${kind}-malformed-url-rejected`,
44
+ crossOriginRejected: `${kind}-cross-origin-url-rejected`,
45
+ originNotAllowed: `${kind}-origin-not-allowed`,
46
+ endpointKindNotPermitted: `${kind}-endpoint-kind-not-permitted`,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Resolve `rawUrl` under the runtime endpoint policy and either
52
+ * return the safe URL string or emit a structured `Diagnostics.warn`
53
+ * describing the rejection reason. Returns `null` for both missing
54
+ * and rejected URLs so callers can bail out uniformly.
55
+ */
56
+ export function allowRuntimeEndpointUrl(
57
+ rawUrl: string | null,
58
+ kind: RuntimeEndpointKind,
59
+ source: string,
60
+ codes?: Partial<RuntimeEndpointDiagnosticCodes>,
61
+ policy: RuntimeEndpointPolicy = readRuntimeEndpointPolicy(),
62
+ ): string | null {
63
+ const resolved = resolveRuntimeUrl(rawUrl, { kind, policy });
64
+ const finalCodes = { ...defaultDiagnosticCodes(kind), ...codes };
65
+
66
+ switch (resolved.type) {
67
+ case 'missing':
68
+ return null;
69
+ case 'allowed':
70
+ return resolved.url;
71
+ case 'malformed':
72
+ Diagnostics.warn({
73
+ source,
74
+ code: finalCodes.malformedUrl,
75
+ message: `Runtime URL "${rawUrl}" was rejected because it is not a valid URL.`,
76
+ });
77
+ return null;
78
+ case 'cross-origin-rejected':
79
+ Diagnostics.warn({
80
+ source,
81
+ code: finalCodes.crossOriginRejected,
82
+ message: `Cross-origin runtime URL "${rawUrl}" was rejected. Runtime endpoints must be same-origin by default.`,
83
+ });
84
+ return null;
85
+ case 'origin-not-allowed':
86
+ Diagnostics.warn({
87
+ source,
88
+ code: finalCodes.originNotAllowed,
89
+ message: `Runtime URL "${rawUrl}" was rejected because origin "${resolved.resolved.origin}" is not allowlisted.`,
90
+ });
91
+ return null;
92
+ case 'kind-not-allowed':
93
+ Diagnostics.warn({
94
+ source,
95
+ code: finalCodes.endpointKindNotPermitted,
96
+ message: `Runtime URL "${rawUrl}" was rejected because endpoint kind "${kind}" is not permitted for cross-origin access.`,
97
+ });
98
+ return null;
99
+ case 'private-ip-rejected':
100
+ Diagnostics.warn({
101
+ source,
102
+ code: `${kind}-private-ip-rejected`,
103
+ message: `Runtime URL "${rawUrl}" was rejected because it resolves to a private or reserved IP address.`,
104
+ });
105
+ return null;
106
+ }
107
+ }
@@ -0,0 +1,85 @@
1
+ import { WASMDispatch, Diagnostics } from '@czap/core';
2
+ import { writeRuntimeGlobal } from './globals.js';
3
+ import { readRuntimeEndpointPolicy } from './policy.js';
4
+ import { allowRuntimeEndpointUrl } from './url-policy.js';
5
+
6
+ const ROOT_WASM_ATTR = 'data-czap-wasm-url';
7
+
8
+ /**
9
+ * Configure (or clear) the root `data-czap-wasm-url` attribute used by
10
+ * the `client:wasm` directive to discover its module URL. Also
11
+ * back-fills any existing `[data-czap-wasm]` elements that lack a
12
+ * per-element override.
13
+ */
14
+ export function configureWasmRuntime(wasmUrl: string | null | undefined): void {
15
+ if (!wasmUrl) {
16
+ document.documentElement.removeAttribute(ROOT_WASM_ATTR);
17
+ return;
18
+ }
19
+
20
+ document.documentElement.setAttribute(ROOT_WASM_ATTR, wasmUrl);
21
+ document.querySelectorAll<HTMLElement>('[data-czap-wasm]').forEach((element) => {
22
+ if (!element.getAttribute(ROOT_WASM_ATTR)) {
23
+ element.setAttribute(ROOT_WASM_ATTR, wasmUrl);
24
+ }
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Resolve the WASM module URL for `element`, falling back to the
30
+ * root-configured URL when no per-element override exists.
31
+ */
32
+ export function resolveWasmUrl(element: HTMLElement): string | null {
33
+ return element.getAttribute(ROOT_WASM_ATTR) ?? document.documentElement.getAttribute(ROOT_WASM_ATTR);
34
+ }
35
+
36
+ /**
37
+ * Load the WASM kernels for `element`, publish them to
38
+ * `window.__CZAP_WASM__`, and dispatch a `czap:wasm-ready` event on
39
+ * `document`. On failure, emits a diagnostic and fires
40
+ * `czap:wasm-error` instead so downstream consumers can degrade.
41
+ */
42
+ export async function loadWasmRuntime(element: HTMLElement): Promise<void> {
43
+ const wasmUrl = allowRuntimeEndpointUrl(
44
+ resolveWasmUrl(element),
45
+ 'wasm',
46
+ 'czap/astro.wasm',
47
+ {
48
+ crossOriginRejected: 'wasm-cross-origin-url-rejected',
49
+ malformedUrl: 'wasm-malformed-url-rejected',
50
+ originNotAllowed: 'wasm-origin-not-allowed',
51
+ endpointKindNotPermitted: 'wasm-endpoint-kind-not-permitted',
52
+ },
53
+ readRuntimeEndpointPolicy(),
54
+ );
55
+ if (!wasmUrl) {
56
+ return;
57
+ }
58
+
59
+ try {
60
+ const kernels = await WASMDispatch.load(wasmUrl);
61
+ writeRuntimeGlobal('__CZAP_WASM__', kernels);
62
+
63
+ document.dispatchEvent(
64
+ new CustomEvent('czap:wasm-ready', {
65
+ detail: { url: wasmUrl },
66
+ }),
67
+ );
68
+ } catch (error) {
69
+ Diagnostics.warn({
70
+ source: 'czap/astro.wasm',
71
+ code: 'wasm-load-failed',
72
+ message: 'WASM runtime failed to load.',
73
+ detail: error instanceof Error ? error.message : 'load-failed',
74
+ cause: error,
75
+ });
76
+ document.dispatchEvent(
77
+ new CustomEvent('czap:wasm-error', {
78
+ detail: {
79
+ url: wasmUrl,
80
+ reason: error instanceof Error ? error.message : 'load-failed',
81
+ },
82
+ }),
83
+ );
84
+ }
85
+ }