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.
Files changed (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +462 -384
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +639 -98
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +470 -0
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. 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 expectedFiles=Object.freeze([
39
- ['ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json','browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json','sdk-source-identity'],
40
- ['ai/browser-kokoro-worker.mjs','browser-runtime/ai/browser-kokoro-worker.mjs','sdk-source-identity'],
41
- ['ai/browser-speech-artifacts.mjs','browser-runtime/ai/browser-speech-artifacts.mjs','sdk-source-identity'],
42
- ['ai/browser-speech-providers.mjs','browser-runtime/ai/browser-speech-providers.mjs','sdk-source-identity'],
43
- ['ai/browser-speech.mjs','browser-runtime/ai/browser-speech.mjs','sdk-source-identity'],
44
- ['ai/browser-wasm-llm-provider.mjs','browser-runtime/ai/browser-wasm-llm-provider.mjs','sdk-source-identity'],
45
- ['ai/browser-wasm.mjs','browser-runtime/ai/browser-wasm.mjs','sdk-source-identity'],
46
- ['ai/browser-whisper-worker.mjs','browser-runtime/ai/browser-whisper-worker.mjs','sdk-source-identity'],
47
- ['ai/browser-wllama-runtime.mjs','browser-runtime/ai/browser-wllama-runtime.mjs','sdk-source-identity'],
48
- ['ai/internal/sha256.mjs','browser-runtime/ai/internal/sha256.mjs','sdk-source-identity'],
49
- ['ai/model-controller.mjs','browser-runtime/ai/model-controller.mjs','sdk-source-identity'],
50
- ['ai/speech-worker-client.mjs','browser-runtime/ai/speech-worker-client.mjs','sdk-source-identity'],
51
- ['ai/speech-worker-runtime.mjs','browser-runtime/ai/speech-worker-runtime.mjs','sdk-source-identity'],
52
- ['ai/wllama/LICENCE','node_modules/@wllama/wllama/LICENCE','vendor-package-identity'],
53
- ['ai/wllama/index.mjs','browser-runtime/ai/wllama/index.mjs','deterministic-derived-vendor'],
54
- ['ai/wllama/llama.cpp-LICENSE','browser-runtime/ai/wllama/llama.cpp-LICENSE','vendor-source-identity'],
55
- ['ai/wllama/wllama.wasm','node_modules/@wllama/wllama/esm/wasm/wllama.wasm','vendor-package-identity'],
56
- ['dependencies/event-pubsub/index.js','node_modules/event-pubsub/index.js','vendor-package-identity'],
57
- ['dependencies/event-pubsub/licence','node_modules/event-pubsub/licence','vendor-package-identity'],
58
- ['dependencies/event-pubsub/package.json','node_modules/event-pubsub/package.json','vendor-package-identity'],
59
- ['dependencies/strong-type/index.js','node_modules/strong-type/index.js','vendor-package-identity'],
60
- ['dependencies/strong-type/licence','node_modules/strong-type/licence','vendor-package-identity'],
61
- ['dependencies/strong-type/package.json','node_modules/strong-type/package.json','vendor-package-identity'],
62
- ['dom-event-instrumentation.mjs','src/dom-event-instrumentation.mjs','sdk-source-identity'],
63
- ['event-manager.mjs','src/event-manager.mjs','sdk-source-identity']
64
- ].map(([filePath,sourcePath,provenance])=>Object.freeze({
65
- path:filePath,sourcePath,provenance
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||value.fileCount!==expectedFiles.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=expectedFiles[index];
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
- ||relative!==expected.path||sourcePath!==expected.sourcePath
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!==SDK_BROWSER_RUNTIME_CONTENT_SHA256){
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
@@ -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);