arcane-os 0.3.1 → 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 +14 -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 +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 +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 +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 +27 -67
- 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 +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/packager/core.mjs
CHANGED
|
@@ -1,220 +1,68 @@
|
|
|
1
|
-
import {createHash,randomBytes} from 'node:crypto';
|
|
2
|
-
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
1
|
import {
|
|
2
|
+
copyFile,
|
|
4
3
|
lstat,
|
|
5
4
|
mkdir,
|
|
6
|
-
open,
|
|
7
5
|
readFile,
|
|
8
6
|
readdir,
|
|
9
7
|
realpath,
|
|
10
8
|
rename,
|
|
11
9
|
rm,
|
|
12
|
-
stat,
|
|
13
10
|
writeFile
|
|
14
11
|
} from 'node:fs/promises';
|
|
15
12
|
import path from 'node:path';
|
|
16
13
|
import {pathToFileURL} from 'node:url';
|
|
17
|
-
import {setTimeout as delay} from 'node:timers/promises';
|
|
18
|
-
import {isDeepStrictEqual} from 'node:util';
|
|
19
14
|
import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
|
|
20
|
-
import {
|
|
21
|
-
import {authenticateRuntimeReceipt,verifyRuntime} from '../runtime.mjs';
|
|
22
|
-
import {
|
|
23
|
-
authenticateSdkBrowserRuntimeReceipt,
|
|
24
|
-
verifySdkBrowserRuntime
|
|
25
|
-
} from '../sdk-browser-runtime.mjs';
|
|
26
|
-
import {
|
|
27
|
-
authenticateWorkspaceRuntimeReceipt,
|
|
28
|
-
verifyWorkspaceRuntime
|
|
29
|
-
} from '../workspace-runtime.mjs';
|
|
15
|
+
import {inspectImportMapHtml} from '../import-map.mjs';
|
|
30
16
|
|
|
31
17
|
export const ROOT_CONFIG_NAME='arcane-packager.json';
|
|
32
18
|
export const APP_CONFIG_NAME='arcane-package.json';
|
|
33
19
|
export const RELEASE_MANIFEST_NAME='ARCANE_APP_RELEASE.json';
|
|
34
20
|
export const PACKAGER_VERSION='arcane-app-packager-v1';
|
|
35
21
|
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const RUNTIME_PROJECTION_ERROR='ARCANE_RUNTIME_PROJECTION_INVALID';
|
|
39
|
-
const GENERATED_PACKAGE_ROOT_PATH_KEYS=new Set([
|
|
40
|
-
RELEASE_MANIFEST_NAME,
|
|
41
|
-
RUNTIME_AUTHORITIES_NAME,
|
|
42
|
-
RUNTIME_PROJECTION_NAME,
|
|
43
|
-
'index.html'
|
|
44
|
-
].map(pathKey));
|
|
45
|
-
|
|
46
|
-
const RENAME_RETRY_CODES=new Set(['EACCES','EBUSY','EPERM']);
|
|
47
|
-
const RENAME_RETRY_LIMIT=20;
|
|
48
|
-
const RENAME_RETRY_DELAY_MS=250;
|
|
49
|
-
const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
50
|
-
const MAX_VERIFIED_APP_FILE_BYTES=64*1024*1024;
|
|
51
|
-
const MAX_SHARED_SNAPSHOT_FILE_COUNT=10000;
|
|
52
|
-
const MAX_SHARED_SNAPSHOT_FILE_BYTES=64*1024*1024;
|
|
53
|
-
const MAX_SHARED_SNAPSHOT_TOTAL_BYTES=64*1024*1024;
|
|
54
|
-
const APP_DESCRIPTOR_NAME='arcane-app.json';
|
|
55
|
-
const LEGACY_APP_REGISTRY_PATH=path.join(
|
|
56
|
-
'machine_bundles',
|
|
57
|
-
'arcane-os-machine-bundle',
|
|
58
|
-
'arcane-apps.json'
|
|
59
|
-
);
|
|
60
|
-
const issuedAppReleaseReceipts=new WeakMap();
|
|
61
|
-
const issuedSharedPayloadSnapshots=new WeakMap();
|
|
62
|
-
let appDescriptorContractsPromise;
|
|
63
|
-
|
|
64
|
-
function fileIdentity(info){
|
|
65
|
-
return Object.freeze({
|
|
66
|
-
device:String(info.dev),
|
|
67
|
-
inode:String(info.ino),
|
|
68
|
-
bytes:Number(info.size),
|
|
69
|
-
modifiedNanoseconds:String(info.mtimeNs),
|
|
70
|
-
changedNanoseconds:String(info.ctimeNs),
|
|
71
|
-
links:String(info.nlink)
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function identityMatches(info,identity){
|
|
76
|
-
return String(info.dev)===identity.device
|
|
77
|
-
&&String(info.ino)===identity.inode
|
|
78
|
-
&&Number(info.size)===identity.bytes
|
|
79
|
-
&&String(info.mtimeNs)===identity.modifiedNanoseconds
|
|
80
|
-
&&String(info.ctimeNs)===identity.changedNanoseconds
|
|
81
|
-
&&String(info.nlink)===identity.links;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function openStableRegularFile(filePath,label,expectedIdentity){
|
|
85
|
-
const before=await lstat(filePath,{bigint:true});
|
|
86
|
-
if(before.isSymbolicLink()||!before.isFile()){
|
|
87
|
-
fail(`${label} must be a regular file, not a link or special entry.`);
|
|
88
|
-
}
|
|
89
|
-
if(expectedIdentity&&!identityMatches(before,expectedIdentity)){
|
|
90
|
-
fail(`${label} changed after its package inventory was selected.`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let handle;
|
|
94
|
-
try{
|
|
95
|
-
handle=await open(filePath,READ_ONLY_NO_FOLLOW);
|
|
96
|
-
}catch(error){
|
|
97
|
-
if(error?.code==='ELOOP')fail(`${label} became a symbolic link.`);
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
100
|
-
try{
|
|
101
|
-
const opened=await handle.stat({bigint:true});
|
|
102
|
-
if(!opened.isFile()||!identityMatches(opened,fileIdentity(before))){
|
|
103
|
-
fail(`${label} changed while it was being opened.`);
|
|
104
|
-
}
|
|
105
|
-
return {handle,identity:fileIdentity(opened)};
|
|
106
|
-
}catch(error){
|
|
107
|
-
await handle.close().catch(()=>{});
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function readStableBytes(filePath,label,expectedIdentity){
|
|
113
|
-
const opened=await openStableRegularFile(filePath,label,expectedIdentity);
|
|
114
|
-
try{
|
|
115
|
-
const bytes=await opened.handle.readFile();
|
|
116
|
-
const after=await opened.handle.stat({bigint:true});
|
|
117
|
-
if(!identityMatches(after,opened.identity)){
|
|
118
|
-
fail(`${label} changed while it was being read.`);
|
|
119
|
-
}
|
|
120
|
-
return {bytes,identity:opened.identity};
|
|
121
|
-
}finally{
|
|
122
|
-
await opened.handle.close();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function renamePackageDirectory(source,destination){
|
|
127
|
-
for(let attempt=0;;attempt++){
|
|
128
|
-
try{
|
|
129
|
-
await rename(source,destination);
|
|
130
|
-
return;
|
|
131
|
-
}catch(error){
|
|
132
|
-
if(!RENAME_RETRY_CODES.has(error?.code)||attempt>=RENAME_RETRY_LIMIT){
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
await delay(RENAME_RETRY_DELAY_MS);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const APP_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
142
|
-
const SAFE_SHARED_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
22
|
+
const APP_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
|
|
23
|
+
const SAFE_SHARED_ID_PATTERN=APP_ID_PATTERN;
|
|
143
24
|
const WINDOWS_RESERVED_NAME=
|
|
144
25
|
/^(?:con|prn|aux|nul|clock\$|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\..*)?$/iu;
|
|
145
|
-
const FORBIDDEN_SEGMENTS=new Set([
|
|
146
|
-
'.agents',
|
|
147
|
-
'.codex',
|
|
148
|
-
'.git',
|
|
149
|
-
'dist',
|
|
150
|
-
'local'
|
|
151
|
-
]);
|
|
152
|
-
const TEXT_CONTROL_PATTERN=/[\x00-\x1f\x7f]/;
|
|
153
26
|
const WINDOWS_UNSAFE_FILENAME_CHARACTER_PATTERN=/[<>"|?*]/u;
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
const MAX_LOCAL_AI_MODELS=64;
|
|
158
|
-
const MAX_MODEL_DEFINITION_BYTES=512*1024;
|
|
159
|
-
const SHA256_PATTERN=/^[a-f0-9]{64}$/u;
|
|
27
|
+
const TEXT_CONTROL_PATTERN=/[\x00-\x1f\x7f]/u;
|
|
28
|
+
const FORBIDDEN_SEGMENTS=new Set(['.agents','.codex','.git','dist','local']);
|
|
29
|
+
const APP_DESCRIPTOR_NAME='arcane-app.json';
|
|
160
30
|
|
|
161
|
-
function fail(message,code){
|
|
31
|
+
function fail(message,code='ARCANE_PACKAGE_INVALID'){
|
|
162
32
|
const error=new Error(message);
|
|
163
|
-
|
|
33
|
+
error.code=code;
|
|
164
34
|
throw error;
|
|
165
35
|
}
|
|
166
36
|
|
|
167
37
|
function throwIfAborted(signal){
|
|
168
|
-
if(!signal?.aborted)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const error=signal.reason instanceof Error
|
|
173
|
-
?signal.reason
|
|
174
|
-
:new Error('Arcane package operation cancelled.');
|
|
175
|
-
if(!error.code){
|
|
176
|
-
error.code='ARCANE_CANCELLED';
|
|
177
|
-
}
|
|
38
|
+
if(!signal?.aborted)return;
|
|
39
|
+
const error=signal.reason instanceof Error?signal.reason:new Error('Arcane package operation cancelled.');
|
|
40
|
+
error.code=error.code||'ARCANE_CANCELLED';
|
|
178
41
|
throw error;
|
|
179
42
|
}
|
|
180
43
|
|
|
181
|
-
async function
|
|
182
|
-
if(typeof onEvent==='function')
|
|
183
|
-
await onEvent(event);
|
|
184
|
-
}
|
|
44
|
+
async function emit(onEvent,event){
|
|
45
|
+
if(typeof onEvent==='function')await onEvent(event);
|
|
185
46
|
}
|
|
186
47
|
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
48
|
+
function compareText(left,right){
|
|
49
|
+
const a=String(left);
|
|
50
|
+
const b=String(right);
|
|
51
|
+
return a<b?-1:a>b?1:0;
|
|
191
52
|
}
|
|
192
53
|
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
return Object.freeze(value.map(item=>immutableJsonCopy(item)));
|
|
196
|
-
}
|
|
197
|
-
if(isPlainObject(value)){
|
|
198
|
-
return Object.freeze(Object.fromEntries(
|
|
199
|
-
Object.entries(value).map(([key,item])=>[key,immutableJsonCopy(item)])
|
|
200
|
-
));
|
|
201
|
-
}
|
|
202
|
-
return value;
|
|
54
|
+
function isPlainObject(value){
|
|
55
|
+
return value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
203
56
|
}
|
|
204
57
|
|
|
205
|
-
function
|
|
206
|
-
return
|
|
58
|
+
function copyJson(value){
|
|
59
|
+
return value===undefined?undefined:JSON.parse(JSON.stringify(value));
|
|
207
60
|
}
|
|
208
61
|
|
|
209
62
|
function assertOnlyKeys(value,allowed,label){
|
|
210
|
-
if(!isPlainObject(value)){
|
|
211
|
-
fail(`${label} must be a JSON object.`);
|
|
212
|
-
}
|
|
213
|
-
|
|
63
|
+
if(!isPlainObject(value))fail(`${label} must be a JSON object.`);
|
|
214
64
|
for(const key of Object.keys(value)){
|
|
215
|
-
if(!allowed.has(key)){
|
|
216
|
-
fail(`${label} has an unsupported key: ${key}`);
|
|
217
|
-
}
|
|
65
|
+
if(!allowed.has(key))fail(`${label} has an unsupported key: ${key}`);
|
|
218
66
|
}
|
|
219
67
|
}
|
|
220
68
|
|
|
@@ -222,7 +70,6 @@ function normalizeWorkspaceRoot(workspaceRoot){
|
|
|
222
70
|
if(typeof workspaceRoot!=='string'||!workspaceRoot.trim()){
|
|
223
71
|
fail('workspaceRoot must be a directory path.');
|
|
224
72
|
}
|
|
225
|
-
|
|
226
73
|
return path.resolve(workspaceRoot);
|
|
227
74
|
}
|
|
228
75
|
|
|
@@ -230,13 +77,8 @@ export function normalizeRelativePath(value,label='path'){
|
|
|
230
77
|
if(typeof value!=='string'||!value||value.includes('\\')||TEXT_CONTROL_PATTERN.test(value)){
|
|
231
78
|
fail(`Unsafe ${label}: ${String(value)}`);
|
|
232
79
|
}
|
|
233
|
-
|
|
234
|
-
if(path.posix.isAbsolute(value)||/^[a-z]:/i.test(value)){
|
|
235
|
-
fail(`Unsafe ${label}: ${value}`);
|
|
236
|
-
}
|
|
237
|
-
|
|
80
|
+
if(path.posix.isAbsolute(value)||/^[a-z]:/iu.test(value))fail(`Unsafe ${label}: ${value}`);
|
|
238
81
|
const segments=value.split('/');
|
|
239
|
-
|
|
240
82
|
for(const segment of segments){
|
|
241
83
|
if(!segment||segment==='.'||segment==='..'||segment.includes(':')
|
|
242
84
|
||WINDOWS_UNSAFE_FILENAME_CHARACTER_PATTERN.test(segment)
|
|
@@ -245,92 +87,59 @@ export function normalizeRelativePath(value,label='path'){
|
|
|
245
87
|
fail(`Unsafe ${label}: ${value}`);
|
|
246
88
|
}
|
|
247
89
|
}
|
|
248
|
-
|
|
249
90
|
return segments.join('/');
|
|
250
91
|
}
|
|
251
92
|
|
|
252
93
|
function normalizeRelativeRoot(value,label){
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
94
|
+
return value==='.'?'.':normalizeRelativePath(value,label);
|
|
95
|
+
}
|
|
256
96
|
|
|
257
|
-
|
|
97
|
+
function pathKey(relative){
|
|
98
|
+
return relative.toLocaleLowerCase('en-US');
|
|
258
99
|
}
|
|
259
100
|
|
|
260
|
-
function
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
101
|
+
function sameOrDescendant(candidate,parent){
|
|
102
|
+
const selected=pathKey(candidate);
|
|
103
|
+
const root=pathKey(parent);
|
|
104
|
+
return selected===root||selected.startsWith(`${root}/`);
|
|
264
105
|
}
|
|
265
106
|
|
|
266
107
|
function resolveInside(root,relative,label,{allowRoot=false}={}){
|
|
267
|
-
const normalized=relative==='.'&&allowRoot
|
|
268
|
-
?'.'
|
|
269
|
-
:normalizeRelativePath(relative,label);
|
|
108
|
+
const normalized=relative==='.'&&allowRoot?'.':normalizeRelativePath(relative,label);
|
|
270
109
|
const candidate=path.resolve(root,...(normalized==='.'?[]:normalized.split('/')));
|
|
271
|
-
|
|
272
|
-
if(!
|
|
110
|
+
const fromRoot=path.relative(path.resolve(root),candidate);
|
|
111
|
+
if((!allowRoot&&fromRoot==='')||fromRoot.startsWith('..')||path.isAbsolute(fromRoot)){
|
|
273
112
|
fail(`${label} leaves its allowed root: ${relative}`);
|
|
274
113
|
}
|
|
275
|
-
|
|
276
114
|
return candidate;
|
|
277
115
|
}
|
|
278
116
|
|
|
279
|
-
function pathKey(relative){
|
|
280
|
-
return relative.toLocaleLowerCase('en-US');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function pathIsSameOrDescendant(candidate,parent){
|
|
284
|
-
const candidateKey=pathKey(candidate);
|
|
285
|
-
const parentKey=pathKey(parent);
|
|
286
|
-
return candidateKey===parentKey||candidateKey.startsWith(`${parentKey}/`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
117
|
function isGlobLike(value){
|
|
290
|
-
return /[*?\[\]{}]
|
|
118
|
+
return /[*?\[\]{}]/u.test(value);
|
|
291
119
|
}
|
|
292
120
|
|
|
293
121
|
function validatePathList(value,label,{required=false}={}){
|
|
294
122
|
if(!Array.isArray(value)||(required&&value.length===0)){
|
|
295
123
|
fail(`${label} must be ${required?'a non-empty':'an'} array of literal relative paths.`);
|
|
296
124
|
}
|
|
297
|
-
|
|
298
|
-
if(value.length>512){
|
|
299
|
-
fail(`${label} is unreasonably large.`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
125
|
const normalized=value.map((entry,index)=>{
|
|
303
126
|
const item=normalizeRelativePath(entry,`${label}[${index}]`);
|
|
304
|
-
|
|
305
|
-
if(isGlobLike(item)){
|
|
306
|
-
fail(`${label}[${index}] must be literal; directories already include descendants.`);
|
|
307
|
-
}
|
|
308
|
-
|
|
127
|
+
if(isGlobLike(item))fail(`${label}[${index}] must be literal; directories include descendants.`);
|
|
309
128
|
return item;
|
|
310
129
|
});
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
for(const item of normalized){
|
|
314
|
-
const key=pathKey(item);
|
|
315
|
-
|
|
316
|
-
if(keys.has(key)){
|
|
317
|
-
fail(`${label} contains a duplicate path: ${item}`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
keys.add(key);
|
|
130
|
+
if(new Set(normalized.map(pathKey)).size!==normalized.length){
|
|
131
|
+
fail(`${label} contains duplicate paths.`);
|
|
321
132
|
}
|
|
322
|
-
|
|
323
133
|
if(required){
|
|
324
|
-
for(let left=0;left<normalized.length;left
|
|
325
|
-
for(let right=left+1;right<normalized.length;right
|
|
326
|
-
if(
|
|
327
|
-
||
|
|
134
|
+
for(let left=0;left<normalized.length;left+=1){
|
|
135
|
+
for(let right=left+1;right<normalized.length;right+=1){
|
|
136
|
+
if(sameOrDescendant(normalized[left],normalized[right])
|
|
137
|
+
||sameOrDescendant(normalized[right],normalized[left])){
|
|
328
138
|
fail(`${label} has overlapping paths: ${normalized[left]} and ${normalized[right]}`);
|
|
329
139
|
}
|
|
330
140
|
}
|
|
331
141
|
}
|
|
332
142
|
}
|
|
333
|
-
|
|
334
143
|
return normalized;
|
|
335
144
|
}
|
|
336
145
|
|
|
@@ -347,163 +156,82 @@ function isAppSourceForbidden(relative){
|
|
|
347
156
|
}
|
|
348
157
|
|
|
349
158
|
function isExcluded(relative,excludes){
|
|
350
|
-
return
|
|
351
|
-
||excludes.some(excluded=>pathIsSameOrDescendant(relative,excluded));
|
|
159
|
+
return excludes.some(excluded=>sameOrDescendant(relative,excluded));
|
|
352
160
|
}
|
|
353
161
|
|
|
354
|
-
function
|
|
355
|
-
if(typeof value!=='string'||!value.trim()
|
|
356
|
-
|
|
357
|
-
fail(`${label} must be plain text no longer than ${maximum} characters.`);
|
|
162
|
+
function assertPresentationText(value,label){
|
|
163
|
+
if(typeof value!=='string'||!value.trim()){
|
|
164
|
+
fail(`${label} must be nonempty text.`);
|
|
358
165
|
}
|
|
359
|
-
|
|
360
|
-
return value.trim();
|
|
166
|
+
return value;
|
|
361
167
|
}
|
|
362
168
|
|
|
363
169
|
export function parseSemver(value){
|
|
364
|
-
if(typeof value!=='string'){
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const match=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
|
|
369
|
-
|
|
370
|
-
if(!match){
|
|
371
|
-
fail(`Invalid semantic version: ${value}`);
|
|
372
|
-
}
|
|
373
|
-
|
|
170
|
+
if(typeof value!=='string')fail(`Invalid semantic version: ${String(value)}`);
|
|
171
|
+
const match=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u.exec(value);
|
|
172
|
+
if(!match)fail(`Invalid semantic version: ${value}`);
|
|
374
173
|
const prerelease=match[4]?match[4].split('.'):[];
|
|
375
|
-
const build=match[5]?match[5].split('.'):[];
|
|
376
|
-
|
|
377
174
|
for(const identifier of prerelease){
|
|
378
|
-
if(/^\d
|
|
175
|
+
if(/^\d+$/u.test(identifier)&&identifier.length>1&&identifier.startsWith('0')){
|
|
379
176
|
fail(`Invalid semantic version: ${value}`);
|
|
380
177
|
}
|
|
381
178
|
}
|
|
382
|
-
|
|
383
179
|
const numbers=match.slice(1,4).map(Number);
|
|
384
|
-
|
|
385
180
|
if(numbers.some(number=>!Number.isSafeInteger(number))){
|
|
386
181
|
fail(`Semantic version component exceeds JavaScript's safe integer range: ${value}`);
|
|
387
182
|
}
|
|
388
|
-
|
|
389
183
|
return {
|
|
390
184
|
major:numbers[0],
|
|
391
185
|
minor:numbers[1],
|
|
392
186
|
patch:numbers[2],
|
|
393
187
|
prerelease,
|
|
394
|
-
build
|
|
188
|
+
build:match[5]?match[5].split('.'):[]
|
|
395
189
|
};
|
|
396
190
|
}
|
|
397
191
|
|
|
398
192
|
function formatSemver(version){
|
|
399
193
|
let rendered=`${version.major}.${version.minor}.${version.patch}`;
|
|
400
|
-
|
|
401
|
-
if(version.
|
|
402
|
-
rendered+=`-${version.prerelease.join('.')}`;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if(version.build?.length){
|
|
406
|
-
rendered+=`+${version.build.join('.')}`;
|
|
407
|
-
}
|
|
408
|
-
|
|
194
|
+
if(version.prerelease?.length)rendered+=`-${version.prerelease.join('.')}`;
|
|
195
|
+
if(version.build?.length)rendered+=`+${version.build.join('.')}`;
|
|
409
196
|
return rendered;
|
|
410
197
|
}
|
|
411
198
|
|
|
412
|
-
function
|
|
413
|
-
const value=preid??'rc';
|
|
414
|
-
|
|
415
|
-
if(typeof value!=='string'||!/^[0-9A-Za-z-]+$/.test(value)
|
|
416
|
-
||(/^\d+$/.test(value)&&value.length>1&&value.startsWith('0'))){
|
|
417
|
-
fail(`Invalid prerelease identifier: ${String(value)}`);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return value;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
export function incrementSemver(value,bump,preid){
|
|
199
|
+
export function incrementSemver(value,bump,preid='rc'){
|
|
424
200
|
const current=parseSemver(value);
|
|
425
|
-
|
|
426
201
|
if(!['major','minor','patch','prerelease'].includes(bump)){
|
|
427
202
|
fail(`Unsupported semantic version bump: ${String(bump)}`);
|
|
428
203
|
}
|
|
429
|
-
|
|
430
|
-
if(bump==='
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if(bump==='minor'){
|
|
435
|
-
return formatSemver({major:current.major,minor:current.minor+1,patch:0});
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if(bump==='patch'){
|
|
439
|
-
return formatSemver({major:current.major,minor:current.minor,patch:current.patch+1});
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const requestedPreid=validatePreid(preid);
|
|
443
|
-
const next={
|
|
444
|
-
major:current.major,
|
|
445
|
-
minor:current.minor,
|
|
446
|
-
patch:current.patch,
|
|
447
|
-
prerelease:[]
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
if(!current.prerelease.length){
|
|
451
|
-
next.patch+=1;
|
|
452
|
-
next.prerelease=[requestedPreid,'0'];
|
|
453
|
-
return formatSemver(next);
|
|
204
|
+
if(bump==='major')return formatSemver({major:current.major+1,minor:0,patch:0});
|
|
205
|
+
if(bump==='minor')return formatSemver({major:current.major,minor:current.minor+1,patch:0});
|
|
206
|
+
if(bump==='patch')return formatSemver({major:current.major,minor:current.minor,patch:current.patch+1});
|
|
207
|
+
if(typeof preid!=='string'||!/^[0-9A-Za-z-]+$/u.test(preid)){
|
|
208
|
+
fail(`Invalid prerelease identifier: ${String(preid)}`);
|
|
454
209
|
}
|
|
455
|
-
|
|
456
|
-
if(current.prerelease[0]!==
|
|
457
|
-
|
|
210
|
+
const next={major:current.major,minor:current.minor,patch:current.patch,prerelease:[]};
|
|
211
|
+
if(current.prerelease[0]!==preid){
|
|
212
|
+
if(current.prerelease.length===0)next.patch+=1;
|
|
213
|
+
next.prerelease=[preid,'0'];
|
|
458
214
|
return formatSemver(next);
|
|
459
215
|
}
|
|
460
|
-
|
|
461
216
|
next.prerelease=[...current.prerelease];
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if(/^\d+$/.test(next.prerelease[index])){
|
|
466
|
-
const number=Number(next.prerelease[index]);
|
|
467
|
-
|
|
468
|
-
if(!Number.isSafeInteger(number)||number===Number.MAX_SAFE_INTEGER){
|
|
469
|
-
fail(`Prerelease number is too large to increment: ${value}`);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
next.prerelease[index]=String(number+1);
|
|
473
|
-
incremented=true;
|
|
474
|
-
break;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if(!incremented){
|
|
479
|
-
next.prerelease.push('0');
|
|
480
|
-
}
|
|
481
|
-
|
|
217
|
+
const numericIndex=next.prerelease.findLastIndex(identifier=>/^\d+$/u.test(identifier));
|
|
218
|
+
if(numericIndex<0)next.prerelease.push('0');
|
|
219
|
+
else next.prerelease[numericIndex]=String(Number(next.prerelease[numericIndex])+1);
|
|
482
220
|
return formatSemver(next);
|
|
483
221
|
}
|
|
484
222
|
|
|
485
|
-
async function
|
|
486
|
-
let
|
|
223
|
+
async function readJson(filePath,label=filePath){
|
|
224
|
+
let text;
|
|
487
225
|
try{
|
|
488
|
-
|
|
226
|
+
const info=await lstat(filePath);
|
|
227
|
+
if(info.isSymbolicLink()||!info.isFile())fail(`${label} must be a real file.`);
|
|
228
|
+
text=await readFile(filePath,'utf8');
|
|
489
229
|
}catch(error){
|
|
490
230
|
if(error?.code==='ENOENT')fail(`${label} does not exist.`);
|
|
491
231
|
throw error;
|
|
492
232
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
return {
|
|
496
|
-
value:JSON.parse(document.bytes.toString('utf8')),
|
|
497
|
-
bytes:document.bytes,
|
|
498
|
-
identity:document.identity
|
|
499
|
-
};
|
|
500
|
-
}catch(error){
|
|
501
|
-
fail(`${label} is not valid JSON: ${error.message}`);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
async function readJson(filePath,label=filePath){
|
|
506
|
-
return (await readJsonDocument(filePath,label)).value;
|
|
233
|
+
try{return JSON.parse(text);}
|
|
234
|
+
catch(error){fail(`${label} is not valid JSON: ${error.message}`);}
|
|
507
235
|
}
|
|
508
236
|
|
|
509
237
|
function validateSharedRoute(route,label){
|
|
@@ -512,3030 +240,558 @@ function validateSharedRoute(route,label){
|
|
|
512
240
|
const destination=normalizeRelativeRoot(route.destination,`${label}.destination`);
|
|
513
241
|
const include=validatePathList(route.include,`${label}.include`,{required:true});
|
|
514
242
|
const exclude=validatePathList(route.exclude??[],`${label}.exclude`);
|
|
515
|
-
|
|
516
243
|
if(source==='.'||source==='apps'||source.startsWith('apps/')
|
|
517
|
-
||source==='dist'||source.startsWith('dist/')
|
|
518
|
-
||source==='node_modules'
|
|
244
|
+
||source==='dist'||source.startsWith('dist/')||source==='node_modules'
|
|
519
245
|
||isAlwaysForbidden(source)){
|
|
520
|
-
fail(`${label}.source is outside the
|
|
246
|
+
fail(`${label}.source is outside the shared-payload boundary: ${source}`);
|
|
521
247
|
}
|
|
522
|
-
|
|
523
248
|
if(destination==='apps'||destination.startsWith('apps/')
|
|
524
|
-
||
|
|
249
|
+
||pathKey(destination)===pathKey(RELEASE_MANIFEST_NAME)){
|
|
525
250
|
fail(`${label}.destination overlaps a reserved package path: ${destination}`);
|
|
526
251
|
}
|
|
527
|
-
|
|
528
|
-
return Object.freeze({source,destination,include,exclude});
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
async function loadRootConfigDocument(workspaceRoot){
|
|
532
|
-
const configPath=path.join(workspaceRoot,ROOT_CONFIG_NAME);
|
|
533
|
-
const document=await readJsonDocument(configPath,ROOT_CONFIG_NAME);
|
|
534
|
-
return {
|
|
535
|
-
...document,
|
|
536
|
-
value:validateRootConfig(document.value,configPath),
|
|
537
|
-
configPath
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
async function loadRootConfig(workspaceRoot){
|
|
542
|
-
return (await loadRootConfigDocument(workspaceRoot)).value;
|
|
252
|
+
return {source,destination,include,exclude};
|
|
543
253
|
}
|
|
544
254
|
|
|
545
255
|
export function validateRootConfig(value,configPath=ROOT_CONFIG_NAME){
|
|
546
256
|
assertOnlyKeys(value,new Set(['schemaVersion','appsRoot','distRoot','sharedPayloads']),ROOT_CONFIG_NAME);
|
|
547
|
-
|
|
548
|
-
if(value.schemaVersion!==1){
|
|
549
|
-
fail(`${ROOT_CONFIG_NAME}.schemaVersion must be 1.`);
|
|
550
|
-
}
|
|
551
|
-
|
|
257
|
+
if(value.schemaVersion!==1)fail(`${ROOT_CONFIG_NAME}.schemaVersion must be 1.`);
|
|
552
258
|
if(value.appsRoot!=='apps'||value.distRoot!=='dist'){
|
|
553
259
|
fail(`${ROOT_CONFIG_NAME} must bind appsRoot to "apps" and distRoot to "dist".`);
|
|
554
260
|
}
|
|
555
|
-
|
|
556
261
|
if(!isPlainObject(value.sharedPayloads)){
|
|
557
262
|
fail(`${ROOT_CONFIG_NAME}.sharedPayloads must be an object.`);
|
|
558
263
|
}
|
|
559
|
-
|
|
560
264
|
const sharedPayloads={};
|
|
561
|
-
|
|
562
265
|
for(const [id,routes] of Object.entries(value.sharedPayloads).sort(([left],[right])=>compareText(left,right))){
|
|
563
|
-
if(!SAFE_SHARED_ID_PATTERN.test(id)){
|
|
564
|
-
fail(`Unsafe shared payload id: ${id}`);
|
|
565
|
-
}
|
|
566
|
-
|
|
266
|
+
if(!SAFE_SHARED_ID_PATTERN.test(id))fail(`Unsafe shared payload id: ${id}`);
|
|
567
267
|
if(!Array.isArray(routes)||routes.length===0){
|
|
568
268
|
fail(`sharedPayloads.${id} must be a non-empty array.`);
|
|
569
269
|
}
|
|
570
|
-
|
|
571
|
-
sharedPayloads[id]=Object.freeze(routes.map((route,index)=>
|
|
270
|
+
sharedPayloads[id]=routes.map((route,index)=>
|
|
572
271
|
validateSharedRoute(route,`sharedPayloads.${id}[${index}]`)
|
|
573
|
-
)
|
|
272
|
+
);
|
|
574
273
|
}
|
|
274
|
+
return {schemaVersion:1,appsRoot:'apps',distRoot:'dist',sharedPayloads,configPath};
|
|
275
|
+
}
|
|
575
276
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
sharedPayloads:Object.freeze(sharedPayloads),
|
|
581
|
-
configPath
|
|
582
|
-
});
|
|
277
|
+
function normalizeOptionalRecord(value,label){
|
|
278
|
+
if(value===undefined)return undefined;
|
|
279
|
+
if(!isPlainObject(value))fail(`${label} must be an object.`);
|
|
280
|
+
return copyJson(value);
|
|
583
281
|
}
|
|
584
282
|
|
|
585
283
|
export function validateAppConfig(value,appId,rootConfig,configPath=`apps/${appId}/${APP_CONFIG_NAME}`){
|
|
586
|
-
assertOnlyKeys(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
'displayName',
|
|
592
|
-
'version',
|
|
593
|
-
'entry',
|
|
594
|
-
'strategy',
|
|
595
|
-
'security',
|
|
596
|
-
'localAIModelPolicy',
|
|
597
|
-
'include',
|
|
598
|
-
'exclude',
|
|
599
|
-
'shared',
|
|
600
|
-
'adapter'
|
|
601
|
-
]),
|
|
602
|
-
`${appId}/${APP_CONFIG_NAME}`
|
|
603
|
-
);
|
|
604
|
-
|
|
605
|
-
if(value.schemaVersion!==1){
|
|
606
|
-
fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
|
|
607
|
-
}
|
|
608
|
-
|
|
284
|
+
assertOnlyKeys(value,new Set([
|
|
285
|
+
'schemaVersion','id','displayName','version','entry','strategy','security',
|
|
286
|
+
'localAIModelPolicy','include','exclude','shared','adapter'
|
|
287
|
+
]),`${appId}/${APP_CONFIG_NAME}`);
|
|
288
|
+
if(value.schemaVersion!==1)fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
|
|
609
289
|
if(value.id!==appId||!APP_ID_PATTERN.test(value.id)){
|
|
610
290
|
fail(`${appId}/${APP_CONFIG_NAME}.id must exactly match its apps directory.`);
|
|
611
291
|
}
|
|
612
|
-
|
|
613
|
-
const displayName=assertSafePresentationText(
|
|
614
|
-
value.displayName,
|
|
615
|
-
`${appId}/${APP_CONFIG_NAME}.displayName`
|
|
616
|
-
);
|
|
292
|
+
const displayName=assertPresentationText(value.displayName,`${appId}/${APP_CONFIG_NAME}.displayName`);
|
|
617
293
|
parseSemver(value.version);
|
|
618
294
|
const entry=normalizeRelativePath(value.entry,`${appId}/${APP_CONFIG_NAME}.entry`);
|
|
619
295
|
const include=validatePathList(value.include,`${appId}/${APP_CONFIG_NAME}.include`,{required:true});
|
|
620
296
|
const exclude=validatePathList(value.exclude??[],`${appId}/${APP_CONFIG_NAME}.exclude`);
|
|
621
|
-
|
|
622
|
-
if(include.some(allowed=>pathIsSameOrDescendant(APP_CONFIG_NAME,allowed))){
|
|
297
|
+
if(include.some(allowed=>sameOrDescendant(APP_CONFIG_NAME,allowed))){
|
|
623
298
|
fail(`${appId}/${APP_CONFIG_NAME}.include must not expose the authored package configuration.`);
|
|
624
299
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
?Object.freeze({verified_only:true,models:Object.freeze([])})
|
|
628
|
-
:validateLocalAIModelPolicy(value.localAIModelPolicy,`${appId}/${APP_CONFIG_NAME}.localAIModelPolicy`);
|
|
629
|
-
const security=validateAppSecurity(value.security,appId);
|
|
630
|
-
|
|
631
|
-
for(const model of localAIModelPolicy.models){
|
|
632
|
-
if(isAlwaysForbidden(model.definition)||isExcluded(model.definition,exclude)
|
|
633
|
-
||!include.some(allowed=>pathIsSameOrDescendant(model.definition,allowed))){
|
|
634
|
-
fail(`${appId}/${APP_CONFIG_NAME}.localAIModelPolicy model definition is not covered by its public include rules: ${model.definition}`);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
if(isAlwaysForbidden(entry)||isExcluded(entry,exclude)
|
|
639
|
-
||!include.some(allowed=>pathIsSameOrDescendant(entry,allowed))){
|
|
300
|
+
if(isAppSourceForbidden(entry)||isExcluded(entry,exclude)
|
|
301
|
+
||!include.some(allowed=>sameOrDescendant(entry,allowed))){
|
|
640
302
|
fail(`${appId}/${APP_CONFIG_NAME}.entry is not covered by its public include rules.`);
|
|
641
303
|
}
|
|
642
|
-
|
|
643
304
|
if(!['static','adapter'].includes(value.strategy)){
|
|
644
305
|
fail(`${appId}/${APP_CONFIG_NAME}.strategy must be "static" or "adapter".`);
|
|
645
306
|
}
|
|
646
|
-
|
|
647
307
|
if(!Array.isArray(value.shared)||new Set(value.shared).size!==value.shared.length){
|
|
648
308
|
fail(`${appId}/${APP_CONFIG_NAME}.shared must be an array of unique shared payload ids.`);
|
|
649
309
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
fail(`${appId}/${APP_CONFIG_NAME}.shared[${index}] references an unknown shared payload: ${String(sharedId)}`);
|
|
310
|
+
for(const [index,id] of value.shared.entries()){
|
|
311
|
+
if(typeof id!=='string'||!Object.hasOwn(rootConfig.sharedPayloads,id)){
|
|
312
|
+
fail(`${appId}/${APP_CONFIG_NAME}.shared[${index}] references an unknown shared payload.`);
|
|
654
313
|
}
|
|
655
314
|
}
|
|
656
|
-
|
|
657
|
-
let adapter=null;
|
|
658
|
-
|
|
315
|
+
let adapter;
|
|
659
316
|
if(value.strategy==='adapter'){
|
|
660
317
|
adapter=normalizeRelativePath(value.adapter,`${appId}/${APP_CONFIG_NAME}.adapter`);
|
|
661
|
-
|
|
662
318
|
if(!adapter.startsWith('scripts/')||path.posix.extname(adapter)!=='.mjs'){
|
|
663
319
|
fail(`${appId}/${APP_CONFIG_NAME}.adapter must be an app-local scripts/*.mjs module.`);
|
|
664
320
|
}
|
|
665
321
|
}else if(value.adapter!==undefined){
|
|
666
322
|
fail(`${appId}/${APP_CONFIG_NAME}.adapter is only valid with strategy "adapter".`);
|
|
667
323
|
}
|
|
668
|
-
|
|
669
|
-
return Object.freeze({
|
|
324
|
+
return {
|
|
670
325
|
schemaVersion:1,
|
|
671
326
|
id:appId,
|
|
672
327
|
displayName,
|
|
673
328
|
version:value.version,
|
|
674
329
|
entry,
|
|
675
330
|
strategy:value.strategy,
|
|
676
|
-
security
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
331
|
+
...(value.security===undefined?{}:{security:normalizeOptionalRecord(
|
|
332
|
+
value.security,
|
|
333
|
+
`${appId}/${APP_CONFIG_NAME}.security`
|
|
334
|
+
)}),
|
|
335
|
+
...(value.localAIModelPolicy===undefined?{}:{localAIModelPolicy:normalizeOptionalRecord(
|
|
336
|
+
value.localAIModelPolicy,
|
|
337
|
+
`${appId}/${APP_CONFIG_NAME}.localAIModelPolicy`
|
|
338
|
+
)}),
|
|
339
|
+
include,
|
|
340
|
+
exclude,
|
|
341
|
+
shared:[...value.shared],
|
|
342
|
+
...(adapter===undefined?{}:{adapter}),
|
|
682
343
|
configPath
|
|
683
|
-
}
|
|
344
|
+
};
|
|
684
345
|
}
|
|
685
346
|
|
|
686
|
-
function
|
|
687
|
-
|
|
688
|
-
|
|
347
|
+
async function realDirectory(location,label){
|
|
348
|
+
const requested=path.resolve(location);
|
|
349
|
+
let info;
|
|
350
|
+
try{info=await lstat(requested);}
|
|
351
|
+
catch(error){
|
|
352
|
+
if(error?.code==='ENOENT')fail(`${label} does not exist: ${requested}.`);
|
|
353
|
+
throw error;
|
|
689
354
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
let parsed;
|
|
696
|
-
try{
|
|
697
|
-
parsed=new URL(origin);
|
|
698
|
-
}catch{
|
|
699
|
-
fail(`${label}[${index}] is not a valid URL origin.`);
|
|
700
|
-
}
|
|
701
|
-
if(parsed.origin!==origin||parsed.hostname.endsWith('.')||parsed.username||parsed.password
|
|
702
|
-
||parsed.pathname!=='/'||parsed.search||parsed.hash){
|
|
703
|
-
fail(`${label}[${index}] must be a canonical allowed origin.`);
|
|
704
|
-
}
|
|
705
|
-
if(parsed.protocol==='http:'){
|
|
706
|
-
if(!allowLoopbackHttp||!['127.0.0.1','[::1]'].includes(parsed.hostname)){
|
|
707
|
-
fail(`${label}[${index}] may use HTTP only for a numeric loopback host.`);
|
|
708
|
-
}
|
|
709
|
-
}else if(parsed.protocol!=='https:'){
|
|
710
|
-
fail(`${label}[${index}] must use HTTPS or an approved loopback HTTP origin.`);
|
|
711
|
-
}
|
|
712
|
-
return parsed.origin;
|
|
713
|
-
});
|
|
714
|
-
if(new Set(origins).size!==origins.length)fail(`${label} must not contain duplicates.`);
|
|
715
|
-
if(JSON.stringify(origins)!==JSON.stringify([...origins].sort(compareText))){
|
|
716
|
-
fail(`${label} must be sorted for deterministic projection.`);
|
|
355
|
+
if(info.isSymbolicLink()||!info.isDirectory())fail(`${label} must be a real directory.`);
|
|
356
|
+
const canonical=await realpath(requested);
|
|
357
|
+
const canonicalInfo=await lstat(canonical);
|
|
358
|
+
if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()){
|
|
359
|
+
fail(`${label} must be a real directory.`);
|
|
717
360
|
}
|
|
718
|
-
return
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
function validateAppSecurity(value,appId){
|
|
722
|
-
if(!isPlainObject(value))fail(`${appId}/${APP_CONFIG_NAME}.security must be an object.`);
|
|
723
|
-
const label=`${appId}/${APP_CONFIG_NAME}.security`;
|
|
724
|
-
assertOnlyKeys(value,new Set(['connectOrigins','frameOrigins','mediaOrigins']),label);
|
|
725
|
-
return Object.freeze({
|
|
726
|
-
connectOrigins:validateOriginList(value.connectOrigins,`${label}.connectOrigins`,{allowLoopbackHttp:true}),
|
|
727
|
-
frameOrigins:validateOriginList(value.frameOrigins,`${label}.frameOrigins`,{allowHttpsScheme:appId==='browser'}),
|
|
728
|
-
mediaOrigins:validateOriginList(value.mediaOrigins,`${label}.mediaOrigins`)
|
|
729
|
-
});
|
|
361
|
+
return canonical;
|
|
730
362
|
}
|
|
731
363
|
|
|
732
|
-
function
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
fail(`${label}
|
|
364
|
+
async function assertContainedRealPath(root,candidate,label){
|
|
365
|
+
const absolute=path.resolve(candidate);
|
|
366
|
+
const fromRoot=path.relative(path.resolve(root),absolute);
|
|
367
|
+
if(fromRoot.startsWith('..')||path.isAbsolute(fromRoot))fail(`${label} leaves its allowed root.`);
|
|
368
|
+
let current=path.resolve(root);
|
|
369
|
+
for(const segment of fromRoot.split(path.sep).filter(Boolean)){
|
|
370
|
+
current=path.join(current,segment);
|
|
371
|
+
const info=await lstat(current);
|
|
372
|
+
if(info.isSymbolicLink())fail(`${label} contains a symbolic link or junction.`);
|
|
741
373
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
374
|
+
const canonicalRoot=await realpath(root);
|
|
375
|
+
const canonicalCandidate=await realpath(absolute);
|
|
376
|
+
const canonicalRelative=path.relative(canonicalRoot,canonicalCandidate);
|
|
377
|
+
if(canonicalRelative.startsWith('..')||path.isAbsolute(canonicalRelative)){
|
|
378
|
+
fail(`${label} resolves outside its allowed root.`);
|
|
745
379
|
}
|
|
380
|
+
}
|
|
746
381
|
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
382
|
+
async function loadContext(requestedWorkspaceRoot,appId){
|
|
383
|
+
const workspaceRoot=await realDirectory(normalizeWorkspaceRoot(requestedWorkspaceRoot),'Workspace root');
|
|
384
|
+
const rootConfigPath=path.join(workspaceRoot,ROOT_CONFIG_NAME);
|
|
385
|
+
const rootConfig=validateRootConfig(await readJson(rootConfigPath,ROOT_CONFIG_NAME),rootConfigPath);
|
|
386
|
+
if(typeof appId!=='string'||!APP_ID_PATTERN.test(appId))fail(`Unsafe app id: ${String(appId)}`);
|
|
387
|
+
const appsRoot=await realDirectory(path.join(workspaceRoot,rootConfig.appsRoot),'Apps root');
|
|
388
|
+
const appRoot=resolveInside(appsRoot,appId,'app id');
|
|
389
|
+
await assertContainedRealPath(appsRoot,appRoot,`apps/${appId}`);
|
|
390
|
+
const configPath=path.join(appRoot,APP_CONFIG_NAME);
|
|
391
|
+
const config=validateAppConfig(await readJson(configPath,`${appId}/${APP_CONFIG_NAME}`),appId,rootConfig,configPath);
|
|
392
|
+
return {
|
|
393
|
+
workspaceRoot,
|
|
394
|
+
rootConfig,
|
|
395
|
+
appsRoot,
|
|
396
|
+
appRoot,
|
|
397
|
+
appId,
|
|
398
|
+
config,
|
|
399
|
+
distRoot:path.join(workspaceRoot,rootConfig.distRoot),
|
|
400
|
+
outputRoot:path.join(workspaceRoot,rootConfig.distRoot,appId)
|
|
401
|
+
};
|
|
402
|
+
}
|
|
763
403
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
404
|
+
function destinationJoin(root,relative){
|
|
405
|
+
return root==='.'?relative:`${root}/${relative}`;
|
|
406
|
+
}
|
|
767
407
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
408
|
+
async function collectSelectedPath({
|
|
409
|
+
sourceRoot,
|
|
410
|
+
selected,
|
|
411
|
+
destination,
|
|
412
|
+
excludes,
|
|
413
|
+
reject,
|
|
414
|
+
records,
|
|
415
|
+
destinations,
|
|
416
|
+
signal,
|
|
417
|
+
label
|
|
418
|
+
}){
|
|
419
|
+
throwIfAborted(signal);
|
|
420
|
+
if(isExcluded(selected,excludes))return;
|
|
421
|
+
if(reject(selected))fail(`${label} selects a reserved private or generated path: ${selected}.`);
|
|
422
|
+
const absolute=resolveInside(sourceRoot,selected,label);
|
|
423
|
+
let info;
|
|
424
|
+
try{info=await lstat(absolute);}
|
|
425
|
+
catch(error){
|
|
426
|
+
if(error?.code==='ENOENT')fail(`${label} does not exist: ${selected}.`);
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
if(info.isSymbolicLink())fail(`${label} contains a symbolic link or junction: ${selected}.`);
|
|
430
|
+
if(info.isDirectory()){
|
|
431
|
+
const entries=await readdir(absolute,{withFileTypes:true});
|
|
432
|
+
entries.sort((left,right)=>compareText(left.name,right.name));
|
|
433
|
+
for(const entry of entries){
|
|
434
|
+
const child=`${selected}/${entry.name}`;
|
|
435
|
+
await collectSelectedPath({
|
|
436
|
+
sourceRoot,
|
|
437
|
+
selected:child,
|
|
438
|
+
destination:`${destination}/${entry.name}`,
|
|
439
|
+
excludes,
|
|
440
|
+
reject,
|
|
441
|
+
records,
|
|
442
|
+
destinations,
|
|
443
|
+
signal,
|
|
444
|
+
label
|
|
445
|
+
});
|
|
773
446
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if(!info.isFile())fail(`${label} contains a non-file entry: ${selected}.`);
|
|
450
|
+
const normalizedDestination=normalizeRelativePath(destination,`${label} destination`);
|
|
451
|
+
if(pathKey(normalizedDestination)===pathKey(RELEASE_MANIFEST_NAME)){
|
|
452
|
+
fail(`${label} overlaps the generated release manifest.`);
|
|
453
|
+
}
|
|
454
|
+
const key=pathKey(normalizedDestination);
|
|
455
|
+
if(destinations.has(key))fail(`Package destination collision: ${normalizedDestination}.`);
|
|
456
|
+
destinations.add(key);
|
|
457
|
+
records.push({source:absolute,destination:normalizedDestination});
|
|
784
458
|
}
|
|
785
459
|
|
|
786
|
-
async function
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
460
|
+
async function collectPackageRecords(context,{signal}={}){
|
|
461
|
+
const records=[];
|
|
462
|
+
const destinations=new Set();
|
|
463
|
+
for(const selected of context.config.include){
|
|
464
|
+
await collectSelectedPath({
|
|
465
|
+
sourceRoot:context.appRoot,
|
|
466
|
+
selected,
|
|
467
|
+
destination:selected,
|
|
468
|
+
excludes:context.config.exclude,
|
|
469
|
+
reject:isAppSourceForbidden,
|
|
470
|
+
records,
|
|
471
|
+
destinations,
|
|
472
|
+
signal,
|
|
473
|
+
label:`apps/${context.appId}`
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
for(const sharedId of context.config.shared){
|
|
477
|
+
for(const route of context.rootConfig.sharedPayloads[sharedId]){
|
|
478
|
+
const sourceRoot=resolveInside(context.workspaceRoot,route.source,`sharedPayloads.${sharedId}.source`);
|
|
479
|
+
await assertContainedRealPath(context.workspaceRoot,sourceRoot,`sharedPayloads.${sharedId}.source`);
|
|
480
|
+
for(const selected of route.include){
|
|
481
|
+
await collectSelectedPath({
|
|
482
|
+
sourceRoot,
|
|
483
|
+
selected,
|
|
484
|
+
destination:destinationJoin(route.destination,selected),
|
|
485
|
+
excludes:route.exclude,
|
|
486
|
+
reject:isAlwaysForbidden,
|
|
487
|
+
records,
|
|
488
|
+
destinations,
|
|
489
|
+
signal,
|
|
490
|
+
label:`sharedPayloads.${sharedId}`
|
|
491
|
+
});
|
|
798
492
|
}
|
|
799
|
-
throw error;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
if(details.isSymbolicLink()||!details.isFile()||details.size<1
|
|
803
|
-
||details.size>MAX_MODEL_DEFINITION_BYTES){
|
|
804
|
-
fail(`${label} must be a non-empty regular file no larger than 512 KiB.`);
|
|
805
493
|
}
|
|
806
494
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const resolvedRoot=path.resolve(root);
|
|
811
|
-
const resolvedCandidate=path.resolve(candidate);
|
|
812
|
-
|
|
813
|
-
if(!isInside(resolvedRoot,resolvedCandidate,{allowEqual:true})){
|
|
814
|
-
fail(`${label} leaves its allowed root.`);
|
|
495
|
+
records.sort((left,right)=>compareText(left.destination,right.destination));
|
|
496
|
+
if(!records.some(record=>pathKey(record.destination)===pathKey(context.config.entry))){
|
|
497
|
+
fail(`Package entry is missing from the selected files: ${context.config.entry}.`);
|
|
815
498
|
}
|
|
499
|
+
return records;
|
|
500
|
+
}
|
|
816
501
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
502
|
+
async function browserDocuments(records){
|
|
503
|
+
const documents=[];
|
|
504
|
+
for(const record of records){
|
|
505
|
+
if(path.posix.extname(record.destination).toLocaleLowerCase('en-US')!=='.html')continue;
|
|
506
|
+
const inspected=inspectImportMapHtml(await readFile(record.source,'utf8'),{
|
|
507
|
+
documentPath:record.destination
|
|
508
|
+
});
|
|
509
|
+
documents.push({path:record.destination,...copyJson(inspected)});
|
|
823
510
|
}
|
|
511
|
+
return documents;
|
|
512
|
+
}
|
|
824
513
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const info=await lstat(current);
|
|
828
|
-
|
|
829
|
-
if(info.isSymbolicLink()){
|
|
830
|
-
fail(`${label} contains a symbolic link or junction: ${current}`);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
const [actualRoot,actualCandidate]=await Promise.all([
|
|
835
|
-
realpath(resolvedRoot),
|
|
836
|
-
realpath(resolvedCandidate)
|
|
837
|
-
]);
|
|
838
|
-
|
|
839
|
-
if(!isInside(actualRoot,actualCandidate,{allowEqual:true})){
|
|
840
|
-
fail(`${label} resolves outside its allowed root.`);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
async function assertSafeDistBoundary(workspaceRoot,distRoot,{create=false}={}){
|
|
845
|
-
await assertNoLinks(workspaceRoot,workspaceRoot,'workspace');
|
|
846
|
-
let details;
|
|
847
|
-
|
|
848
|
-
try{
|
|
849
|
-
details=await lstat(distRoot);
|
|
850
|
-
}catch(error){
|
|
851
|
-
if(error?.code!=='ENOENT'){
|
|
852
|
-
throw error;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if(!create){
|
|
856
|
-
return false;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
try{
|
|
860
|
-
await mkdir(distRoot);
|
|
861
|
-
}catch(createError){
|
|
862
|
-
if(createError?.code!=='EEXIST'){
|
|
863
|
-
throw createError;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
details=await lstat(distRoot);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
if(details.isSymbolicLink()||!details.isDirectory()){
|
|
871
|
-
fail('dist must be a real workspace directory, not a link, junction, or special entry.');
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
await assertNoLinks(workspaceRoot,distRoot,'dist');
|
|
875
|
-
return true;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
async function assertOptionalSafeOutput(distRoot,outputRoot,appId){
|
|
879
|
-
let details;
|
|
880
|
-
|
|
881
|
-
try{
|
|
882
|
-
details=await lstat(outputRoot);
|
|
883
|
-
}catch(error){
|
|
884
|
-
if(error?.code==='ENOENT'){
|
|
885
|
-
return false;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
throw error;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
if(details.isSymbolicLink()||!details.isDirectory()){
|
|
892
|
-
fail(`dist/${appId} must be a real directory, not a link, junction, or special entry.`);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
await assertNoLinks(distRoot,outputRoot,`dist/${appId}`);
|
|
896
|
-
return true;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
async function appDescriptorContracts(){
|
|
900
|
-
appDescriptorContractsPromise??=import('../app-descriptor.mjs');
|
|
901
|
-
return appDescriptorContractsPromise;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
async function optionalRegularFileIdentity(filePath,label){
|
|
905
|
-
let info;
|
|
906
|
-
try{
|
|
907
|
-
info=await lstat(filePath,{bigint:true});
|
|
908
|
-
}catch(error){
|
|
909
|
-
if(error?.code==='ENOENT')return null;
|
|
910
|
-
throw error;
|
|
911
|
-
}
|
|
912
|
-
if(info.isSymbolicLink()||!info.isFile()){
|
|
913
|
-
fail(`${label} must be a regular file, not a link or special entry.`);
|
|
914
|
-
}
|
|
915
|
-
return fileIdentity(info);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function recordedIdentityMatches(left,right){
|
|
919
|
-
if(left===null||right===null)return left===right;
|
|
920
|
-
return left.device===right.device
|
|
921
|
-
&&left.inode===right.inode
|
|
922
|
-
&&left.bytes===right.bytes
|
|
923
|
-
&&left.modifiedNanoseconds===right.modifiedNanoseconds
|
|
924
|
-
&&left.changedNanoseconds===right.changedNanoseconds
|
|
925
|
-
&&left.links===right.links;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
async function createAppDescriptorAuthority(context,packageDocument,{signal}={}){
|
|
929
|
-
throwIfAborted(signal);
|
|
930
|
-
const contracts=await appDescriptorContracts();
|
|
931
|
-
const descriptorPath=path.join(context.appRoot,APP_DESCRIPTOR_NAME);
|
|
932
|
-
const descriptorIdentity=await optionalRegularFileIdentity(
|
|
933
|
-
descriptorPath,
|
|
934
|
-
`apps/${context.config.id}/${APP_DESCRIPTOR_NAME}`
|
|
935
|
-
);
|
|
936
|
-
let descriptor;
|
|
937
|
-
let source;
|
|
938
|
-
let sourcePath;
|
|
939
|
-
let sourceIdentity;
|
|
940
|
-
|
|
941
|
-
if(descriptorIdentity){
|
|
942
|
-
const descriptorDocument=await readJsonDocument(
|
|
943
|
-
descriptorPath,
|
|
944
|
-
`apps/${context.config.id}/${APP_DESCRIPTOR_NAME}`,
|
|
945
|
-
{expectedIdentity:descriptorIdentity}
|
|
946
|
-
);
|
|
947
|
-
descriptor=contracts.validateAppDescriptor(descriptorDocument.value,{
|
|
948
|
-
appId:context.config.id
|
|
949
|
-
});
|
|
950
|
-
if(!isDeepStrictEqual(
|
|
951
|
-
contracts.projectPackageManifest(descriptor),
|
|
952
|
-
packageDocument.value
|
|
953
|
-
)){
|
|
954
|
-
fail(`${APP_DESCRIPTOR_NAME} does not project exactly to ${APP_CONFIG_NAME}.`);
|
|
955
|
-
}
|
|
956
|
-
source='authored';
|
|
957
|
-
sourcePath=descriptorPath;
|
|
958
|
-
sourceIdentity=descriptorDocument.identity;
|
|
959
|
-
}else{
|
|
960
|
-
const registryPath=path.join(context.workspaceRoot,LEGACY_APP_REGISTRY_PATH);
|
|
961
|
-
const registryBefore=await optionalRegularFileIdentity(
|
|
962
|
-
registryPath,
|
|
963
|
-
'Arcane native app registry'
|
|
964
|
-
);
|
|
965
|
-
const loaded=await contracts.loadAppDescriptor({
|
|
966
|
-
workspaceRoot:context.workspaceRoot,
|
|
967
|
-
appRoot:context.appRoot,
|
|
968
|
-
appId:context.config.id,
|
|
969
|
-
packageManifest:packageDocument.value
|
|
970
|
-
});
|
|
971
|
-
const [descriptorAfter,registryAfter]=await Promise.all([
|
|
972
|
-
optionalRegularFileIdentity(
|
|
973
|
-
descriptorPath,
|
|
974
|
-
`apps/${context.config.id}/${APP_DESCRIPTOR_NAME}`
|
|
975
|
-
),
|
|
976
|
-
optionalRegularFileIdentity(registryPath,'Arcane native app registry')
|
|
977
|
-
]);
|
|
978
|
-
if(descriptorAfter!==null||!recordedIdentityMatches(registryBefore,registryAfter)){
|
|
979
|
-
fail(`The descriptor source for ${context.config.id} changed while it was selected.`);
|
|
980
|
-
}
|
|
981
|
-
descriptor=loaded.descriptor;
|
|
982
|
-
source=loaded.source;
|
|
983
|
-
sourcePath=registryBefore?registryPath:null;
|
|
984
|
-
sourceIdentity=registryBefore;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const canonicalDescriptor=immutableJsonCopy(descriptor);
|
|
988
|
-
return Object.freeze({
|
|
989
|
-
descriptor:canonicalDescriptor,
|
|
990
|
-
descriptorSha256:contracts.appDescriptorSha256(canonicalDescriptor),
|
|
991
|
-
source,
|
|
992
|
-
sourcePath,
|
|
993
|
-
sourceIdentity,
|
|
994
|
-
packageConfigPath:context.config.configPath,
|
|
995
|
-
packageConfigIdentity:packageDocument.identity
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
async function assertAppDescriptorAuthorityCurrent(context,{signal}={}){
|
|
1000
|
-
const expected=context.descriptorAuthority;
|
|
1001
|
-
if(!expected)fail('Canonical app descriptor authority is unavailable.');
|
|
1002
|
-
const packageDocument=await readJsonDocument(
|
|
1003
|
-
expected.packageConfigPath,
|
|
1004
|
-
expected.packageConfigPath,
|
|
1005
|
-
{expectedIdentity:expected.packageConfigIdentity}
|
|
1006
|
-
);
|
|
1007
|
-
const config=validateAppConfig(
|
|
1008
|
-
packageDocument.value,
|
|
1009
|
-
context.config.id,
|
|
1010
|
-
context.rootConfig,
|
|
1011
|
-
expected.packageConfigPath
|
|
1012
|
-
);
|
|
1013
|
-
const current=await createAppDescriptorAuthority(
|
|
1014
|
-
{...context,config},
|
|
1015
|
-
packageDocument,
|
|
1016
|
-
{signal}
|
|
1017
|
-
);
|
|
1018
|
-
if(current.descriptorSha256!==expected.descriptorSha256
|
|
1019
|
-
||current.source!==expected.source
|
|
1020
|
-
||current.sourcePath!==expected.sourcePath
|
|
1021
|
-
||!recordedIdentityMatches(current.sourceIdentity,expected.sourceIdentity)
|
|
1022
|
-
||!recordedIdentityMatches(
|
|
1023
|
-
current.packageConfigIdentity,
|
|
1024
|
-
expected.packageConfigIdentity
|
|
1025
|
-
)){
|
|
1026
|
-
fail(`The canonical descriptor authority for ${context.config.id} changed during packaging.`);
|
|
1027
|
-
}
|
|
1028
|
-
return expected;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async function assertValidatedDescriptorAuthority(validation,descriptorAuthority){
|
|
1032
|
-
if(validation===undefined||validation?.app?.descriptor===undefined)return;
|
|
1033
|
-
const descriptor=validation?.app?.descriptor;
|
|
1034
|
-
const contracts=await appDescriptorContracts();
|
|
1035
|
-
if(contracts.appDescriptorSha256(descriptor)!==descriptorAuthority.descriptorSha256){
|
|
1036
|
-
fail('Source validation returned a different canonical Arcane application descriptor.');
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
function packagedRuntimeAuthorities(receipt){
|
|
1041
|
-
const arcane=receipt?.sources?.arcane;
|
|
1042
|
-
const sdkBrowser=receipt?.sources?.sdkBrowser;
|
|
1043
|
-
if(receipt?.kind!=='arcane-workspace-runtime-verification'
|
|
1044
|
-
||!Number.isSafeInteger(receipt.fileCount)||receipt.fileCount<1
|
|
1045
|
-
||!Number.isSafeInteger(receipt.totalBytes)||receipt.totalBytes<1
|
|
1046
|
-
||!SHA256_PATTERN.test(receipt.contentSha256??'')
|
|
1047
|
-
||!isPlainObject(arcane)||arcane.authority!=='arcane-os-sdk'
|
|
1048
|
-
||!SHA256_PATTERN.test(arcane.manifestSha256??'')
|
|
1049
|
-
||!SHA256_PATTERN.test(arcane.contentSha256??'')
|
|
1050
|
-
||!isPlainObject(arcane.source)
|
|
1051
|
-
||!isPlainObject(sdkBrowser)||sdkBrowser.authority!=='arcane-os-sdk'
|
|
1052
|
-
||!SHA256_PATTERN.test(sdkBrowser.manifestSha256??'')
|
|
1053
|
-
||!SHA256_PATTERN.test(sdkBrowser.contentSha256??'')
|
|
1054
|
-
||!isPlainObject(sdkBrowser.source)
|
|
1055
|
-
||!Array.isArray(sdkBrowser.source.dependencies)){
|
|
1056
|
-
fail('The composed workspace runtime receipt is missing its source authorities.');
|
|
1057
|
-
}
|
|
1058
|
-
return immutableJsonCopy({
|
|
1059
|
-
schemaVersion:1,
|
|
1060
|
-
kind:'arcane-app-runtime-authorities',
|
|
1061
|
-
sdkVersion:receipt.sdkVersion,
|
|
1062
|
-
projection:{
|
|
1063
|
-
fileCount:receipt.fileCount,
|
|
1064
|
-
totalBytes:receipt.totalBytes,
|
|
1065
|
-
contentSha256:receipt.contentSha256
|
|
1066
|
-
},
|
|
1067
|
-
sources:{
|
|
1068
|
-
arcane:{
|
|
1069
|
-
authority:arcane.authority,
|
|
1070
|
-
manifestSha256:arcane.manifestSha256,
|
|
1071
|
-
contentSha256:arcane.contentSha256,
|
|
1072
|
-
source:arcane.source
|
|
1073
|
-
},
|
|
1074
|
-
sdkBrowser:{
|
|
1075
|
-
authority:sdkBrowser.authority,
|
|
1076
|
-
manifestSha256:sdkBrowser.manifestSha256,
|
|
1077
|
-
contentSha256:sdkBrowser.contentSha256,
|
|
1078
|
-
source:sdkBrowser.source
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function packagedRuntimeProjection(receipt){
|
|
1085
|
-
const authorities=packagedRuntimeAuthorities(receipt);
|
|
1086
|
-
if(!Array.isArray(receipt.files)){
|
|
1087
|
-
fail('The composed workspace runtime receipt is missing its file inventory.',RUNTIME_PROJECTION_ERROR);
|
|
1088
|
-
}
|
|
1089
|
-
const files=[];
|
|
1090
|
-
let previous=null;
|
|
1091
|
-
let totalBytes=0;
|
|
1092
|
-
for(const [index,file] of receipt.files.entries()){
|
|
1093
|
-
if(!isPlainObject(file)){
|
|
1094
|
-
fail(`Workspace runtime projection files[${index}] is invalid.`,RUNTIME_PROJECTION_ERROR);
|
|
1095
|
-
}
|
|
1096
|
-
let relative;
|
|
1097
|
-
try{
|
|
1098
|
-
relative=normalizeRelativePath(file.path,`workspace runtime projection files[${index}].path`);
|
|
1099
|
-
}catch{
|
|
1100
|
-
fail(`Workspace runtime projection files[${index}].path is invalid.`,RUNTIME_PROJECTION_ERROR);
|
|
1101
|
-
}
|
|
1102
|
-
if(relative!==file.path
|
|
1103
|
-
||!Number.isSafeInteger(file.bytes)||file.bytes<0
|
|
1104
|
-
||!SHA256_PATTERN.test(file.sha256??'')
|
|
1105
|
-
||previous!==null&&compareText(previous,relative)>=0){
|
|
1106
|
-
fail(`Workspace runtime projection files[${index}] is invalid.`,RUNTIME_PROJECTION_ERROR);
|
|
1107
|
-
}
|
|
1108
|
-
totalBytes+=file.bytes;
|
|
1109
|
-
if(!Number.isSafeInteger(totalBytes)){
|
|
1110
|
-
fail('Workspace runtime projection byte total is invalid.',RUNTIME_PROJECTION_ERROR);
|
|
1111
|
-
}
|
|
1112
|
-
previous=relative;
|
|
1113
|
-
files.push({path:relative,bytes:file.bytes,sha256:file.sha256});
|
|
1114
|
-
}
|
|
1115
|
-
const contentSha256=createHash('sha256')
|
|
1116
|
-
.update(JSON.stringify(files))
|
|
1117
|
-
.digest('hex');
|
|
1118
|
-
if(files.length!==receipt.fileCount
|
|
1119
|
-
||totalBytes!==receipt.totalBytes
|
|
1120
|
-
||contentSha256!==receipt.contentSha256
|
|
1121
|
-
||files.length!==authorities.projection.fileCount
|
|
1122
|
-
||totalBytes!==authorities.projection.totalBytes
|
|
1123
|
-
||contentSha256!==authorities.projection.contentSha256){
|
|
1124
|
-
fail(
|
|
1125
|
-
'The workspace runtime file inventory does not match its admitted projection authority.',
|
|
1126
|
-
RUNTIME_PROJECTION_ERROR
|
|
1127
|
-
);
|
|
1128
|
-
}
|
|
1129
|
-
return immutableJsonCopy({
|
|
1130
|
-
schemaVersion:1,
|
|
1131
|
-
kind:'arcane-app-runtime-projection',
|
|
1132
|
-
sdkVersion:receipt.sdkVersion,
|
|
1133
|
-
pathPrefix:'arcane/',
|
|
1134
|
-
fileCount:files.length,
|
|
1135
|
-
totalBytes,
|
|
1136
|
-
contentSha256,
|
|
1137
|
-
files
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
async function hasExternalRuntimeAdmission(context){
|
|
1142
|
-
const lockPath=path.join(context.workspaceRoot,'arcane.lock.json');
|
|
1143
|
-
try{
|
|
1144
|
-
const info=await lstat(lockPath);
|
|
1145
|
-
if(info.isSymbolicLink()||!info.isFile()){
|
|
1146
|
-
fail('arcane.lock.json must be a real file before runtime provenance can be packaged.');
|
|
1147
|
-
}
|
|
1148
|
-
return true;
|
|
1149
|
-
}catch(error){
|
|
1150
|
-
if(error?.code==='ENOENT')return false;
|
|
1151
|
-
throw error;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
async function validateExternalRuntimeAdmission(context,{signal,onEvent}={}){
|
|
1156
|
-
const {validateWorkspace}=await import('../workspace.mjs');
|
|
1157
|
-
const validation=await validateWorkspace({
|
|
1158
|
-
workspaceRoot:context.workspaceRoot,
|
|
1159
|
-
appId:context.config.id,
|
|
1160
|
-
signal,
|
|
1161
|
-
onEvent
|
|
1162
|
-
});
|
|
1163
|
-
if(validation.workspaceMode!=='external'){
|
|
1164
|
-
fail('A workspace with arcane.lock.json must use the external SDK runtime contract.');
|
|
1165
|
-
}
|
|
1166
|
-
return validation;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
async function integratedWorkspaceCandidate(context){
|
|
1170
|
-
const packagePath=path.join(context.workspaceRoot,'package.json');
|
|
1171
|
-
let info;
|
|
1172
|
-
try{
|
|
1173
|
-
info=await lstat(packagePath);
|
|
1174
|
-
}catch(error){
|
|
1175
|
-
if(error?.code==='ENOENT')return false;
|
|
1176
|
-
throw error;
|
|
1177
|
-
}
|
|
1178
|
-
if(info.isSymbolicLink()||!info.isFile()){
|
|
1179
|
-
fail('workspace package.json must be a real file.');
|
|
1180
|
-
}
|
|
1181
|
-
const document=await readJsonDocument(packagePath,'workspace package.json');
|
|
1182
|
-
return document.value?.name==='arcane-os'&&document.value?.type==='module';
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
function packageRuntimeLocations(context,validation){
|
|
1186
|
-
const installation=validation?.sdkInstallation;
|
|
1187
|
-
const licenseRoute=context.rootConfig.sharedPayloads['browser-runtime']?.find(
|
|
1188
|
-
route=>route.destination==='licenses/arcane-os'
|
|
1189
|
-
);
|
|
1190
|
-
if(validation?.workspaceMode!=='external'||!isPlainObject(installation)
|
|
1191
|
-
||typeof installation.packageSource!=='string'
|
|
1192
|
-
||typeof installation.canonicalPackageRoot!=='string'
|
|
1193
|
-
||typeof installation.runtimeRoot!=='string'
|
|
1194
|
-
||typeof installation.browserRuntimeRoot!=='string'
|
|
1195
|
-
||licenseRoute?.source!==installation.packageSource
|
|
1196
|
-
||path.resolve(installation.runtimeRoot)
|
|
1197
|
-
!==path.join(path.resolve(installation.canonicalPackageRoot),'runtime')
|
|
1198
|
-
||path.resolve(installation.browserRuntimeRoot)
|
|
1199
|
-
!==path.join(path.resolve(installation.canonicalPackageRoot),'browser-runtime')){
|
|
1200
|
-
fail('External workspace validation did not return its bound SDK installation authority.');
|
|
1201
|
-
}
|
|
1202
|
-
return Object.freeze({
|
|
1203
|
-
runtimeRoot:installation.runtimeRoot,
|
|
1204
|
-
browserRuntimeRoot:installation.browserRuntimeRoot
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
async function authenticatePackageRuntimeVerificationState(context,state,{signal,validation}={}){
|
|
1209
|
-
assertOnlyKeys(
|
|
1210
|
-
state,
|
|
1211
|
-
new Set(['runtimeReceipt','sdkBrowserRuntimeReceipt','workspaceRuntimeReceipt']),
|
|
1212
|
-
'runtime verification state'
|
|
1213
|
-
);
|
|
1214
|
-
const descriptors=Object.getOwnPropertyDescriptors(state);
|
|
1215
|
-
const required=['runtimeReceipt','sdkBrowserRuntimeReceipt','workspaceRuntimeReceipt'];
|
|
1216
|
-
if(required.some(key=>!Object.hasOwn(descriptors,key)||!Object.hasOwn(descriptors[key],'value'))){
|
|
1217
|
-
fail('The runtime verification state must use fixed receipt references, not accessors.');
|
|
1218
|
-
}
|
|
1219
|
-
const snapshot=Object.freeze({
|
|
1220
|
-
runtimeReceipt:descriptors.runtimeReceipt.value,
|
|
1221
|
-
sdkBrowserRuntimeReceipt:descriptors.sdkBrowserRuntimeReceipt.value,
|
|
1222
|
-
workspaceRuntimeReceipt:descriptors.workspaceRuntimeReceipt.value
|
|
1223
|
-
});
|
|
1224
|
-
if(!snapshot.runtimeReceipt
|
|
1225
|
-
||!snapshot.sdkBrowserRuntimeReceipt
|
|
1226
|
-
||!snapshot.workspaceRuntimeReceipt){
|
|
1227
|
-
fail('The runtime verification state must contain all three authenticated receipts.');
|
|
1228
|
-
}
|
|
1229
|
-
const {runtimeRoot,browserRuntimeRoot}=packageRuntimeLocations(context,validation);
|
|
1230
|
-
await authenticateRuntimeReceipt(snapshot.runtimeReceipt,{runtimeRoot,signal});
|
|
1231
|
-
await authenticateSdkBrowserRuntimeReceipt(snapshot.sdkBrowserRuntimeReceipt,{
|
|
1232
|
-
browserRuntimeRoot,
|
|
1233
|
-
signal
|
|
1234
|
-
});
|
|
1235
|
-
await authenticateWorkspaceRuntimeReceipt(snapshot.workspaceRuntimeReceipt,{
|
|
1236
|
-
workspaceRoot:context.workspaceRoot,
|
|
1237
|
-
signal
|
|
1238
|
-
});
|
|
1239
|
-
const workspaceReceipt=snapshot.workspaceRuntimeReceipt;
|
|
1240
|
-
const expectedArcaneSource={
|
|
1241
|
-
authority:'arcane-os-sdk',
|
|
1242
|
-
location:snapshot.runtimeReceipt.canonicalLocation,
|
|
1243
|
-
manifestSha256:snapshot.runtimeReceipt.manifestSha256,
|
|
1244
|
-
contentSha256:snapshot.runtimeReceipt.contentSha256,
|
|
1245
|
-
source:snapshot.runtimeReceipt.source
|
|
1246
|
-
};
|
|
1247
|
-
const expectedBrowserSource={
|
|
1248
|
-
authority:'arcane-os-sdk',
|
|
1249
|
-
location:snapshot.sdkBrowserRuntimeReceipt.canonicalLocation,
|
|
1250
|
-
manifestSha256:snapshot.sdkBrowserRuntimeReceipt.manifestSha256,
|
|
1251
|
-
contentSha256:snapshot.sdkBrowserRuntimeReceipt.contentSha256,
|
|
1252
|
-
source:snapshot.sdkBrowserRuntimeReceipt.source
|
|
1253
|
-
};
|
|
1254
|
-
if(workspaceReceipt.sourceRuntimeLocation!==snapshot.runtimeReceipt.canonicalLocation
|
|
1255
|
-
||workspaceReceipt.sourceManifestSha256!==snapshot.runtimeReceipt.manifestSha256
|
|
1256
|
-
||workspaceReceipt.sourceContentSha256!==snapshot.runtimeReceipt.contentSha256
|
|
1257
|
-
||workspaceReceipt.sourceBrowserRuntimeLocation
|
|
1258
|
-
!==snapshot.sdkBrowserRuntimeReceipt.canonicalLocation
|
|
1259
|
-
||workspaceReceipt.sourceBrowserManifestSha256
|
|
1260
|
-
!==snapshot.sdkBrowserRuntimeReceipt.manifestSha256
|
|
1261
|
-
||workspaceReceipt.sourceBrowserContentSha256
|
|
1262
|
-
!==snapshot.sdkBrowserRuntimeReceipt.contentSha256
|
|
1263
|
-
||workspaceReceipt.sdkVersion!==snapshot.runtimeReceipt.sdkVersion
|
|
1264
|
-
||workspaceReceipt.sdkVersion!==snapshot.sdkBrowserRuntimeReceipt.sdkVersion
|
|
1265
|
-
||!isDeepStrictEqual(workspaceReceipt.sources?.arcane,expectedArcaneSource)
|
|
1266
|
-
||!isDeepStrictEqual(workspaceReceipt.sources?.sdkBrowser,expectedBrowserSource)){
|
|
1267
|
-
fail('The workspace runtime receipt is not bound to the supplied source runtime receipts.');
|
|
1268
|
-
}
|
|
1269
|
-
return snapshot;
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
async function issuePackageRuntimeVerificationState(context,{signal,onEvent,validation}={}){
|
|
1273
|
-
const {runtimeRoot,browserRuntimeRoot}=packageRuntimeLocations(context,validation);
|
|
1274
|
-
const [runtimeReceipt,sdkBrowserRuntimeReceipt]=await Promise.all([
|
|
1275
|
-
verifyRuntime({runtimeRoot,signal,onEvent}),
|
|
1276
|
-
verifySdkBrowserRuntime({browserRuntimeRoot,signal,onEvent})
|
|
1277
|
-
]);
|
|
1278
|
-
const workspaceRuntimeReceipt=await verifyWorkspaceRuntime({
|
|
1279
|
-
workspaceRoot:context.workspaceRoot,
|
|
1280
|
-
runtimeRoot,
|
|
1281
|
-
runtimeReceipt,
|
|
1282
|
-
browserRuntimeRoot,
|
|
1283
|
-
sdkBrowserRuntimeReceipt,
|
|
1284
|
-
signal,
|
|
1285
|
-
onEvent
|
|
1286
|
-
});
|
|
1287
|
-
return Object.freeze({runtimeReceipt,sdkBrowserRuntimeReceipt,workspaceRuntimeReceipt});
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
function orderedBrowserDocumentPaths(entry,paths,label){
|
|
1291
|
-
const selected=new Map();
|
|
1292
|
-
for(const relative of paths){
|
|
1293
|
-
const normalized=normalizeRelativePath(relative,label);
|
|
1294
|
-
const key=pathKey(normalized);
|
|
1295
|
-
const prior=selected.get(key);
|
|
1296
|
-
if(prior!==undefined){
|
|
1297
|
-
fail(`Package destination collision: ${prior} and ${normalized}.`);
|
|
1298
|
-
}
|
|
1299
|
-
selected.set(key,normalized);
|
|
1300
|
-
}
|
|
1301
|
-
const selectedEntry=selected.get(pathKey(entry));
|
|
1302
|
-
if(selectedEntry!==entry){
|
|
1303
|
-
fail(`The configured entry file was not found in the package payload: ${entry}`);
|
|
1304
|
-
}
|
|
1305
|
-
return Object.freeze([
|
|
1306
|
-
entry,
|
|
1307
|
-
...[...selected.values()]
|
|
1308
|
-
.filter(relative=>relative!==entry
|
|
1309
|
-
&&isHtmlDocument(relative))
|
|
1310
|
-
.sort(compareText)
|
|
1311
|
-
]);
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
function isHtmlDocument(relative){
|
|
1315
|
-
const extension=path.posix.extname(relative).toLowerCase();
|
|
1316
|
-
return extension==='.html'||extension==='.htm';
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
async function packageImportMapDocuments(context,{signal}={}){
|
|
1320
|
-
const {workspaceRoot,appRoot,config}=context;
|
|
1321
|
-
const files=await enumerateRoute({
|
|
1322
|
-
workspaceRoot,
|
|
1323
|
-
sourceRoot:appRoot,
|
|
1324
|
-
destinationRoot:`apps/${config.id}`,
|
|
1325
|
-
include:config.include,
|
|
1326
|
-
exclude:config.exclude,
|
|
1327
|
-
label:`apps.${config.id}`,
|
|
1328
|
-
appPayload:true,
|
|
1329
|
-
signal
|
|
1330
|
-
});
|
|
1331
|
-
return orderedBrowserDocumentPaths(
|
|
1332
|
-
config.entry,
|
|
1333
|
-
files.map(file=>file.sourceRelative),
|
|
1334
|
-
`apps.${config.id} browser document`
|
|
1335
|
-
);
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
async function refreshPackageImportMap(context,{
|
|
1339
|
-
runtimeVerificationState,
|
|
1340
|
-
workspaceOperationLease,
|
|
1341
|
-
signal,
|
|
1342
|
-
onEvent
|
|
1343
|
-
}={}){
|
|
1344
|
-
const external=await hasExternalRuntimeAdmission(context);
|
|
1345
|
-
if(!external&&!await integratedWorkspaceCandidate(context)){
|
|
1346
|
-
if(runtimeVerificationState!==undefined){
|
|
1347
|
-
fail('A runtime verification state cannot be supplied without an external runtime admission.');
|
|
1348
|
-
}
|
|
1349
|
-
return Object.freeze({importMapReceipt:null,runtimeVerificationState:null});
|
|
1350
|
-
}
|
|
1351
|
-
const {validateWorkspace}=await import('../workspace.mjs');
|
|
1352
|
-
const validation=await validateWorkspace({
|
|
1353
|
-
workspaceRoot:context.workspaceRoot,
|
|
1354
|
-
appId:context.config.id,
|
|
1355
|
-
allowMissingManagedImportMap:true,
|
|
1356
|
-
signal,
|
|
1357
|
-
onEvent
|
|
1358
|
-
});
|
|
1359
|
-
if(validation.workspaceMode!=='external'&&runtimeVerificationState!==undefined){
|
|
1360
|
-
fail('A runtime verification state cannot be supplied to an integrated workspace.');
|
|
1361
|
-
}
|
|
1362
|
-
if(validation.workspaceMode==='integrated'
|
|
1363
|
-
&&validation.config.browserRuntimeLayout==='integrated-legacy'){
|
|
1364
|
-
return Object.freeze({
|
|
1365
|
-
importMapReceipt:Object.freeze({skipped:true,workspaceMode:'integrated'}),
|
|
1366
|
-
runtimeVerificationState:null
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
let workspaceRuntimeReceipt;
|
|
1371
|
-
let authenticatedRuntimeState=null;
|
|
1372
|
-
if(validation.workspaceMode==='external'){
|
|
1373
|
-
authenticatedRuntimeState=runtimeVerificationState===undefined
|
|
1374
|
-
?await issuePackageRuntimeVerificationState(context,{signal,onEvent,validation})
|
|
1375
|
-
:await authenticatePackageRuntimeVerificationState(
|
|
1376
|
-
context,
|
|
1377
|
-
runtimeVerificationState,
|
|
1378
|
-
{signal,validation}
|
|
1379
|
-
);
|
|
1380
|
-
workspaceRuntimeReceipt=authenticatedRuntimeState.workspaceRuntimeReceipt;
|
|
1381
|
-
}
|
|
1382
|
-
const documents=await packageImportMapDocuments(context,{signal});
|
|
1383
|
-
const importMapReceipt=await generateImportMap({
|
|
1384
|
-
workspaceRoot:context.workspaceRoot,
|
|
1385
|
-
appId:context.config.id,
|
|
1386
|
-
appRoot:context.appRoot,
|
|
1387
|
-
entry:context.config.entry,
|
|
1388
|
-
documents,
|
|
1389
|
-
workspaceRuntimeReceipt,
|
|
1390
|
-
workspaceOperationLease,
|
|
1391
|
-
signal,
|
|
1392
|
-
onEvent
|
|
1393
|
-
});
|
|
1394
|
-
return Object.freeze({importMapReceipt,runtimeVerificationState:authenticatedRuntimeState});
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
function authenticatedImportMapReceipt(receipt){
|
|
1398
|
-
if(receipt==null||receipt.skipped===true)return receipt;
|
|
1399
|
-
if(receipt.committed!==true||!Array.isArray(receipt.cleanupWarnings)){
|
|
1400
|
-
fail(
|
|
1401
|
-
'The generated import-map receipt is incomplete; the package release was not assembled.'
|
|
1402
|
-
);
|
|
1403
|
-
}
|
|
1404
|
-
if(receipt.cleanupWarnings.length!==0){
|
|
1405
|
-
fail(
|
|
1406
|
-
'The generated import map committed with cleanup warnings; the package release '
|
|
1407
|
-
+`was not assembled: ${receipt.cleanupWarnings.join('; ')}`,
|
|
1408
|
-
'ARCANE_IMPORT_MAP_CLEANUP_FAILED'
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
return receipt;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
function importMapReceiptFiles(context,receipt){
|
|
1415
|
-
if(receipt==null||receipt.skipped===true)return Object.freeze([]);
|
|
1416
|
-
if(!Array.isArray(receipt.files)||receipt.files.length<2
|
|
1417
|
-
||!Number.isSafeInteger(receipt.documentCount)||receipt.documentCount<1
|
|
1418
|
-
||!Array.isArray(receipt.documentPaths)
|
|
1419
|
-
||receipt.documentPaths.length!==receipt.documentCount
|
|
1420
|
-
||receipt.files.length!==receipt.documentCount+1){
|
|
1421
|
-
fail('The generated import-map receipt does not bind its committed artifact and browser documents.');
|
|
1422
|
-
}
|
|
1423
|
-
const records=[];
|
|
1424
|
-
const artifact=receipt.files[0];
|
|
1425
|
-
assertOnlyKeys(
|
|
1426
|
-
artifact,
|
|
1427
|
-
new Set(['role','path','bytes','sha256']),
|
|
1428
|
-
'import-map receipt files[0]'
|
|
1429
|
-
);
|
|
1430
|
-
const artifactPath=`apps/${context.config.id}/modules/arcane.importmap.json`;
|
|
1431
|
-
if(artifact.role!=='artifact'
|
|
1432
|
-
||normalizeRelativePath(artifact.path,'import-map receipt files[0].path')!==artifactPath
|
|
1433
|
-
||!Number.isSafeInteger(artifact.bytes)||artifact.bytes<1
|
|
1434
|
-
||!SHA256_PATTERN.test(artifact.sha256??'')){
|
|
1435
|
-
fail('The generated import-map receipt artifact record is invalid.');
|
|
1436
|
-
}
|
|
1437
|
-
records.push(Object.freeze({...artifact}));
|
|
1438
|
-
|
|
1439
|
-
const seen=new Set();
|
|
1440
|
-
let previousDocument=null;
|
|
1441
|
-
for(const [documentIndex,documentPath] of receipt.documentPaths.entries()){
|
|
1442
|
-
const index=documentIndex+1;
|
|
1443
|
-
if(typeof documentPath!=='string'||!path.isAbsolute(documentPath)){
|
|
1444
|
-
fail(
|
|
1445
|
-
`The generated import-map receipt documentPaths[${documentIndex}] is invalid.`
|
|
1446
|
-
);
|
|
1447
|
-
}
|
|
1448
|
-
const resolved=path.resolve(documentPath);
|
|
1449
|
-
if(!isInside(context.appRoot,resolved)){
|
|
1450
|
-
fail(
|
|
1451
|
-
`The generated import-map receipt documentPaths[${documentIndex}] leaves its `
|
|
1452
|
-
+'application root.'
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
const relative=normalizeRelativePath(
|
|
1456
|
-
path.relative(context.appRoot,resolved).replaceAll('\\','/'),
|
|
1457
|
-
`import-map receipt documentPaths[${documentIndex}]`
|
|
1458
|
-
);
|
|
1459
|
-
const key=pathKey(relative);
|
|
1460
|
-
if(seen.has(key)){
|
|
1461
|
-
fail(`The generated import-map receipt repeats a browser document: ${relative}.`);
|
|
1462
|
-
}
|
|
1463
|
-
seen.add(key);
|
|
1464
|
-
if(documentIndex===0&&relative!==context.config.entry){
|
|
1465
|
-
fail('The generated import-map receipt does not preserve its configured entry document.');
|
|
1466
|
-
}
|
|
1467
|
-
if(documentIndex>0){
|
|
1468
|
-
if(!isHtmlDocument(relative)
|
|
1469
|
-
||previousDocument!==null&&compareText(previousDocument,relative)>=0){
|
|
1470
|
-
fail(`The generated import-map receipt browser document order is invalid: ${relative}.`);
|
|
1471
|
-
}
|
|
1472
|
-
previousDocument=relative;
|
|
1473
|
-
}
|
|
1474
|
-
const record=receipt.files[index];
|
|
1475
|
-
assertOnlyKeys(
|
|
1476
|
-
record,
|
|
1477
|
-
new Set(['role','path','bytes','sha256']),
|
|
1478
|
-
`import-map receipt files[${index}]`
|
|
1479
|
-
);
|
|
1480
|
-
const wanted={
|
|
1481
|
-
role:documentIndex===0?'entry':'document',
|
|
1482
|
-
path:`apps/${context.config.id}/${relative}`
|
|
1483
|
-
};
|
|
1484
|
-
if(record.role!==wanted.role
|
|
1485
|
-
||normalizeRelativePath(record.path,`import-map receipt files[${index}].path`)
|
|
1486
|
-
!==wanted.path
|
|
1487
|
-
||!Number.isSafeInteger(record.bytes)||record.bytes<1
|
|
1488
|
-
||!SHA256_PATTERN.test(record.sha256??'')){
|
|
1489
|
-
fail(`The generated import-map receipt ${wanted.role} record is invalid.`);
|
|
1490
|
-
}
|
|
1491
|
-
records.push(Object.freeze({...record}));
|
|
1492
|
-
}
|
|
1493
|
-
return Object.freeze(records);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
async function authenticateImportMapFiles(context,receipt,{signal}={}){
|
|
1497
|
-
const records=importMapReceiptFiles(context,receipt);
|
|
1498
|
-
for(const record of records){
|
|
1499
|
-
const filePath=resolveInside(
|
|
1500
|
-
context.workspaceRoot,
|
|
1501
|
-
record.path,
|
|
1502
|
-
`import-map receipt ${record.role} path`
|
|
1503
|
-
);
|
|
1504
|
-
let verified;
|
|
1505
|
-
try{
|
|
1506
|
-
verified=await sha256WithIdentity(filePath,{
|
|
1507
|
-
signal,
|
|
1508
|
-
label:`import-map receipt ${record.role}`
|
|
1509
|
-
});
|
|
1510
|
-
}catch(error){
|
|
1511
|
-
fail(
|
|
1512
|
-
`The generated import-map ${record.role} is unavailable after commit: ${error.message}`
|
|
1513
|
-
);
|
|
1514
|
-
}
|
|
1515
|
-
if(verified.identity.bytes!==record.bytes||verified.sha256!==record.sha256){
|
|
1516
|
-
fail(`The generated import-map ${record.role} changed after it was committed.`);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
return records;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
async function authenticateCollectedImportMapFiles(context,files,records,{signal}={}){
|
|
1523
|
-
if(records.length>0){
|
|
1524
|
-
const appPrefix=`apps/${context.config.id}/`;
|
|
1525
|
-
const expectedDocuments=orderedBrowserDocumentPaths(
|
|
1526
|
-
context.config.entry,
|
|
1527
|
-
files
|
|
1528
|
-
.filter(file=>file.destination.startsWith(appPrefix))
|
|
1529
|
-
.map(file=>file.destination.slice(appPrefix.length)),
|
|
1530
|
-
`apps.${context.config.id} collected browser document`
|
|
1531
|
-
).map(relative=>`${appPrefix}${relative}`);
|
|
1532
|
-
const committedDocuments=records.slice(1).map(record=>record.path);
|
|
1533
|
-
if(!isDeepStrictEqual(committedDocuments,expectedDocuments)){
|
|
1534
|
-
fail('The generated import-map receipt does not bind every packaged browser document.');
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
for(const record of records){
|
|
1538
|
-
const collected=files.find(file=>file.destination===record.path);
|
|
1539
|
-
if(!collected||collected.bytes!==record.bytes){
|
|
1540
|
-
fail(`The package payload does not contain the committed import-map ${record.role}.`);
|
|
1541
|
-
}
|
|
1542
|
-
const verified=await sha256WithIdentity(collected.source,{
|
|
1543
|
-
signal,
|
|
1544
|
-
expectedIdentity:collected.identity,
|
|
1545
|
-
label:`collected import-map ${record.role}`
|
|
1546
|
-
});
|
|
1547
|
-
if(verified.identity.bytes!==record.bytes||verified.sha256!==record.sha256){
|
|
1548
|
-
fail(`The collected import-map ${record.role} does not match its committed receipt.`);
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
function authenticatePackagedImportMapFiles(release,records){
|
|
1554
|
-
for(const record of records){
|
|
1555
|
-
const packaged=release.files.find(file=>file.path===record.path);
|
|
1556
|
-
if(!packaged||packaged.bytes!==record.bytes||packaged.sha256!==record.sha256){
|
|
1557
|
-
fail(`The packaged import-map ${record.role} does not match its committed receipt.`);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
async function prepareRuntimeAuthorityState(context,{
|
|
1563
|
-
validation,
|
|
1564
|
-
runtimeVerificationState,
|
|
1565
|
-
signal,
|
|
1566
|
-
onEvent
|
|
1567
|
-
}={}){
|
|
1568
|
-
if(!await hasExternalRuntimeAdmission(context)){
|
|
1569
|
-
if(runtimeVerificationState!==undefined){
|
|
1570
|
-
fail('A runtime verification state cannot be supplied to an integrated workspace.');
|
|
1571
|
-
}
|
|
1572
|
-
return null;
|
|
1573
|
-
}
|
|
1574
|
-
const workspaceValidation=await validateExternalRuntimeAdmission(context,{signal,onEvent});
|
|
1575
|
-
|
|
1576
|
-
let verificationState=null;
|
|
1577
|
-
let receipt=null;
|
|
1578
|
-
if(runtimeVerificationState!==undefined){
|
|
1579
|
-
verificationState=await authenticatePackageRuntimeVerificationState(
|
|
1580
|
-
context,
|
|
1581
|
-
runtimeVerificationState,
|
|
1582
|
-
{signal,validation:workspaceValidation}
|
|
1583
|
-
);
|
|
1584
|
-
receipt=verificationState.workspaceRuntimeReceipt;
|
|
1585
|
-
}else if(validation?.kind==='arcane-workspace-runtime-verification'){
|
|
1586
|
-
receipt=validation;
|
|
1587
|
-
await authenticateWorkspaceRuntimeReceipt(receipt,{
|
|
1588
|
-
workspaceRoot:context.workspaceRoot,
|
|
1589
|
-
signal
|
|
1590
|
-
});
|
|
1591
|
-
}else{
|
|
1592
|
-
verificationState=await issuePackageRuntimeVerificationState(context,{
|
|
1593
|
-
signal,
|
|
1594
|
-
onEvent,
|
|
1595
|
-
validation:workspaceValidation
|
|
1596
|
-
});
|
|
1597
|
-
receipt=verificationState.workspaceRuntimeReceipt;
|
|
1598
|
-
}
|
|
1599
|
-
return Object.freeze({
|
|
1600
|
-
receipt,
|
|
1601
|
-
document:packagedRuntimeAuthorities(receipt),
|
|
1602
|
-
projectionDocument:packagedRuntimeProjection(receipt),
|
|
1603
|
-
verificationState
|
|
1604
|
-
});
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
async function authenticateRuntimeAuthorityState(context,state,{signal,onEvent}={}){
|
|
1608
|
-
if(state===null){
|
|
1609
|
-
if(await hasExternalRuntimeAdmission(context)){
|
|
1610
|
-
fail('External runtime authority admission appeared during package verification.');
|
|
1611
|
-
}
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
if(!await hasExternalRuntimeAdmission(context)){
|
|
1615
|
-
fail('External runtime authority admission disappeared during package verification.');
|
|
1616
|
-
}
|
|
1617
|
-
const workspaceValidation=await validateExternalRuntimeAdmission(context,{signal,onEvent});
|
|
1618
|
-
if(state.verificationState){
|
|
1619
|
-
await authenticatePackageRuntimeVerificationState(
|
|
1620
|
-
context,
|
|
1621
|
-
state.verificationState,
|
|
1622
|
-
{signal,validation:workspaceValidation}
|
|
1623
|
-
);
|
|
1624
|
-
}else{
|
|
1625
|
-
await authenticateWorkspaceRuntimeReceipt(state.receipt,{
|
|
1626
|
-
workspaceRoot:context.workspaceRoot,
|
|
1627
|
-
signal
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
|
-
if(!isDeepStrictEqual(packagedRuntimeAuthorities(state.receipt),state.document)){
|
|
1631
|
-
fail('External runtime source authorities changed during package verification.');
|
|
1632
|
-
}
|
|
1633
|
-
if(!isDeepStrictEqual(packagedRuntimeProjection(state.receipt),state.projectionDocument)){
|
|
1634
|
-
fail(
|
|
1635
|
-
'External runtime projection inventory changed during package verification.',
|
|
1636
|
-
RUNTIME_PROJECTION_ERROR
|
|
1637
|
-
);
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
async function getAppContext({
|
|
1642
|
-
workspaceRoot:requestedWorkspaceRoot,
|
|
1643
|
-
appId,
|
|
1644
|
-
bindDescriptorAuthority=false,
|
|
1645
|
-
signal
|
|
1646
|
-
}){
|
|
1647
|
-
const workspaceRoot=normalizeWorkspaceRoot(requestedWorkspaceRoot);
|
|
1648
|
-
|
|
1649
|
-
if(typeof appId!=='string'||!APP_ID_PATTERN.test(appId)){
|
|
1650
|
-
fail(`Invalid app id: ${String(appId)}`);
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
const rootConfig=await loadRootConfig(workspaceRoot);
|
|
1654
|
-
const appsRoot=path.join(workspaceRoot,rootConfig.appsRoot);
|
|
1655
|
-
const appRoot=resolveInside(appsRoot,appId,'app id');
|
|
1656
|
-
let appInfo;
|
|
1657
|
-
|
|
1658
|
-
try{
|
|
1659
|
-
appInfo=await lstat(appRoot);
|
|
1660
|
-
}catch(error){
|
|
1661
|
-
if(error?.code==='ENOENT'){
|
|
1662
|
-
const available=(await readdir(appsRoot,{withFileTypes:true}))
|
|
1663
|
-
.filter(entry=>entry.isDirectory()&&APP_ID_PATTERN.test(entry.name))
|
|
1664
|
-
.map(entry=>entry.name)
|
|
1665
|
-
.sort(compareText);
|
|
1666
|
-
fail(`Unknown app "${appId}". Available apps: ${available.join(', ')||'[none]'}.`);
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
throw error;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
if(appInfo.isSymbolicLink()||!appInfo.isDirectory()){
|
|
1673
|
-
fail(`apps/${appId} must be a real directory, not a link or special entry.`);
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
await assertNoLinks(appsRoot,appRoot,`apps/${appId}`);
|
|
1677
|
-
const configPath=path.join(appRoot,APP_CONFIG_NAME);
|
|
1678
|
-
const configDocument=await readJsonDocument(
|
|
1679
|
-
configPath,
|
|
1680
|
-
`apps/${appId}/${APP_CONFIG_NAME}`
|
|
1681
|
-
);
|
|
1682
|
-
const config=validateAppConfig(configDocument.value,appId,rootConfig,configPath);
|
|
1683
|
-
await validateLocalAIModelDefinitions(appRoot,config);
|
|
1684
|
-
const distRoot=path.join(workspaceRoot,rootConfig.distRoot);
|
|
1685
|
-
const outputRoot=resolveInside(distRoot,appId,'package output');
|
|
1686
|
-
const distExists=await assertSafeDistBoundary(workspaceRoot,distRoot);
|
|
1687
|
-
|
|
1688
|
-
if(distExists){
|
|
1689
|
-
await assertOptionalSafeOutput(distRoot,outputRoot,appId);
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
const context={workspaceRoot,rootConfig,appsRoot,appRoot,distRoot,outputRoot,config};
|
|
1693
|
-
if(!bindDescriptorAuthority)return context;
|
|
1694
|
-
return {
|
|
1695
|
-
...context,
|
|
1696
|
-
descriptorAuthority:await createAppDescriptorAuthority(context,configDocument,{signal})
|
|
1697
|
-
};
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
async function enumerateRoute({
|
|
1701
|
-
workspaceRoot,
|
|
1702
|
-
sourceRoot,
|
|
1703
|
-
destinationRoot,
|
|
1704
|
-
include,
|
|
1705
|
-
exclude,
|
|
1706
|
-
label,
|
|
1707
|
-
appPayload=false,
|
|
1708
|
-
signal
|
|
1709
|
-
}){
|
|
1710
|
-
throwIfAborted(signal);
|
|
1711
|
-
await assertNoLinks(workspaceRoot,sourceRoot,`${label}.source`);
|
|
1712
|
-
const files=[];
|
|
1713
|
-
|
|
1714
|
-
async function visit(absolute,relative){
|
|
1715
|
-
throwIfAborted(signal);
|
|
1716
|
-
if(isExcluded(relative,exclude)||(appPayload&&isAppSourceForbidden(relative))){
|
|
1717
|
-
return;
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
const info=await lstat(absolute,{bigint:true});
|
|
1721
|
-
|
|
1722
|
-
if(info.isSymbolicLink()){
|
|
1723
|
-
fail(`${label} contains a symbolic link or junction: ${relative}`);
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
if(info.isDirectory()){
|
|
1727
|
-
const entries=await readdir(absolute,{withFileTypes:true});
|
|
1728
|
-
|
|
1729
|
-
for(const entry of entries.sort((left,right)=>compareText(left.name,right.name))){
|
|
1730
|
-
throwIfAborted(signal);
|
|
1731
|
-
const childRelative=`${relative}/${entry.name}`;
|
|
1732
|
-
|
|
1733
|
-
if(isExcluded(childRelative,exclude)
|
|
1734
|
-
||(appPayload&&isAppSourceForbidden(childRelative))){
|
|
1735
|
-
continue;
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
if(entry.isSymbolicLink()){
|
|
1739
|
-
fail(`${label} contains a symbolic link or junction: ${childRelative}`);
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
await visit(path.join(absolute,entry.name),childRelative);
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
return;
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
if(!info.isFile()){
|
|
1749
|
-
fail(`${label} contains a non-file entry: ${relative}`);
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
const destination=destinationRoot==='.'
|
|
1753
|
-
?relative
|
|
1754
|
-
:`${destinationRoot}/${relative}`;
|
|
1755
|
-
const bytes=Number(info.size);
|
|
1756
|
-
if(!Number.isSafeInteger(bytes)||bytes<0){
|
|
1757
|
-
fail(`${label} contains a file whose size is not safely representable: ${relative}`);
|
|
1758
|
-
}
|
|
1759
|
-
files.push({
|
|
1760
|
-
source:absolute,
|
|
1761
|
-
sourceRelative:relative,
|
|
1762
|
-
destination:normalizeRelativePath(destination,`${label} destination`),
|
|
1763
|
-
bytes,
|
|
1764
|
-
identity:fileIdentity(info),
|
|
1765
|
-
label
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
for(const allowed of include){
|
|
1770
|
-
throwIfAborted(signal);
|
|
1771
|
-
if(isExcluded(allowed,exclude)||(appPayload&&isAppSourceForbidden(allowed))){
|
|
1772
|
-
continue;
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
const candidate=resolveInside(sourceRoot,allowed,`${label}.include`);
|
|
1776
|
-
|
|
1777
|
-
try{
|
|
1778
|
-
await assertNoLinks(sourceRoot,candidate,`${label}.include "${allowed}"`);
|
|
1779
|
-
}catch(error){
|
|
1780
|
-
if(error?.code==='ENOENT'){
|
|
1781
|
-
fail(`${label}.include does not exist: ${allowed}`);
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
throw error;
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
await visit(candidate,allowed);
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
return files;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
function workspaceLocationIdentity(info){
|
|
1794
|
-
return Object.freeze({
|
|
1795
|
-
device:String(info.dev),
|
|
1796
|
-
inode:String(info.ino)
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
function workspaceLocationMatches(info,identity){
|
|
1801
|
-
return String(info.dev)===identity.device&&String(info.ino)===identity.inode;
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
function normalizeSharedPayloadSelection(sharedPayloadIds,rootConfig,{required=false}={}){
|
|
1805
|
-
const selected=sharedPayloadIds===undefined
|
|
1806
|
-
?Object.keys(rootConfig.sharedPayloads)
|
|
1807
|
-
:sharedPayloadIds;
|
|
1808
|
-
|
|
1809
|
-
if(!Array.isArray(selected)||selected.length>256||(required&&selected.length===0)){
|
|
1810
|
-
fail('sharedPayloadIds must be a non-empty array with at most 256 entries.');
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
const normalized=[];
|
|
1814
|
-
const seen=new Set();
|
|
1815
|
-
for(const [index,id] of selected.entries()){
|
|
1816
|
-
if(typeof id!=='string'||!SAFE_SHARED_ID_PATTERN.test(id)
|
|
1817
|
-
||!Object.hasOwn(rootConfig.sharedPayloads,id)){
|
|
1818
|
-
fail(`sharedPayloadIds[${index}] references an unknown shared payload: ${String(id)}`);
|
|
1819
|
-
}
|
|
1820
|
-
if(seen.has(id))fail(`sharedPayloadIds contains a duplicate shared payload: ${id}`);
|
|
1821
|
-
seen.add(id);
|
|
1822
|
-
normalized.push(id);
|
|
1823
|
-
}
|
|
1824
|
-
return Object.freeze(normalized.sort(compareText));
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
function assertSnapshotCoverage(state,requiredIds){
|
|
1828
|
-
for(const id of requiredIds){
|
|
1829
|
-
if(!Object.hasOwn(state.filesBySharedPayload,id)){
|
|
1830
|
-
fail(`Shared payload snapshot does not include the required payload: ${id}`);
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
async function authenticateSharedPayloadSnapshotState(receipt,{
|
|
1836
|
-
workspaceRoot,
|
|
1837
|
-
sharedPayloadIds,
|
|
1838
|
-
signal
|
|
1839
|
-
}={}){
|
|
1840
|
-
throwIfAborted(signal);
|
|
1841
|
-
const state=issuedSharedPayloadSnapshots.get(receipt);
|
|
1842
|
-
if(!state)fail('Shared payload snapshot was not issued by this SDK process.');
|
|
1843
|
-
const requested=normalizeWorkspaceRoot(workspaceRoot);
|
|
1844
|
-
let workspaceInfo;
|
|
1845
|
-
let canonicalWorkspaceRoot;
|
|
1846
|
-
try{
|
|
1847
|
-
workspaceInfo=await lstat(requested,{bigint:true});
|
|
1848
|
-
canonicalWorkspaceRoot=await realpath(requested);
|
|
1849
|
-
}catch(error){
|
|
1850
|
-
fail(`Shared payload snapshot workspace is unavailable: ${error.message}`);
|
|
1851
|
-
}
|
|
1852
|
-
if(workspaceInfo.isSymbolicLink()||!workspaceInfo.isDirectory()
|
|
1853
|
-
||canonicalWorkspaceRoot!==state.canonicalWorkspaceRoot
|
|
1854
|
-
||receipt.canonicalWorkspaceRoot!==canonicalWorkspaceRoot
|
|
1855
|
-
||!workspaceLocationMatches(workspaceInfo,state.workspaceIdentity)){
|
|
1856
|
-
fail('Shared payload snapshot belongs to a different workspace identity.');
|
|
1857
|
-
}
|
|
1858
|
-
let configInfo;
|
|
1859
|
-
try{
|
|
1860
|
-
configInfo=await lstat(state.rootConfigPath,{bigint:true});
|
|
1861
|
-
}catch(error){
|
|
1862
|
-
fail(`Shared payload snapshot root configuration changed: ${error.message}`);
|
|
1863
|
-
}
|
|
1864
|
-
if(configInfo.isSymbolicLink()||!configInfo.isFile()
|
|
1865
|
-
||!identityMatches(configInfo,state.rootConfigIdentity)){
|
|
1866
|
-
fail('Shared payload snapshot root configuration changed after preparation.');
|
|
1867
|
-
}
|
|
1868
|
-
if(sharedPayloadIds!==undefined){
|
|
1869
|
-
if(!Array.isArray(sharedPayloadIds)||sharedPayloadIds.length>256){
|
|
1870
|
-
fail('sharedPayloadIds must be an array with at most 256 entries.');
|
|
1871
|
-
}
|
|
1872
|
-
const seen=new Set();
|
|
1873
|
-
for(const [index,id] of sharedPayloadIds.entries()){
|
|
1874
|
-
if(typeof id!=='string'||!SAFE_SHARED_ID_PATTERN.test(id)||seen.has(id)){
|
|
1875
|
-
fail(`sharedPayloadIds[${index}] is invalid or duplicated.`);
|
|
1876
|
-
}
|
|
1877
|
-
seen.add(id);
|
|
1878
|
-
}
|
|
1879
|
-
assertSnapshotCoverage(state,sharedPayloadIds);
|
|
1880
|
-
}
|
|
1881
|
-
throwIfAborted(signal);
|
|
1882
|
-
return state;
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
export async function prepareSharedPayloadSnapshot({
|
|
1886
|
-
workspaceRoot:requestedWorkspaceRoot,
|
|
1887
|
-
sharedPayloadIds,
|
|
1888
|
-
signal,
|
|
1889
|
-
onEvent
|
|
1890
|
-
}={}){
|
|
1891
|
-
await emitOperation(onEvent,{type:'shared-payload.snapshot.started'});
|
|
1892
|
-
throwIfAborted(signal);
|
|
1893
|
-
const resolvedWorkspaceRoot=normalizeWorkspaceRoot(requestedWorkspaceRoot);
|
|
1894
|
-
const initialWorkspaceInfo=await lstat(resolvedWorkspaceRoot,{bigint:true});
|
|
1895
|
-
if(initialWorkspaceInfo.isSymbolicLink()||!initialWorkspaceInfo.isDirectory()){
|
|
1896
|
-
fail('Shared payload snapshot workspace must be a real directory.');
|
|
1897
|
-
}
|
|
1898
|
-
const canonicalWorkspaceRoot=await realpath(resolvedWorkspaceRoot);
|
|
1899
|
-
await assertNoLinks(canonicalWorkspaceRoot,canonicalWorkspaceRoot,'shared payload snapshot workspace');
|
|
1900
|
-
const rootConfigDocument=await loadRootConfigDocument(canonicalWorkspaceRoot);
|
|
1901
|
-
const selectedIds=normalizeSharedPayloadSelection(
|
|
1902
|
-
sharedPayloadIds,
|
|
1903
|
-
rootConfigDocument.value,
|
|
1904
|
-
{required:true}
|
|
1905
|
-
);
|
|
1906
|
-
const retained=[];
|
|
1907
|
-
const retainedBySharedPayload=Object.fromEntries(selectedIds.map(id=>[id,[]]));
|
|
1908
|
-
let totalBytes=0;
|
|
1909
|
-
let completedFiles=0;
|
|
1910
|
-
|
|
1911
|
-
for(const sharedPayloadId of selectedIds){
|
|
1912
|
-
const routes=rootConfigDocument.value.sharedPayloads[sharedPayloadId];
|
|
1913
|
-
for(const [routeIndex,route] of routes.entries()){
|
|
1914
|
-
throwIfAborted(signal);
|
|
1915
|
-
const sourceRoot=resolveInside(
|
|
1916
|
-
canonicalWorkspaceRoot,
|
|
1917
|
-
route.source,
|
|
1918
|
-
`sharedPayloads.${sharedPayloadId}[${routeIndex}].source`
|
|
1919
|
-
);
|
|
1920
|
-
const files=await enumerateRoute({
|
|
1921
|
-
workspaceRoot:canonicalWorkspaceRoot,
|
|
1922
|
-
sourceRoot,
|
|
1923
|
-
destinationRoot:route.destination,
|
|
1924
|
-
include:route.include,
|
|
1925
|
-
exclude:route.exclude,
|
|
1926
|
-
label:`sharedPayloads.${sharedPayloadId}[${routeIndex}]`,
|
|
1927
|
-
signal
|
|
1928
|
-
});
|
|
1929
|
-
for(const file of files){
|
|
1930
|
-
throwIfAborted(signal);
|
|
1931
|
-
if(retained.length>=MAX_SHARED_SNAPSHOT_FILE_COUNT){
|
|
1932
|
-
fail(`Shared payload snapshot exceeds ${MAX_SHARED_SNAPSHOT_FILE_COUNT} files.`);
|
|
1933
|
-
}
|
|
1934
|
-
if(file.bytes>MAX_SHARED_SNAPSHOT_FILE_BYTES){
|
|
1935
|
-
fail(`Shared payload snapshot file exceeds ${MAX_SHARED_SNAPSHOT_FILE_BYTES} bytes: ${file.sourceRelative}`);
|
|
1936
|
-
}
|
|
1937
|
-
if(totalBytes+file.bytes>MAX_SHARED_SNAPSHOT_TOTAL_BYTES){
|
|
1938
|
-
fail(`Shared payload snapshot exceeds ${MAX_SHARED_SNAPSHOT_TOTAL_BYTES} retained bytes.`);
|
|
1939
|
-
}
|
|
1940
|
-
const stable=await readStableBytes(file.source,file.label,file.identity);
|
|
1941
|
-
if(stable.bytes.length!==file.bytes){
|
|
1942
|
-
fail(`Shared payload snapshot file changed size while retained: ${file.sourceRelative}`);
|
|
1943
|
-
}
|
|
1944
|
-
const digest=createHash('sha256').update(stable.bytes).digest('hex');
|
|
1945
|
-
const source=route.source==='.'
|
|
1946
|
-
?file.sourceRelative
|
|
1947
|
-
:`${route.source}/${file.sourceRelative}`;
|
|
1948
|
-
const record=Object.freeze({
|
|
1949
|
-
...file,
|
|
1950
|
-
sharedPayloadId,
|
|
1951
|
-
routeIndex,
|
|
1952
|
-
sourceRelative:normalizeRelativePath(source,'shared payload snapshot source'),
|
|
1953
|
-
retainedBytes:stable.bytes,
|
|
1954
|
-
sha256:digest
|
|
1955
|
-
});
|
|
1956
|
-
retained.push(record);
|
|
1957
|
-
retainedBySharedPayload[sharedPayloadId].push(record);
|
|
1958
|
-
totalBytes+=stable.bytes.length;
|
|
1959
|
-
completedFiles+=1;
|
|
1960
|
-
await emitOperation(onEvent,{
|
|
1961
|
-
type:'shared-payload.snapshot.progress',
|
|
1962
|
-
current:completedFiles,
|
|
1963
|
-
completedBytes:totalBytes,
|
|
1964
|
-
sharedPayloadId,
|
|
1965
|
-
path:record.sourceRelative
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
const [finalWorkspaceInfo,finalConfigInfo]=await Promise.all([
|
|
1972
|
-
lstat(canonicalWorkspaceRoot,{bigint:true}),
|
|
1973
|
-
lstat(rootConfigDocument.configPath,{bigint:true})
|
|
1974
|
-
]);
|
|
1975
|
-
const workspaceIdentity=workspaceLocationIdentity(initialWorkspaceInfo);
|
|
1976
|
-
if(finalWorkspaceInfo.isSymbolicLink()||!finalWorkspaceInfo.isDirectory()
|
|
1977
|
-
||!workspaceLocationMatches(finalWorkspaceInfo,workspaceIdentity)
|
|
1978
|
-
||finalConfigInfo.isSymbolicLink()||!finalConfigInfo.isFile()
|
|
1979
|
-
||!identityMatches(finalConfigInfo,rootConfigDocument.identity)){
|
|
1980
|
-
fail('Shared payload snapshot workspace or root configuration changed during preparation.');
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
const inventory=retained.map(record=>({
|
|
1984
|
-
sharedPayloadId:record.sharedPayloadId,
|
|
1985
|
-
routeIndex:record.routeIndex,
|
|
1986
|
-
source:record.sourceRelative,
|
|
1987
|
-
destination:record.destination,
|
|
1988
|
-
bytes:record.bytes,
|
|
1989
|
-
sha256:record.sha256
|
|
1990
|
-
})).sort((left,right)=>compareText(JSON.stringify(left),JSON.stringify(right)));
|
|
1991
|
-
const receipt=immutableJsonCopy({
|
|
1992
|
-
schemaVersion:1,
|
|
1993
|
-
kind:'arcane-shared-payload-snapshot',
|
|
1994
|
-
canonicalWorkspaceRoot,
|
|
1995
|
-
workspaceIdentity,
|
|
1996
|
-
rootConfig:Object.freeze({
|
|
1997
|
-
path:ROOT_CONFIG_NAME,
|
|
1998
|
-
identity:rootConfigDocument.identity,
|
|
1999
|
-
sha256:createHash('sha256').update(rootConfigDocument.bytes).digest('hex')
|
|
2000
|
-
}),
|
|
2001
|
-
sharedPayloadIds:selectedIds,
|
|
2002
|
-
files:inventory,
|
|
2003
|
-
fileCount:inventory.length,
|
|
2004
|
-
totalBytes,
|
|
2005
|
-
contentSha256:createHash('sha256').update(JSON.stringify(inventory)).digest('hex')
|
|
2006
|
-
});
|
|
2007
|
-
const filesBySharedPayload=Object.freeze(Object.fromEntries(
|
|
2008
|
-
Object.entries(retainedBySharedPayload).map(([id,files])=>[id,Object.freeze(files)])
|
|
2009
|
-
));
|
|
2010
|
-
issuedSharedPayloadSnapshots.set(receipt,Object.freeze({
|
|
2011
|
-
receipt,
|
|
2012
|
-
canonicalWorkspaceRoot,
|
|
2013
|
-
workspaceIdentity,
|
|
2014
|
-
rootConfigPath:rootConfigDocument.configPath,
|
|
2015
|
-
rootConfigIdentity:rootConfigDocument.identity,
|
|
2016
|
-
files:Object.freeze(retained),
|
|
2017
|
-
filesBySharedPayload
|
|
2018
|
-
}));
|
|
2019
|
-
await emitOperation(onEvent,{
|
|
2020
|
-
type:'shared-payload.snapshot.completed',
|
|
2021
|
-
fileCount:receipt.fileCount,
|
|
2022
|
-
totalBytes:receipt.totalBytes,
|
|
2023
|
-
contentSha256:receipt.contentSha256
|
|
2024
|
-
});
|
|
2025
|
-
return receipt;
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
export async function authenticateSharedPayloadSnapshot(receipt,options={}){
|
|
2029
|
-
await authenticateSharedPayloadSnapshotState(receipt,options);
|
|
2030
|
-
return receipt;
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
async function collectPackageFiles(context,{signal,sharedPayloadState}={}){
|
|
2034
|
-
const {workspaceRoot,appRoot,config,rootConfig}=context;
|
|
2035
|
-
const files=await enumerateRoute({
|
|
2036
|
-
workspaceRoot,
|
|
2037
|
-
sourceRoot:appRoot,
|
|
2038
|
-
destinationRoot:`apps/${config.id}`,
|
|
2039
|
-
include:config.include,
|
|
2040
|
-
exclude:config.exclude,
|
|
2041
|
-
label:`apps.${config.id}`,
|
|
2042
|
-
appPayload:true,
|
|
2043
|
-
signal
|
|
2044
|
-
});
|
|
2045
|
-
|
|
2046
|
-
if(sharedPayloadState){
|
|
2047
|
-
assertSnapshotCoverage(sharedPayloadState,config.shared);
|
|
2048
|
-
for(const sharedId of config.shared){
|
|
2049
|
-
files.push(...sharedPayloadState.filesBySharedPayload[sharedId]);
|
|
2050
|
-
}
|
|
2051
|
-
}else{
|
|
2052
|
-
for(const sharedId of config.shared){
|
|
2053
|
-
const routes=rootConfig.sharedPayloads[sharedId];
|
|
2054
|
-
|
|
2055
|
-
for(const [index,route] of routes.entries()){
|
|
2056
|
-
const sourceRoot=resolveInside(
|
|
2057
|
-
workspaceRoot,
|
|
2058
|
-
route.source,
|
|
2059
|
-
`sharedPayloads.${sharedId}[${index}].source`
|
|
2060
|
-
);
|
|
2061
|
-
files.push(...await enumerateRoute({
|
|
2062
|
-
workspaceRoot,
|
|
2063
|
-
sourceRoot,
|
|
2064
|
-
destinationRoot:route.destination,
|
|
2065
|
-
include:route.include,
|
|
2066
|
-
exclude:route.exclude,
|
|
2067
|
-
label:`sharedPayloads.${sharedId}[${index}]`,
|
|
2068
|
-
signal
|
|
2069
|
-
}));
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
const destinations=new Map();
|
|
2075
|
-
|
|
2076
|
-
for(const file of files){
|
|
2077
|
-
throwIfAborted(signal);
|
|
2078
|
-
if(GENERATED_PACKAGE_ROOT_PATH_KEYS.has(pathKey(file.destination))){
|
|
2079
|
-
fail(`${file.label} collides with generated package path: ${file.destination}`);
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
const key=pathKey(file.destination);
|
|
2083
|
-
|
|
2084
|
-
if(destinations.has(key)){
|
|
2085
|
-
fail(`Package destination collision: ${file.destination} from ${file.source} and ${destinations.get(key).source}.`);
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
destinations.set(key,file);
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
const expectedEntry=`apps/${config.id}/${config.entry}`;
|
|
2092
|
-
|
|
2093
|
-
if(!destinations.has(pathKey(expectedEntry))){
|
|
2094
|
-
fail(`The configured entry file was not found in the package payload: ${expectedEntry}`);
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
return files.sort((left,right)=>compareText(left.destination,right.destination));
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
async function copyPackageFiles(files,outputRoot,{signal,onEvent}={}){
|
|
2101
|
-
let completedBytes=0;
|
|
2102
|
-
const buffer=Buffer.allocUnsafe(1024*1024);
|
|
2103
|
-
|
|
2104
|
-
for(const [index,file] of files.entries()){
|
|
2105
|
-
throwIfAborted(signal);
|
|
2106
|
-
const destination=resolveInside(outputRoot,file.destination,'package destination');
|
|
2107
|
-
await mkdir(path.dirname(destination),{recursive:true});
|
|
2108
|
-
if(file.retainedBytes!==undefined){
|
|
2109
|
-
if(!Buffer.isBuffer(file.retainedBytes)||file.retainedBytes.length!==file.bytes){
|
|
2110
|
-
fail(`Retained shared payload bytes are invalid for ${file.destination}.`);
|
|
2111
|
-
}
|
|
2112
|
-
const output=await open(destination,'wx');
|
|
2113
|
-
let copiedBytes=0;
|
|
2114
|
-
try{
|
|
2115
|
-
throwIfAborted(signal);
|
|
2116
|
-
await output.writeFile(file.retainedBytes);
|
|
2117
|
-
copiedBytes=file.retainedBytes.length;
|
|
2118
|
-
const outputAfter=await output.stat({bigint:true});
|
|
2119
|
-
if(Number(outputAfter.size)!==copiedBytes){
|
|
2120
|
-
fail(`Could not finish writing retained shared payload ${file.destination}.`);
|
|
2121
|
-
}
|
|
2122
|
-
}finally{
|
|
2123
|
-
await output.close().catch(()=>{});
|
|
2124
|
-
}
|
|
2125
|
-
completedBytes+=copiedBytes;
|
|
2126
|
-
await emitOperation(onEvent,{
|
|
2127
|
-
type:'package.copy.progress',
|
|
2128
|
-
current:index+1,
|
|
2129
|
-
total:files.length,
|
|
2130
|
-
completedBytes,
|
|
2131
|
-
path:file.destination
|
|
2132
|
-
});
|
|
2133
|
-
continue;
|
|
2134
|
-
}
|
|
2135
|
-
const source=await openStableRegularFile(file.source,file.label,file.identity);
|
|
2136
|
-
let output;
|
|
2137
|
-
let copiedBytes=0;
|
|
2138
|
-
try{
|
|
2139
|
-
output=await open(destination,'wx');
|
|
2140
|
-
while(true){
|
|
2141
|
-
throwIfAborted(signal);
|
|
2142
|
-
const {bytesRead}=await source.handle.read(buffer,0,buffer.length,null);
|
|
2143
|
-
if(bytesRead===0)break;
|
|
2144
|
-
let written=0;
|
|
2145
|
-
while(written<bytesRead){
|
|
2146
|
-
const result=await output.write(buffer,written,bytesRead-written,null);
|
|
2147
|
-
if(result.bytesWritten<=0)fail(`Could not finish writing ${file.destination}.`);
|
|
2148
|
-
written+=result.bytesWritten;
|
|
2149
|
-
}
|
|
2150
|
-
copiedBytes+=bytesRead;
|
|
2151
|
-
}
|
|
2152
|
-
const [sourceAfter,outputAfter]=await Promise.all([
|
|
2153
|
-
source.handle.stat({bigint:true}),
|
|
2154
|
-
output.stat({bigint:true})
|
|
2155
|
-
]);
|
|
2156
|
-
if(!identityMatches(sourceAfter,file.identity)||copiedBytes!==file.bytes
|
|
2157
|
-
||Number(outputAfter.size)!==copiedBytes){
|
|
2158
|
-
fail(`${file.label} changed while ${file.destination} was being copied.`);
|
|
2159
|
-
}
|
|
2160
|
-
}finally{
|
|
2161
|
-
await output?.close().catch(()=>{});
|
|
2162
|
-
await source.handle.close().catch(()=>{});
|
|
2163
|
-
}
|
|
2164
|
-
completedBytes+=copiedBytes;
|
|
2165
|
-
await emitOperation(onEvent,{
|
|
2166
|
-
type:'package.copy.progress',
|
|
2167
|
-
current:index+1,
|
|
2168
|
-
total:files.length,
|
|
2169
|
-
completedBytes,
|
|
2170
|
-
path:file.destination
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
function escapeHtml(value){
|
|
2176
|
-
return String(value)
|
|
2177
|
-
.replaceAll('&','&')
|
|
2178
|
-
.replaceAll('<','<')
|
|
2179
|
-
.replaceAll('>','>')
|
|
2180
|
-
.replaceAll('"','"');
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
async function materializeBasePackage(context,outputRoot,files,{signal,onEvent}={}){
|
|
2184
|
-
throwIfAborted(signal);
|
|
2185
|
-
await mkdir(outputRoot,{recursive:true});
|
|
2186
|
-
await assertNoLinks(context.distRoot,outputRoot,'package staging root');
|
|
2187
|
-
await copyPackageFiles(files,outputRoot,{signal,onEvent});
|
|
2188
|
-
throwIfAborted(signal);
|
|
2189
|
-
const start=`./apps/${context.config.id}/${context.config.entry}`;
|
|
2190
|
-
const title=escapeHtml(context.config.displayName);
|
|
2191
|
-
await writeFile(
|
|
2192
|
-
path.join(outputRoot,'index.html'),
|
|
2193
|
-
[
|
|
2194
|
-
'<!doctype html>',
|
|
2195
|
-
'<meta charset="utf-8">',
|
|
2196
|
-
`<meta http-equiv="refresh" content="0; url=${escapeHtml(start)}">`,
|
|
2197
|
-
`<title>${title}</title>`,
|
|
2198
|
-
`<a href="${escapeHtml(start)}">Open ${title}</a>`,
|
|
2199
|
-
''
|
|
2200
|
-
].join('\n'),
|
|
2201
|
-
'utf8'
|
|
2202
|
-
);
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
|
-
async function sha256WithIdentity(filePath,{signal,expectedIdentity,label=filePath}={}){
|
|
2206
|
-
throwIfAborted(signal);
|
|
2207
|
-
const hash=createHash('sha256');
|
|
2208
|
-
const opened=await openStableRegularFile(filePath,label,expectedIdentity);
|
|
2209
|
-
const buffer=Buffer.allocUnsafe(1024*1024);
|
|
2210
|
-
|
|
2211
|
-
try{
|
|
2212
|
-
while(true){
|
|
2213
|
-
throwIfAborted(signal);
|
|
2214
|
-
const {bytesRead}=await opened.handle.read(buffer,0,buffer.length,null);
|
|
2215
|
-
|
|
2216
|
-
if(bytesRead===0){
|
|
2217
|
-
break;
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
hash.update(buffer.subarray(0,bytesRead));
|
|
2221
|
-
}
|
|
2222
|
-
const after=await opened.handle.stat({bigint:true});
|
|
2223
|
-
if(!identityMatches(after,opened.identity))fail(`${label} changed while it was being hashed.`);
|
|
2224
|
-
}finally{
|
|
2225
|
-
await opened.handle.close();
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
return {sha256:hash.digest('hex'),identity:opened.identity};
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
async function sha256(filePath,options={}){
|
|
2232
|
-
return (await sha256WithIdentity(filePath,options)).sha256;
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
async function listOutputFiles(root,{signal}={}){
|
|
2236
|
-
const files=[];
|
|
2237
|
-
|
|
2238
|
-
async function visit(directory,relativeRoot=''){
|
|
2239
|
-
throwIfAborted(signal);
|
|
2240
|
-
const entries=await readdir(directory,{withFileTypes:true});
|
|
2241
|
-
|
|
2242
|
-
for(const entry of entries.sort((left,right)=>compareText(left.name,right.name))){
|
|
2243
|
-
throwIfAborted(signal);
|
|
2244
|
-
const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
|
|
2245
|
-
const absolute=path.join(directory,entry.name);
|
|
2246
|
-
|
|
2247
|
-
if(isAlwaysForbidden(relative)){
|
|
2248
|
-
fail(`Package contains a globally forbidden path: ${relative}`);
|
|
2249
|
-
}
|
|
2250
|
-
if(path.posix.basename(relative)===APP_CONFIG_NAME){
|
|
2251
|
-
fail(`Package contains authored configuration that must remain outside the browser payload: ${relative}`);
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
if(entry.isSymbolicLink()){
|
|
2255
|
-
fail(`Package contains a symbolic link or junction: ${relative}`);
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
if(entry.isDirectory()){
|
|
2259
|
-
await visit(absolute,relative);
|
|
2260
|
-
}else if(entry.isFile()){
|
|
2261
|
-
files.push({absolute,relative});
|
|
2262
|
-
}else{
|
|
2263
|
-
fail(`Package contains a non-file entry: ${relative}`);
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
await visit(root);
|
|
2269
|
-
return files.sort((left,right)=>compareText(left.relative,right.relative));
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
async function inventoryEntries(root,{signal,onEvent}={}){
|
|
2273
|
-
const files=(await listOutputFiles(root,{signal})).filter(file=>
|
|
2274
|
-
file.relative!==RELEASE_MANIFEST_NAME
|
|
2275
|
-
);
|
|
2276
|
-
const entries=[];
|
|
2277
|
-
const identities=[];
|
|
2278
|
-
|
|
2279
|
-
let completedBytes=0;
|
|
2280
|
-
|
|
2281
|
-
for(const [index,file] of files.entries()){
|
|
2282
|
-
throwIfAborted(signal);
|
|
2283
|
-
const verified=await sha256WithIdentity(file.absolute,{
|
|
2284
|
-
signal,
|
|
2285
|
-
label:`package file ${file.relative}`
|
|
2286
|
-
});
|
|
2287
|
-
entries.push({
|
|
2288
|
-
path:file.relative,
|
|
2289
|
-
bytes:verified.identity.bytes,
|
|
2290
|
-
sha256:verified.sha256
|
|
2291
|
-
});
|
|
2292
|
-
identities.push(Object.freeze({path:file.relative,...verified.identity}));
|
|
2293
|
-
completedBytes+=verified.identity.bytes;
|
|
2294
|
-
await emitOperation(onEvent,{
|
|
2295
|
-
type:'package.hash.progress',
|
|
2296
|
-
current:index+1,
|
|
2297
|
-
total:files.length,
|
|
2298
|
-
completedBytes,
|
|
2299
|
-
path:file.relative
|
|
2300
|
-
});
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
return {entries,identities};
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
async function assertArtifactState(root,identities,{signal}={}){
|
|
2307
|
-
throwIfAborted(signal);
|
|
2308
|
-
const requested=path.resolve(root);
|
|
2309
|
-
const rootBefore=await lstat(requested,{bigint:true});
|
|
2310
|
-
if(rootBefore.isSymbolicLink()||!rootBefore.isDirectory()){
|
|
2311
|
-
fail('App release root must be a real directory.');
|
|
2312
|
-
}
|
|
2313
|
-
const canonical=await realpath(requested);
|
|
2314
|
-
const actualPaths=(await listOutputFiles(canonical,{signal}))
|
|
2315
|
-
.map(file=>file.relative)
|
|
2316
|
-
.sort(compareText);
|
|
2317
|
-
const expectedPaths=identities.map(identity=>identity.path).sort(compareText);
|
|
2318
|
-
if(JSON.stringify(actualPaths)!==JSON.stringify(expectedPaths)){
|
|
2319
|
-
fail('App release inventory changed after verification.');
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
for(const identity of identities){
|
|
2323
|
-
throwIfAborted(signal);
|
|
2324
|
-
const filePath=resolveInside(canonical,identity.path,'verified app release path');
|
|
2325
|
-
const info=await lstat(filePath,{bigint:true});
|
|
2326
|
-
if(info.isSymbolicLink()||!info.isFile()||!identityMatches(info,identity)){
|
|
2327
|
-
fail(`App release file changed after verification: ${identity.path}`);
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
const rootAfter=await lstat(canonical,{bigint:true});
|
|
2331
|
-
if(!rootAfter.isDirectory()||rootAfter.isSymbolicLink()
|
|
2332
|
-
||!identityMatches(rootAfter,fileIdentity(rootBefore))){
|
|
2333
|
-
fail('App release root changed while its verification state was authenticated.');
|
|
2334
|
-
}
|
|
2335
|
-
return {canonical,rootIdentity:fileIdentity(rootAfter)};
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
function appReleasePackageBinding(config){
|
|
2339
|
-
if(!isPlainObject(config)){
|
|
2340
|
-
fail('App release package binding is missing.');
|
|
2341
|
-
}
|
|
2342
|
-
return immutableJsonCopy({
|
|
2343
|
-
schemaVersion:1,
|
|
2344
|
-
id:config.id,
|
|
2345
|
-
displayName:config.displayName,
|
|
2346
|
-
version:config.version,
|
|
2347
|
-
entry:config.entry,
|
|
2348
|
-
strategy:config.strategy,
|
|
2349
|
-
security:config.security,
|
|
2350
|
-
localAIModelPolicy:config.localAIModelPolicy??{verified_only:true,models:[]},
|
|
2351
|
-
include:[...(config.include??[])],
|
|
2352
|
-
exclude:[...(config.exclude??[])],
|
|
2353
|
-
shared:[...(config.shared??[])],
|
|
2354
|
-
adapter:config.adapter??null
|
|
2355
|
-
});
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
async function issueAppReleaseReceipt(root,release,identities,{
|
|
2359
|
-
signal,
|
|
2360
|
-
packageConfig,
|
|
2361
|
-
descriptorAuthority
|
|
2362
|
-
}={}){
|
|
2363
|
-
const state=await assertArtifactState(root,identities,{signal});
|
|
2364
|
-
const packageBinding=appReleasePackageBinding(packageConfig);
|
|
2365
|
-
if(!descriptorAuthority?.descriptor
|
|
2366
|
-
||!/^[a-f0-9]{64}$/u.test(descriptorAuthority.descriptorSha256)){
|
|
2367
|
-
fail('Canonical app descriptor authority is required to issue a release receipt.');
|
|
2368
|
-
}
|
|
2369
|
-
const receipt={
|
|
2370
|
-
schemaVersion:1,
|
|
2371
|
-
kind:'arcane-app-release-verification',
|
|
2372
|
-
generation:randomBytes(16).toString('hex'),
|
|
2373
|
-
canonicalLocation:state.canonical,
|
|
2374
|
-
builder:release.builder,
|
|
2375
|
-
app:immutableJsonCopy(release.app),
|
|
2376
|
-
policySha256:release.policySha256,
|
|
2377
|
-
files:immutableJsonCopy(release.files),
|
|
2378
|
-
fileCount:release.fileCount,
|
|
2379
|
-
totalBytes:release.totalBytes,
|
|
2380
|
-
contentSha256:release.contentSha256
|
|
2381
|
-
};
|
|
2382
|
-
Object.defineProperty(receipt,'identities',{
|
|
2383
|
-
value:Object.freeze([...identities]),
|
|
2384
|
-
enumerable:false,
|
|
2385
|
-
writable:false,
|
|
2386
|
-
configurable:false
|
|
2387
|
-
});
|
|
2388
|
-
Object.freeze(receipt);
|
|
2389
|
-
issuedAppReleaseReceipts.set(receipt,Object.freeze({
|
|
2390
|
-
canonicalLocation:state.canonical,
|
|
2391
|
-
rootIdentity:state.rootIdentity,
|
|
2392
|
-
identities:receipt.identities,
|
|
2393
|
-
packageBinding,
|
|
2394
|
-
descriptorAuthority:Object.freeze({
|
|
2395
|
-
descriptor:descriptorAuthority.descriptor,
|
|
2396
|
-
descriptorSha256:descriptorAuthority.descriptorSha256,
|
|
2397
|
-
source:descriptorAuthority.source
|
|
2398
|
-
})
|
|
2399
|
-
}));
|
|
2400
|
-
return receipt;
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
async function authenticateAppReleaseReceiptState(receipt,{
|
|
2404
|
-
releaseRoot,
|
|
2405
|
-
expectedPackageConfig,
|
|
2406
|
-
signal
|
|
2407
|
-
}={}){
|
|
2408
|
-
const state=issuedAppReleaseReceipts.get(receipt);
|
|
2409
|
-
if(!state)fail('App release receipt was not issued by this SDK process.');
|
|
2410
|
-
if(typeof releaseRoot!=='string'||!releaseRoot.trim())fail('releaseRoot is required to authenticate an app release receipt.');
|
|
2411
|
-
const requested=path.resolve(releaseRoot);
|
|
2412
|
-
const canonical=await realpath(requested);
|
|
2413
|
-
if(canonical!==state.canonicalLocation||receipt.canonicalLocation!==canonical){
|
|
2414
|
-
fail('App release receipt belongs to a different release location.');
|
|
2415
|
-
}
|
|
2416
|
-
const rootInfo=await lstat(canonical,{bigint:true});
|
|
2417
|
-
if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()
|
|
2418
|
-
||!identityMatches(rootInfo,state.rootIdentity)){
|
|
2419
|
-
fail('App release root changed after its receipt was issued.');
|
|
2420
|
-
}
|
|
2421
|
-
if(expectedPackageConfig!==undefined
|
|
2422
|
-
&&JSON.stringify(appReleasePackageBinding(expectedPackageConfig))
|
|
2423
|
-
!==JSON.stringify(state.packageBinding)){
|
|
2424
|
-
fail('App release receipt belongs to a different authored package policy.');
|
|
2425
|
-
}
|
|
2426
|
-
await assertArtifactState(canonical,state.identities,{signal});
|
|
2427
|
-
return state;
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
export async function authenticateAppReleaseReceipt(receipt,options={}){
|
|
2431
|
-
await authenticateAppReleaseReceiptState(receipt,options);
|
|
2432
|
-
return receipt;
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
export async function authenticateAppReleaseAuthority(receipt,{
|
|
2436
|
-
releaseRoot,
|
|
2437
|
-
expectedPackageConfig,
|
|
2438
|
-
expectedDescriptor,
|
|
2439
|
-
signal
|
|
2440
|
-
}={}){
|
|
2441
|
-
const state=await authenticateAppReleaseReceiptState(receipt,{
|
|
2442
|
-
releaseRoot,
|
|
2443
|
-
expectedPackageConfig,
|
|
2444
|
-
signal
|
|
2445
|
-
});
|
|
2446
|
-
const authority=state.descriptorAuthority;
|
|
2447
|
-
if(!authority?.descriptor||!/^[a-f0-9]{64}$/u.test(authority.descriptorSha256)){
|
|
2448
|
-
fail('App release receipt is missing its canonical descriptor authority.');
|
|
2449
|
-
}
|
|
2450
|
-
if(expectedDescriptor!==undefined){
|
|
2451
|
-
const contracts=await appDescriptorContracts();
|
|
2452
|
-
if(contracts.appDescriptorSha256(expectedDescriptor)!==authority.descriptorSha256){
|
|
2453
|
-
fail('App release receipt belongs to a different canonical app descriptor.');
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
|
-
return Object.freeze({
|
|
2457
|
-
receipt,
|
|
2458
|
-
descriptor:authority.descriptor,
|
|
2459
|
-
descriptorSha256:authority.descriptorSha256,
|
|
2460
|
-
source:authority.source
|
|
2461
|
-
});
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
export async function readVerifiedAppReleaseFile(receipt,{
|
|
2465
|
-
releaseRoot,
|
|
2466
|
-
relativePath,
|
|
2467
|
-
signal
|
|
2468
|
-
}={}){
|
|
2469
|
-
throwIfAborted(signal);
|
|
2470
|
-
const state=issuedAppReleaseReceipts.get(receipt);
|
|
2471
|
-
if(!state)fail('App release receipt was not issued by this SDK process.');
|
|
2472
|
-
if(typeof releaseRoot!=='string'||!releaseRoot.trim()){
|
|
2473
|
-
fail('releaseRoot is required to read a verified app release file.');
|
|
2474
|
-
}
|
|
2475
|
-
const requested=path.resolve(releaseRoot);
|
|
2476
|
-
const canonical=await realpath(requested);
|
|
2477
|
-
if(canonical!==state.canonicalLocation||receipt.canonicalLocation!==canonical){
|
|
2478
|
-
fail('App release receipt belongs to a different release location.');
|
|
2479
|
-
}
|
|
2480
|
-
const rootInfo=await lstat(canonical,{bigint:true});
|
|
2481
|
-
if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()
|
|
2482
|
-
||!identityMatches(rootInfo,state.rootIdentity)){
|
|
2483
|
-
fail('App release root changed after its receipt was issued.');
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
const normalized=normalizeRelativePath(relativePath,'verified app release path');
|
|
2487
|
-
const file=receipt.files.find(candidate=>pathKey(candidate.path)===pathKey(normalized));
|
|
2488
|
-
if(!file)fail(`Path is not in the verified app release inventory: ${normalized}.`);
|
|
2489
|
-
if(file.bytes>MAX_VERIFIED_APP_FILE_BYTES){
|
|
2490
|
-
fail(
|
|
2491
|
-
`Verified browser file exceeds the ${MAX_VERIFIED_APP_FILE_BYTES}-byte development serving limit: ${file.path}.`,
|
|
2492
|
-
'ARCANE_POLICY_DENIED'
|
|
2493
|
-
);
|
|
2494
|
-
}
|
|
2495
|
-
const identity=state.identities.find(candidate=>
|
|
2496
|
-
pathKey(candidate.path)===pathKey(file.path)
|
|
2497
|
-
);
|
|
2498
|
-
if(!identity)fail(`Verified app release identity is missing for ${file.path}.`);
|
|
2499
|
-
const filePath=resolveInside(canonical,file.path,'verified app release path');
|
|
2500
|
-
const opened=await openStableRegularFile(filePath,`verified app release file ${file.path}`,identity);
|
|
2501
|
-
try{
|
|
2502
|
-
throwIfAborted(signal);
|
|
2503
|
-
const bytes=await opened.handle.readFile();
|
|
2504
|
-
throwIfAborted(signal);
|
|
2505
|
-
const after=await opened.handle.stat({bigint:true});
|
|
2506
|
-
if(!identityMatches(after,opened.identity)||bytes.length!==file.bytes){
|
|
2507
|
-
fail(`Verified app release file changed while it was being read: ${file.path}.`);
|
|
2508
|
-
}
|
|
2509
|
-
const digest=createHash('sha256').update(bytes).digest('hex');
|
|
2510
|
-
if(digest!==file.sha256){
|
|
2511
|
-
fail(`Verified app release file hash changed: ${file.path}.`);
|
|
2512
|
-
}
|
|
2513
|
-
const current=await lstat(filePath,{bigint:true});
|
|
2514
|
-
if(current.isSymbolicLink()||!current.isFile()||!identityMatches(current,identity)){
|
|
2515
|
-
fail(`Verified app release path changed while it was being read: ${file.path}.`);
|
|
2516
|
-
}
|
|
2517
|
-
const canonicalFile=await realpath(filePath);
|
|
2518
|
-
if(!isInside(canonical,canonicalFile)){
|
|
2519
|
-
fail(`Verified app release path left its release root: ${file.path}.`);
|
|
2520
|
-
}
|
|
2521
|
-
return bytes;
|
|
2522
|
-
}finally{
|
|
2523
|
-
await opened.handle.close();
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
function normalizedRoutePolicy(route){
|
|
2528
|
-
return {
|
|
2529
|
-
source:route.source,
|
|
2530
|
-
destination:route.destination,
|
|
2531
|
-
include:[...route.include].sort(compareText),
|
|
2532
|
-
exclude:[...route.exclude].sort(compareText)
|
|
2533
|
-
};
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
async function packagePolicySha256(context,{signal}={}){
|
|
2537
|
-
throwIfAborted(signal);
|
|
2538
|
-
const {config,rootConfig,appRoot}=context;
|
|
2539
|
-
let adapter=null;
|
|
2540
|
-
|
|
2541
|
-
if(config.adapter){
|
|
2542
|
-
const adapterPath=resolveInside(appRoot,config.adapter,`${config.id} adapter`);
|
|
2543
|
-
await assertNoLinks(appRoot,adapterPath,`${config.id} adapter`);
|
|
2544
|
-
adapter={
|
|
2545
|
-
path:config.adapter,
|
|
2546
|
-
sha256:await sha256(adapterPath,{signal})
|
|
2547
|
-
};
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
const shared=[...config.shared]
|
|
2551
|
-
.sort(compareText)
|
|
2552
|
-
.map(id=>({
|
|
2553
|
-
id,
|
|
2554
|
-
routes:rootConfig.sharedPayloads[id]
|
|
2555
|
-
.map(normalizedRoutePolicy)
|
|
2556
|
-
.sort((left,right)=>compareText(JSON.stringify(left),JSON.stringify(right)))
|
|
2557
|
-
}));
|
|
2558
|
-
const policy={
|
|
2559
|
-
strategy:config.strategy,
|
|
2560
|
-
security:config.security,
|
|
2561
|
-
localAIModelPolicy:{...config.localAIModelPolicy},
|
|
2562
|
-
include:[...config.include].sort(compareText),
|
|
2563
|
-
exclude:[...config.exclude].sort(compareText),
|
|
2564
|
-
shared,
|
|
2565
|
-
adapter
|
|
2566
|
-
};
|
|
2567
|
-
|
|
2568
|
-
return createHash('sha256')
|
|
2569
|
-
.update(JSON.stringify(policy))
|
|
2570
|
-
.digest('hex');
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
async function writeReleaseManifest(root,context,version,{signal,onEvent}={}){
|
|
2574
|
-
const {config}=context;
|
|
2575
|
-
const inventory=await inventoryEntries(root,{signal,onEvent});
|
|
2576
|
-
const files=inventory.entries;
|
|
2577
|
-
const totalBytes=files.reduce((total,file)=>total+file.bytes,0);
|
|
2578
|
-
const contentSha256=createHash('sha256')
|
|
2579
|
-
.update(JSON.stringify(files))
|
|
2580
|
-
.digest('hex');
|
|
2581
|
-
const release={
|
|
2582
|
-
schemaVersion:1,
|
|
2583
|
-
builder:PACKAGER_VERSION,
|
|
2584
|
-
app:{
|
|
2585
|
-
id:config.id,
|
|
2586
|
-
displayName:config.displayName,
|
|
2587
|
-
version,
|
|
2588
|
-
entry:config.entry,
|
|
2589
|
-
start:`./apps/${config.id}/${config.entry}`,
|
|
2590
|
-
security:config.security,
|
|
2591
|
-
localAIModelPolicy:{...config.localAIModelPolicy}
|
|
2592
|
-
},
|
|
2593
|
-
policySha256:await packagePolicySha256(context,{signal}),
|
|
2594
|
-
fileCount:files.length,
|
|
2595
|
-
totalBytes,
|
|
2596
|
-
contentSha256,
|
|
2597
|
-
files
|
|
2598
|
-
};
|
|
2599
|
-
|
|
2600
|
-
const manifestPath=path.join(root,RELEASE_MANIFEST_NAME);
|
|
2601
|
-
await writeFile(
|
|
2602
|
-
manifestPath,
|
|
2603
|
-
`${JSON.stringify(release,null,2)}\n`,
|
|
2604
|
-
{encoding:'utf8',flag:'wx'}
|
|
2605
|
-
);
|
|
2606
|
-
const manifest=await openStableRegularFile(manifestPath,RELEASE_MANIFEST_NAME);
|
|
2607
|
-
await manifest.handle.close();
|
|
2608
|
-
return {
|
|
2609
|
-
release,
|
|
2610
|
-
identities:Object.freeze([
|
|
2611
|
-
...inventory.identities,
|
|
2612
|
-
Object.freeze({path:RELEASE_MANIFEST_NAME,...manifest.identity})
|
|
2613
|
-
])
|
|
2614
|
-
};
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
async function writeRuntimeAuthorities(root,state){
|
|
2618
|
-
if(state==null)return;
|
|
2619
|
-
const authorityPath=path.join(root,RUNTIME_AUTHORITIES_NAME);
|
|
2620
|
-
await writeFile(
|
|
2621
|
-
authorityPath,
|
|
2622
|
-
`${JSON.stringify(state.document,null,2)}\n`,
|
|
2623
|
-
{encoding:'utf8',flag:'wx'}
|
|
2624
|
-
);
|
|
2625
|
-
const authority=await openStableRegularFile(authorityPath,RUNTIME_AUTHORITIES_NAME);
|
|
2626
|
-
await authority.handle.close();
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
async function writeRuntimeProjection(root,state){
|
|
2630
|
-
if(state==null)return;
|
|
2631
|
-
const projectionPath=path.join(root,RUNTIME_PROJECTION_NAME);
|
|
2632
|
-
await writeFile(
|
|
2633
|
-
projectionPath,
|
|
2634
|
-
`${JSON.stringify(state.projectionDocument,null,2)}\n`,
|
|
2635
|
-
{encoding:'utf8',flag:'wx'}
|
|
2636
|
-
);
|
|
2637
|
-
const projection=await openStableRegularFile(projectionPath,RUNTIME_PROJECTION_NAME);
|
|
2638
|
-
await projection.handle.close();
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
async function verifyRuntimeAuthorities(root,state){
|
|
2642
|
-
const authorityPath=path.join(root,RUNTIME_AUTHORITIES_NAME);
|
|
2643
|
-
if(state==null){
|
|
2644
|
-
try{
|
|
2645
|
-
await lstat(authorityPath);
|
|
2646
|
-
fail(`${RUNTIME_AUTHORITIES_NAME} is not allowed without an external runtime authority.`);
|
|
2647
|
-
}catch(error){
|
|
2648
|
-
if(error?.code!=='ENOENT')throw error;
|
|
2649
|
-
}
|
|
2650
|
-
return;
|
|
2651
|
-
}
|
|
2652
|
-
const document=await readJsonDocument(authorityPath,RUNTIME_AUTHORITIES_NAME);
|
|
2653
|
-
if(!isDeepStrictEqual(document.value,state.document)){
|
|
2654
|
-
fail(`${RUNTIME_AUTHORITIES_NAME} does not match the admitted workspace runtime authorities.`);
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
async function verifyRuntimeProjection(root,state,release){
|
|
2659
|
-
const projectionPath=path.join(root,RUNTIME_PROJECTION_NAME);
|
|
2660
|
-
const releaseRecords=release.files.filter(file=>file.path===RUNTIME_PROJECTION_NAME);
|
|
2661
|
-
if(state==null){
|
|
2662
|
-
if(releaseRecords.length!==0){
|
|
2663
|
-
fail(
|
|
2664
|
-
`${RUNTIME_PROJECTION_NAME} is not allowed without an external runtime authority.`,
|
|
2665
|
-
RUNTIME_PROJECTION_ERROR
|
|
2666
|
-
);
|
|
2667
|
-
}
|
|
2668
|
-
try{
|
|
2669
|
-
await lstat(projectionPath);
|
|
2670
|
-
fail(
|
|
2671
|
-
`${RUNTIME_PROJECTION_NAME} is not allowed without an external runtime authority.`,
|
|
2672
|
-
RUNTIME_PROJECTION_ERROR
|
|
2673
|
-
);
|
|
2674
|
-
}catch(error){
|
|
2675
|
-
if(error?.code!=='ENOENT')throw error;
|
|
2676
|
-
}
|
|
2677
|
-
return;
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
let document;
|
|
2681
|
-
try{
|
|
2682
|
-
document=await readJsonDocument(projectionPath,RUNTIME_PROJECTION_NAME);
|
|
2683
|
-
}catch(error){
|
|
2684
|
-
fail(
|
|
2685
|
-
`${RUNTIME_PROJECTION_NAME} could not be authenticated: ${error.message}`,
|
|
2686
|
-
RUNTIME_PROJECTION_ERROR
|
|
2687
|
-
);
|
|
2688
|
-
}
|
|
2689
|
-
const expectedBytes=Buffer.from(`${JSON.stringify(state.projectionDocument,null,2)}\n`,'utf8');
|
|
2690
|
-
if(!document.bytes.equals(expectedBytes)
|
|
2691
|
-
||!isDeepStrictEqual(document.value,state.projectionDocument)){
|
|
2692
|
-
fail(
|
|
2693
|
-
`${RUNTIME_PROJECTION_NAME} does not match the admitted workspace runtime inventory.`,
|
|
2694
|
-
RUNTIME_PROJECTION_ERROR
|
|
2695
|
-
);
|
|
2696
|
-
}
|
|
2697
|
-
const expectedSha256=createHash('sha256').update(expectedBytes).digest('hex');
|
|
2698
|
-
const record=releaseRecords[0];
|
|
2699
|
-
if(releaseRecords.length!==1
|
|
2700
|
-
||record.bytes!==expectedBytes.length
|
|
2701
|
-
||record.sha256!==expectedSha256){
|
|
2702
|
-
fail(
|
|
2703
|
-
`${RUNTIME_PROJECTION_NAME} is not authenticated by the packaged release inventory.`,
|
|
2704
|
-
RUNTIME_PROJECTION_ERROR
|
|
2705
|
-
);
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
function verifyRuntimeProjectionAuthority(release,state){
|
|
2710
|
-
if(state==null)return;
|
|
2711
|
-
const projection=release.files
|
|
2712
|
-
.filter(file=>file.path.startsWith('arcane/'))
|
|
2713
|
-
.map(file=>({
|
|
2714
|
-
path:file.path.slice('arcane/'.length),
|
|
2715
|
-
bytes:file.bytes,
|
|
2716
|
-
sha256:file.sha256
|
|
2717
|
-
}));
|
|
2718
|
-
const totalBytes=projection.reduce((total,file)=>total+file.bytes,0);
|
|
2719
|
-
const contentSha256=createHash('sha256')
|
|
2720
|
-
.update(JSON.stringify(projection))
|
|
2721
|
-
.digest('hex');
|
|
2722
|
-
const expected=state.document.projection;
|
|
2723
|
-
if(projection.length!==expected.fileCount
|
|
2724
|
-
||totalBytes!==expected.totalBytes
|
|
2725
|
-
||contentSha256!==expected.contentSha256){
|
|
2726
|
-
fail('The packaged arcane runtime does not match its admitted runtime authorities.');
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
|
|
2730
|
-
function expectedReleaseApp(context,version){
|
|
2731
|
-
const {config}=context;
|
|
2732
|
-
return {
|
|
2733
|
-
id:config.id,
|
|
2734
|
-
displayName:config.displayName,
|
|
2735
|
-
version,
|
|
2736
|
-
entry:config.entry,
|
|
2737
|
-
start:`./apps/${config.id}/${config.entry}`,
|
|
2738
|
-
security:config.security,
|
|
2739
|
-
localAIModelPolicy:{...config.localAIModelPolicy}
|
|
2740
|
-
};
|
|
2741
|
-
}
|
|
2742
|
-
|
|
2743
|
-
async function verifyFreshStaticRelease(root,context,version,releaseState,{
|
|
2744
|
-
runtimeAuthorityState,
|
|
2745
|
-
signal
|
|
2746
|
-
}={}){
|
|
2747
|
-
throwIfAborted(signal);
|
|
2748
|
-
const {config}=context;
|
|
2749
|
-
const expectedApp=expectedReleaseApp(context,version);
|
|
2750
|
-
const release=releaseState.release;
|
|
2751
|
-
|
|
2752
|
-
if(release?.schemaVersion!==1||release?.builder!==PACKAGER_VERSION
|
|
2753
|
-
||JSON.stringify(release?.app)!==JSON.stringify(expectedApp)
|
|
2754
|
-
||!Array.isArray(release?.files)
|
|
2755
|
-
||release.fileCount!==release.files.length){
|
|
2756
|
-
fail(`${config.id}/${RELEASE_MANIFEST_NAME} identity is invalid.`);
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
// This staging tree is owned by this operation and no adapter has run. The
|
|
2760
|
-
// release inventory was produced from the final bytes immediately before
|
|
2761
|
-
// this check, so reuse that receipt instead of hashing every file twice.
|
|
2762
|
-
const rootIndex=path.join(root,'index.html');
|
|
2763
|
-
const entry=resolveInside(root,expectedApp.start.slice(2),'package entry');
|
|
2764
|
-
const [indexInfo,entryInfo,manifestInfo]=await Promise.all([
|
|
2765
|
-
lstat(rootIndex),
|
|
2766
|
-
lstat(entry),
|
|
2767
|
-
lstat(path.join(root,RELEASE_MANIFEST_NAME))
|
|
2768
|
-
]);
|
|
2769
|
-
|
|
2770
|
-
if(!indexInfo.isFile()||indexInfo.isSymbolicLink()
|
|
2771
|
-
||!entryInfo.isFile()||entryInfo.isSymbolicLink()
|
|
2772
|
-
||!manifestInfo.isFile()||manifestInfo.isSymbolicLink()){
|
|
2773
|
-
fail(`Package entry files for ${config.id} are invalid.`);
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
await verifyRuntimeAuthorities(root,runtimeAuthorityState);
|
|
2777
|
-
await verifyRuntimeProjection(root,runtimeAuthorityState,release);
|
|
2778
|
-
verifyRuntimeProjectionAuthority(release,runtimeAuthorityState);
|
|
2779
|
-
|
|
2780
|
-
return releaseState;
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
|
-
async function verifyGenericRelease(root,context,version,{
|
|
2784
|
-
runtimeAuthorityState,
|
|
2785
|
-
signal,
|
|
2786
|
-
onEvent
|
|
2787
|
-
}={}){
|
|
2788
|
-
const {config}=context;
|
|
2789
|
-
const manifestDocument=await readJsonDocument(
|
|
2790
|
-
path.join(root,RELEASE_MANIFEST_NAME),
|
|
2791
|
-
`${config.id}/${RELEASE_MANIFEST_NAME}`
|
|
2792
|
-
);
|
|
2793
|
-
const release=manifestDocument.value;
|
|
2794
|
-
const expectedApp=expectedReleaseApp(context,version);
|
|
2795
|
-
|
|
2796
|
-
if(release?.schemaVersion!==1||release?.builder!==PACKAGER_VERSION
|
|
2797
|
-
||JSON.stringify(release?.app)!==JSON.stringify(expectedApp)
|
|
2798
|
-
||release?.policySha256!==await packagePolicySha256(context,{signal})
|
|
2799
|
-
||!Array.isArray(release?.files)){
|
|
2800
|
-
fail(`${config.id}/${RELEASE_MANIFEST_NAME} identity is invalid.`);
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
const inventory=await inventoryEntries(root,{signal,onEvent});
|
|
2804
|
-
const actualFiles=inventory.entries;
|
|
2805
|
-
const totalBytes=actualFiles.reduce((total,file)=>total+file.bytes,0);
|
|
2806
|
-
const contentSha256=createHash('sha256')
|
|
2807
|
-
.update(JSON.stringify(actualFiles))
|
|
2808
|
-
.digest('hex');
|
|
2809
|
-
|
|
2810
|
-
if(release.fileCount!==actualFiles.length
|
|
2811
|
-
||release.totalBytes!==totalBytes
|
|
2812
|
-
||release.contentSha256!==contentSha256
|
|
2813
|
-
||JSON.stringify(release.files)!==JSON.stringify(actualFiles)){
|
|
2814
|
-
fail(`${config.id}/${RELEASE_MANIFEST_NAME} does not match the package tree.`);
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
const rootIndex=path.join(root,'index.html');
|
|
2818
|
-
const entry=resolveInside(root,expectedApp.start.slice(2),'package entry');
|
|
2819
|
-
const [indexInfo,entryInfo]=await Promise.all([lstat(rootIndex),lstat(entry)]);
|
|
2820
|
-
|
|
2821
|
-
if(!indexInfo.isFile()||indexInfo.isSymbolicLink()
|
|
2822
|
-
||!entryInfo.isFile()||entryInfo.isSymbolicLink()){
|
|
2823
|
-
fail(`Package entry files for ${config.id} are invalid.`);
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
await verifyRuntimeAuthorities(root,runtimeAuthorityState);
|
|
2827
|
-
await verifyRuntimeProjection(root,runtimeAuthorityState,release);
|
|
2828
|
-
verifyRuntimeProjectionAuthority(release,runtimeAuthorityState);
|
|
2829
|
-
|
|
2830
|
-
return {
|
|
2831
|
-
release,
|
|
2832
|
-
identities:Object.freeze([
|
|
2833
|
-
...inventory.identities,
|
|
2834
|
-
Object.freeze({path:RELEASE_MANIFEST_NAME,...manifestDocument.identity})
|
|
2835
|
-
])
|
|
2836
|
-
};
|
|
2837
|
-
}
|
|
2838
|
-
|
|
2839
|
-
async function loadAdapter(context){
|
|
2840
|
-
if(context.config.strategy!=='adapter'){
|
|
2841
|
-
return null;
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
|
-
const adapterPath=resolveInside(
|
|
2845
|
-
context.appRoot,
|
|
2846
|
-
context.config.adapter,
|
|
2847
|
-
`${context.config.id} adapter`
|
|
2848
|
-
);
|
|
2849
|
-
await assertNoLinks(context.appRoot,adapterPath,`${context.config.id} adapter`);
|
|
2850
|
-
const details=await stat(adapterPath);
|
|
2851
|
-
|
|
2852
|
-
if(!details.isFile()){
|
|
2853
|
-
fail(`${context.config.id} adapter is not a regular file.`);
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
const adapterBytes=await readFile(adapterPath);
|
|
2857
|
-
|
|
2858
|
-
if(adapterBytes.includes(0x0d)){
|
|
2859
|
-
fail(`${context.config.id} adapter must use canonical LF line endings.`);
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
const module=await import(`${pathToFileURL(adapterPath).href}?mtime=${details.mtimeMs}`);
|
|
2863
|
-
|
|
2864
|
-
if(typeof module.buildArcanePackage!=='function'
|
|
2865
|
-
||typeof module.verifyArcanePackage!=='function'){
|
|
2866
|
-
fail(`${context.config.id} adapter must export buildArcanePackage and verifyArcanePackage.`);
|
|
2867
|
-
}
|
|
2868
|
-
|
|
2869
|
-
return module;
|
|
2870
|
-
}
|
|
2871
|
-
|
|
2872
|
-
async function verifyBuiltPackage(context,outputRoot,version,adapter,{
|
|
2873
|
-
runtimeAuthorityState,
|
|
2874
|
-
signal,
|
|
2875
|
-
onEvent
|
|
2876
|
-
}={}){
|
|
2877
|
-
throwIfAborted(signal);
|
|
2878
|
-
if(adapter){
|
|
2879
|
-
await adapter.verifyArcanePackage({
|
|
2880
|
-
workspaceRoot:context.workspaceRoot,
|
|
2881
|
-
appRoot:context.appRoot,
|
|
2882
|
-
outputRoot,
|
|
2883
|
-
config:context.config,
|
|
2884
|
-
version,
|
|
2885
|
-
signal,
|
|
2886
|
-
onEvent
|
|
2887
|
-
});
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
return verifyGenericRelease(outputRoot,context,version,{
|
|
2891
|
-
runtimeAuthorityState,
|
|
2892
|
-
signal,
|
|
2893
|
-
onEvent
|
|
2894
|
-
});
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
|
-
async function writeAppVersion(context,version){
|
|
2898
|
-
parseSemver(version);
|
|
2899
|
-
const raw=await readJson(context.config.configPath,context.config.configPath);
|
|
2900
|
-
raw.version=version;
|
|
2901
|
-
const temporary=`${context.config.configPath}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
2902
|
-
const backup=`${context.config.configPath}.bak-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
2903
|
-
await writeFile(temporary,`${JSON.stringify(raw,null,2)}\n`,'utf8');
|
|
2904
|
-
|
|
2905
|
-
let originalMoved=false;
|
|
2906
|
-
let replacementInstalled=false;
|
|
2907
|
-
|
|
514
|
+
async function optionalDescriptor(context){
|
|
515
|
+
const descriptorPath=path.join(context.appRoot,APP_DESCRIPTOR_NAME);
|
|
2908
516
|
try{
|
|
2909
|
-
await
|
|
2910
|
-
|
|
2911
|
-
await
|
|
2912
|
-
replacementInstalled=true;
|
|
517
|
+
const info=await lstat(descriptorPath);
|
|
518
|
+
if(info.isSymbolicLink()||!info.isFile())fail(`${APP_DESCRIPTOR_NAME} must be a real file.`);
|
|
519
|
+
return await readJson(descriptorPath,APP_DESCRIPTOR_NAME);
|
|
2913
520
|
}catch(error){
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
if(originalMoved&&!replacementInstalled){
|
|
2917
|
-
try{
|
|
2918
|
-
await rename(backup,context.config.configPath);
|
|
2919
|
-
}catch{
|
|
2920
|
-
// Preserve the original error; the backup path remains recoverable.
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
|
|
521
|
+
if(error?.code==='ENOENT')return null;
|
|
2924
522
|
throw error;
|
|
2925
523
|
}
|
|
2926
|
-
|
|
2927
|
-
await rm(backup,{force:true}).catch(()=>{});
|
|
2928
|
-
}
|
|
2929
|
-
|
|
2930
|
-
function resolveTargetVersion(current,{bump,exactVersion,preid}={}){
|
|
2931
|
-
if(bump&&exactVersion){
|
|
2932
|
-
fail('Choose either a semantic version bump or an exact version, not both.');
|
|
2933
|
-
}
|
|
2934
|
-
|
|
2935
|
-
if(exactVersion!==undefined){
|
|
2936
|
-
parseSemver(exactVersion);
|
|
2937
|
-
|
|
2938
|
-
if(exactVersion===current){
|
|
2939
|
-
fail(`Version is already ${current}.`);
|
|
2940
|
-
}
|
|
2941
|
-
|
|
2942
|
-
return exactVersion;
|
|
2943
|
-
}
|
|
2944
|
-
|
|
2945
|
-
return bump?incrementSemver(current,bump,preid):current;
|
|
2946
524
|
}
|
|
2947
525
|
|
|
2948
|
-
async function
|
|
2949
|
-
const
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
}
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
let released=false;
|
|
2969
|
-
|
|
2970
|
-
return async()=>{
|
|
2971
|
-
if(released){
|
|
2972
|
-
return;
|
|
2973
|
-
}
|
|
2974
|
-
|
|
2975
|
-
released=true;
|
|
2976
|
-
const cleanupErrors=[];
|
|
2977
|
-
|
|
2978
|
-
try{
|
|
2979
|
-
await handle.close();
|
|
2980
|
-
}catch(error){
|
|
2981
|
-
cleanupErrors.push(`close failed: ${error.message}`);
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
|
-
try{
|
|
2985
|
-
await rm(lockPath,{force:true});
|
|
2986
|
-
}catch(error){
|
|
2987
|
-
cleanupErrors.push(`remove failed: ${error.message}`);
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
if(cleanupErrors.length){
|
|
2991
|
-
console.error(
|
|
2992
|
-
`Arcane packager completed but could not fully clean ${lockPath} (${cleanupErrors.join('; ')}). Remove the stale lock before the next operation.`
|
|
2993
|
-
);
|
|
2994
|
-
}
|
|
526
|
+
async function inspectContext(context,{signal}={}){
|
|
527
|
+
const records=await collectPackageRecords(context,{signal});
|
|
528
|
+
return {
|
|
529
|
+
appId:context.appId,
|
|
530
|
+
displayName:context.config.displayName,
|
|
531
|
+
version:context.config.version,
|
|
532
|
+
entry:context.config.entry,
|
|
533
|
+
strategy:context.config.strategy,
|
|
534
|
+
include:[...context.config.include],
|
|
535
|
+
exclude:[...context.config.exclude],
|
|
536
|
+
shared:[...context.config.shared],
|
|
537
|
+
...(context.config.security===undefined?{}:{security:copyJson(context.config.security)}),
|
|
538
|
+
...(context.config.localAIModelPolicy===undefined?{}:{
|
|
539
|
+
localAIModelPolicy:copyJson(context.config.localAIModelPolicy)
|
|
540
|
+
}),
|
|
541
|
+
...(context.config.adapter===undefined?{}:{adapter:context.config.adapter}),
|
|
542
|
+
descriptor:await optionalDescriptor(context),
|
|
543
|
+
browserDocuments:await browserDocuments(records),
|
|
544
|
+
files:records.map(record=>record.destination),
|
|
545
|
+
output:path.relative(context.workspaceRoot,context.outputRoot).split(path.sep).join('/')
|
|
2995
546
|
};
|
|
2996
547
|
}
|
|
2997
548
|
|
|
2998
|
-
async function
|
|
2999
|
-
const
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
const rootConfig=await loadRootConfig(resolvedWorkspace);
|
|
3006
|
-
const distRoot=path.join(resolvedWorkspace,rootConfig.distRoot);
|
|
3007
|
-
await assertSafeDistBoundary(resolvedWorkspace,distRoot,{create:true});
|
|
3008
|
-
return acquirePackageLock(distRoot,appId);
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
async function readDistVersion(outputRoot){
|
|
3012
|
-
try{
|
|
3013
|
-
const release=await readJson(path.join(outputRoot,RELEASE_MANIFEST_NAME));
|
|
3014
|
-
return typeof release?.app?.version==='string'?release.app.version:null;
|
|
3015
|
-
}catch{
|
|
3016
|
-
return null;
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
export async function discoverApps({workspaceRoot:requestedWorkspaceRoot}){
|
|
3021
|
-
const workspaceRoot=normalizeWorkspaceRoot(requestedWorkspaceRoot);
|
|
3022
|
-
const rootConfig=await loadRootConfig(workspaceRoot);
|
|
3023
|
-
const appsRoot=path.join(workspaceRoot,rootConfig.appsRoot);
|
|
549
|
+
export async function discoverApps({workspaceRoot:requestedWorkspaceRoot}={}){
|
|
550
|
+
const workspaceRoot=await realDirectory(normalizeWorkspaceRoot(requestedWorkspaceRoot),'Workspace root');
|
|
551
|
+
const rootConfig=validateRootConfig(
|
|
552
|
+
await readJson(path.join(workspaceRoot,ROOT_CONFIG_NAME),ROOT_CONFIG_NAME),
|
|
553
|
+
path.join(workspaceRoot,ROOT_CONFIG_NAME)
|
|
554
|
+
);
|
|
555
|
+
const appsRoot=await realDirectory(path.join(workspaceRoot,rootConfig.appsRoot),'Apps root');
|
|
3024
556
|
const entries=await readdir(appsRoot,{withFileTypes:true});
|
|
3025
557
|
const apps=[];
|
|
3026
|
-
|
|
3027
558
|
for(const entry of entries.sort((left,right)=>compareText(left.name,right.name))){
|
|
3028
|
-
if(!APP_ID_PATTERN.test(entry.name)
|
|
3029
|
-
continue;
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
if(entry.isSymbolicLink()){
|
|
3033
|
-
apps.push({
|
|
3034
|
-
id:entry.name,
|
|
3035
|
-
displayName:entry.name,
|
|
3036
|
-
configured:false,
|
|
3037
|
-
status:'unsafe-link',
|
|
3038
|
-
version:null,
|
|
3039
|
-
distVersion:null
|
|
3040
|
-
});
|
|
3041
|
-
continue;
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
if(!entry.isDirectory()){
|
|
3045
|
-
continue;
|
|
3046
|
-
}
|
|
3047
|
-
|
|
559
|
+
if(!entry.isDirectory()||!APP_ID_PATTERN.test(entry.name))continue;
|
|
3048
560
|
const configPath=path.join(appsRoot,entry.name,APP_CONFIG_NAME);
|
|
3049
|
-
|
|
3050
561
|
try{
|
|
3051
|
-
const
|
|
3052
|
-
apps.push(
|
|
3053
|
-
id:entry.name,
|
|
3054
|
-
displayName:context.config.displayName,
|
|
3055
|
-
configured:true,
|
|
3056
|
-
status:'ready',
|
|
3057
|
-
version:context.config.version,
|
|
3058
|
-
distVersion:await readDistVersion(context.outputRoot),
|
|
3059
|
-
strategy:context.config.strategy,
|
|
3060
|
-
entry:context.config.entry,
|
|
3061
|
-
output:path.relative(workspaceRoot,context.outputRoot).replaceAll('\\','/')
|
|
3062
|
-
});
|
|
562
|
+
const info=await lstat(configPath);
|
|
563
|
+
if(!info.isSymbolicLink()&&info.isFile())apps.push(entry.name);
|
|
3063
564
|
}catch(error){
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
configured=false;
|
|
3070
|
-
}
|
|
565
|
+
if(error?.code!=='ENOENT')throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return apps;
|
|
569
|
+
}
|
|
3071
570
|
|
|
3072
|
-
|
|
571
|
+
export async function inspectApp({workspaceRoot,appId,signal}={}){
|
|
572
|
+
throwIfAborted(signal);
|
|
573
|
+
const context=await loadContext(workspaceRoot,appId);
|
|
574
|
+
return inspectContext(context,{signal});
|
|
575
|
+
}
|
|
3073
576
|
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
577
|
+
async function copyRecords(records,stagingRoot,{signal,onEvent}={}){
|
|
578
|
+
for(const record of records){
|
|
579
|
+
throwIfAborted(signal);
|
|
580
|
+
const destination=resolveInside(stagingRoot,record.destination,'package destination');
|
|
581
|
+
await mkdir(path.dirname(destination),{recursive:true});
|
|
582
|
+
await copyFile(record.source,destination);
|
|
583
|
+
await emit(onEvent,{type:'package.file.copied',path:record.destination});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
3082
586
|
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
587
|
+
async function listOutputFiles(root,{signal}={}){
|
|
588
|
+
const files=[];
|
|
589
|
+
async function visit(directory,relativeRoot=''){
|
|
590
|
+
throwIfAborted(signal);
|
|
591
|
+
const entries=await readdir(directory,{withFileTypes:true});
|
|
592
|
+
entries.sort((left,right)=>compareText(left.name,right.name));
|
|
593
|
+
for(const entry of entries){
|
|
594
|
+
const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
|
|
595
|
+
const absolute=path.join(directory,entry.name);
|
|
596
|
+
const info=await lstat(absolute);
|
|
597
|
+
if(info.isSymbolicLink())fail(`Package output contains a symbolic link: ${relative}.`);
|
|
598
|
+
if(info.isDirectory())await visit(absolute,relative);
|
|
599
|
+
else if(info.isFile())files.push(relative);
|
|
600
|
+
else fail(`Package output contains a non-file entry: ${relative}.`);
|
|
3092
601
|
}
|
|
3093
602
|
}
|
|
3094
|
-
|
|
3095
|
-
return
|
|
603
|
+
await visit(root);
|
|
604
|
+
return files.sort(compareText);
|
|
3096
605
|
}
|
|
3097
606
|
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
const
|
|
3101
|
-
|
|
3102
|
-
const
|
|
3103
|
-
|
|
3104
|
-
.
|
|
3105
|
-
|
|
607
|
+
async function loadAdapter(context){
|
|
608
|
+
if(context.config.strategy!=='adapter')return null;
|
|
609
|
+
const adapterPath=resolveInside(context.appRoot,context.config.adapter,`${context.appId} adapter`);
|
|
610
|
+
await assertContainedRealPath(context.appRoot,adapterPath,`${context.appId} adapter`);
|
|
611
|
+
const module=await import(`${pathToFileURL(adapterPath).href}?source=${Date.now()}`);
|
|
612
|
+
if(typeof module.buildArcanePackage!=='function'){
|
|
613
|
+
fail(`${context.appId} adapter must export buildArcanePackage.`);
|
|
614
|
+
}
|
|
615
|
+
return module;
|
|
616
|
+
}
|
|
3106
617
|
|
|
618
|
+
function releaseManifest(context,files){
|
|
3107
619
|
return {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
:undefined
|
|
620
|
+
schemaVersion:1,
|
|
621
|
+
kind:'arcane-app-release',
|
|
622
|
+
packagerVersion:PACKAGER_VERSION,
|
|
623
|
+
app:{
|
|
624
|
+
id:context.appId,
|
|
625
|
+
displayName:context.config.displayName,
|
|
626
|
+
version:context.config.version,
|
|
627
|
+
entry:context.config.entry,
|
|
628
|
+
strategy:context.config.strategy,
|
|
629
|
+
shared:[...context.config.shared],
|
|
630
|
+
...(context.config.security===undefined?{}:{security:copyJson(context.config.security)}),
|
|
631
|
+
...(context.config.localAIModelPolicy===undefined?{}:{
|
|
632
|
+
localAIModelPolicy:copyJson(context.config.localAIModelPolicy)
|
|
633
|
+
})
|
|
634
|
+
},
|
|
635
|
+
files:[...files]
|
|
3125
636
|
};
|
|
3126
637
|
}
|
|
3127
638
|
|
|
3128
|
-
async function
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
signal,
|
|
3141
|
-
onEvent,
|
|
3142
|
-
validateSourceState
|
|
3143
|
-
}){
|
|
3144
|
-
throwIfAborted(signal);
|
|
3145
|
-
if(validateSourceState!==undefined&&typeof validateSourceState!=='function'){
|
|
3146
|
-
fail('validateSourceState must be a function when provided.');
|
|
639
|
+
async function replaceDirectory(stagingRoot,outputRoot){
|
|
640
|
+
const backupRoot=`${outputRoot}.backup-${process.pid}-${Date.now()}`;
|
|
641
|
+
let backedUp=false;
|
|
642
|
+
try{
|
|
643
|
+
const existing=await lstat(outputRoot);
|
|
644
|
+
if(existing.isSymbolicLink()||!existing.isDirectory()){
|
|
645
|
+
fail('Existing package output must be a real directory.');
|
|
646
|
+
}
|
|
647
|
+
await rename(outputRoot,backupRoot);
|
|
648
|
+
backedUp=true;
|
|
649
|
+
}catch(error){
|
|
650
|
+
if(error?.code!=='ENOENT')throw error;
|
|
3147
651
|
}
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const currentVersion=context.config.version;
|
|
3155
|
-
const version=resolveTargetVersion(currentVersion,{bump,exactVersion,preid});
|
|
3156
|
-
const importMapFiles=dryRun
|
|
3157
|
-
?Object.freeze([])
|
|
3158
|
-
:await authenticateImportMapFiles(context,importMapReceipt,{signal});
|
|
3159
|
-
const files=await collectPackageFiles(context,{
|
|
3160
|
-
signal,
|
|
3161
|
-
sharedPayloadState:authenticatedSharedPayloadState
|
|
3162
|
-
});
|
|
3163
|
-
if(!dryRun){
|
|
3164
|
-
await authenticateCollectedImportMapFiles(context,files,importMapFiles,{signal});
|
|
652
|
+
try{
|
|
653
|
+
await rename(stagingRoot,outputRoot);
|
|
654
|
+
if(backedUp)await rm(backupRoot,{recursive:true});
|
|
655
|
+
}catch(error){
|
|
656
|
+
if(backedUp)await rename(backupRoot,outputRoot).catch(()=>{});
|
|
657
|
+
throw error;
|
|
3165
658
|
}
|
|
3166
|
-
|
|
3167
|
-
app:appId,
|
|
3168
|
-
currentVersion,
|
|
3169
|
-
version,
|
|
3170
|
-
bump:bump??null,
|
|
3171
|
-
dryRun:Boolean(dryRun),
|
|
3172
|
-
output:path.relative(context.workspaceRoot,context.outputRoot).replaceAll('\\','/'),
|
|
3173
|
-
baseFileCount:files.length,
|
|
3174
|
-
baseBytes:files.reduce((total,file)=>total+file.bytes,0),
|
|
3175
|
-
strategy:context.config.strategy
|
|
3176
|
-
};
|
|
659
|
+
}
|
|
3177
660
|
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
661
|
+
async function packageWithContext(context,options={}){
|
|
662
|
+
const {signal,onEvent}=options;
|
|
663
|
+
const inspected=await inspectContext(context,{signal});
|
|
664
|
+
if(options.dryRun){
|
|
665
|
+
return {
|
|
666
|
+
appId:context.appId,
|
|
667
|
+
version:context.config.version,
|
|
668
|
+
output:inspected.output,
|
|
669
|
+
dryRun:true,
|
|
670
|
+
files:[...inspected.files]
|
|
671
|
+
};
|
|
3181
672
|
}
|
|
3182
|
-
|
|
3183
|
-
const
|
|
3184
|
-
|
|
3185
|
-
const
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
673
|
+
await mkdir(context.distRoot,{recursive:true});
|
|
674
|
+
const distInfo=await lstat(context.distRoot);
|
|
675
|
+
if(distInfo.isSymbolicLink()||!distInfo.isDirectory())fail('dist must be a real directory.');
|
|
676
|
+
const stagingRoot=path.join(
|
|
677
|
+
context.distRoot,
|
|
678
|
+
`.${context.appId}-staging-${process.pid}-${Date.now()}`
|
|
679
|
+
);
|
|
680
|
+
await mkdir(stagingRoot);
|
|
3190
681
|
let promoted=false;
|
|
3191
|
-
let operationSucceeded=false;
|
|
3192
|
-
let rollbackRestored=false;
|
|
3193
|
-
let runtimeAuthorityState=null;
|
|
3194
|
-
|
|
3195
682
|
try{
|
|
3196
|
-
await
|
|
3197
|
-
|
|
3198
|
-
await
|
|
3199
|
-
await rm(failedOutput,{recursive:true,force:true}).catch(()=>{});
|
|
3200
|
-
adapter=await loadAdapter(context);
|
|
3201
|
-
throwIfAborted(signal);
|
|
3202
|
-
|
|
683
|
+
const records=await collectPackageRecords(context,{signal});
|
|
684
|
+
const copyBase=()=>copyRecords(records,stagingRoot,{signal,onEvent});
|
|
685
|
+
const adapter=await loadAdapter(context);
|
|
3203
686
|
if(adapter){
|
|
3204
|
-
let prepared=false;
|
|
3205
687
|
await adapter.buildArcanePackage({
|
|
688
|
+
appId:context.appId,
|
|
3206
689
|
workspaceRoot:context.workspaceRoot,
|
|
3207
690
|
appRoot:context.appRoot,
|
|
3208
|
-
outputRoot:
|
|
3209
|
-
|
|
3210
|
-
version,
|
|
3211
|
-
signal,
|
|
3212
|
-
onEvent,
|
|
3213
|
-
deferFinalVerification:true,
|
|
3214
|
-
prepareBase:async outputRoot=>{
|
|
3215
|
-
if(prepared){
|
|
3216
|
-
fail(`${appId} adapter requested its base payload more than once.`);
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
prepared=true;
|
|
3220
|
-
const requestedRoot=path.resolve(outputRoot);
|
|
3221
|
-
|
|
3222
|
-
if(requestedRoot!==path.resolve(staging)
|
|
3223
|
-
&&requestedRoot!==path.resolve(stagingTemporary)){
|
|
3224
|
-
fail(`${appId} adapter requested its base payload outside its assigned staging roots.`);
|
|
3225
|
-
}
|
|
3226
|
-
|
|
3227
|
-
await materializeBasePackage(context,outputRoot,files,{signal,onEvent});
|
|
3228
|
-
}
|
|
3229
|
-
});
|
|
3230
|
-
|
|
3231
|
-
if(!prepared){
|
|
3232
|
-
fail(`${appId} adapter did not materialize the configured public base payload.`);
|
|
3233
|
-
}
|
|
3234
|
-
}else{
|
|
3235
|
-
await materializeBasePackage(context,staging,files,{signal,onEvent});
|
|
3236
|
-
}
|
|
3237
|
-
|
|
3238
|
-
throwIfAborted(signal);
|
|
3239
|
-
let sourceValidation;
|
|
3240
|
-
if(validateSourceState){
|
|
3241
|
-
sourceValidation=await validateSourceState({signal});
|
|
3242
|
-
await assertValidatedDescriptorAuthority(
|
|
3243
|
-
sourceValidation,
|
|
3244
|
-
context.descriptorAuthority
|
|
3245
|
-
);
|
|
3246
|
-
}
|
|
3247
|
-
runtimeAuthorityState=await prepareRuntimeAuthorityState(context,{
|
|
3248
|
-
validation:sourceValidation,
|
|
3249
|
-
runtimeVerificationState,
|
|
3250
|
-
signal,
|
|
3251
|
-
onEvent
|
|
3252
|
-
});
|
|
3253
|
-
await writeRuntimeAuthorities(staging,runtimeAuthorityState);
|
|
3254
|
-
await writeRuntimeProjection(staging,runtimeAuthorityState);
|
|
3255
|
-
const releaseState=await writeReleaseManifest(staging,context,version,{signal,onEvent});
|
|
3256
|
-
const verifiedRelease=adapter
|
|
3257
|
-
?await verifyBuiltPackage(context,staging,version,adapter,{
|
|
3258
|
-
runtimeAuthorityState,
|
|
691
|
+
outputRoot:stagingRoot,
|
|
692
|
+
copyBase,
|
|
3259
693
|
signal,
|
|
3260
694
|
onEvent
|
|
3261
|
-
})
|
|
3262
|
-
:await verifyFreshStaticRelease(staging,context,version,releaseState,{
|
|
3263
|
-
runtimeAuthorityState,
|
|
3264
|
-
signal
|
|
3265
695
|
});
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
if(validateSourceState){
|
|
3269
|
-
const validation=await validateSourceState({signal});
|
|
3270
|
-
await assertValidatedDescriptorAuthority(
|
|
3271
|
-
validation,
|
|
3272
|
-
context.descriptorAuthority
|
|
3273
|
-
);
|
|
696
|
+
}else{
|
|
697
|
+
await copyBase();
|
|
3274
698
|
}
|
|
3275
|
-
await
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
await authenticateSharedPayloadSnapshotState(sharedPayloadSnapshot,{
|
|
3279
|
-
workspaceRoot:context.workspaceRoot,
|
|
3280
|
-
sharedPayloadIds:context.config.shared,
|
|
3281
|
-
signal
|
|
3282
|
-
});
|
|
699
|
+
const files=await listOutputFiles(stagingRoot,{signal});
|
|
700
|
+
if(files.some(file=>pathKey(file)===pathKey(RELEASE_MANIFEST_NAME))){
|
|
701
|
+
fail(`Package content must not author ${RELEASE_MANIFEST_NAME}.`);
|
|
3283
702
|
}
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
await assertArtifactState(staging,verifiedRelease.identities,{signal});
|
|
3287
|
-
throwIfAborted(signal);
|
|
3288
|
-
|
|
3289
|
-
try{
|
|
3290
|
-
await lstat(context.outputRoot);
|
|
3291
|
-
await renamePackageDirectory(context.outputRoot,backup);
|
|
3292
|
-
movedExisting=true;
|
|
3293
|
-
}catch(error){
|
|
3294
|
-
if(error?.code!=='ENOENT'){
|
|
3295
|
-
throw error;
|
|
3296
|
-
}
|
|
703
|
+
if(!files.some(file=>pathKey(file)===pathKey(context.config.entry))){
|
|
704
|
+
fail(`Package output is missing its entry file: ${context.config.entry}.`);
|
|
3297
705
|
}
|
|
3298
|
-
|
|
706
|
+
const manifest=releaseManifest(context,files);
|
|
707
|
+
await writeFile(
|
|
708
|
+
path.join(stagingRoot,RELEASE_MANIFEST_NAME),
|
|
709
|
+
`${JSON.stringify(manifest,null,2)}\n`,
|
|
710
|
+
'utf8'
|
|
711
|
+
);
|
|
3299
712
|
throwIfAborted(signal);
|
|
3300
|
-
await
|
|
713
|
+
await replaceDirectory(stagingRoot,context.outputRoot);
|
|
3301
714
|
promoted=true;
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
context.outputRoot,
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
{
|
|
3309
|
-
signal,
|
|
3310
|
-
packageConfig:{...context.config,version},
|
|
3311
|
-
descriptorAuthority:context.descriptorAuthority
|
|
3312
|
-
}
|
|
3313
|
-
);
|
|
3314
|
-
|
|
3315
|
-
if(version!==currentVersion){
|
|
3316
|
-
await writeAppVersion(context,version);
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3319
|
-
operationSucceeded=true;
|
|
715
|
+
await emit(onEvent,{
|
|
716
|
+
type:'package.completed',
|
|
717
|
+
appId:context.appId,
|
|
718
|
+
outputRoot:context.outputRoot,
|
|
719
|
+
files:[...files]
|
|
720
|
+
});
|
|
3320
721
|
return {
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
722
|
+
appId:context.appId,
|
|
723
|
+
version:context.config.version,
|
|
724
|
+
output:path.relative(context.workspaceRoot,context.outputRoot).split(path.sep).join('/'),
|
|
725
|
+
outputRoot:context.outputRoot,
|
|
726
|
+
manifest,
|
|
727
|
+
files:[...files]
|
|
3327
728
|
};
|
|
3328
|
-
}catch(error){
|
|
3329
|
-
const rollbackErrors=[];
|
|
3330
|
-
let targetVacated=!promoted;
|
|
3331
|
-
|
|
3332
|
-
if(promoted){
|
|
3333
|
-
try{
|
|
3334
|
-
await renamePackageDirectory(context.outputRoot,failedOutput);
|
|
3335
|
-
targetVacated=true;
|
|
3336
|
-
}catch(moveError){
|
|
3337
|
-
targetVacated=false;
|
|
3338
|
-
rollbackErrors.push(`could not move the failed package aside: ${moveError.message}`);
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
if(movedExisting&&targetVacated){
|
|
3343
|
-
try{
|
|
3344
|
-
await renamePackageDirectory(backup,context.outputRoot);
|
|
3345
|
-
rollbackRestored=true;
|
|
3346
|
-
}catch(restoreError){
|
|
3347
|
-
rollbackErrors.push(`could not restore the previous package from ${backup}: ${restoreError.message}`);
|
|
3348
|
-
}
|
|
3349
|
-
}
|
|
3350
|
-
|
|
3351
|
-
if(rollbackErrors.length){
|
|
3352
|
-
error.message+=` Rollback warning: ${rollbackErrors.join('; ')}. Preserve ${backup} until manually recovered.`;
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
throw error;
|
|
3356
729
|
}finally{
|
|
3357
|
-
await rm(
|
|
3358
|
-
await rm(stagingTemporary,{recursive:true,force:true}).catch(()=>{});
|
|
3359
|
-
await rm(failedOutput,{recursive:true,force:true}).catch(()=>{});
|
|
3360
|
-
|
|
3361
|
-
if(operationSucceeded||rollbackRestored||!movedExisting){
|
|
3362
|
-
await rm(backup,{recursive:true,force:true}).catch(()=>{});
|
|
3363
|
-
}
|
|
730
|
+
if(!promoted)await rm(stagingRoot,{recursive:true,force:true}).catch(()=>{});
|
|
3364
731
|
}
|
|
3365
732
|
}
|
|
3366
733
|
|
|
3367
|
-
export async function packageApp(options){
|
|
3368
|
-
|
|
3369
|
-
const
|
|
3370
|
-
|
|
3371
|
-
appId:options?.appId,
|
|
3372
|
-
bindDescriptorAuthority:true,
|
|
3373
|
-
signal:options?.signal
|
|
3374
|
-
});
|
|
3375
|
-
throwIfAborted(options?.signal);
|
|
3376
|
-
const authenticatedSharedPayloadState=options?.sharedPayloadSnapshot===undefined
|
|
3377
|
-
?null
|
|
3378
|
-
:await authenticateSharedPayloadSnapshotState(options.sharedPayloadSnapshot,{
|
|
3379
|
-
workspaceRoot:context.workspaceRoot,
|
|
3380
|
-
sharedPayloadIds:context.config.shared,
|
|
3381
|
-
signal:options?.signal
|
|
3382
|
-
});
|
|
3383
|
-
if(options?.dryRun){
|
|
3384
|
-
if(options?.runtimeVerificationState!==undefined){
|
|
3385
|
-
if(!await hasExternalRuntimeAdmission(context)){
|
|
3386
|
-
fail('A runtime verification state cannot be supplied to an integrated workspace.');
|
|
3387
|
-
}
|
|
3388
|
-
const validation=await validateExternalRuntimeAdmission(context,{
|
|
3389
|
-
signal:options?.signal,
|
|
3390
|
-
onEvent:options?.onEvent
|
|
3391
|
-
});
|
|
3392
|
-
await authenticatePackageRuntimeVerificationState(
|
|
3393
|
-
context,
|
|
3394
|
-
options.runtimeVerificationState,
|
|
3395
|
-
{signal:options?.signal,validation}
|
|
3396
|
-
);
|
|
3397
|
-
}
|
|
3398
|
-
return packageAppUnlocked({...options,context,authenticatedSharedPayloadState});
|
|
3399
|
-
}
|
|
734
|
+
export async function packageApp(options={}){
|
|
735
|
+
const context=await loadContext(options.workspaceRoot,options.appId);
|
|
736
|
+
const execute=()=>packageWithContext(context,options);
|
|
737
|
+
if(options.workspaceOperationLease)return execute();
|
|
3400
738
|
return withWorkspaceOperationLock({
|
|
3401
739
|
workspaceRoot:context.workspaceRoot,
|
|
3402
740
|
operation:'package',
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
},async workspaceOperationLease=>{
|
|
3407
|
-
await assertSafeDistBoundary(context.workspaceRoot,context.distRoot,{create:true});
|
|
3408
|
-
const releaseLock=await acquirePackageLock(context.distRoot,options?.appId);
|
|
3409
|
-
try{
|
|
3410
|
-
const refreshed=await refreshPackageImportMap(context,{
|
|
3411
|
-
runtimeVerificationState:options?.runtimeVerificationState,
|
|
3412
|
-
workspaceOperationLease,
|
|
3413
|
-
signal:options?.signal,
|
|
3414
|
-
onEvent:options?.onEvent
|
|
3415
|
-
});
|
|
3416
|
-
const importMapReceipt=authenticatedImportMapReceipt(refreshed.importMapReceipt);
|
|
3417
|
-
const packaged=await packageAppUnlocked({
|
|
3418
|
-
...options,
|
|
3419
|
-
context,
|
|
3420
|
-
authenticatedSharedPayloadState,
|
|
3421
|
-
importMapReceipt,
|
|
3422
|
-
runtimeVerificationState:refreshed.runtimeVerificationState??undefined
|
|
3423
|
-
});
|
|
3424
|
-
return {...packaged,importMapReceipt};
|
|
3425
|
-
}finally{
|
|
3426
|
-
await releaseLock();
|
|
3427
|
-
}
|
|
3428
|
-
});
|
|
741
|
+
signal:options.signal,
|
|
742
|
+
onEvent:options.onEvent
|
|
743
|
+
},execute);
|
|
3429
744
|
}
|
|
3430
745
|
|
|
3431
|
-
export async function verifyApp({
|
|
3432
|
-
workspaceRoot,
|
|
3433
|
-
appId,
|
|
3434
|
-
runtimeVerificationState,
|
|
3435
|
-
signal,
|
|
3436
|
-
onEvent
|
|
3437
|
-
}){
|
|
746
|
+
export async function verifyApp({workspaceRoot,appId,signal,onEvent}={}){
|
|
3438
747
|
throwIfAborted(signal);
|
|
3439
|
-
const context=await
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
const receipt=await issueAppReleaseReceipt(
|
|
3461
|
-
context.outputRoot,
|
|
3462
|
-
release,
|
|
3463
|
-
releaseState.identities,
|
|
3464
|
-
{
|
|
3465
|
-
signal,
|
|
3466
|
-
packageConfig:context.config,
|
|
3467
|
-
descriptorAuthority:context.descriptorAuthority
|
|
3468
|
-
}
|
|
3469
|
-
);
|
|
3470
|
-
|
|
748
|
+
const context=await loadContext(workspaceRoot,appId);
|
|
749
|
+
const outputRoot=await realDirectory(context.outputRoot,`dist/${appId}`);
|
|
750
|
+
const manifest=await readJson(path.join(outputRoot,RELEASE_MANIFEST_NAME),RELEASE_MANIFEST_NAME);
|
|
751
|
+
if(!isPlainObject(manifest)||manifest.schemaVersion!==1||manifest.kind!=='arcane-app-release'
|
|
752
|
+
||manifest.packagerVersion!==PACKAGER_VERSION||manifest.app?.id!==appId
|
|
753
|
+
||manifest.app?.version!==context.config.version||!Array.isArray(manifest.files)){
|
|
754
|
+
fail(`${RELEASE_MANIFEST_NAME} is malformed.`);
|
|
755
|
+
}
|
|
756
|
+
const expected=manifest.files.map((file,index)=>normalizeRelativePath(
|
|
757
|
+
file,
|
|
758
|
+
`${RELEASE_MANIFEST_NAME}.files[${index}]`
|
|
759
|
+
)).sort(compareText);
|
|
760
|
+
if(new Set(expected.map(pathKey)).size!==expected.length){
|
|
761
|
+
fail(`${RELEASE_MANIFEST_NAME} contains duplicate files.`);
|
|
762
|
+
}
|
|
763
|
+
const actual=(await listOutputFiles(outputRoot,{signal}))
|
|
764
|
+
.filter(file=>pathKey(file)!==pathKey(RELEASE_MANIFEST_NAME));
|
|
765
|
+
if(JSON.stringify(actual)!==JSON.stringify(expected)){
|
|
766
|
+
fail('Packaged file inventory differs from its release manifest.');
|
|
767
|
+
}
|
|
768
|
+
await emit(onEvent,{type:'package.inspected',appId,outputRoot,files:[...actual]});
|
|
3471
769
|
return {
|
|
3472
|
-
app:appId,
|
|
3473
|
-
// The verified manifest is already bound to this exact configured
|
|
3474
|
-
// version; do not couple the public verify result to a nested app
|
|
3475
|
-
// representation used by a particular schema generation.
|
|
3476
|
-
version:context.config.version,
|
|
3477
|
-
output:path.relative(context.workspaceRoot,context.outputRoot).replaceAll('\\','/'),
|
|
3478
|
-
fileCount:release.fileCount,
|
|
3479
|
-
totalBytes:release.totalBytes,
|
|
3480
|
-
contentSha256:release.contentSha256,
|
|
3481
770
|
verified:true,
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
workspaceRoot,
|
|
3488
|
-
appId,
|
|
3489
|
-
bump,
|
|
3490
|
-
preid,
|
|
3491
|
-
exactVersion,
|
|
3492
|
-
dryRun=false
|
|
3493
|
-
}){
|
|
3494
|
-
const context=await getAppContext({workspaceRoot,appId});
|
|
3495
|
-
const currentVersion=context.config.version;
|
|
3496
|
-
const version=resolveTargetVersion(currentVersion,{bump,exactVersion,preid});
|
|
3497
|
-
|
|
3498
|
-
if(version===currentVersion){
|
|
3499
|
-
fail('A bump level or exact version is required.');
|
|
3500
|
-
}
|
|
3501
|
-
|
|
3502
|
-
if(!dryRun){
|
|
3503
|
-
await writeAppVersion(context,version);
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
return {
|
|
3507
|
-
app:appId,
|
|
3508
|
-
currentVersion,
|
|
3509
|
-
version,
|
|
3510
|
-
bump:bump??null,
|
|
3511
|
-
dryRun:Boolean(dryRun)
|
|
771
|
+
appId,
|
|
772
|
+
version:context.config.version,
|
|
773
|
+
outputRoot,
|
|
774
|
+
manifest:copyJson(manifest),
|
|
775
|
+
files:[...actual]
|
|
3512
776
|
};
|
|
3513
777
|
}
|
|
3514
778
|
|
|
3515
|
-
export async function bumpVersion(
|
|
3516
|
-
|
|
3517
|
-
|
|
779
|
+
export async function bumpVersion({workspaceRoot,appId,bump='patch',preid,signal,onEvent}={}){
|
|
780
|
+
throwIfAborted(signal);
|
|
781
|
+
const context=await loadContext(workspaceRoot,appId);
|
|
782
|
+
const nextVersion=incrementSemver(context.config.version,bump,preid);
|
|
783
|
+
const configDocument=await readJson(context.config.configPath,`${appId}/${APP_CONFIG_NAME}`);
|
|
784
|
+
configDocument.version=nextVersion;
|
|
785
|
+
const descriptorPath=path.join(context.appRoot,APP_DESCRIPTOR_NAME);
|
|
786
|
+
let descriptor=null;
|
|
787
|
+
try{
|
|
788
|
+
descriptor=await readJson(descriptorPath,APP_DESCRIPTOR_NAME);
|
|
789
|
+
descriptor.version=nextVersion;
|
|
790
|
+
}catch(error){
|
|
791
|
+
if(error?.code!=='ARCANE_PACKAGE_INVALID'||!String(error.message).includes('does not exist'))throw error;
|
|
3518
792
|
}
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
});
|
|
3524
|
-
return withWorkspaceOperationLock({
|
|
3525
|
-
workspaceRoot:context.workspaceRoot,
|
|
3526
|
-
operation:'version-bump',
|
|
3527
|
-
workspaceOperationLease:options?.workspaceOperationLease,
|
|
3528
|
-
signal:options?.signal,
|
|
3529
|
-
onEvent:options?.onEvent
|
|
3530
|
-
},async()=>{
|
|
3531
|
-
const releaseLock=await acquireOperationLock(
|
|
3532
|
-
context.workspaceRoot,
|
|
3533
|
-
options?.appId
|
|
3534
|
-
);
|
|
3535
|
-
try{
|
|
3536
|
-
return await bumpVersionUnlocked({...options,workspaceRoot:context.workspaceRoot});
|
|
3537
|
-
}finally{
|
|
3538
|
-
await releaseLock();
|
|
3539
|
-
}
|
|
3540
|
-
});
|
|
793
|
+
await writeFile(context.config.configPath,`${JSON.stringify(configDocument,null,2)}\n`,'utf8');
|
|
794
|
+
if(descriptor)await writeFile(descriptorPath,`${JSON.stringify(descriptor,null,2)}\n`,'utf8');
|
|
795
|
+
await emit(onEvent,{type:'package.version.updated',appId,version:nextVersion});
|
|
796
|
+
return {appId,previousVersion:context.config.version,version:nextVersion};
|
|
3541
797
|
}
|