arcane-os 0.2.2 → 0.3.0
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 +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
package/src/mail.mjs
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
2
|
+
import {
|
|
3
|
+
deleteMailCredential,
|
|
4
|
+
getMailCredentialStatus,
|
|
5
|
+
readMailCredential,
|
|
6
|
+
setMailCredential
|
|
7
|
+
} from './mail-credentials.mjs';
|
|
8
|
+
import {sendResendMail,startResendMailServer} from './mail-server.mjs';
|
|
9
|
+
|
|
10
|
+
export const MAIL_COMMAND_ACTIONS=Object.freeze([
|
|
11
|
+
'key-set',
|
|
12
|
+
'key-status',
|
|
13
|
+
'key-delete',
|
|
14
|
+
'send',
|
|
15
|
+
'serve'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const ACTION_SET=new Set(MAIL_COMMAND_ACTIONS);
|
|
19
|
+
|
|
20
|
+
function usage(message){
|
|
21
|
+
throw new ArcaneError(ERROR_CODES.usage,message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateOptions(options){
|
|
25
|
+
if(!options||typeof options!=='object'||Array.isArray(options)){
|
|
26
|
+
usage('Mail command options must be an object.');
|
|
27
|
+
}
|
|
28
|
+
if(typeof options.action!=='string'||!ACTION_SET.has(options.action)){
|
|
29
|
+
usage(`Mail action must be one of: ${MAIL_COMMAND_ACTIONS.join(', ')}.`);
|
|
30
|
+
}
|
|
31
|
+
return options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dependency(options,name,fallback){
|
|
35
|
+
const value=options[name]??fallback;
|
|
36
|
+
if(typeof value!=='function'){
|
|
37
|
+
usage(`${name} must be a function when supplied.`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function recipientList(value,label){
|
|
43
|
+
const entries=Array.isArray(value)
|
|
44
|
+
? value
|
|
45
|
+
: typeof value==='string'
|
|
46
|
+
? value.split(',')
|
|
47
|
+
: null;
|
|
48
|
+
if(!entries||entries.length===0||entries.length>50){
|
|
49
|
+
usage(`${label} must contain one to 50 comma-separated email addresses.`);
|
|
50
|
+
}
|
|
51
|
+
const result=entries.map(function normalizeMailRecipient(entry){
|
|
52
|
+
if(typeof entry!=='string'||!entry.trim()){
|
|
53
|
+
usage(`${label} contains an empty email address.`);
|
|
54
|
+
}
|
|
55
|
+
return entry.trim();
|
|
56
|
+
});
|
|
57
|
+
if(new Set(result.map(function lowercaseRecipient(entry){
|
|
58
|
+
return entry.toLowerCase();
|
|
59
|
+
})).size!==result.length){
|
|
60
|
+
usage(`${label} must not contain duplicate email addresses.`);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function originList(value){
|
|
66
|
+
const entries=Array.isArray(value)?value:[value];
|
|
67
|
+
if(entries.length===0||entries.some(function invalidOrigin(entry){
|
|
68
|
+
return typeof entry!=='string'||!entry.trim();
|
|
69
|
+
})){
|
|
70
|
+
usage('Mail serve requires at least one exact allowed origin.');
|
|
71
|
+
}
|
|
72
|
+
return entries.map(function normalizeAllowedOrigin(entry){return entry.trim();});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function credentialOptions(options){
|
|
76
|
+
return {
|
|
77
|
+
profile:options.profile,
|
|
78
|
+
platform:options.platform,
|
|
79
|
+
systemRoot:options.systemRoot,
|
|
80
|
+
temporaryDirectory:options.temporaryDirectory,
|
|
81
|
+
spawnImpl:options.spawnImpl,
|
|
82
|
+
runner:options.credentialRunner,
|
|
83
|
+
signal:options.signal,
|
|
84
|
+
timeoutMs:options.credentialTimeoutMs
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function setCredential(options){
|
|
89
|
+
const readSecret=dependency(options,'readSecret',null);
|
|
90
|
+
const store=dependency(options,'setCredential',setMailCredential);
|
|
91
|
+
throwIfAborted(options.signal);
|
|
92
|
+
let secret='';
|
|
93
|
+
try{
|
|
94
|
+
secret=await readSecret();
|
|
95
|
+
throwIfAborted(options.signal);
|
|
96
|
+
return await store({...credentialOptions(options),secret});
|
|
97
|
+
}finally{
|
|
98
|
+
secret='';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function credentialStatus(options){
|
|
103
|
+
const status=dependency(options,'getCredentialStatus',getMailCredentialStatus);
|
|
104
|
+
throwIfAborted(options.signal);
|
|
105
|
+
return status(credentialOptions(options));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function deleteCredential(options){
|
|
109
|
+
const remove=dependency(options,'deleteCredential',deleteMailCredential);
|
|
110
|
+
throwIfAborted(options.signal);
|
|
111
|
+
return remove(credentialOptions(options));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function safeSendFailure(result){
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
provider:'resend',
|
|
117
|
+
status:result.status,
|
|
118
|
+
classification:result.classification,
|
|
119
|
+
requestId:result.requestId,
|
|
120
|
+
providerStatus:result.providerStatus,
|
|
121
|
+
recipientCount:result.recipientCount,
|
|
122
|
+
retryable:result.retryable===true,
|
|
123
|
+
uncertain:result.uncertain===true,
|
|
124
|
+
...(typeof result.code==='string'?{code:result.code}:{}),
|
|
125
|
+
...(Number.isSafeInteger(result.retryAfterMs)&&result.retryAfterMs>0
|
|
126
|
+
?{retryAfterMs:result.retryAfterMs}
|
|
127
|
+
:{})
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function sendMail(options){
|
|
132
|
+
const readReport=dependency(options,'readReport',null);
|
|
133
|
+
const readCredential=dependency(options,'readCredential',readMailCredential);
|
|
134
|
+
const send=dependency(options,'sendMail',sendResendMail);
|
|
135
|
+
throwIfAborted(options.signal);
|
|
136
|
+
const report=await readReport();
|
|
137
|
+
throwIfAborted(options.signal);
|
|
138
|
+
let apiKey=await readCredential(credentialOptions(options));
|
|
139
|
+
if(apiKey===null){
|
|
140
|
+
throw new ArcaneError(
|
|
141
|
+
ERROR_CODES.prerequisiteMissing,
|
|
142
|
+
`No Resend credential is configured for profile ${String(options.profile)}.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if(typeof apiKey!=='string'||!apiKey){
|
|
146
|
+
throw new ArcaneError(
|
|
147
|
+
ERROR_CODES.operationFailed,
|
|
148
|
+
'The configured Resend credential could not be read.'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
try{
|
|
152
|
+
throwIfAborted(options.signal);
|
|
153
|
+
const result=await send({
|
|
154
|
+
apiKey,
|
|
155
|
+
appId:'arcane-cli',
|
|
156
|
+
fetchImpl:options.fetchImpl,
|
|
157
|
+
from:options.from,
|
|
158
|
+
maxProviderResponseBytes:options.maxProviderResponseBytes,
|
|
159
|
+
maxRequestBytes:options.maxRequestBytes,
|
|
160
|
+
onEvent:options.onEvent,
|
|
161
|
+
providerTimeoutMs:options.requestTimeout,
|
|
162
|
+
report,
|
|
163
|
+
reportKey:options.reportKey,
|
|
164
|
+
requestIdFactory:options.requestIdFactory,
|
|
165
|
+
retryableDelayMs:options.retryableDelayMs,
|
|
166
|
+
signal:options.signal
|
|
167
|
+
});
|
|
168
|
+
if(result?.classification==='accepted'&&result.status==='accepted'){
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
const details=safeSendFailure(result||{});
|
|
172
|
+
throw new ArcaneError(
|
|
173
|
+
ERROR_CODES.operationFailed,
|
|
174
|
+
`Resend did not authoritatively accept the mail request (${details.classification||'unknown'}).`,
|
|
175
|
+
{details}
|
|
176
|
+
);
|
|
177
|
+
}finally{
|
|
178
|
+
apiKey='';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function serveMail(options){
|
|
183
|
+
const readCredential=dependency(options,'readCredential',readMailCredential);
|
|
184
|
+
const readAppKey=dependency(options,'readAppKey',null);
|
|
185
|
+
const startServer=dependency(options,'startServer',startResendMailServer);
|
|
186
|
+
throwIfAborted(options.signal);
|
|
187
|
+
let apiKey=await readCredential(credentialOptions(options));
|
|
188
|
+
if(apiKey===null){
|
|
189
|
+
throw new ArcaneError(
|
|
190
|
+
ERROR_CODES.prerequisiteMissing,
|
|
191
|
+
`No Resend credential is configured for profile ${String(options.profile)}.`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if(typeof apiKey!=='string'||!apiKey){
|
|
195
|
+
throw new ArcaneError(
|
|
196
|
+
ERROR_CODES.operationFailed,
|
|
197
|
+
'The configured Resend credential could not be read.'
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
let appKey='';
|
|
201
|
+
try{
|
|
202
|
+
throwIfAborted(options.signal);
|
|
203
|
+
appKey=await readAppKey();
|
|
204
|
+
if(typeof appKey!=='string'||!appKey){
|
|
205
|
+
throw new ArcaneError(
|
|
206
|
+
ERROR_CODES.operationFailed,
|
|
207
|
+
'The local Mail gateway app key could not be read.'
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
throwIfAborted(options.signal);
|
|
211
|
+
const recipientAllowlist=recipientList(options.allowTo,'allowTo');
|
|
212
|
+
const errorRecipients=options.errorTo===undefined
|
|
213
|
+
? recipientAllowlist
|
|
214
|
+
: recipientList(options.errorTo,'errorTo');
|
|
215
|
+
return await startServer({
|
|
216
|
+
apiKey,
|
|
217
|
+
appKey,
|
|
218
|
+
appId:options.appId,
|
|
219
|
+
allowedOrigins:originList(options.origin),
|
|
220
|
+
bodyQueueTimeoutMs:options.bodyQueueTimeoutMs,
|
|
221
|
+
bodyTimeoutMs:options.bodyTimeoutMs,
|
|
222
|
+
errorRecipients,
|
|
223
|
+
fetchImpl:options.fetchImpl,
|
|
224
|
+
from:options.from,
|
|
225
|
+
host:options.host,
|
|
226
|
+
maxConcurrentBodyReads:options.maxConcurrentBodyReads,
|
|
227
|
+
maxConcurrentSends:options.maxConcurrentSends,
|
|
228
|
+
maxMessageBytes:options.maxMessageBytes,
|
|
229
|
+
maxProviderResponseBytes:options.maxProviderResponseBytes,
|
|
230
|
+
maxQueuedBodyReads:options.maxQueuedBodyReads,
|
|
231
|
+
maxQueuedSends:options.maxQueuedSends,
|
|
232
|
+
maxQueuedMessageBytes:options.maxQueuedMessageBytes,
|
|
233
|
+
maxRequestBytes:options.maxRequestBytes,
|
|
234
|
+
onEvent:options.onEvent,
|
|
235
|
+
port:options.port,
|
|
236
|
+
providerTimeoutMs:options.requestTimeout,
|
|
237
|
+
recipientAllowlist,
|
|
238
|
+
requestIdFactory:options.requestIdFactory,
|
|
239
|
+
retryableDelayMs:options.retryableDelayMs,
|
|
240
|
+
sendQueueTimeoutMs:options.sendQueueTimeoutMs,
|
|
241
|
+
signal:options.signal
|
|
242
|
+
});
|
|
243
|
+
}finally{
|
|
244
|
+
apiKey='';
|
|
245
|
+
appKey='';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function executeMailCommand(rawOptions={}){
|
|
250
|
+
const options=validateOptions(rawOptions);
|
|
251
|
+
switch(options.action){
|
|
252
|
+
case 'key-set':return setCredential(options);
|
|
253
|
+
case 'key-status':return credentialStatus(options);
|
|
254
|
+
case 'key-delete':return deleteCredential(options);
|
|
255
|
+
case 'send':return sendMail(options);
|
|
256
|
+
case 'serve':return serveMail(options);
|
|
257
|
+
default:usage('Unsupported Mail action.');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export default executeMailCommand;
|
|
@@ -35,40 +35,36 @@ const expectedAiComponents=Object.freeze(
|
|
|
35
35
|
JSON.parse(canonicalAiComponentsBytes.toString('utf8'))
|
|
36
36
|
);
|
|
37
37
|
|
|
38
|
-
const
|
|
39
|
-
['
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const expectedDirectories=Object.freeze([...new Set(expectedFiles.flatMap(file=>{
|
|
69
|
-
const segments=file.path.split('/');
|
|
70
|
-
return segments.slice(0,-1).map((_,index)=>segments.slice(0,index+1).join('/'));
|
|
71
|
-
}))].sort(compareText));
|
|
38
|
+
const admittedDependencyFiles=Object.freeze({
|
|
39
|
+
'event-pubsub':Object.freeze(['index.js','licence','package.json']),
|
|
40
|
+
'strong-type':Object.freeze(['index.js','licence','package.json'])
|
|
41
|
+
});
|
|
42
|
+
const exactRuntimeFileAuthorities=Object.freeze({
|
|
43
|
+
'ai/wllama/LICENCE':Object.freeze({
|
|
44
|
+
sourcePath:'node_modules/@wllama/wllama/LICENCE',
|
|
45
|
+
provenance:'vendor-package-identity'
|
|
46
|
+
}),
|
|
47
|
+
'ai/wllama/index.mjs':Object.freeze({
|
|
48
|
+
sourcePath:'browser-runtime/ai/wllama/index.mjs',
|
|
49
|
+
provenance:'deterministic-derived-vendor'
|
|
50
|
+
}),
|
|
51
|
+
'ai/wllama/llama.cpp-LICENSE':Object.freeze({
|
|
52
|
+
sourcePath:'browser-runtime/ai/wllama/llama.cpp-LICENSE',
|
|
53
|
+
provenance:'vendor-source-identity'
|
|
54
|
+
}),
|
|
55
|
+
'ai/wllama/wllama.wasm':Object.freeze({
|
|
56
|
+
sourcePath:'node_modules/@wllama/wllama/esm/wasm/wllama.wasm',
|
|
57
|
+
provenance:'vendor-package-identity'
|
|
58
|
+
}),
|
|
59
|
+
'dom-event-instrumentation.mjs':Object.freeze({
|
|
60
|
+
sourcePath:'src/dom-event-instrumentation.mjs',
|
|
61
|
+
provenance:'sdk-source-identity'
|
|
62
|
+
}),
|
|
63
|
+
'event-manager.mjs':Object.freeze({
|
|
64
|
+
sourcePath:'src/event-manager.mjs',
|
|
65
|
+
provenance:'sdk-source-identity'
|
|
66
|
+
})
|
|
67
|
+
});
|
|
72
68
|
|
|
73
69
|
function fail(message,code='ARCANE_SDK_BROWSER_INTEGRITY_FAILED'){
|
|
74
70
|
const error=new Error(message);
|
|
@@ -101,7 +97,8 @@ function safeInventoryPath(value,label='manifest'){
|
|
|
101
97
|
if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
|
|
102
98
|
||value.normalize('NFC')!==value||path.posix.isAbsolute(value)
|
|
103
99
|
||path.posix.normalize(value)!==value||value==='.'||value.startsWith('../')
|
|
104
|
-
||value.includes('/../')
|
|
100
|
+
||value.includes('/../')
|
|
101
|
+
||value.split('/').some(segment=>!segment||segment.startsWith('.'))){
|
|
105
102
|
fail(`SDK browser runtime ${label} contains an unsafe path: ${String(value)}.`);
|
|
106
103
|
}
|
|
107
104
|
return value;
|
|
@@ -111,6 +108,46 @@ function collisionKey(value){
|
|
|
111
108
|
return value.normalize('NFC').toLocaleLowerCase('en-US');
|
|
112
109
|
}
|
|
113
110
|
|
|
111
|
+
function runtimeFileAuthority(value){
|
|
112
|
+
const relative=safeInventoryPath(value);
|
|
113
|
+
const exact=Object.hasOwn(exactRuntimeFileAuthorities,relative)
|
|
114
|
+
?exactRuntimeFileAuthorities[relative]
|
|
115
|
+
:null;
|
|
116
|
+
if(exact)return Object.freeze({path:relative,...exact});
|
|
117
|
+
|
|
118
|
+
if(relative.startsWith('ai/wllama/')){
|
|
119
|
+
fail(`SDK browser runtime Wllama asset is not admitted: ${relative}.`);
|
|
120
|
+
}
|
|
121
|
+
if(relative.startsWith('ai/')&&['.mjs','.json'].includes(path.posix.extname(relative))){
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
path:relative,
|
|
124
|
+
sourcePath:`browser-runtime/${relative}`,
|
|
125
|
+
provenance:'sdk-source-identity'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if(relative.startsWith('dependencies/')){
|
|
129
|
+
const [directory,packageName,fileName,...extra]=relative.split('/');
|
|
130
|
+
const admitted=Object.hasOwn(admittedDependencyFiles,packageName)
|
|
131
|
+
?admittedDependencyFiles[packageName]
|
|
132
|
+
:null;
|
|
133
|
+
if(directory==='dependencies'&&extra.length===0&&admitted?.includes(fileName)){
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
path:relative,
|
|
136
|
+
sourcePath:`node_modules/${packageName}/${fileName}`,
|
|
137
|
+
provenance:'vendor-package-identity'
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
fail(`SDK browser runtime path has no admitted source authority: ${relative}.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function inventoryDirectories(files){
|
|
145
|
+
return Object.freeze([...new Set(files.flatMap(file=>{
|
|
146
|
+
const segments=file.path.split('/');
|
|
147
|
+
return segments.slice(0,-1).map((_,index)=>segments.slice(0,index+1).join('/'));
|
|
148
|
+
}))].sort(compareText));
|
|
149
|
+
}
|
|
150
|
+
|
|
114
151
|
function immutableInventory(files){
|
|
115
152
|
return Object.freeze(files.map(file=>Object.freeze({...file})));
|
|
116
153
|
}
|
|
@@ -181,7 +218,7 @@ function validateSource(source){
|
|
|
181
218
|
});
|
|
182
219
|
}
|
|
183
220
|
|
|
184
|
-
function validateRelease(value){
|
|
221
|
+
function validateRelease(value,{expectedContentSha256=SDK_BROWSER_RUNTIME_CONTENT_SHA256}={}){
|
|
185
222
|
if(!value||typeof value!=='object'||Array.isArray(value)){
|
|
186
223
|
fail('SDK browser runtime manifest must be a JSON object.');
|
|
187
224
|
}
|
|
@@ -197,8 +234,8 @@ function validateRelease(value){
|
|
|
197
234
|
fail('SDK browser runtime manifest sdkVersion is incompatible with this SDK.');
|
|
198
235
|
}
|
|
199
236
|
const source=validateSource(value.source);
|
|
200
|
-
if(!Array.isArray(value.files)||!Number.isSafeInteger(value.fileCount)
|
|
201
|
-
||value.fileCount!==value.files.length
|
|
237
|
+
if(!Array.isArray(value.files)||value.files.length===0||!Number.isSafeInteger(value.fileCount)
|
|
238
|
+
||value.fileCount!==value.files.length
|
|
202
239
|
||!Number.isSafeInteger(value.totalBytes)||value.totalBytes<0
|
|
203
240
|
||!SHA256_PATTERN.test(value.contentSha256)){
|
|
204
241
|
fail('SDK browser runtime manifest inventory summary is invalid.');
|
|
@@ -206,6 +243,7 @@ function validateRelease(value){
|
|
|
206
243
|
let previous='';
|
|
207
244
|
let totalBytes=0;
|
|
208
245
|
const collisionKeys=new Set();
|
|
246
|
+
const sourceCollisionKeys=new Set();
|
|
209
247
|
const files=value.files.map((entry,index)=>{
|
|
210
248
|
if(!entry||typeof entry!=='object'||Array.isArray(entry)
|
|
211
249
|
||!exactKeys(entry,['bytes','path','provenance','sha256','sourcePath'])){
|
|
@@ -213,11 +251,13 @@ function validateRelease(value){
|
|
|
213
251
|
}
|
|
214
252
|
const relative=safeInventoryPath(entry.path);
|
|
215
253
|
const sourcePath=safeInventoryPath(entry.sourcePath,'source inventory');
|
|
216
|
-
const expected=
|
|
254
|
+
const expected=runtimeFileAuthority(relative);
|
|
217
255
|
const key=collisionKey(relative);
|
|
256
|
+
const sourceKey=collisionKey(sourcePath);
|
|
218
257
|
if(relative===MANIFEST_NAME||collisionKeys.has(key)
|
|
258
|
+
||sourceCollisionKeys.has(sourceKey)
|
|
219
259
|
||(previous&&compareText(previous,relative)>=0)
|
|
220
|
-
||
|
|
260
|
+
||sourcePath!==expected.sourcePath
|
|
221
261
|
||entry.provenance!==expected.provenance){
|
|
222
262
|
fail(`SDK browser runtime manifest inventory is invalid at ${relative}.`);
|
|
223
263
|
}
|
|
@@ -225,6 +265,7 @@ function validateRelease(value){
|
|
|
225
265
|
fail(`SDK browser runtime manifest metadata is invalid for ${relative}.`);
|
|
226
266
|
}
|
|
227
267
|
collisionKeys.add(key);
|
|
268
|
+
sourceCollisionKeys.add(sourceKey);
|
|
228
269
|
previous=relative;
|
|
229
270
|
totalBytes+=entry.bytes;
|
|
230
271
|
if(!Number.isSafeInteger(totalBytes)){
|
|
@@ -245,12 +286,15 @@ function validateRelease(value){
|
|
|
245
286
|
if(aggregate!==value.contentSha256){
|
|
246
287
|
fail('SDK browser runtime manifest contentSha256 does not match its inventory.');
|
|
247
288
|
}
|
|
248
|
-
if(value.contentSha256!==
|
|
289
|
+
if(expectedContentSha256!==null&&value.contentSha256!==expectedContentSha256){
|
|
249
290
|
fail('SDK browser runtime manifest does not match the trusted SDK browser closure.');
|
|
250
291
|
}
|
|
251
292
|
return Object.freeze({...value,source,files:immutableInventory(files)});
|
|
252
293
|
}
|
|
253
294
|
|
|
295
|
+
const validatedCanonicalRelease=validateRelease(canonicalRelease,{expectedContentSha256:null});
|
|
296
|
+
const expectedDirectories=inventoryDirectories(validatedCanonicalRelease.files);
|
|
297
|
+
|
|
254
298
|
function sameIdentity(before,after){
|
|
255
299
|
return before.dev===after.dev&&before.ino===after.ino&&before.size===after.size
|
|
256
300
|
&&before.mtimeNs===after.mtimeNs&&before.ctimeNs===after.ctimeNs
|
package/src/testing-loader.mjs
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
const TESTING_SPECIFIER='arcane-os/testing';
|
|
2
2
|
const TESTING_URL=new URL('./testing.mjs',import.meta.url).href;
|
|
3
|
+
const RUNTIME_STRONG_TYPE_SPECIFIER='../../node_modules/strong-type/index.js';
|
|
4
|
+
const RUNTIME_ARCANE_URL=new URL('../runtime/arcane/',import.meta.url).href;
|
|
5
|
+
const STRONG_TYPE_URL=new URL('../runtime/strong-type/index.js',import.meta.url).href;
|
|
3
6
|
|
|
4
7
|
export function resolve(specifier,context,nextResolve){
|
|
5
8
|
if(specifier===TESTING_SPECIFIER){
|
|
6
9
|
return {url:TESTING_URL,shortCircuit:true};
|
|
7
10
|
}
|
|
11
|
+
if(specifier===RUNTIME_STRONG_TYPE_SPECIFIER
|
|
12
|
+
&&context.parentURL?.startsWith(RUNTIME_ARCANE_URL)){
|
|
13
|
+
return {url:STRONG_TYPE_URL,shortCircuit:true};
|
|
14
|
+
}
|
|
8
15
|
return nextResolve(specifier,context);
|
|
9
16
|
}
|
package/src/toolchain.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
|
43
43
|
import {createEventQueue} from './event-queue.mjs';
|
|
44
44
|
import {ARCANE_MACHINE_BUNDLE_VERSION} from './constants.mjs';
|
|
45
45
|
import {checkForSdkUpdate} from './update-check.mjs';
|
|
46
|
+
import {executeMailCommand} from './mail.mjs';
|
|
46
47
|
import {
|
|
47
48
|
APP_BUNDLE_EXTENSION,
|
|
48
49
|
createAppReleaseBundle,
|
|
@@ -1542,6 +1543,7 @@ export async function executeOperation(command,options={}){
|
|
|
1542
1543
|
build:buildApplication,
|
|
1543
1544
|
run:runApplication,
|
|
1544
1545
|
'update-check':checkSdkUpdate,
|
|
1546
|
+
mail:executeMailCommand,
|
|
1545
1547
|
targets:async options=>describeTargets(options),
|
|
1546
1548
|
repo:repositoryApplication
|
|
1547
1549
|
};
|
|
@@ -1573,6 +1575,7 @@ export function createToolchain(defaults={}){
|
|
|
1573
1575
|
build:options=>buildApplication({...defaults,...options}),
|
|
1574
1576
|
run:options=>runApplication({...defaults,...options}),
|
|
1575
1577
|
updateCheck:options=>checkSdkUpdate({...defaults,...options}),
|
|
1578
|
+
mail:options=>executeMailCommand({...defaults,...options}),
|
|
1576
1579
|
targets:options=>describeTargets({...defaults,...options}),
|
|
1577
1580
|
repository:options=>repositoryApplication({...defaults,...options})
|
|
1578
1581
|
});
|
package/src/workspace.mjs
CHANGED
|
@@ -476,7 +476,7 @@ function validateLock(lock,sdkDeclaration){
|
|
|
476
476
|
return lock;
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
async function resolveInstalledSdkInstallation(workspaceRoot,declaration){
|
|
479
|
+
export async function resolveInstalledSdkInstallation(workspaceRoot,declaration){
|
|
480
480
|
let current=workspaceRoot;
|
|
481
481
|
for(const segment of declaration.packageSource.split('/')){
|
|
482
482
|
current=path.join(current,segment);
|