arcane-os 0.1.0-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +154 -0
- package/COMMERCIAL-LICENSE.md +13 -0
- package/LICENSE +661 -0
- package/NOTICE +70 -0
- package/README.md +448 -0
- package/bin/arcane-test.mjs +741 -0
- package/bin/arcane.mjs +5 -0
- package/docs/architecture.md +234 -0
- package/docs/compatibility.md +36 -0
- package/docs/event-manager.md +166 -0
- package/docs/platform-targets.md +108 -0
- package/docs/publishing.md +203 -0
- package/docs/reference/README.md +117 -0
- package/docs/reference/arcane-ollama.md +288 -0
- package/docs/reference/availability-and-normalization.md +123 -0
- package/docs/reference/behavioral-testing.md +86 -0
- package/docs/reference/cli.md +569 -0
- package/docs/reference/core/README.md +62 -0
- package/docs/reference/core/arcane-ai-contracts.md +873 -0
- package/docs/reference/core/arcane-api.md +601 -0
- package/docs/reference/core/arcane-entities.md +65 -0
- package/docs/reference/core/arcane-events.md +134 -0
- package/docs/reference/core/ollama-module.md +181 -0
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
- package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
- package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
- package/docs/reference/event-manager.md +957 -0
- package/docs/reference/inventory/package-api.json +2632 -0
- package/docs/reference/inventory/runtime-components.json +934 -0
- package/docs/reference/inventory/runtime-entities.json +26 -0
- package/docs/reference/inventory/runtime-modules.json +1249 -0
- package/docs/reference/protocols.md +242 -0
- package/docs/reference/runtime-components.md +1098 -0
- package/docs/reference/runtime-entities.md +303 -0
- package/docs/reference/runtime-modules.md +2010 -0
- package/docs/reference/sdk-api.md +4901 -0
- package/docs/roadmap.md +79 -0
- package/docs/work-amplification.md +124 -0
- package/node_modules/event-pubsub/CHANGELOG.md +55 -0
- package/node_modules/event-pubsub/MIGRATION.md +70 -0
- package/node_modules/event-pubsub/README.md +363 -0
- package/node_modules/event-pubsub/SECURITY.md +37 -0
- package/node_modules/event-pubsub/index.js +141 -0
- package/node_modules/event-pubsub/licence +21 -0
- package/node_modules/event-pubsub/package.json +59 -0
- package/node_modules/strong-type/README.md +408 -0
- package/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/strong-type/index.js +1151 -0
- package/node_modules/strong-type/licence +21 -0
- package/node_modules/strong-type/node.js +125 -0
- package/node_modules/strong-type/package.json +61 -0
- package/package.json +95 -0
- package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
- package/runtime/arcane/components/app-bar.html +468 -0
- package/runtime/arcane/components/assistant-panel.html +715 -0
- package/runtime/arcane/components/calculator.html +8 -0
- package/runtime/arcane/components/chart.html +655 -0
- package/runtime/arcane/components/chat.html +1225 -0
- package/runtime/arcane/components/conversation-view.html +13 -0
- package/runtime/arcane/components/dashboard-config.html +341 -0
- package/runtime/arcane/components/data-maintenance.html +112 -0
- package/runtime/arcane/components/data-view.html +92 -0
- package/runtime/arcane/components/directory-picker.html +197 -0
- package/runtime/arcane/components/document-inspector.html +252 -0
- package/runtime/arcane/components/file-drop.html +264 -0
- package/runtime/arcane/components/file-inspector.html +293 -0
- package/runtime/arcane/components/file-manager.html +1715 -0
- package/runtime/arcane/components/header.html +142 -0
- package/runtime/arcane/components/integration-settings.html +14 -0
- package/runtime/arcane/components/local-ai-status.html +360 -0
- package/runtime/arcane/components/markdown-document.html +1048 -0
- package/runtime/arcane/components/markdown-editor.html +360 -0
- package/runtime/arcane/components/media-embed.html +8 -0
- package/runtime/arcane/components/modal.html +402 -0
- package/runtime/arcane/components/output-panel.html +259 -0
- package/runtime/arcane/components/preferences-form.html +135 -0
- package/runtime/arcane/components/record-timeline.html +105 -0
- package/runtime/arcane/components/relationship-board.html +116 -0
- package/runtime/arcane/components/screen-capture.html +8 -0
- package/runtime/arcane/components/source-code-viewer.html +441 -0
- package/runtime/arcane/components/source-explanation.html +124 -0
- package/runtime/arcane/components/speech.html +365 -0
- package/runtime/arcane/components/summary-strip.html +177 -0
- package/runtime/arcane/components/table.html +77 -0
- package/runtime/arcane/components/task-progress.html +282 -0
- package/runtime/arcane/components/terminal-workspace.html +65 -0
- package/runtime/arcane/components/theme-editor.html +41 -0
- package/runtime/arcane/components/theme-switcher.html +46 -0
- package/runtime/arcane/components/unified-inbox.html +20 -0
- package/runtime/arcane/components/voice-transcription.html +476 -0
- package/runtime/arcane/components/weather-widget.html +8 -0
- package/runtime/arcane/components/web-navigator.html +239 -0
- package/runtime/arcane/css/communications.css +1 -0
- package/runtime/arcane/css/dashboard-config.css +45 -0
- package/runtime/arcane/css/document-site.css +981 -0
- package/runtime/arcane/css/layout.css +438 -0
- package/runtime/arcane/css/primitives.css +321 -0
- package/runtime/arcane/css/theme.css +112 -0
- package/runtime/arcane/css/utility-workspace.css +1 -0
- package/runtime/arcane/entities/ApiModelRecord.js +20 -0
- package/runtime/arcane/entities/Calculation.js +13 -0
- package/runtime/arcane/entities/Chat.js +581 -0
- package/runtime/arcane/entities/CommunicationMessage.js +29 -0
- package/runtime/arcane/entities/CommunicationThread.js +21 -0
- package/runtime/arcane/entities/Document.js +10 -0
- package/runtime/arcane/entities/File.js +143 -0
- package/runtime/arcane/entities/Image.js +135 -0
- package/runtime/arcane/entities/IntentEnvelope.js +834 -0
- package/runtime/arcane/entities/Preference.js +83 -0
- package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
- package/runtime/arcane/entities/TerminalSession.js +46 -0
- package/runtime/arcane/entities/Theme.js +107 -0
- package/runtime/arcane/entities/User.js +1046 -0
- package/runtime/arcane/entities/Weather.js +23 -0
- package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
- package/runtime/arcane/img/arrow-left.png +0 -0
- package/runtime/arcane/img/arrow-right.png +0 -0
- package/runtime/arcane/img/doc.svg +5 -0
- package/runtime/arcane/img/folder.svg +4 -0
- package/runtime/arcane/img/image.svg +5 -0
- package/runtime/arcane/img/refresh.png +0 -0
- package/runtime/arcane/img/send.svg +9 -0
- package/runtime/arcane/img/trash.svg +5 -0
- package/runtime/arcane/img/upload.svg +5 -0
- package/runtime/arcane/modules/AI.js +2048 -0
- package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
- package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
- package/runtime/arcane/modules/AIResponseLength.js +42 -0
- package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
- package/runtime/arcane/modules/AnsiText.js +53 -0
- package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
- package/runtime/arcane/modules/AppDataScope.js +245 -0
- package/runtime/arcane/modules/AppearancePreferences.js +28 -0
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
- package/runtime/arcane/modules/AsyncBoundary.js +161 -0
- package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
- package/runtime/arcane/modules/CalculatorEngine.js +20 -0
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
- package/runtime/arcane/modules/ChartLibrary.js +35 -0
- package/runtime/arcane/modules/ChatRecords.js +13 -0
- package/runtime/arcane/modules/CommunicationAppController.js +43 -0
- package/runtime/arcane/modules/CommunicationHub.js +16 -0
- package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
- package/runtime/arcane/modules/ComponentContracts.js +586 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
- package/runtime/arcane/modules/ConversationActionItems.js +488 -0
- package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
- package/runtime/arcane/modules/ConversationTimebox.js +527 -0
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
- package/runtime/arcane/modules/DBLS.js +171 -0
- package/runtime/arcane/modules/DBOPFS.js +1154 -0
- package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
- package/runtime/arcane/modules/DataMaintenance.js +91 -0
- package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
- package/runtime/arcane/modules/DirectoryPicker.js +109 -0
- package/runtime/arcane/modules/DocumentNavigation.js +223 -0
- package/runtime/arcane/modules/Errors.js +1025 -0
- package/runtime/arcane/modules/GifEncoder.js +29 -0
- package/runtime/arcane/modules/HTMLImport.js +114 -0
- package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
- package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
- package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
- package/runtime/arcane/modules/MD.js +111 -0
- package/runtime/arcane/modules/Mail.js +352 -0
- package/runtime/arcane/modules/MailTransport.mjs +180 -0
- package/runtime/arcane/modules/Marked.min.js +71 -0
- package/runtime/arcane/modules/MemoryRecords.js +44 -0
- package/runtime/arcane/modules/MessageAdvisory.js +38 -0
- package/runtime/arcane/modules/ModelDefinition.js +189 -0
- package/runtime/arcane/modules/Ollama.js +74 -0
- package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
- package/runtime/arcane/modules/OllamaSettings.js +24 -0
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
- package/runtime/arcane/modules/PreferenceStore.js +109 -0
- package/runtime/arcane/modules/QRCode.min.js +1 -0
- package/runtime/arcane/modules/Questionnaire.js +61 -0
- package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
- package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
- package/runtime/arcane/modules/RecordReviewStore.js +94 -0
- package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
- package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
- package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
- package/runtime/arcane/modules/ScreenCapture.js +20 -0
- package/runtime/arcane/modules/SpeechPlayback.js +581 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
- package/runtime/arcane/modules/SystemAppearance.js +20 -0
- package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
- package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
- package/runtime/arcane/modules/TerminalClient.js +52 -0
- package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
- package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
- package/runtime/arcane/modules/ThemeManager.js +131 -0
- package/runtime/arcane/modules/TimeGuard.js +149 -0
- package/runtime/arcane/modules/ToolCallRouter.js +83 -0
- package/runtime/arcane/modules/WaitForComponent.js +102 -0
- package/runtime/arcane/modules/YouTubeMedia.js +16 -0
- package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
- package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
- package/runtime/arcane/modules/uPlot.min.css +1 -0
- package/runtime/arcane/security/arcane-network-policy.json +6 -0
- package/runtime/strong-type/index.js +352 -0
- package/runtime/strong-type/licence +21 -0
- package/runtime/strong-type/package.json +45 -0
- package/schemas/arcane-app-bundle.schema.json +125 -0
- package/schemas/arcane-app.schema.json +329 -0
- package/schemas/arcane-lock.schema.json +86 -0
- package/schemas/arcane-package.schema.json +224 -0
- package/schemas/cli-event.schema.json +122 -0
- package/schemas/event-stack.schema.json +152 -0
- package/schemas/native-build-plan.schema.json +176 -0
- package/schemas/target-adapter.schema.json +117 -0
- package/src/app-descriptor.mjs +500 -0
- package/src/cli/main.mjs +561 -0
- package/src/constants.mjs +31 -0
- package/src/dev-server.mjs +718 -0
- package/src/doctor.mjs +315 -0
- package/src/dom-event-instrumentation.mjs +594 -0
- package/src/errors.mjs +75 -0
- package/src/event-manager.mjs +1342 -0
- package/src/event-queue.mjs +138 -0
- package/src/events.mjs +219 -0
- package/src/index.mjs +177 -0
- package/src/integrated-provider-loader.mjs +432 -0
- package/src/native-plan.mjs +698 -0
- package/src/native-provider-loader.mjs +1126 -0
- package/src/packager/core.mjs +2691 -0
- package/src/process.mjs +353 -0
- package/src/release-bundle.mjs +2523 -0
- package/src/repository.mjs +90 -0
- package/src/runtime.mjs +452 -0
- package/src/scaffold.mjs +380 -0
- package/src/targets/index.mjs +436 -0
- package/src/templates/assets/app-icon.png +0 -0
- package/src/templates/workspace-template.mjs +388 -0
- package/src/testing-loader.mjs +9 -0
- package/src/testing.mjs +427 -0
- package/src/toolchain.mjs +1335 -0
- package/src/update-check.mjs +307 -0
- package/src/workspace.mjs +449 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import {randomBytes,timingSafeEqual} from 'node:crypto';
|
|
2
|
+
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
|
+
import {lstat,open,realpath} from 'node:fs/promises';
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
authenticateAppReleaseReceipt,
|
|
8
|
+
readVerifiedAppReleaseFile,
|
|
9
|
+
RELEASE_MANIFEST_NAME
|
|
10
|
+
} from './packager/core.mjs';
|
|
11
|
+
import {
|
|
12
|
+
authenticateRuntimeReceipt,
|
|
13
|
+
readVerifiedRuntimeFile,
|
|
14
|
+
verifyRuntime
|
|
15
|
+
} from './runtime.mjs';
|
|
16
|
+
import {resolveWorkspace} from './workspace.mjs';
|
|
17
|
+
import {createEventQueue} from './event-queue.mjs';
|
|
18
|
+
|
|
19
|
+
const MIME_TYPES=new Map([
|
|
20
|
+
['.css','text/css; charset=utf-8'],
|
|
21
|
+
['.gif','image/gif'],
|
|
22
|
+
['.html','text/html; charset=utf-8'],
|
|
23
|
+
['.ico','image/x-icon'],
|
|
24
|
+
['.jpeg','image/jpeg'],
|
|
25
|
+
['.jpg','image/jpeg'],
|
|
26
|
+
['.js','text/javascript; charset=utf-8'],
|
|
27
|
+
['.json','application/json; charset=utf-8'],
|
|
28
|
+
['.mjs','text/javascript; charset=utf-8'],
|
|
29
|
+
['.png','image/png'],
|
|
30
|
+
['.svg','image/svg+xml; charset=utf-8'],
|
|
31
|
+
['.txt','text/plain; charset=utf-8'],
|
|
32
|
+
['.webp','image/webp'],
|
|
33
|
+
['.woff','font/woff'],
|
|
34
|
+
['.woff2','font/woff2']
|
|
35
|
+
]);
|
|
36
|
+
const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
37
|
+
const MAX_DEVELOPMENT_FILE_BYTES=64*1024*1024;
|
|
38
|
+
const MAX_CONCURRENT_FILE_RESPONSES=4;
|
|
39
|
+
const MAX_PENDING_FILE_RESPONSES=256;
|
|
40
|
+
const SESSION_COOKIE='Arcane-Dev-Session';
|
|
41
|
+
const PRIVATE_SOURCE_SEGMENTS=new Set([
|
|
42
|
+
'arcane-app.json','arcane-package.json','test','tests','scripts','node_modules','dist','local'
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// This server is a loopback-only development host, not a production policy
|
|
46
|
+
// boundary. The bundled runtime currently needs inline component execution,
|
|
47
|
+
// workers, remote provider requests, media, and embedded web content. The
|
|
48
|
+
// unguessable session cookie and exact numeric Host check protect this broad
|
|
49
|
+
// development CSP from being exposed as a general network service.
|
|
50
|
+
const DEVELOPMENT_CSP=[
|
|
51
|
+
"default-src 'self'",
|
|
52
|
+
"script-src 'self' 'unsafe-inline'",
|
|
53
|
+
"style-src 'self' 'unsafe-inline'",
|
|
54
|
+
"img-src 'self' data: blob: http: https:",
|
|
55
|
+
"font-src 'self' data:",
|
|
56
|
+
"connect-src 'self' data: blob: http: https: ws: wss:",
|
|
57
|
+
"worker-src 'self' blob:",
|
|
58
|
+
"frame-src 'self' data: blob: http: https:",
|
|
59
|
+
"media-src 'self' data: blob: http: https:",
|
|
60
|
+
"object-src 'none'",
|
|
61
|
+
"frame-ancestors 'none'",
|
|
62
|
+
"base-uri 'self'",
|
|
63
|
+
"form-action 'self'"
|
|
64
|
+
].join('; ');
|
|
65
|
+
|
|
66
|
+
function fail(message,code='ARCANE_OPERATION_FAILED'){
|
|
67
|
+
const error=new Error(message);
|
|
68
|
+
error.code=code;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function throwIfAborted(signal){
|
|
73
|
+
if(!signal?.aborted)return;
|
|
74
|
+
const error=signal.reason instanceof Error?signal.reason:new Error('Operation cancelled.');
|
|
75
|
+
error.code=error.code||'ARCANE_CANCELLED';
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function responseSecurityHeaders(contentSecurityPolicy=DEVELOPMENT_CSP){
|
|
80
|
+
return {
|
|
81
|
+
'cache-control':'no-store',
|
|
82
|
+
'x-content-type-options':'nosniff',
|
|
83
|
+
'content-security-policy':contentSecurityPolicy,
|
|
84
|
+
'cross-origin-resource-policy':'same-origin',
|
|
85
|
+
'referrer-policy':'no-referrer'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function deny(response,status,message){
|
|
90
|
+
response.writeHead(status,{
|
|
91
|
+
'content-type':'text/plain; charset=utf-8',
|
|
92
|
+
...responseSecurityHeaders("default-src 'none'; frame-ancestors 'none'; base-uri 'none'")
|
|
93
|
+
});
|
|
94
|
+
response.end(`${message}\n`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseRequestTarget(rawUrl){
|
|
98
|
+
const raw=String(rawUrl||'/');
|
|
99
|
+
if(!raw.startsWith('/'))return null;
|
|
100
|
+
const rawPath=raw.split(/[?#]/u,1)[0];
|
|
101
|
+
let decoded;
|
|
102
|
+
try{decoded=decodeURIComponent(rawPath);}
|
|
103
|
+
catch{return null;}
|
|
104
|
+
if(!decoded.startsWith('/')||/[\x00-\x1f\x7f]/u.test(decoded)
|
|
105
|
+
||/[<>"|?*]/u.test(decoded)||decoded.includes('\\'))return null;
|
|
106
|
+
const segments=decoded.split('/').filter(Boolean);
|
|
107
|
+
if(segments.some(segment=>segment==='.'||segment==='..'||segment.includes(':')
|
|
108
|
+
||segment.endsWith('.')||segment.endsWith(' ')
|
|
109
|
+
||/^(?:con|prn|aux|nul|clock\$|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\..*)?$/iu
|
|
110
|
+
.test(segment)))return null;
|
|
111
|
+
let parsed;
|
|
112
|
+
try{parsed=new URL(raw,'http://127.0.0.1');}
|
|
113
|
+
catch{return null;}
|
|
114
|
+
return {segments,path:decoded,searchParams:parsed.searchParams};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveInside(root,segments){
|
|
118
|
+
const candidate=path.resolve(root,...segments);
|
|
119
|
+
const relative=path.relative(root,candidate);
|
|
120
|
+
if(relative.startsWith('..')||path.isAbsolute(relative))return null;
|
|
121
|
+
return candidate;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function identityMatches(info,identity){
|
|
125
|
+
return !identity||(
|
|
126
|
+
String(info.dev)===identity.device
|
|
127
|
+
&&String(info.ino)===identity.inode
|
|
128
|
+
&&Number(info.size)===identity.bytes
|
|
129
|
+
&&String(info.mtimeNs)===identity.modifiedNanoseconds
|
|
130
|
+
&&String(info.ctimeNs)===identity.changedNanoseconds
|
|
131
|
+
&&(identity.links===undefined||String(info.nlink)===identity.links)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function inventoryKey(value){
|
|
136
|
+
return process.platform==='win32'?value.toLowerCase():value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function identityMap(identities,prefix=''){
|
|
140
|
+
const normalizedPrefix=prefix?`${prefix}/`:'';
|
|
141
|
+
const result=new Map();
|
|
142
|
+
for(const identity of identities||[]){
|
|
143
|
+
if(!identity.path.startsWith(normalizedPrefix))continue;
|
|
144
|
+
const relative=identity.path.slice(normalizedPrefix.length);
|
|
145
|
+
if(relative)result.set(inventoryKey(relative),identity);
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createFileWorkLimiter(){
|
|
151
|
+
let active=0;
|
|
152
|
+
const pending=[];
|
|
153
|
+
const release=()=>{
|
|
154
|
+
const next=pending.shift();
|
|
155
|
+
if(next)next();
|
|
156
|
+
else active-=1;
|
|
157
|
+
};
|
|
158
|
+
return async work=>{
|
|
159
|
+
if(active>=MAX_CONCURRENT_FILE_RESPONSES){
|
|
160
|
+
if(pending.length>=MAX_PENDING_FILE_RESPONSES){
|
|
161
|
+
fail('Development server file queue is full.','ARCANE_BACKPRESSURE');
|
|
162
|
+
}
|
|
163
|
+
await new Promise(resolve=>pending.push(resolve));
|
|
164
|
+
}else{
|
|
165
|
+
active+=1;
|
|
166
|
+
}
|
|
167
|
+
try{
|
|
168
|
+
return await work();
|
|
169
|
+
}finally{
|
|
170
|
+
release();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function openSafeFile(root,segments,expectedIdentity){
|
|
176
|
+
const candidate=resolveInside(root,segments);
|
|
177
|
+
if(!candidate)return null;
|
|
178
|
+
let current=root;
|
|
179
|
+
for(const segment of segments){
|
|
180
|
+
current=path.join(current,segment);
|
|
181
|
+
let info;
|
|
182
|
+
try{info=await lstat(current,{bigint:true});}
|
|
183
|
+
catch(error){
|
|
184
|
+
if(error?.code==='ENOENT')return null;
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
if(info.isSymbolicLink())return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const before=await lstat(candidate,{bigint:true});
|
|
191
|
+
if(before.isSymbolicLink()||!before.isFile()||!identityMatches(before,expectedIdentity))return null;
|
|
192
|
+
let handle;
|
|
193
|
+
try{
|
|
194
|
+
handle=await open(candidate,READ_ONLY_NO_FOLLOW);
|
|
195
|
+
}catch(error){
|
|
196
|
+
if(error?.code==='ENOENT'||error?.code==='ELOOP')return null;
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
try{
|
|
200
|
+
const opened=await handle.stat({bigint:true});
|
|
201
|
+
if(!opened.isFile()||!identityMatches(opened,expectedIdentity)
|
|
202
|
+
||!identityMatches(opened,{
|
|
203
|
+
device:String(before.dev),
|
|
204
|
+
inode:String(before.ino),
|
|
205
|
+
bytes:Number(before.size),
|
|
206
|
+
modifiedNanoseconds:String(before.mtimeNs),
|
|
207
|
+
changedNanoseconds:String(before.ctimeNs),
|
|
208
|
+
links:String(before.nlink)
|
|
209
|
+
})){
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if(Number(opened.size)>MAX_DEVELOPMENT_FILE_BYTES){
|
|
213
|
+
fail(
|
|
214
|
+
`Development file exceeds the ${MAX_DEVELOPMENT_FILE_BYTES}-byte serving limit.`,
|
|
215
|
+
'ARCANE_POLICY_DENIED'
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const bytes=await handle.readFile();
|
|
219
|
+
const after=await handle.stat({bigint:true});
|
|
220
|
+
if(!identityMatches(after,{
|
|
221
|
+
device:String(opened.dev),
|
|
222
|
+
inode:String(opened.ino),
|
|
223
|
+
bytes:Number(opened.size),
|
|
224
|
+
modifiedNanoseconds:String(opened.mtimeNs),
|
|
225
|
+
changedNanoseconds:String(opened.ctimeNs),
|
|
226
|
+
links:String(opened.nlink)
|
|
227
|
+
})||bytes.length!==Number(opened.size)){
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const canonicalCandidate=await realpath(candidate);
|
|
231
|
+
const relative=path.relative(root,canonicalCandidate);
|
|
232
|
+
if(relative.startsWith('..')||path.isAbsolute(relative)){
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const currentInfo=await lstat(candidate,{bigint:true});
|
|
236
|
+
if(!identityMatches(currentInfo,{
|
|
237
|
+
device:String(opened.dev),
|
|
238
|
+
inode:String(opened.ino),
|
|
239
|
+
bytes:Number(opened.size),
|
|
240
|
+
modifiedNanoseconds:String(opened.mtimeNs),
|
|
241
|
+
changedNanoseconds:String(opened.ctimeNs),
|
|
242
|
+
links:String(opened.nlink)
|
|
243
|
+
})){
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return {candidate:canonicalCandidate,bytes,size:bytes.length};
|
|
247
|
+
}catch(error){
|
|
248
|
+
if(error?.code==='ENOENT')return null;
|
|
249
|
+
throw error;
|
|
250
|
+
}finally{
|
|
251
|
+
await handle.close().catch(()=>{});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function sendFile(response,opened,{head=false}={}){
|
|
256
|
+
const extension=path.extname(opened.candidate).toLowerCase();
|
|
257
|
+
response.writeHead(200,{
|
|
258
|
+
'content-type':MIME_TYPES.get(extension)||'application/octet-stream',
|
|
259
|
+
'content-length':opened.size,
|
|
260
|
+
...responseSecurityHeaders()
|
|
261
|
+
});
|
|
262
|
+
await new Promise((resolve,reject)=>{
|
|
263
|
+
let settled=false;
|
|
264
|
+
const cleanup=()=>{
|
|
265
|
+
response.removeListener('error',failed);
|
|
266
|
+
response.removeListener('finish',completed);
|
|
267
|
+
response.removeListener('close',completed);
|
|
268
|
+
};
|
|
269
|
+
const completed=()=>{
|
|
270
|
+
if(settled)return;
|
|
271
|
+
settled=true;
|
|
272
|
+
cleanup();
|
|
273
|
+
resolve();
|
|
274
|
+
};
|
|
275
|
+
const failed=error=>{
|
|
276
|
+
if(settled)return;
|
|
277
|
+
settled=true;
|
|
278
|
+
cleanup();
|
|
279
|
+
reject(error);
|
|
280
|
+
};
|
|
281
|
+
response.once('error',failed);
|
|
282
|
+
response.once('finish',completed);
|
|
283
|
+
response.once('close',completed);
|
|
284
|
+
response.end(head?undefined:opened.bytes);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sourcePathAllowed(relative,manifest){
|
|
289
|
+
const posix=relative.join('/');
|
|
290
|
+
if(!posix)return false;
|
|
291
|
+
const segments=posix.split('/');
|
|
292
|
+
if(segments.some(segment=>segment.startsWith('.')
|
|
293
|
+
||PRIVATE_SOURCE_SEGMENTS.has(segment.toLowerCase())))return false;
|
|
294
|
+
const comparable=inventoryKey(posix);
|
|
295
|
+
const excluded=(manifest.exclude||[]).some(item=>{
|
|
296
|
+
const candidate=inventoryKey(item);
|
|
297
|
+
return comparable===candidate||comparable.startsWith(`${candidate}/`);
|
|
298
|
+
});
|
|
299
|
+
if(excluded)return false;
|
|
300
|
+
return manifest.include.some(item=>{
|
|
301
|
+
const candidate=inventoryKey(item);
|
|
302
|
+
return comparable===candidate||comparable.startsWith(`${candidate}/`);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function sharedPathAllowed(relative,route){
|
|
307
|
+
const posix=relative.join('/');
|
|
308
|
+
if(!posix||relative.some(segment=>segment.startsWith('.')))return false;
|
|
309
|
+
const comparable=inventoryKey(posix);
|
|
310
|
+
const excluded=(route.exclude??[]).some(item=>{
|
|
311
|
+
const candidate=inventoryKey(item);
|
|
312
|
+
return comparable===candidate||comparable.startsWith(`${candidate}/`);
|
|
313
|
+
});
|
|
314
|
+
if(excluded)return false;
|
|
315
|
+
return route.include.some(item=>{
|
|
316
|
+
const candidate=inventoryKey(item);
|
|
317
|
+
return comparable===candidate||comparable.startsWith(`${candidate}/`);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function sourceRoutes(workspaceRoot,appId,{runtimeReceipt,signal,onEvent}){
|
|
322
|
+
const resolved=await resolveWorkspace({workspaceRoot,appId});
|
|
323
|
+
if(resolved.config.workspaceMode==='integrated'){
|
|
324
|
+
return {
|
|
325
|
+
workspaceRoot:resolved.workspaceRoot,
|
|
326
|
+
workspaceMode:'integrated',
|
|
327
|
+
appId:resolved.appId,
|
|
328
|
+
startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
|
|
329
|
+
mappings:[
|
|
330
|
+
{
|
|
331
|
+
prefix:['apps',resolved.appId],
|
|
332
|
+
root:resolved.appRoot,
|
|
333
|
+
allow:relative=>sourcePathAllowed(relative,resolved.app.manifest)
|
|
334
|
+
},
|
|
335
|
+
...resolved.config.sharedPayloads['browser-runtime'].map(route=>({
|
|
336
|
+
prefix:route.destination.split('/'),
|
|
337
|
+
root:path.join(resolved.workspaceRoot,...route.source.split('/')),
|
|
338
|
+
allow:relative=>sharedPathAllowed(relative,route)
|
|
339
|
+
}))
|
|
340
|
+
]
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const runtimeRoot=path.join(
|
|
344
|
+
resolved.workspaceRoot,
|
|
345
|
+
'node_modules',
|
|
346
|
+
'arcane-os',
|
|
347
|
+
'runtime'
|
|
348
|
+
);
|
|
349
|
+
const verified=runtimeReceipt??await verifyRuntime({runtimeRoot,signal,onEvent});
|
|
350
|
+
await authenticateRuntimeReceipt(verified,{runtimeRoot});
|
|
351
|
+
const arcaneIdentities=identityMap(verified.identities,'arcane');
|
|
352
|
+
const strongTypeIdentities=identityMap(verified.identities,'strong-type');
|
|
353
|
+
return {
|
|
354
|
+
workspaceRoot:resolved.workspaceRoot,
|
|
355
|
+
workspaceMode:'external',
|
|
356
|
+
appId:resolved.appId,
|
|
357
|
+
startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
|
|
358
|
+
mappings:[
|
|
359
|
+
{
|
|
360
|
+
prefix:['apps',resolved.appId],
|
|
361
|
+
root:resolved.appRoot,
|
|
362
|
+
allow:relative=>sourcePathAllowed(relative,resolved.app.manifest)
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
prefix:['arcane'],
|
|
366
|
+
root:path.join(runtimeRoot,'arcane'),
|
|
367
|
+
identities:arcaneIdentities,
|
|
368
|
+
read:relative=>readVerifiedRuntimeFile(verified,{
|
|
369
|
+
runtimeRoot,
|
|
370
|
+
relativePath:`arcane/${relative.join('/')}`,
|
|
371
|
+
signal
|
|
372
|
+
}),
|
|
373
|
+
allow:relative=>arcaneIdentities.has(inventoryKey(relative.join('/')))
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
prefix:['node_modules','strong-type'],
|
|
377
|
+
root:path.join(runtimeRoot,'strong-type'),
|
|
378
|
+
identities:strongTypeIdentities,
|
|
379
|
+
read:relative=>readVerifiedRuntimeFile(verified,{
|
|
380
|
+
runtimeRoot,
|
|
381
|
+
relativePath:`strong-type/${relative.join('/')}`,
|
|
382
|
+
signal
|
|
383
|
+
}),
|
|
384
|
+
allow:relative=>strongTypeIdentities.has(inventoryKey(relative.join('/')))
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function packagedRoutes(releaseRoot,releaseReceipt,{signal}={}){
|
|
391
|
+
if(typeof releaseRoot!=='string'||!releaseRoot.trim())fail('releaseRoot is required in packaged mode.','ARCANE_USAGE');
|
|
392
|
+
if(!releaseReceipt)fail('An authenticated release receipt is required in packaged mode.','ARCANE_POLICY_DENIED');
|
|
393
|
+
const requested=path.resolve(releaseRoot);
|
|
394
|
+
await authenticateAppReleaseReceipt(releaseReceipt,{releaseRoot:requested,signal});
|
|
395
|
+
const info=await lstat(requested);
|
|
396
|
+
if(info.isSymbolicLink()||!info.isDirectory())fail('Packaged release root must be a real directory.');
|
|
397
|
+
const canonical=await realpath(requested);
|
|
398
|
+
const identities=identityMap(releaseReceipt.identities);
|
|
399
|
+
return {
|
|
400
|
+
workspaceRoot:null,
|
|
401
|
+
appId:null,
|
|
402
|
+
startPath:'/index.html',
|
|
403
|
+
mappings:[{
|
|
404
|
+
prefix:[],
|
|
405
|
+
root:canonical,
|
|
406
|
+
identities,
|
|
407
|
+
read:relative=>readVerifiedAppReleaseFile(releaseReceipt,{
|
|
408
|
+
releaseRoot:canonical,
|
|
409
|
+
relativePath:relative.join('/'),
|
|
410
|
+
signal
|
|
411
|
+
}),
|
|
412
|
+
allow:relative=>identities.has(inventoryKey(relative.join('/')))
|
|
413
|
+
}]
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function tokenMatches(value,expected){
|
|
418
|
+
if(typeof value!=='string')return false;
|
|
419
|
+
const received=Buffer.from(value,'utf8');
|
|
420
|
+
const wanted=Buffer.from(expected,'utf8');
|
|
421
|
+
return received.length===wanted.length&&timingSafeEqual(received,wanted);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function hasSessionCookie(request,cookieName,sessionToken){
|
|
425
|
+
const matches=String(request.headers.cookie||'')
|
|
426
|
+
.split(';')
|
|
427
|
+
.map(part=>part.trim())
|
|
428
|
+
.filter(part=>part.startsWith(`${cookieName}=`))
|
|
429
|
+
.map(part=>part.slice(cookieName.length+1));
|
|
430
|
+
return matches.length===1&&tokenMatches(matches[0],sessionToken);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function listen(server,{host,port,signal}){
|
|
434
|
+
return new Promise((resolve,reject)=>{
|
|
435
|
+
const cleanup=()=>{
|
|
436
|
+
signal?.removeEventListener('abort',abort);
|
|
437
|
+
server.removeListener('error',failed);
|
|
438
|
+
};
|
|
439
|
+
const abort=()=>server.close(()=>{
|
|
440
|
+
cleanup();
|
|
441
|
+
reject(signal.reason||new Error('Operation cancelled.'));
|
|
442
|
+
});
|
|
443
|
+
const failed=error=>{
|
|
444
|
+
cleanup();
|
|
445
|
+
reject(error);
|
|
446
|
+
};
|
|
447
|
+
signal?.addEventListener('abort',abort,{once:true});
|
|
448
|
+
server.once('error',failed);
|
|
449
|
+
server.listen(port,host,()=>{
|
|
450
|
+
cleanup();
|
|
451
|
+
resolve();
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function startOwnedDevServer({
|
|
457
|
+
workspaceRoot=process.cwd(),
|
|
458
|
+
appId,
|
|
459
|
+
mode='source',
|
|
460
|
+
releaseRoot,
|
|
461
|
+
host='127.0.0.1',
|
|
462
|
+
port=0,
|
|
463
|
+
signal,
|
|
464
|
+
runtimeReceipt,
|
|
465
|
+
releaseReceipt
|
|
466
|
+
}={},events,releaseSignal){
|
|
467
|
+
throwIfAborted(signal);
|
|
468
|
+
if(mode!=='source'&&mode!=='packaged')fail(`Unsupported server mode: ${String(mode)}.`,'ARCANE_USAGE');
|
|
469
|
+
if(host!=='127.0.0.1'&&host!=='::1'){
|
|
470
|
+
fail('Development server host must be a numeric loopback address (127.0.0.1 or ::1).','ARCANE_POLICY_DENIED');
|
|
471
|
+
}
|
|
472
|
+
if(!Number.isInteger(port)||port<0||port>65535)fail('port must be an integer from 0 through 65535.','ARCANE_USAGE');
|
|
473
|
+
await events.send({type:'server.starting',mode,host,port,appId});
|
|
474
|
+
const routeSet=mode==='source'
|
|
475
|
+
?await sourceRoutes(workspaceRoot,appId,{
|
|
476
|
+
runtimeReceipt,
|
|
477
|
+
signal,
|
|
478
|
+
onEvent:event=>events.send(event)
|
|
479
|
+
})
|
|
480
|
+
:await packagedRoutes(releaseRoot,releaseReceipt,{signal});
|
|
481
|
+
for(const mapping of routeSet.mappings){
|
|
482
|
+
const info=await lstat(mapping.root);
|
|
483
|
+
if(info.isSymbolicLink()||!info.isDirectory())fail(`Server route root must be a real directory: ${mapping.root}.`);
|
|
484
|
+
mapping.root=await realpath(mapping.root);
|
|
485
|
+
}
|
|
486
|
+
const sessionToken=randomBytes(32).toString('hex');
|
|
487
|
+
const sessionCookieName=`${SESSION_COOKIE}-${randomBytes(8).toString('hex')}`;
|
|
488
|
+
let expectedAuthority=null;
|
|
489
|
+
const requestTasks=new Set();
|
|
490
|
+
const runFileWork=createFileWorkLimiter();
|
|
491
|
+
const server=http.createServer((request,response)=>{
|
|
492
|
+
let task;
|
|
493
|
+
task=(async()=>{
|
|
494
|
+
if(!expectedAuthority){deny(response,503,'Server is starting.');return;}
|
|
495
|
+
if(request.headers.host!==expectedAuthority){
|
|
496
|
+
deny(response,421,'Misdirected request.');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if(request.method!=='GET'&&request.method!=='HEAD'){
|
|
500
|
+
deny(response,405,'Method not allowed.');
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const target=parseRequestTarget(request.url);
|
|
504
|
+
if(!target){deny(response,400,'Invalid request path.');return;}
|
|
505
|
+
const queryKeys=[...target.searchParams.keys()];
|
|
506
|
+
const isBootstrap=request.method==='GET'
|
|
507
|
+
&&target.path===routeSet.startPath
|
|
508
|
+
&&queryKeys.length===1
|
|
509
|
+
&&queryKeys[0]==='arcane_session'
|
|
510
|
+
&&tokenMatches(target.searchParams.get('arcane_session'),sessionToken);
|
|
511
|
+
if(isBootstrap){
|
|
512
|
+
response.writeHead(302,{
|
|
513
|
+
location:routeSet.startPath,
|
|
514
|
+
'set-cookie':`${sessionCookieName}=${sessionToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
515
|
+
...responseSecurityHeaders()
|
|
516
|
+
});
|
|
517
|
+
response.end();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if(!hasSessionCookie(request,sessionCookieName,sessionToken)){
|
|
521
|
+
deny(response,403,'Development server session required.');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const {segments}=target;
|
|
525
|
+
if(mode==='packaged'&&segments.some(segment=>
|
|
526
|
+
segment.toLowerCase()===RELEASE_MANIFEST_NAME.toLowerCase()
|
|
527
|
+
)){
|
|
528
|
+
deny(response,404,'Not found.');
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if(segments.length===0){
|
|
532
|
+
response.writeHead(302,{location:routeSet.startPath,...responseSecurityHeaders()});
|
|
533
|
+
response.end();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const mapping=routeSet.mappings.find(route=>
|
|
537
|
+
route.prefix.every((segment,index)=>segments[index]===segment)
|
|
538
|
+
);
|
|
539
|
+
if(!mapping){deny(response,404,'Not found.');return;}
|
|
540
|
+
const relative=segments.slice(mapping.prefix.length);
|
|
541
|
+
if(relative.length===0){deny(response,404,'Not found.');return;}
|
|
542
|
+
if(mapping.allow&&!mapping.allow(relative)){deny(response,404,'Not found.');return;}
|
|
543
|
+
await runFileWork(async()=>{
|
|
544
|
+
const expectedIdentity=mapping.identities?.get(inventoryKey(relative.join('/')));
|
|
545
|
+
const opened=mapping.read
|
|
546
|
+
?{
|
|
547
|
+
candidate:path.join(mapping.root,...relative),
|
|
548
|
+
bytes:await mapping.read(relative)
|
|
549
|
+
}
|
|
550
|
+
:await openSafeFile(mapping.root,relative,expectedIdentity);
|
|
551
|
+
if(!opened){deny(response,404,'Not found.');return;}
|
|
552
|
+
opened.size=opened.bytes.length;
|
|
553
|
+
await sendFile(response,opened,{head:request.method==='HEAD'});
|
|
554
|
+
});
|
|
555
|
+
})().catch(async error=>{
|
|
556
|
+
await events.enqueue({type:'server.request.failed',message:error.message});
|
|
557
|
+
if(!response.headersSent){
|
|
558
|
+
const status=error?.code==='ARCANE_POLICY_DENIED'?413
|
|
559
|
+
:error?.code==='ARCANE_BACKPRESSURE'?503
|
|
560
|
+
:500;
|
|
561
|
+
deny(response,status,'Internal server error.');
|
|
562
|
+
}
|
|
563
|
+
else response.destroy(error);
|
|
564
|
+
}).finally(()=>{
|
|
565
|
+
requestTasks.delete(task);
|
|
566
|
+
});
|
|
567
|
+
requestTasks.add(task);
|
|
568
|
+
});
|
|
569
|
+
await listen(server,{host,port,signal});
|
|
570
|
+
const address=server.address();
|
|
571
|
+
if(!address||typeof address==='string'){
|
|
572
|
+
server.close();
|
|
573
|
+
fail('Development server did not expose a TCP address.');
|
|
574
|
+
}
|
|
575
|
+
const visibleHost=address.family==='IPv6'?`[${address.address}]`:address.address;
|
|
576
|
+
const endpoint=new URL(`http://${visibleHost}:${address.port}`);
|
|
577
|
+
expectedAuthority=endpoint.host;
|
|
578
|
+
const origin=endpoint.origin;
|
|
579
|
+
const cleanUrl=`${origin}${routeSet.startPath}`;
|
|
580
|
+
const url=`${cleanUrl}?arcane_session=${sessionToken}`;
|
|
581
|
+
let closeInitiated=false;
|
|
582
|
+
let lifecycleSettlementStarted=false;
|
|
583
|
+
let operationalError=null;
|
|
584
|
+
let resolveLifecycle;
|
|
585
|
+
let rejectLifecycle;
|
|
586
|
+
const lifecycle=new Promise((resolve,reject)=>{
|
|
587
|
+
resolveLifecycle=resolve;
|
|
588
|
+
rejectLifecycle=reject;
|
|
589
|
+
});
|
|
590
|
+
// A library consumer may only use the raw server. Keep a later lifecycle
|
|
591
|
+
// rejection observable without allowing it to become unhandled.
|
|
592
|
+
void lifecycle.catch(()=>{});
|
|
593
|
+
|
|
594
|
+
const finishLifecycle=async()=>{
|
|
595
|
+
if(lifecycleSettlementStarted){
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
lifecycleSettlementStarted=true;
|
|
599
|
+
signal?.removeEventListener('abort',abort);
|
|
600
|
+
server.removeListener('error',serverFailed);
|
|
601
|
+
try{
|
|
602
|
+
while(requestTasks.size>0){
|
|
603
|
+
await Promise.allSettled([...requestTasks]);
|
|
604
|
+
}
|
|
605
|
+
await events.send({
|
|
606
|
+
type:'server.stopped',
|
|
607
|
+
host:address.address,
|
|
608
|
+
port:address.port
|
|
609
|
+
});
|
|
610
|
+
await events.drain();
|
|
611
|
+
if(operationalError){
|
|
612
|
+
rejectLifecycle(operationalError);
|
|
613
|
+
}else{
|
|
614
|
+
resolveLifecycle();
|
|
615
|
+
}
|
|
616
|
+
}catch(error){
|
|
617
|
+
rejectLifecycle(events.error??operationalError??error);
|
|
618
|
+
}finally{
|
|
619
|
+
releaseSignal();
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const close=error=>{
|
|
624
|
+
if(error&&!operationalError){
|
|
625
|
+
operationalError=error;
|
|
626
|
+
}
|
|
627
|
+
if(!closeInitiated){
|
|
628
|
+
closeInitiated=true;
|
|
629
|
+
try{
|
|
630
|
+
server.close(closeError=>{
|
|
631
|
+
if(closeError){
|
|
632
|
+
operationalError??=closeError;
|
|
633
|
+
void finishLifecycle();
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
}catch(closeError){
|
|
637
|
+
operationalError??=closeError;
|
|
638
|
+
void finishLifecycle();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return lifecycle;
|
|
642
|
+
};
|
|
643
|
+
const abort=()=>{void close().catch(()=>{});};
|
|
644
|
+
const serverFailed=error=>{void close(error).catch(()=>{});};
|
|
645
|
+
signal?.addEventListener('abort',abort,{once:true});
|
|
646
|
+
server.on('error',serverFailed);
|
|
647
|
+
server.once('close',()=>{void finishLifecycle();});
|
|
648
|
+
const result={
|
|
649
|
+
server,
|
|
650
|
+
mode,
|
|
651
|
+
workspaceRoot:routeSet.workspaceRoot,
|
|
652
|
+
appId:routeSet.appId,
|
|
653
|
+
host:address.address,
|
|
654
|
+
port:address.port,
|
|
655
|
+
origin,
|
|
656
|
+
cleanUrl,
|
|
657
|
+
url,
|
|
658
|
+
close,
|
|
659
|
+
closed:lifecycle,
|
|
660
|
+
lifecycle
|
|
661
|
+
};
|
|
662
|
+
try{
|
|
663
|
+
await events.send({
|
|
664
|
+
type:'server.started',
|
|
665
|
+
mode,
|
|
666
|
+
host:result.host,
|
|
667
|
+
port:result.port,
|
|
668
|
+
url,
|
|
669
|
+
appId:result.appId
|
|
670
|
+
});
|
|
671
|
+
throwIfAborted(signal);
|
|
672
|
+
}catch(error){
|
|
673
|
+
try{
|
|
674
|
+
await close();
|
|
675
|
+
}catch(lifecycleError){
|
|
676
|
+
throw events.error??lifecycleError;
|
|
677
|
+
}
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export async function startDevServer(options={}){
|
|
684
|
+
const inputSignal=options.signal;
|
|
685
|
+
throwIfAborted(inputSignal);
|
|
686
|
+
const controller=new AbortController();
|
|
687
|
+
const forwardAbort=()=>controller.abort(inputSignal?.reason);
|
|
688
|
+
inputSignal?.addEventListener('abort',forwardAbort,{once:true});
|
|
689
|
+
if(inputSignal?.aborted){
|
|
690
|
+
forwardAbort();
|
|
691
|
+
}
|
|
692
|
+
let released=false;
|
|
693
|
+
const releaseSignal=()=>{
|
|
694
|
+
if(released){
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
released=true;
|
|
698
|
+
inputSignal?.removeEventListener('abort',forwardAbort);
|
|
699
|
+
};
|
|
700
|
+
const events=createEventQueue(options.onEvent,{
|
|
701
|
+
onFailure:error=>controller.abort(error)
|
|
702
|
+
});
|
|
703
|
+
try{
|
|
704
|
+
return await startOwnedDevServer(
|
|
705
|
+
{...options,signal:controller.signal},
|
|
706
|
+
events,
|
|
707
|
+
releaseSignal
|
|
708
|
+
);
|
|
709
|
+
}catch(error){
|
|
710
|
+
releaseSignal();
|
|
711
|
+
try{
|
|
712
|
+
await events.drain();
|
|
713
|
+
}catch(callbackFailure){
|
|
714
|
+
throw callbackFailure;
|
|
715
|
+
}
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
}
|