arcane-os 0.3.0 → 0.3.2

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 (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -0,0 +1,588 @@
1
+ import {createReadStream} from 'node:fs';
2
+ import {stat} from 'node:fs/promises';
3
+ import http from 'node:http';
4
+ import https from 'node:https';
5
+ import path from 'node:path';
6
+ import {pipeline} from 'node:stream/promises';
7
+ import {fileURLToPath} from 'node:url';
8
+
9
+ const MIME_TYPES = new Map([
10
+ ['.css', 'text/css; charset=utf-8'],
11
+ ['.gif', 'image/gif'],
12
+ ['.gguf', 'application/octet-stream'],
13
+ ['.html', 'text/html; charset=utf-8'],
14
+ ['.ico', 'image/x-icon'],
15
+ ['.jpeg', 'image/jpeg'],
16
+ ['.jpg', 'image/jpeg'],
17
+ ['.js', 'text/javascript; charset=utf-8'],
18
+ ['.json', 'application/json; charset=utf-8'],
19
+ ['.map', 'application/json; charset=utf-8'],
20
+ ['.md', 'text/markdown; charset=utf-8'],
21
+ ['.mjs', 'text/javascript; charset=utf-8'],
22
+ ['.modelfile', 'text/plain; charset=utf-8'],
23
+ ['.mp3', 'audio/mpeg'],
24
+ ['.ogg', 'audio/ogg'],
25
+ ['.png', 'image/png'],
26
+ ['.svg', 'image/svg+xml; charset=utf-8'],
27
+ ['.txt', 'text/plain; charset=utf-8'],
28
+ ['.wasm', 'application/wasm'],
29
+ ['.wav', 'audio/wav'],
30
+ ['.webmanifest', 'application/manifest+json; charset=utf-8'],
31
+ ['.webp', 'image/webp'],
32
+ ['.woff', 'font/woff'],
33
+ ['.woff2', 'font/woff2']
34
+ ]);
35
+
36
+ function fail(message, code = 'ARCANE_SOURCE_SERVER_OPTIONS_INVALID') {
37
+ const error = new Error(message);
38
+ error.code = code;
39
+ throw error;
40
+ }
41
+
42
+ function throwIfAborted(signal) {
43
+ if (!signal?.aborted) {
44
+ return;
45
+ }
46
+ if (signal.reason instanceof Error) {
47
+ throw signal.reason;
48
+ }
49
+ const error = new Error('Source server start cancelled.');
50
+ error.code = 'ARCANE_CANCELLED';
51
+ throw error;
52
+ }
53
+
54
+ function normalizeHost(value) {
55
+ const host = String(value ?? '127.0.0.1').trim();
56
+ if (!host) {
57
+ fail('Source server host must be a nonempty string.');
58
+ }
59
+ return host;
60
+ }
61
+
62
+ function normalizePort(value) {
63
+ const port = Number(value ?? 0);
64
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
65
+ fail('Source server port must be an integer from 0 through 65535.');
66
+ }
67
+ return port;
68
+ }
69
+
70
+ function pathSegments(value, fieldName) {
71
+ const text = String(value);
72
+ if (!text || text.includes('\\') || /[\x00-\x1f\x7f]/u.test(text)) {
73
+ fail(`${fieldName} must use a nonempty URL path without control characters or backslashes.`);
74
+ }
75
+ const segments = text.split('/');
76
+ if (segments.some(function invalidSegment(segment) {
77
+ return !segment || segment === '.' || segment === '..';
78
+ })) {
79
+ fail(`${fieldName} contains an invalid URL path segment.`);
80
+ }
81
+ return segments;
82
+ }
83
+
84
+ function normalizeUrlPath(value, fieldName, preserveTrailingSlash = false) {
85
+ const raw = String(value ?? '/');
86
+ if (!raw.startsWith('/') || raw.includes('?') || raw.includes('#')) {
87
+ fail(`${fieldName} must be an absolute URL path without a query or fragment.`);
88
+ }
89
+ if (raw === '/') {
90
+ return raw;
91
+ }
92
+ const hasTrailingSlash = raw.endsWith('/');
93
+ const content = hasTrailingSlash ? raw.slice(1, -1) : raw.slice(1);
94
+ const normalized = `/${pathSegments(content, fieldName).join('/')}`;
95
+ return preserveTrailingSlash && hasTrailingSlash ? `${normalized}/` : normalized;
96
+ }
97
+
98
+ function normalizeRelativePath(value, fieldName, allowDirectory = false) {
99
+ const raw = String(value ?? '');
100
+ if (!raw || raw.startsWith('/') || raw.includes('?') || raw.includes('#')) {
101
+ fail(`${fieldName} must be a nonempty relative URL path.`);
102
+ }
103
+ const directory = allowDirectory && raw.endsWith('/');
104
+ const content = directory ? raw.slice(0, -1) : raw;
105
+ const normalized = pathSegments(content, fieldName).join('/');
106
+ return directory ? `${normalized}/` : normalized;
107
+ }
108
+
109
+ function normalizeRoot(value, fieldName) {
110
+ if (value instanceof URL) {
111
+ if (value.protocol !== 'file:') {
112
+ fail(`${fieldName} URL must use the file: protocol.`);
113
+ }
114
+ return path.resolve(fileURLToPath(value));
115
+ }
116
+ if (typeof value !== 'string' || !value.trim()) {
117
+ fail(`${fieldName} must be a filesystem path or file URL.`);
118
+ }
119
+ return path.resolve(value);
120
+ }
121
+
122
+ function normalizeInclude(value, fieldName) {
123
+ if (value === undefined) {
124
+ return null;
125
+ }
126
+ if (!Array.isArray(value)) {
127
+ fail(`${fieldName} must be an array when supplied.`);
128
+ }
129
+ return value.map(function normalizeEntry(entry, index) {
130
+ return normalizeRelativePath(entry, `${fieldName}[${index}]`, true);
131
+ });
132
+ }
133
+
134
+ function normalizeMount(value, index) {
135
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
136
+ fail(`mounts[${index}] must be an object.`);
137
+ }
138
+ if (value.urlPath === undefined) {
139
+ fail(`mounts[${index}].urlPath is required.`);
140
+ }
141
+ return {
142
+ urlPath: normalizeUrlPath(value.urlPath, `mounts[${index}].urlPath`),
143
+ root: normalizeRoot(value.root, `mounts[${index}].root`),
144
+ index: value.index === undefined
145
+ ? null
146
+ : normalizeRelativePath(value.index, `mounts[${index}].index`),
147
+ include: normalizeInclude(value.include, `mounts[${index}].include`)
148
+ };
149
+ }
150
+
151
+ function normalizeMounts(value) {
152
+ if (!Array.isArray(value) || value.length === 0) {
153
+ fail('Source server mounts must be a nonempty array.');
154
+ }
155
+ const mounts = value.map(normalizeMount);
156
+ const seen = new Set();
157
+ for (const mount of mounts) {
158
+ if (seen.has(mount.urlPath)) {
159
+ fail(`Source server mount URL path is duplicated: ${mount.urlPath}`);
160
+ }
161
+ seen.add(mount.urlPath);
162
+ }
163
+ return mounts.slice().sort(function longestUrlPathFirst(left, right) {
164
+ return right.urlPath.length - left.urlPath.length;
165
+ });
166
+ }
167
+
168
+ function parseRequestPath(rawUrl) {
169
+ let parsed;
170
+ try {
171
+ parsed = new URL(String(rawUrl || '/'), 'http://localhost');
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ let decoded;
177
+ try {
178
+ decoded = decodeURIComponent(parsed.pathname);
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ if (!decoded.startsWith('/') || decoded.includes('\\')
184
+ || /[\x00-\x1f\x7f]/u.test(decoded)) {
185
+ return null;
186
+ }
187
+ if (decoded !== '/' && decoded.split('/').some(function invalidSegment(segment, index) {
188
+ if (index === 0 || (index === decoded.split('/').length - 1 && !segment)) {
189
+ return false;
190
+ }
191
+ return !segment || segment === '.' || segment === '..';
192
+ })) {
193
+ return null;
194
+ }
195
+ return decoded;
196
+ }
197
+
198
+ function mountForPath(mounts, requestPath) {
199
+ return mounts.find(function matchesMount(mount) {
200
+ return mount.urlPath === '/'
201
+ || requestPath === mount.urlPath
202
+ || requestPath.startsWith(`${mount.urlPath}/`);
203
+ }) || null;
204
+ }
205
+
206
+ function relativePathForRequest(mount, requestPath) {
207
+ const offset = mount.urlPath === '/' ? 1 : mount.urlPath.length;
208
+ let relativePath = requestPath.slice(offset);
209
+ if (relativePath.startsWith('/')) {
210
+ relativePath = relativePath.slice(1);
211
+ }
212
+ if (!relativePath || relativePath.endsWith('/')) {
213
+ if (!mount.index) {
214
+ return null;
215
+ }
216
+ relativePath = `${relativePath}${mount.index}`;
217
+ }
218
+ return relativePath;
219
+ }
220
+
221
+ function includedByMount(mount, relativePath) {
222
+ if (mount.include === null) {
223
+ return true;
224
+ }
225
+ return mount.include.some(function matchesInclude(entry) {
226
+ return entry.endsWith('/')
227
+ ? relativePath.startsWith(entry)
228
+ : relativePath === entry;
229
+ });
230
+ }
231
+
232
+ function resolveMountedPath(mount, relativePath) {
233
+ const candidate = path.resolve(mount.root, ...relativePath.split('/'));
234
+ const relative = path.relative(mount.root, candidate);
235
+ if (relative === '..' || relative.startsWith(`..${path.sep}`)
236
+ || path.isAbsolute(relative)) {
237
+ return null;
238
+ }
239
+ return candidate;
240
+ }
241
+
242
+ async function regularFile(filePath) {
243
+ try {
244
+ const information = await stat(filePath);
245
+ return information.isFile() ? information : null;
246
+ }
247
+ catch (error) {
248
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
249
+ return null;
250
+ }
251
+ throw error;
252
+ }
253
+ }
254
+
255
+ function contentType(filePath) {
256
+ return MIME_TYPES.get(path.extname(filePath).toLowerCase())
257
+ || 'application/octet-stream';
258
+ }
259
+
260
+ function responseHeaders(settings) {
261
+ const headers = {
262
+ 'cache-control': 'no-cache'
263
+ };
264
+ if (settings.crossOriginIsolated) {
265
+ headers['cross-origin-opener-policy'] = 'same-origin';
266
+ headers['cross-origin-embedder-policy'] = 'require-corp';
267
+ }
268
+ return headers;
269
+ }
270
+
271
+ function sendText(response, method, status, message, headers = {}) {
272
+ response.writeHead(status, {
273
+ ...headers,
274
+ 'content-type': 'text/plain; charset=utf-8'
275
+ });
276
+ response.end(method === 'HEAD' ? undefined : `${message}\n`);
277
+ }
278
+
279
+ function parseRange(value, size) {
280
+ if (value === undefined) {
281
+ return null;
282
+ }
283
+ if (typeof value !== 'string' || size === 0) {
284
+ return false;
285
+ }
286
+ const match = /^bytes=(\d*)-(\d*)$/u.exec(value.trim());
287
+ if (!match || (!match[1] && !match[2])) {
288
+ return false;
289
+ }
290
+ let start;
291
+ let end;
292
+ if (!match[1]) {
293
+ const suffixLength = Number(match[2]);
294
+ if (!Number.isSafeInteger(suffixLength) || suffixLength <= 0) {
295
+ return false;
296
+ }
297
+ start = Math.max(0, size - suffixLength);
298
+ end = size - 1;
299
+ }
300
+ else {
301
+ start = Number(match[1]);
302
+ end = match[2] ? Number(match[2]) : size - 1;
303
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end)
304
+ || start < 0 || end < start || start >= size) {
305
+ return false;
306
+ }
307
+ end = Math.min(end, size - 1);
308
+ }
309
+ return {start, end};
310
+ }
311
+
312
+ async function streamFile(request, response, filePath, information, settings) {
313
+ const range = parseRange(request.headers.range, information.size);
314
+ if (range === false) {
315
+ sendText(response, request.method, 416, 'Range Not Satisfiable', {
316
+ ...responseHeaders(settings),
317
+ 'accept-ranges': 'bytes',
318
+ 'content-range': `bytes */${information.size}`
319
+ });
320
+ return;
321
+ }
322
+ const headers = {
323
+ ...responseHeaders(settings),
324
+ 'accept-ranges': 'bytes',
325
+ 'content-type': contentType(filePath)
326
+ };
327
+ let status = 200;
328
+ let streamOptions;
329
+ if (range) {
330
+ status = 206;
331
+ headers['content-range'] = `bytes ${range.start}-${range.end}/${information.size}`;
332
+ headers['content-length'] = range.end - range.start + 1;
333
+ streamOptions = range;
334
+ }
335
+ else {
336
+ headers['content-length'] = information.size;
337
+ }
338
+ response.writeHead(status, headers);
339
+ if (request.method === 'HEAD') {
340
+ response.end();
341
+ return;
342
+ }
343
+ await pipeline(createReadStream(filePath, streamOptions), response);
344
+ }
345
+
346
+ async function publishEvent(onEvent, type, detail) {
347
+ if (typeof onEvent !== 'function') {
348
+ return;
349
+ }
350
+ try {
351
+ await onEvent({type, at: new Date().toISOString(), ...detail});
352
+ }
353
+ catch (error) {
354
+ console.error('[arcane source server] Event listener failed.', error);
355
+ }
356
+ }
357
+
358
+ async function serveRequest(request, response, settings) {
359
+ const method = String(request.method || 'GET').toUpperCase();
360
+ if (method !== 'GET' && method !== 'HEAD') {
361
+ sendText(response, method, 405, 'Method Not Allowed', {
362
+ ...responseHeaders(settings),
363
+ allow: 'GET, HEAD'
364
+ });
365
+ return;
366
+ }
367
+ const requestPath = parseRequestPath(request.url);
368
+ if (!requestPath) {
369
+ sendText(response, method, 400, 'Bad Request', responseHeaders(settings));
370
+ return;
371
+ }
372
+ if (requestPath === '/' && settings.startPath !== '/') {
373
+ response.writeHead(302, {
374
+ ...responseHeaders(settings),
375
+ location: settings.startPath
376
+ });
377
+ response.end();
378
+ return;
379
+ }
380
+ const mount = mountForPath(settings.mounts, requestPath);
381
+ const relativePath = mount ? relativePathForRequest(mount, requestPath) : null;
382
+ if (!mount || !relativePath || !includedByMount(mount, relativePath)) {
383
+ sendText(response, method, 404, 'Not Found', responseHeaders(settings));
384
+ return;
385
+ }
386
+ const filePath = resolveMountedPath(mount, relativePath);
387
+ const information = filePath ? await regularFile(filePath) : null;
388
+ if (!information) {
389
+ sendText(response, method, 404, 'Not Found', responseHeaders(settings));
390
+ return;
391
+ }
392
+ await streamFile(request, response, filePath, information, settings);
393
+ }
394
+
395
+ function createRequestListener(settings) {
396
+ return async function sourceServerRequest(request, response) {
397
+ try {
398
+ await serveRequest(request, response, settings);
399
+ }
400
+ catch (error) {
401
+ console.error('[arcane source server] Request failed.', error);
402
+ await publishEvent(settings.onEvent, 'source-server.request.failed', {
403
+ method: request.method,
404
+ url: request.url,
405
+ error
406
+ });
407
+ if (response.headersSent) {
408
+ response.destroy(error);
409
+ return;
410
+ }
411
+ sendText(response, request.method, 500, 'Internal Server Error',
412
+ responseHeaders(settings));
413
+ }
414
+ };
415
+ }
416
+
417
+ function createServer(settings) {
418
+ const listener = createRequestListener(settings);
419
+ if (settings.tls === undefined || settings.tls === null || settings.tls === false) {
420
+ return http.createServer(listener);
421
+ }
422
+ if (typeof settings.tls !== 'object' || Array.isArray(settings.tls)) {
423
+ fail('Source server tls must be a Node HTTPS options object when supplied.');
424
+ }
425
+ return https.createServer(settings.tls, listener);
426
+ }
427
+
428
+ function listen(server, host, port, onOperationalError) {
429
+ return new Promise(function settleListen(resolve, reject) {
430
+ let listening = false;
431
+ function cleanupBeforeListening() {
432
+ server.off('error', onError);
433
+ server.off('listening', onListening);
434
+ }
435
+ function onError(error) {
436
+ if (listening) {
437
+ onOperationalError(error);
438
+ return;
439
+ }
440
+ cleanupBeforeListening();
441
+ reject(error);
442
+ }
443
+ function onListening() {
444
+ listening = true;
445
+ server.off('listening', onListening);
446
+ resolve(onError);
447
+ }
448
+ server.on('error', onError);
449
+ server.once('listening', onListening);
450
+ server.listen(port, host);
451
+ });
452
+ }
453
+
454
+ function urlHostname(host) {
455
+ const publicHost = host === '0.0.0.0' || host === '::' ? 'localhost' : host;
456
+ return publicHost.includes(':') ? `[${publicHost}]` : publicHost;
457
+ }
458
+
459
+ function closeFunction(server) {
460
+ let activeClose = null;
461
+ return function close() {
462
+ if (activeClose) {
463
+ return activeClose;
464
+ }
465
+ if (!server.listening) {
466
+ return Promise.resolve();
467
+ }
468
+ activeClose = new Promise(function settleClose(resolve, reject) {
469
+ server.close(function onClosed(error) {
470
+ if (error) {
471
+ activeClose = null;
472
+ reject(error);
473
+ return;
474
+ }
475
+ resolve();
476
+ });
477
+ });
478
+ return activeClose;
479
+ };
480
+ }
481
+
482
+ export async function startSourceExampleServer(options = {}) {
483
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
484
+ fail('Source server options must be an object.');
485
+ }
486
+ if (options.signal && (typeof options.signal.addEventListener !== 'function'
487
+ || typeof options.signal.removeEventListener !== 'function')) {
488
+ fail('Source server signal must be an AbortSignal when supplied.');
489
+ }
490
+ throwIfAborted(options.signal);
491
+ const settings = {
492
+ mounts: normalizeMounts(options.mounts),
493
+ startPath: normalizeUrlPath(options.startPath ?? '/', 'startPath', true),
494
+ host: normalizeHost(options.host),
495
+ port: normalizePort(options.port),
496
+ crossOriginIsolated: options.crossOriginIsolated === true,
497
+ tls: options.tls,
498
+ onEvent: options.onEvent
499
+ };
500
+ const server = createServer(settings);
501
+ const close = closeFunction(server);
502
+ let operationalError = null;
503
+ let serverErrorListener = null;
504
+ let abortListener = null;
505
+ let settleClosed;
506
+ let rejectClosed;
507
+ const closed = new Promise(function waitForClose(resolve, reject) {
508
+ settleClosed = resolve;
509
+ rejectClosed = reject;
510
+ });
511
+ closed.catch(function retainObservableLifecycle() {
512
+ return undefined;
513
+ });
514
+ server.once('close', function observeClose() {
515
+ if (serverErrorListener) {
516
+ server.off('error', serverErrorListener);
517
+ }
518
+ if (abortListener) {
519
+ options.signal.removeEventListener('abort', abortListener);
520
+ }
521
+ if (operationalError) {
522
+ rejectClosed(operationalError);
523
+ return;
524
+ }
525
+ settleClosed();
526
+ });
527
+ function reportOperationalError(error) {
528
+ operationalError = operationalError || error;
529
+ console.error('[arcane source server] Server failed.', error);
530
+ publishEvent(settings.onEvent, 'source-server.failed', {error})
531
+ .catch(function reportEventFailure(eventError) {
532
+ console.error('[arcane source server] Failure event could not be reported.',
533
+ eventError);
534
+ });
535
+ close().catch(function reportOperationalCloseFailure(closeError) {
536
+ console.error('[arcane source server] Failure close failed.', closeError);
537
+ });
538
+ }
539
+ await publishEvent(settings.onEvent, 'source-server.starting', {
540
+ host: settings.host,
541
+ port: settings.port
542
+ });
543
+ throwIfAborted(options.signal);
544
+ serverErrorListener = await listen(server, settings.host, settings.port,
545
+ reportOperationalError);
546
+ const address = server.address();
547
+ if (!address || typeof address === 'string') {
548
+ await new Promise(function closeUnknownAddress(resolve) {
549
+ server.close(resolve);
550
+ });
551
+ fail('Source server did not expose a TCP address.', 'ARCANE_SOURCE_SERVER_ADDRESS_UNAVAILABLE');
552
+ }
553
+ const protocol = settings.tls ? 'https:' : 'http:';
554
+ const origin = `${protocol}//${urlHostname(settings.host)}:${address.port}`;
555
+ const url = `${origin}${settings.startPath}`;
556
+ if (options.signal) {
557
+ abortListener = function closeAfterAbort() {
558
+ close().catch(function reportCloseFailure(error) {
559
+ console.error('[arcane source server] Abort close failed.', error);
560
+ });
561
+ };
562
+ options.signal.addEventListener('abort', abortListener, {once: true});
563
+ if (options.signal.aborted) {
564
+ await close();
565
+ throwIfAborted(options.signal);
566
+ }
567
+ }
568
+ await publishEvent(settings.onEvent, 'source-server.started', {
569
+ host: settings.host,
570
+ port: address.port,
571
+ origin,
572
+ url
573
+ });
574
+ if (options.signal?.aborted) {
575
+ await close();
576
+ throwIfAborted(options.signal);
577
+ }
578
+ return {
579
+ server,
580
+ protocol,
581
+ host: settings.host,
582
+ port: address.port,
583
+ origin,
584
+ url,
585
+ close,
586
+ closed
587
+ };
588
+ }