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.
- 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 +887 -909
- package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
- 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 -146
- package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
- 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 +112 -68
- 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 +1050 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
- 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 +18 -151
- 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 +30 -324
- 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 +40 -62
- 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 +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- 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 +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +112 -779
- 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 -187
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1252
- 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 -677
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2960
- package/docs/reference/sdk-api.md +0 -6694
- 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
|
]);
|
|
@@ -57,27 +39,6 @@ const SDK_RUNTIME_SOURCE_PRIVATE_MANIFESTS=new Set([
|
|
|
57
39
|
'arcane_app_release.json','arcane_runtime_release.json','arcane_sdk_browser_release.json'
|
|
58
40
|
]);
|
|
59
41
|
|
|
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
42
|
function fail(message,code='ARCANE_OPERATION_FAILED'){
|
|
82
43
|
const error=new Error(message);
|
|
83
44
|
error.code=code;
|
|
@@ -91,20 +52,9 @@ function throwIfAborted(signal){
|
|
|
91
52
|
throw error;
|
|
92
53
|
}
|
|
93
54
|
|
|
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
55
|
function deny(response,status,message){
|
|
105
56
|
response.writeHead(status,{
|
|
106
|
-
'content-type':'text/plain; charset=utf-8'
|
|
107
|
-
...responseSecurityHeaders("default-src 'none'; frame-ancestors 'none'; base-uri 'none'")
|
|
57
|
+
'content-type':'text/plain; charset=utf-8'
|
|
108
58
|
});
|
|
109
59
|
response.end(`${message}\n`);
|
|
110
60
|
}
|
|
@@ -136,17 +86,6 @@ function resolveInside(root,segments){
|
|
|
136
86
|
return candidate;
|
|
137
87
|
}
|
|
138
88
|
|
|
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
89
|
function inventoryKey(value){
|
|
151
90
|
return process.platform==='win32'?value.toLowerCase():value;
|
|
152
91
|
}
|
|
@@ -235,35 +174,6 @@ async function verifySdkRuntimeSourceRoot(sourceRoot,workspaceRoot,appId,{signal
|
|
|
235
174
|
'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
|
|
236
175
|
}
|
|
237
176
|
|
|
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
177
|
const roots=[
|
|
268
178
|
{
|
|
269
179
|
path:'runtime/arcane',
|
|
@@ -299,36 +209,23 @@ async function verifySdkRuntimeSourceRoot(sourceRoot,workspaceRoot,appId,{signal
|
|
|
299
209
|
path:root.path
|
|
300
210
|
});
|
|
301
211
|
}
|
|
302
|
-
const runtime=
|
|
212
|
+
const runtime={
|
|
303
213
|
mode:'sdk-source',
|
|
304
214
|
protocol:SDK_RUNTIME_SOURCE_PROTOCOL,
|
|
305
|
-
sdkVersion:SDK_VERSION,
|
|
306
215
|
mutable:true,
|
|
307
216
|
distributionAuthority:false,
|
|
308
217
|
sourceRoot:canonicalRoot
|
|
309
|
-
}
|
|
218
|
+
};
|
|
310
219
|
await emitRuntimeSourceEvent(onEvent,{
|
|
311
220
|
type:'runtime.source.mount.ready',
|
|
312
221
|
appId,
|
|
313
222
|
canonicalRoot,
|
|
314
|
-
sdkVersion:SDK_VERSION,
|
|
315
223
|
protocol:SDK_RUNTIME_SOURCE_PROTOCOL,
|
|
316
224
|
routeCount:mappings.length
|
|
317
225
|
});
|
|
318
226
|
return {mappings,runtime};
|
|
319
227
|
}
|
|
320
228
|
|
|
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
229
|
function routePrefixKey(prefix){
|
|
333
230
|
return prefix.map(segment=>segment.normalize('NFC').toLowerCase()).join('/');
|
|
334
231
|
}
|
|
@@ -355,38 +252,17 @@ function deterministicMappings(mappings){
|
|
|
355
252
|
}
|
|
356
253
|
|
|
357
254
|
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
|
-
};
|
|
255
|
+
return async work=>work();
|
|
380
256
|
}
|
|
381
257
|
|
|
382
|
-
async function openSafeFile(root,segments
|
|
258
|
+
async function openSafeFile(root,segments){
|
|
383
259
|
const candidate=resolveInside(root,segments);
|
|
384
260
|
if(!candidate)return null;
|
|
385
261
|
let current=root;
|
|
386
262
|
for(const segment of segments){
|
|
387
263
|
current=path.join(current,segment);
|
|
388
264
|
let info;
|
|
389
|
-
try{info=await lstat(current
|
|
265
|
+
try{info=await lstat(current);}
|
|
390
266
|
catch(error){
|
|
391
267
|
if(error?.code==='ENOENT')return null;
|
|
392
268
|
throw error;
|
|
@@ -394,8 +270,10 @@ async function openSafeFile(root,segments,expectedIdentity){
|
|
|
394
270
|
if(info.isSymbolicLink())return null;
|
|
395
271
|
}
|
|
396
272
|
|
|
397
|
-
const
|
|
398
|
-
if(
|
|
273
|
+
const currentInfo=await lstat(candidate);
|
|
274
|
+
if(currentInfo.isSymbolicLink()||!currentInfo.isFile())return null;
|
|
275
|
+
const canonicalCandidate=await realpath(candidate);
|
|
276
|
+
if(!pathIsWithin(root,canonicalCandidate))return null;
|
|
399
277
|
let handle;
|
|
400
278
|
try{
|
|
401
279
|
handle=await open(candidate,READ_ONLY_NO_FOLLOW);
|
|
@@ -404,53 +282,15 @@ async function openSafeFile(root,segments,expectedIdentity){
|
|
|
404
282
|
throw error;
|
|
405
283
|
}
|
|
406
284
|
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
|
-
})){
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
if(Number(opened.size)>MAX_DEVELOPMENT_FILE_BYTES){
|
|
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)){
|
|
285
|
+
const opened=await handle.stat();
|
|
286
|
+
if(!opened.isFile())return null;
|
|
287
|
+
const content=await handle.readFile();
|
|
288
|
+
const servedCandidate=await realpath(candidate);
|
|
289
|
+
if(!pathIsWithin(root,servedCandidate)
|
|
290
|
+
||canonicalLocationKey(servedCandidate)!==canonicalLocationKey(canonicalCandidate)){
|
|
440
291
|
return null;
|
|
441
292
|
}
|
|
442
|
-
|
|
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};
|
|
293
|
+
return {candidate:servedCandidate,content};
|
|
454
294
|
}catch(error){
|
|
455
295
|
if(error?.code==='ENOENT')return null;
|
|
456
296
|
throw error;
|
|
@@ -463,8 +303,7 @@ async function sendFile(response,opened,{head=false}={}){
|
|
|
463
303
|
const extension=path.extname(opened.candidate).toLowerCase();
|
|
464
304
|
response.writeHead(200,{
|
|
465
305
|
'content-type':MIME_TYPES.get(extension)||'application/octet-stream',
|
|
466
|
-
'content-length':opened.
|
|
467
|
-
...responseSecurityHeaders()
|
|
306
|
+
'content-length':opened.content.byteLength
|
|
468
307
|
});
|
|
469
308
|
await new Promise((resolve,reject)=>{
|
|
470
309
|
let settled=false;
|
|
@@ -488,7 +327,7 @@ async function sendFile(response,opened,{head=false}={}){
|
|
|
488
327
|
response.once('error',failed);
|
|
489
328
|
response.once('finish',completed);
|
|
490
329
|
response.once('close',completed);
|
|
491
|
-
response.end(head?undefined:opened.
|
|
330
|
+
response.end(head?undefined:opened.content);
|
|
492
331
|
});
|
|
493
332
|
}
|
|
494
333
|
|
|
@@ -526,7 +365,6 @@ function sharedPathAllowed(relative,route){
|
|
|
526
365
|
}
|
|
527
366
|
|
|
528
367
|
async function sourceRoutes(workspaceRoot,appId,{
|
|
529
|
-
workspaceRuntimeReceipt,
|
|
530
368
|
sdkRuntimeSourceRoot,
|
|
531
369
|
signal,
|
|
532
370
|
onEvent
|
|
@@ -570,58 +408,6 @@ async function sourceRoutes(workspaceRoot,appId,{
|
|
|
570
408
|
};
|
|
571
409
|
}
|
|
572
410
|
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
411
|
return {
|
|
626
412
|
workspaceRoot:resolved.workspaceRoot,
|
|
627
413
|
workspaceMode:'external',
|
|
@@ -632,61 +418,27 @@ async function sourceRoutes(workspaceRoot,appId,{
|
|
|
632
418
|
{
|
|
633
419
|
prefix:['arcane'],
|
|
634
420
|
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('/')))
|
|
421
|
+
allow:sdkArcaneSourcePathAllowed
|
|
642
422
|
}
|
|
643
423
|
]
|
|
644
424
|
};
|
|
645
425
|
}
|
|
646
426
|
|
|
647
|
-
async function packagedRoutes(releaseRoot
|
|
427
|
+
async function packagedRoutes(releaseRoot){
|
|
648
428
|
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
429
|
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);
|
|
430
|
+
const canonical=await canonicalRealDirectory(requested,'Packaged release root');
|
|
656
431
|
return {
|
|
657
432
|
workspaceRoot:null,
|
|
658
433
|
appId:null,
|
|
659
434
|
startPath:'/index.html',
|
|
660
435
|
mappings:[{
|
|
661
436
|
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('/')))
|
|
437
|
+
root:canonical
|
|
670
438
|
}]
|
|
671
439
|
};
|
|
672
440
|
}
|
|
673
441
|
|
|
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
442
|
function listen(server,{host,port,signal}){
|
|
691
443
|
return new Promise((resolve,reject)=>{
|
|
692
444
|
const cleanup=()=>{
|
|
@@ -718,9 +470,7 @@ async function startOwnedDevServer({
|
|
|
718
470
|
host='127.0.0.1',
|
|
719
471
|
port=0,
|
|
720
472
|
signal,
|
|
721
|
-
|
|
722
|
-
sdkRuntimeSourceRoot,
|
|
723
|
-
releaseReceipt
|
|
473
|
+
sdkRuntimeSourceRoot
|
|
724
474
|
}={},events,releaseSignal){
|
|
725
475
|
throwIfAborted(signal);
|
|
726
476
|
if(mode!=='source'&&mode!=='packaged')fail(`Unsupported server mode: ${String(mode)}.`,'ARCANE_USAGE');
|
|
@@ -744,65 +494,31 @@ async function startOwnedDevServer({
|
|
|
744
494
|
});
|
|
745
495
|
const routeSet=mode==='source'
|
|
746
496
|
?await sourceRoutes(workspaceRoot,appId,{
|
|
747
|
-
workspaceRuntimeReceipt,
|
|
748
497
|
sdkRuntimeSourceRoot,
|
|
749
498
|
signal,
|
|
750
499
|
onEvent:event=>events.send(event)
|
|
751
500
|
})
|
|
752
|
-
:await packagedRoutes(releaseRoot
|
|
501
|
+
:await packagedRoutes(releaseRoot);
|
|
753
502
|
const mappings=deterministicMappings(routeSet.mappings);
|
|
754
503
|
for(const mapping of mappings){
|
|
755
504
|
const info=await lstat(mapping.root);
|
|
756
505
|
if(info.isSymbolicLink()||!info.isDirectory())fail(`Server route root must be a real directory: ${mapping.root}.`);
|
|
757
506
|
mapping.root=await realpath(mapping.root);
|
|
758
507
|
}
|
|
759
|
-
const sessionToken=randomBytes(32).toString('hex');
|
|
760
|
-
const sessionCookieName=`${SESSION_COOKIE}-${randomBytes(8).toString('hex')}`;
|
|
761
|
-
let expectedAuthority=null;
|
|
762
508
|
const requestTasks=new Set();
|
|
763
509
|
const runFileWork=createFileWorkLimiter();
|
|
764
510
|
const server=http.createServer((request,response)=>{
|
|
765
511
|
let task;
|
|
766
512
|
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
513
|
if(request.method!=='GET'&&request.method!=='HEAD'){
|
|
773
514
|
deny(response,405,'Method not allowed.');
|
|
774
515
|
return;
|
|
775
516
|
}
|
|
776
517
|
const target=parseRequestTarget(request.url);
|
|
777
518
|
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
519
|
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
520
|
if(segments.length===0){
|
|
805
|
-
response.writeHead(302,{location:routeSet.startPath
|
|
521
|
+
response.writeHead(302,{location:routeSet.startPath});
|
|
806
522
|
response.end();
|
|
807
523
|
return;
|
|
808
524
|
}
|
|
@@ -814,23 +530,14 @@ async function startOwnedDevServer({
|
|
|
814
530
|
if(relative.length===0){deny(response,404,'Not found.');return;}
|
|
815
531
|
if(mapping.allow&&!mapping.allow(relative)){deny(response,404,'Not found.');return;}
|
|
816
532
|
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);
|
|
533
|
+
const opened=await openSafeFile(mapping.root,relative);
|
|
824
534
|
if(!opened){deny(response,404,'Not found.');return;}
|
|
825
|
-
opened.size=opened.bytes.length;
|
|
826
535
|
await sendFile(response,opened,{head:request.method==='HEAD'});
|
|
827
536
|
});
|
|
828
537
|
})().catch(async error=>{
|
|
829
538
|
await events.enqueue({type:'server.request.failed',message:error.message});
|
|
830
539
|
if(!response.headersSent){
|
|
831
|
-
const status=error?.code==='
|
|
832
|
-
:error?.code==='ARCANE_BACKPRESSURE'?503
|
|
833
|
-
:500;
|
|
540
|
+
const status=error?.code==='ARCANE_BACKPRESSURE'?503:500;
|
|
834
541
|
deny(response,status,'Internal server error.');
|
|
835
542
|
}
|
|
836
543
|
else response.destroy(error);
|
|
@@ -847,10 +554,9 @@ async function startOwnedDevServer({
|
|
|
847
554
|
}
|
|
848
555
|
const visibleHost=address.family==='IPv6'?`[${address.address}]`:address.address;
|
|
849
556
|
const endpoint=new URL(`http://${visibleHost}:${address.port}`);
|
|
850
|
-
expectedAuthority=endpoint.host;
|
|
851
557
|
const origin=endpoint.origin;
|
|
852
558
|
const cleanUrl=`${origin}${routeSet.startPath}`;
|
|
853
|
-
const url
|
|
559
|
+
const url=cleanUrl;
|
|
854
560
|
let closeInitiated=false;
|
|
855
561
|
let lifecycleSettlementStarted=false;
|
|
856
562
|
let operationalError=null;
|