arcane-os 0.3.1 → 0.3.3
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 +27 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
- package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -148
- package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +113 -69
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1042 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +19 -149
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +42 -327
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +48 -66
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +492 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +48 -79
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +55 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +133 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +109 -1558
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -185
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1295
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -719
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2965
- package/docs/reference/sdk-api.md +0 -6698
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
package/src/dev-server.mjs
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
import {randomBytes,timingSafeEqual} from 'node:crypto';
|
|
2
1
|
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
2
|
import {lstat,open,realpath} from 'node:fs/promises';
|
|
4
3
|
import http from 'node:http';
|
|
5
4
|
import path from 'node:path';
|
|
6
|
-
import {
|
|
7
|
-
authenticateAppReleaseReceipt,
|
|
8
|
-
readVerifiedAppReleaseFile,
|
|
9
|
-
RELEASE_MANIFEST_NAME
|
|
10
|
-
} from './packager/core.mjs';
|
|
11
|
-
import {
|
|
12
|
-
authenticateWorkspaceRuntimeReceipt,
|
|
13
|
-
readVerifiedWorkspaceRuntimeFile,
|
|
14
|
-
verifyWorkspaceRuntime
|
|
15
|
-
} from './workspace-runtime.mjs';
|
|
16
|
-
import {verifyRuntime} from './runtime.mjs';
|
|
17
|
-
import {verifySdkBrowserRuntime} from './sdk-browser-runtime.mjs';
|
|
18
|
-
import {resolveWorkspace,validateWorkspace} from './workspace.mjs';
|
|
5
|
+
import {resolveWorkspace} from './workspace.mjs';
|
|
19
6
|
import {createEventQueue} from './event-queue.mjs';
|
|
20
|
-
import {SDK_NAME,SDK_VERSION} from './constants.mjs';
|
|
21
7
|
|
|
22
8
|
const MIME_TYPES=new Map([
|
|
23
9
|
['.css','text/css; charset=utf-8'],
|
|
@@ -38,10 +24,6 @@ const MIME_TYPES=new Map([
|
|
|
38
24
|
['.woff2','font/woff2']
|
|
39
25
|
]);
|
|
40
26
|
const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
41
|
-
const MAX_DEVELOPMENT_FILE_BYTES=64*1024*1024;
|
|
42
|
-
const MAX_CONCURRENT_FILE_RESPONSES=4;
|
|
43
|
-
const MAX_PENDING_FILE_RESPONSES=256;
|
|
44
|
-
const SESSION_COOKIE='Arcane-Dev-Session';
|
|
45
27
|
const PRIVATE_SOURCE_SEGMENTS=new Set([
|
|
46
28
|
'arcane-app.json','arcane-package.json','test','tests','scripts','node_modules','dist','local'
|
|
47
29
|
]);
|
|
@@ -49,6 +31,11 @@ const SDK_RUNTIME_SOURCE_PROTOCOL='arcane-sdk-runtime-source/1';
|
|
|
49
31
|
const SDK_RUNTIME_SOURCE_ARCANE_ROOTS=new Set([
|
|
50
32
|
'components','css','entities','img','modules','security'
|
|
51
33
|
]);
|
|
34
|
+
const SDK_INSTALLED_ARCANE_ROOTS=new Set([
|
|
35
|
+
...SDK_RUNTIME_SOURCE_ARCANE_ROOTS,
|
|
36
|
+
'dependencies',
|
|
37
|
+
'sdk'
|
|
38
|
+
]);
|
|
52
39
|
const SDK_RUNTIME_SOURCE_PRIVATE_SEGMENTS=new Set([
|
|
53
40
|
'node_modules','.git','.hg','.svn','cvs'
|
|
54
41
|
]);
|
|
@@ -57,27 +44,6 @@ const SDK_RUNTIME_SOURCE_PRIVATE_MANIFESTS=new Set([
|
|
|
57
44
|
'arcane_app_release.json','arcane_runtime_release.json','arcane_sdk_browser_release.json'
|
|
58
45
|
]);
|
|
59
46
|
|
|
60
|
-
// This server is a loopback-only development host, not a production policy
|
|
61
|
-
// boundary. The bundled runtime currently needs inline component execution,
|
|
62
|
-
// workers, remote provider requests, media, and embedded web content. The
|
|
63
|
-
// unguessable session cookie and exact numeric Host check protect this broad
|
|
64
|
-
// development CSP from being exposed as a general network service.
|
|
65
|
-
const DEVELOPMENT_CSP=[
|
|
66
|
-
"default-src 'self'",
|
|
67
|
-
"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'",
|
|
68
|
-
"style-src 'self' 'unsafe-inline'",
|
|
69
|
-
"img-src 'self' data: blob: http: https:",
|
|
70
|
-
"font-src 'self' data:",
|
|
71
|
-
"connect-src 'self' data: blob: http: https: ws: wss:",
|
|
72
|
-
"worker-src 'self' blob:",
|
|
73
|
-
"frame-src 'self' data: blob: http: https:",
|
|
74
|
-
"media-src 'self' data: blob: http: https:",
|
|
75
|
-
"object-src 'none'",
|
|
76
|
-
"frame-ancestors 'none'",
|
|
77
|
-
"base-uri 'self'",
|
|
78
|
-
"form-action 'self'"
|
|
79
|
-
].join('; ');
|
|
80
|
-
|
|
81
47
|
function fail(message,code='ARCANE_OPERATION_FAILED'){
|
|
82
48
|
const error=new Error(message);
|
|
83
49
|
error.code=code;
|
|
@@ -91,20 +57,9 @@ function throwIfAborted(signal){
|
|
|
91
57
|
throw error;
|
|
92
58
|
}
|
|
93
59
|
|
|
94
|
-
function responseSecurityHeaders(contentSecurityPolicy=DEVELOPMENT_CSP){
|
|
95
|
-
return {
|
|
96
|
-
'cache-control':'no-store',
|
|
97
|
-
'x-content-type-options':'nosniff',
|
|
98
|
-
'content-security-policy':contentSecurityPolicy,
|
|
99
|
-
'cross-origin-resource-policy':'same-origin',
|
|
100
|
-
'referrer-policy':'no-referrer'
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
60
|
function deny(response,status,message){
|
|
105
61
|
response.writeHead(status,{
|
|
106
|
-
'content-type':'text/plain; charset=utf-8'
|
|
107
|
-
...responseSecurityHeaders("default-src 'none'; frame-ancestors 'none'; base-uri 'none'")
|
|
62
|
+
'content-type':'text/plain; charset=utf-8'
|
|
108
63
|
});
|
|
109
64
|
response.end(`${message}\n`);
|
|
110
65
|
}
|
|
@@ -136,17 +91,6 @@ function resolveInside(root,segments){
|
|
|
136
91
|
return candidate;
|
|
137
92
|
}
|
|
138
93
|
|
|
139
|
-
function identityMatches(info,identity){
|
|
140
|
-
return !identity||(
|
|
141
|
-
String(info.dev)===identity.device
|
|
142
|
-
&&String(info.ino)===identity.inode
|
|
143
|
-
&&Number(info.size)===identity.bytes
|
|
144
|
-
&&String(info.mtimeNs)===identity.modifiedNanoseconds
|
|
145
|
-
&&String(info.ctimeNs)===identity.changedNanoseconds
|
|
146
|
-
&&(identity.links===undefined||String(info.nlink)===identity.links)
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
94
|
function inventoryKey(value){
|
|
151
95
|
return process.platform==='win32'?value.toLowerCase():value;
|
|
152
96
|
}
|
|
@@ -187,7 +131,7 @@ async function canonicalRealDirectory(requested,label){
|
|
|
187
131
|
return canonical;
|
|
188
132
|
}
|
|
189
133
|
|
|
190
|
-
function sdkRuntimeSourcePathAllowed(relative,{
|
|
134
|
+
function sdkRuntimeSourcePathAllowed(relative,{arcaneRoots=null}={}){
|
|
191
135
|
if(relative.length===0)return false;
|
|
192
136
|
const normalized=relative.map(function normalizeRuntimeSourceSegment(segment){
|
|
193
137
|
return segment.normalize('NFC').toLowerCase();
|
|
@@ -196,11 +140,15 @@ function sdkRuntimeSourcePathAllowed(relative,{arcaneRoot=false}={}){
|
|
|
196
140
|
return segment.startsWith('.')||SDK_RUNTIME_SOURCE_PRIVATE_SEGMENTS.has(segment)
|
|
197
141
|
||SDK_RUNTIME_SOURCE_PRIVATE_MANIFESTS.has(segment);
|
|
198
142
|
}))return false;
|
|
199
|
-
return
|
|
143
|
+
return arcaneRoots===null||arcaneRoots.has(normalized[0]);
|
|
200
144
|
}
|
|
201
145
|
|
|
202
146
|
function sdkArcaneSourcePathAllowed(relative){
|
|
203
|
-
return sdkRuntimeSourcePathAllowed(relative,{
|
|
147
|
+
return sdkRuntimeSourcePathAllowed(relative,{arcaneRoots:SDK_RUNTIME_SOURCE_ARCANE_ROOTS});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sdkInstalledArcanePathAllowed(relative){
|
|
151
|
+
return sdkRuntimeSourcePathAllowed(relative,{arcaneRoots:SDK_INSTALLED_ARCANE_ROOTS});
|
|
204
152
|
}
|
|
205
153
|
|
|
206
154
|
function sdkDependencySourcePathAllowed(relative){
|
|
@@ -235,35 +183,6 @@ async function verifySdkRuntimeSourceRoot(sourceRoot,workspaceRoot,appId,{signal
|
|
|
235
183
|
'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
|
|
236
184
|
}
|
|
237
185
|
|
|
238
|
-
let packageFile;
|
|
239
|
-
try{
|
|
240
|
-
packageFile=await openSafeFile(canonicalRoot,['package.json']);
|
|
241
|
-
}catch{
|
|
242
|
-
fail('SDK runtime source package.json must be a readable bounded real file.',
|
|
243
|
-
'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
|
|
244
|
-
}
|
|
245
|
-
if(!packageFile){
|
|
246
|
-
fail('SDK runtime source root must contain a real package.json.',
|
|
247
|
-
'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
|
|
248
|
-
}
|
|
249
|
-
let packageDocument;
|
|
250
|
-
try{
|
|
251
|
-
packageDocument=JSON.parse(packageFile.bytes.toString('utf8'));
|
|
252
|
-
}catch{
|
|
253
|
-
fail('SDK runtime source package.json must be valid JSON.',
|
|
254
|
-
'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
|
|
255
|
-
}
|
|
256
|
-
if(!packageDocument||Array.isArray(packageDocument)||packageDocument.name!==SDK_NAME){
|
|
257
|
-
fail(`SDK runtime source package name must be exactly ${SDK_NAME}.`,
|
|
258
|
-
'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
|
|
259
|
-
}
|
|
260
|
-
if(packageDocument.version!==SDK_VERSION){
|
|
261
|
-
fail(
|
|
262
|
-
`SDK runtime source version must exactly match the executing SDK (${SDK_VERSION}).`,
|
|
263
|
-
'ARCANE_DEV_RUNTIME_VERSION_MISMATCH'
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
186
|
const roots=[
|
|
268
187
|
{
|
|
269
188
|
path:'runtime/arcane',
|
|
@@ -299,36 +218,23 @@ async function verifySdkRuntimeSourceRoot(sourceRoot,workspaceRoot,appId,{signal
|
|
|
299
218
|
path:root.path
|
|
300
219
|
});
|
|
301
220
|
}
|
|
302
|
-
const runtime=
|
|
221
|
+
const runtime={
|
|
303
222
|
mode:'sdk-source',
|
|
304
223
|
protocol:SDK_RUNTIME_SOURCE_PROTOCOL,
|
|
305
|
-
sdkVersion:SDK_VERSION,
|
|
306
224
|
mutable:true,
|
|
307
225
|
distributionAuthority:false,
|
|
308
226
|
sourceRoot:canonicalRoot
|
|
309
|
-
}
|
|
227
|
+
};
|
|
310
228
|
await emitRuntimeSourceEvent(onEvent,{
|
|
311
229
|
type:'runtime.source.mount.ready',
|
|
312
230
|
appId,
|
|
313
231
|
canonicalRoot,
|
|
314
|
-
sdkVersion:SDK_VERSION,
|
|
315
232
|
protocol:SDK_RUNTIME_SOURCE_PROTOCOL,
|
|
316
233
|
routeCount:mappings.length
|
|
317
234
|
});
|
|
318
235
|
return {mappings,runtime};
|
|
319
236
|
}
|
|
320
237
|
|
|
321
|
-
function identityMap(identities,prefix=''){
|
|
322
|
-
const normalizedPrefix=prefix?`${prefix}/`:'';
|
|
323
|
-
const result=new Map();
|
|
324
|
-
for(const identity of identities||[]){
|
|
325
|
-
if(!identity.path.startsWith(normalizedPrefix))continue;
|
|
326
|
-
const relative=identity.path.slice(normalizedPrefix.length);
|
|
327
|
-
if(relative)result.set(inventoryKey(relative),identity);
|
|
328
|
-
}
|
|
329
|
-
return result;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
238
|
function routePrefixKey(prefix){
|
|
333
239
|
return prefix.map(segment=>segment.normalize('NFC').toLowerCase()).join('/');
|
|
334
240
|
}
|
|
@@ -355,38 +261,17 @@ function deterministicMappings(mappings){
|
|
|
355
261
|
}
|
|
356
262
|
|
|
357
263
|
function createFileWorkLimiter(){
|
|
358
|
-
|
|
359
|
-
const pending=[];
|
|
360
|
-
const release=()=>{
|
|
361
|
-
const next=pending.shift();
|
|
362
|
-
if(next)next();
|
|
363
|
-
else active-=1;
|
|
364
|
-
};
|
|
365
|
-
return async work=>{
|
|
366
|
-
if(active>=MAX_CONCURRENT_FILE_RESPONSES){
|
|
367
|
-
if(pending.length>=MAX_PENDING_FILE_RESPONSES){
|
|
368
|
-
fail('Development server file queue is full.','ARCANE_BACKPRESSURE');
|
|
369
|
-
}
|
|
370
|
-
await new Promise(resolve=>pending.push(resolve));
|
|
371
|
-
}else{
|
|
372
|
-
active+=1;
|
|
373
|
-
}
|
|
374
|
-
try{
|
|
375
|
-
return await work();
|
|
376
|
-
}finally{
|
|
377
|
-
release();
|
|
378
|
-
}
|
|
379
|
-
};
|
|
264
|
+
return async work=>work();
|
|
380
265
|
}
|
|
381
266
|
|
|
382
|
-
async function openSafeFile(root,segments
|
|
267
|
+
async function openSafeFile(root,segments){
|
|
383
268
|
const candidate=resolveInside(root,segments);
|
|
384
269
|
if(!candidate)return null;
|
|
385
270
|
let current=root;
|
|
386
271
|
for(const segment of segments){
|
|
387
272
|
current=path.join(current,segment);
|
|
388
273
|
let info;
|
|
389
|
-
try{info=await lstat(current
|
|
274
|
+
try{info=await lstat(current);}
|
|
390
275
|
catch(error){
|
|
391
276
|
if(error?.code==='ENOENT')return null;
|
|
392
277
|
throw error;
|
|
@@ -394,8 +279,10 @@ async function openSafeFile(root,segments,expectedIdentity){
|
|
|
394
279
|
if(info.isSymbolicLink())return null;
|
|
395
280
|
}
|
|
396
281
|
|
|
397
|
-
const
|
|
398
|
-
if(
|
|
282
|
+
const currentInfo=await lstat(candidate);
|
|
283
|
+
if(currentInfo.isSymbolicLink()||!currentInfo.isFile())return null;
|
|
284
|
+
const canonicalCandidate=await realpath(candidate);
|
|
285
|
+
if(!pathIsWithin(root,canonicalCandidate))return null;
|
|
399
286
|
let handle;
|
|
400
287
|
try{
|
|
401
288
|
handle=await open(candidate,READ_ONLY_NO_FOLLOW);
|
|
@@ -404,53 +291,15 @@ async function openSafeFile(root,segments,expectedIdentity){
|
|
|
404
291
|
throw error;
|
|
405
292
|
}
|
|
406
293
|
try{
|
|
407
|
-
const opened=await handle.stat(
|
|
408
|
-
if(!opened.isFile()
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
modifiedNanoseconds:String(before.mtimeNs),
|
|
414
|
-
changedNanoseconds:String(before.ctimeNs),
|
|
415
|
-
links:String(before.nlink)
|
|
416
|
-
})){
|
|
294
|
+
const opened=await handle.stat();
|
|
295
|
+
if(!opened.isFile())return null;
|
|
296
|
+
const content=await handle.readFile();
|
|
297
|
+
const servedCandidate=await realpath(candidate);
|
|
298
|
+
if(!pathIsWithin(root,servedCandidate)
|
|
299
|
+
||canonicalLocationKey(servedCandidate)!==canonicalLocationKey(canonicalCandidate)){
|
|
417
300
|
return null;
|
|
418
301
|
}
|
|
419
|
-
|
|
420
|
-
fail(
|
|
421
|
-
`Development file exceeds the ${MAX_DEVELOPMENT_FILE_BYTES}-byte serving limit.`,
|
|
422
|
-
'ARCANE_POLICY_DENIED'
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
const bytes=await handle.readFile();
|
|
426
|
-
const after=await handle.stat({bigint:true});
|
|
427
|
-
if(!identityMatches(after,{
|
|
428
|
-
device:String(opened.dev),
|
|
429
|
-
inode:String(opened.ino),
|
|
430
|
-
bytes:Number(opened.size),
|
|
431
|
-
modifiedNanoseconds:String(opened.mtimeNs),
|
|
432
|
-
changedNanoseconds:String(opened.ctimeNs),
|
|
433
|
-
links:String(opened.nlink)
|
|
434
|
-
})||bytes.length!==Number(opened.size)){
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
const canonicalCandidate=await realpath(candidate);
|
|
438
|
-
const relative=path.relative(root,canonicalCandidate);
|
|
439
|
-
if(relative.startsWith('..')||path.isAbsolute(relative)){
|
|
440
|
-
return null;
|
|
441
|
-
}
|
|
442
|
-
const currentInfo=await lstat(candidate,{bigint:true});
|
|
443
|
-
if(!identityMatches(currentInfo,{
|
|
444
|
-
device:String(opened.dev),
|
|
445
|
-
inode:String(opened.ino),
|
|
446
|
-
bytes:Number(opened.size),
|
|
447
|
-
modifiedNanoseconds:String(opened.mtimeNs),
|
|
448
|
-
changedNanoseconds:String(opened.ctimeNs),
|
|
449
|
-
links:String(opened.nlink)
|
|
450
|
-
})){
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
|
-
return {candidate:canonicalCandidate,bytes,size:bytes.length};
|
|
302
|
+
return {candidate:servedCandidate,content};
|
|
454
303
|
}catch(error){
|
|
455
304
|
if(error?.code==='ENOENT')return null;
|
|
456
305
|
throw error;
|
|
@@ -463,8 +312,7 @@ async function sendFile(response,opened,{head=false}={}){
|
|
|
463
312
|
const extension=path.extname(opened.candidate).toLowerCase();
|
|
464
313
|
response.writeHead(200,{
|
|
465
314
|
'content-type':MIME_TYPES.get(extension)||'application/octet-stream',
|
|
466
|
-
'content-length':opened.
|
|
467
|
-
...responseSecurityHeaders()
|
|
315
|
+
'content-length':opened.content.byteLength
|
|
468
316
|
});
|
|
469
317
|
await new Promise((resolve,reject)=>{
|
|
470
318
|
let settled=false;
|
|
@@ -488,7 +336,7 @@ async function sendFile(response,opened,{head=false}={}){
|
|
|
488
336
|
response.once('error',failed);
|
|
489
337
|
response.once('finish',completed);
|
|
490
338
|
response.once('close',completed);
|
|
491
|
-
response.end(head?undefined:opened.
|
|
339
|
+
response.end(head?undefined:opened.content);
|
|
492
340
|
});
|
|
493
341
|
}
|
|
494
342
|
|
|
@@ -526,7 +374,6 @@ function sharedPathAllowed(relative,route){
|
|
|
526
374
|
}
|
|
527
375
|
|
|
528
376
|
async function sourceRoutes(workspaceRoot,appId,{
|
|
529
|
-
workspaceRuntimeReceipt,
|
|
530
377
|
sdkRuntimeSourceRoot,
|
|
531
378
|
signal,
|
|
532
379
|
onEvent
|
|
@@ -570,58 +417,6 @@ async function sourceRoutes(workspaceRoot,appId,{
|
|
|
570
417
|
};
|
|
571
418
|
}
|
|
572
419
|
const runtimeRoot=path.join(resolved.workspaceRoot,'arcane');
|
|
573
|
-
const validation=await validateWorkspace({
|
|
574
|
-
workspaceRoot:resolved.workspaceRoot,
|
|
575
|
-
appId:resolved.appId,
|
|
576
|
-
allowMissingManagedImportMap:true,
|
|
577
|
-
signal
|
|
578
|
-
});
|
|
579
|
-
const sdkInstallation=validation.sdkInstallation;
|
|
580
|
-
if(!sdkInstallation
|
|
581
|
-
||typeof sdkInstallation.runtimeRoot!=='string'
|
|
582
|
-
||typeof sdkInstallation.browserRuntimeRoot!=='string'){
|
|
583
|
-
fail(
|
|
584
|
-
'Validated external workspace is missing its bound SDK installation authority.',
|
|
585
|
-
'ARCANE_WORKSPACE_INVALID'
|
|
586
|
-
);
|
|
587
|
-
}
|
|
588
|
-
let verified=workspaceRuntimeReceipt;
|
|
589
|
-
if(!verified){
|
|
590
|
-
const sdkRuntimeRoot=sdkInstallation.runtimeRoot;
|
|
591
|
-
const sdkRuntimeReceipt=await verifyRuntime({
|
|
592
|
-
runtimeRoot:sdkRuntimeRoot,
|
|
593
|
-
signal
|
|
594
|
-
});
|
|
595
|
-
const sdkBrowserRuntimeRoot=sdkInstallation.browserRuntimeRoot;
|
|
596
|
-
const sdkBrowserRuntimeReceipt=await verifySdkBrowserRuntime({
|
|
597
|
-
browserRuntimeRoot:sdkBrowserRuntimeRoot,
|
|
598
|
-
signal
|
|
599
|
-
});
|
|
600
|
-
verified=await verifyWorkspaceRuntime({
|
|
601
|
-
workspaceRoot:resolved.workspaceRoot,
|
|
602
|
-
runtimeRoot:sdkRuntimeRoot,
|
|
603
|
-
runtimeReceipt:sdkRuntimeReceipt,
|
|
604
|
-
browserRuntimeRoot:sdkBrowserRuntimeRoot,
|
|
605
|
-
sdkBrowserRuntimeReceipt,
|
|
606
|
-
signal
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
verified=await authenticateWorkspaceRuntimeReceipt(verified,{
|
|
610
|
-
workspaceRoot:resolved.workspaceRoot,
|
|
611
|
-
signal
|
|
612
|
-
});
|
|
613
|
-
if(typeof verified.sourceRuntimeLocation!=='string'
|
|
614
|
-
||typeof verified.sourceBrowserRuntimeLocation!=='string'
|
|
615
|
-
||canonicalLocationKey(verified.sourceRuntimeLocation)
|
|
616
|
-
!==canonicalLocationKey(sdkInstallation.runtimeRoot)
|
|
617
|
-
||canonicalLocationKey(verified.sourceBrowserRuntimeLocation)
|
|
618
|
-
!==canonicalLocationKey(sdkInstallation.browserRuntimeRoot)){
|
|
619
|
-
fail(
|
|
620
|
-
'Workspace runtime receipt sources do not match the bound SDK installation authority.',
|
|
621
|
-
'ARCANE_INTEGRITY_FAILED'
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
const arcaneIdentities=identityMap(verified.identities);
|
|
625
420
|
return {
|
|
626
421
|
workspaceRoot:resolved.workspaceRoot,
|
|
627
422
|
workspaceMode:'external',
|
|
@@ -632,61 +427,27 @@ async function sourceRoutes(workspaceRoot,appId,{
|
|
|
632
427
|
{
|
|
633
428
|
prefix:['arcane'],
|
|
634
429
|
root:runtimeRoot,
|
|
635
|
-
|
|
636
|
-
read:relative=>readVerifiedWorkspaceRuntimeFile(verified,{
|
|
637
|
-
workspaceRoot:resolved.workspaceRoot,
|
|
638
|
-
relativePath:relative.join('/'),
|
|
639
|
-
signal
|
|
640
|
-
}),
|
|
641
|
-
allow:relative=>arcaneIdentities.has(inventoryKey(relative.join('/')))
|
|
430
|
+
allow:sdkInstalledArcanePathAllowed
|
|
642
431
|
}
|
|
643
432
|
]
|
|
644
433
|
};
|
|
645
434
|
}
|
|
646
435
|
|
|
647
|
-
async function packagedRoutes(releaseRoot
|
|
436
|
+
async function packagedRoutes(releaseRoot){
|
|
648
437
|
if(typeof releaseRoot!=='string'||!releaseRoot.trim())fail('releaseRoot is required in packaged mode.','ARCANE_USAGE');
|
|
649
|
-
if(!releaseReceipt)fail('An authenticated release receipt is required in packaged mode.','ARCANE_POLICY_DENIED');
|
|
650
438
|
const requested=path.resolve(releaseRoot);
|
|
651
|
-
await
|
|
652
|
-
const info=await lstat(requested);
|
|
653
|
-
if(info.isSymbolicLink()||!info.isDirectory())fail('Packaged release root must be a real directory.');
|
|
654
|
-
const canonical=await realpath(requested);
|
|
655
|
-
const identities=identityMap(releaseReceipt.identities);
|
|
439
|
+
const canonical=await canonicalRealDirectory(requested,'Packaged release root');
|
|
656
440
|
return {
|
|
657
441
|
workspaceRoot:null,
|
|
658
442
|
appId:null,
|
|
659
443
|
startPath:'/index.html',
|
|
660
444
|
mappings:[{
|
|
661
445
|
prefix:[],
|
|
662
|
-
root:canonical
|
|
663
|
-
identities,
|
|
664
|
-
read:relative=>readVerifiedAppReleaseFile(releaseReceipt,{
|
|
665
|
-
releaseRoot:canonical,
|
|
666
|
-
relativePath:relative.join('/'),
|
|
667
|
-
signal
|
|
668
|
-
}),
|
|
669
|
-
allow:relative=>identities.has(inventoryKey(relative.join('/')))
|
|
446
|
+
root:canonical
|
|
670
447
|
}]
|
|
671
448
|
};
|
|
672
449
|
}
|
|
673
450
|
|
|
674
|
-
function tokenMatches(value,expected){
|
|
675
|
-
if(typeof value!=='string')return false;
|
|
676
|
-
const received=Buffer.from(value,'utf8');
|
|
677
|
-
const wanted=Buffer.from(expected,'utf8');
|
|
678
|
-
return received.length===wanted.length&&timingSafeEqual(received,wanted);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function hasSessionCookie(request,cookieName,sessionToken){
|
|
682
|
-
const matches=String(request.headers.cookie||'')
|
|
683
|
-
.split(';')
|
|
684
|
-
.map(part=>part.trim())
|
|
685
|
-
.filter(part=>part.startsWith(`${cookieName}=`))
|
|
686
|
-
.map(part=>part.slice(cookieName.length+1));
|
|
687
|
-
return matches.length===1&&tokenMatches(matches[0],sessionToken);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
451
|
function listen(server,{host,port,signal}){
|
|
691
452
|
return new Promise((resolve,reject)=>{
|
|
692
453
|
const cleanup=()=>{
|
|
@@ -718,9 +479,7 @@ async function startOwnedDevServer({
|
|
|
718
479
|
host='127.0.0.1',
|
|
719
480
|
port=0,
|
|
720
481
|
signal,
|
|
721
|
-
|
|
722
|
-
sdkRuntimeSourceRoot,
|
|
723
|
-
releaseReceipt
|
|
482
|
+
sdkRuntimeSourceRoot
|
|
724
483
|
}={},events,releaseSignal){
|
|
725
484
|
throwIfAborted(signal);
|
|
726
485
|
if(mode!=='source'&&mode!=='packaged')fail(`Unsupported server mode: ${String(mode)}.`,'ARCANE_USAGE');
|
|
@@ -744,65 +503,31 @@ async function startOwnedDevServer({
|
|
|
744
503
|
});
|
|
745
504
|
const routeSet=mode==='source'
|
|
746
505
|
?await sourceRoutes(workspaceRoot,appId,{
|
|
747
|
-
workspaceRuntimeReceipt,
|
|
748
506
|
sdkRuntimeSourceRoot,
|
|
749
507
|
signal,
|
|
750
508
|
onEvent:event=>events.send(event)
|
|
751
509
|
})
|
|
752
|
-
:await packagedRoutes(releaseRoot
|
|
510
|
+
:await packagedRoutes(releaseRoot);
|
|
753
511
|
const mappings=deterministicMappings(routeSet.mappings);
|
|
754
512
|
for(const mapping of mappings){
|
|
755
513
|
const info=await lstat(mapping.root);
|
|
756
514
|
if(info.isSymbolicLink()||!info.isDirectory())fail(`Server route root must be a real directory: ${mapping.root}.`);
|
|
757
515
|
mapping.root=await realpath(mapping.root);
|
|
758
516
|
}
|
|
759
|
-
const sessionToken=randomBytes(32).toString('hex');
|
|
760
|
-
const sessionCookieName=`${SESSION_COOKIE}-${randomBytes(8).toString('hex')}`;
|
|
761
|
-
let expectedAuthority=null;
|
|
762
517
|
const requestTasks=new Set();
|
|
763
518
|
const runFileWork=createFileWorkLimiter();
|
|
764
519
|
const server=http.createServer((request,response)=>{
|
|
765
520
|
let task;
|
|
766
521
|
task=(async()=>{
|
|
767
|
-
if(!expectedAuthority){deny(response,503,'Server is starting.');return;}
|
|
768
|
-
if(request.headers.host!==expectedAuthority){
|
|
769
|
-
deny(response,421,'Misdirected request.');
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
522
|
if(request.method!=='GET'&&request.method!=='HEAD'){
|
|
773
523
|
deny(response,405,'Method not allowed.');
|
|
774
524
|
return;
|
|
775
525
|
}
|
|
776
526
|
const target=parseRequestTarget(request.url);
|
|
777
527
|
if(!target){deny(response,400,'Invalid request path.');return;}
|
|
778
|
-
const queryKeys=[...target.searchParams.keys()];
|
|
779
|
-
const isBootstrap=request.method==='GET'
|
|
780
|
-
&&target.path===routeSet.startPath
|
|
781
|
-
&&queryKeys.length===1
|
|
782
|
-
&&queryKeys[0]==='arcane_session'
|
|
783
|
-
&&tokenMatches(target.searchParams.get('arcane_session'),sessionToken);
|
|
784
|
-
if(isBootstrap){
|
|
785
|
-
response.writeHead(302,{
|
|
786
|
-
location:routeSet.startPath,
|
|
787
|
-
'set-cookie':`${sessionCookieName}=${sessionToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
788
|
-
...responseSecurityHeaders()
|
|
789
|
-
});
|
|
790
|
-
response.end();
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
if(!hasSessionCookie(request,sessionCookieName,sessionToken)){
|
|
794
|
-
deny(response,403,'Development server session required.');
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
528
|
const {segments}=target;
|
|
798
|
-
if(mode==='packaged'&&segments.some(segment=>
|
|
799
|
-
segment.toLowerCase()===RELEASE_MANIFEST_NAME.toLowerCase()
|
|
800
|
-
)){
|
|
801
|
-
deny(response,404,'Not found.');
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
529
|
if(segments.length===0){
|
|
805
|
-
response.writeHead(302,{location:routeSet.startPath
|
|
530
|
+
response.writeHead(302,{location:routeSet.startPath});
|
|
806
531
|
response.end();
|
|
807
532
|
return;
|
|
808
533
|
}
|
|
@@ -814,23 +539,14 @@ async function startOwnedDevServer({
|
|
|
814
539
|
if(relative.length===0){deny(response,404,'Not found.');return;}
|
|
815
540
|
if(mapping.allow&&!mapping.allow(relative)){deny(response,404,'Not found.');return;}
|
|
816
541
|
await runFileWork(async()=>{
|
|
817
|
-
const
|
|
818
|
-
const opened=mapping.read
|
|
819
|
-
?{
|
|
820
|
-
candidate:path.join(mapping.root,...relative),
|
|
821
|
-
bytes:await mapping.read(relative)
|
|
822
|
-
}
|
|
823
|
-
:await openSafeFile(mapping.root,relative,expectedIdentity);
|
|
542
|
+
const opened=await openSafeFile(mapping.root,relative);
|
|
824
543
|
if(!opened){deny(response,404,'Not found.');return;}
|
|
825
|
-
opened.size=opened.bytes.length;
|
|
826
544
|
await sendFile(response,opened,{head:request.method==='HEAD'});
|
|
827
545
|
});
|
|
828
546
|
})().catch(async error=>{
|
|
829
547
|
await events.enqueue({type:'server.request.failed',message:error.message});
|
|
830
548
|
if(!response.headersSent){
|
|
831
|
-
const status=error?.code==='
|
|
832
|
-
:error?.code==='ARCANE_BACKPRESSURE'?503
|
|
833
|
-
:500;
|
|
549
|
+
const status=error?.code==='ARCANE_BACKPRESSURE'?503:500;
|
|
834
550
|
deny(response,status,'Internal server error.');
|
|
835
551
|
}
|
|
836
552
|
else response.destroy(error);
|
|
@@ -847,10 +563,9 @@ async function startOwnedDevServer({
|
|
|
847
563
|
}
|
|
848
564
|
const visibleHost=address.family==='IPv6'?`[${address.address}]`:address.address;
|
|
849
565
|
const endpoint=new URL(`http://${visibleHost}:${address.port}`);
|
|
850
|
-
expectedAuthority=endpoint.host;
|
|
851
566
|
const origin=endpoint.origin;
|
|
852
567
|
const cleanUrl=`${origin}${routeSet.startPath}`;
|
|
853
|
-
const url
|
|
568
|
+
const url=cleanUrl;
|
|
854
569
|
let closeInitiated=false;
|
|
855
570
|
let lifecycleSettlementStarted=false;
|
|
856
571
|
let operationalError=null;
|