arcane-os 0.5.17 → 0.5.18
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 +24 -0
- package/README.md +9 -9
- package/bin/arcane-test.mjs +16 -13
- package/browser-runtime/ai/browser-speech-artifacts.mjs +41 -38
- package/browser-runtime/ai/browser-speech-providers.mjs +46 -43
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +127 -124
- package/browser-runtime/ai/browser-wasm.mjs +6 -3
- package/browser-runtime/ai/browser-wllama-runtime.mjs +26 -23
- package/browser-runtime/ai/model-controller.mjs +94 -91
- package/browser-runtime/ai/speech-worker-client.mjs +12 -9
- package/browser-runtime/ai/speech-worker-runtime.mjs +56 -53
- package/browser-runtime/dependencies/strong-type/index.js +207 -82
- package/browser-runtime/dependencies/strong-type/package.json +15 -6
- package/browser-runtime/dom-event-instrumentation.mjs +24 -20
- package/browser-runtime/event-manager.mjs +111 -108
- package/browser-runtime/speech-text.mjs +5 -1
- package/docs/reference/ai/browser-speech.md +198 -5
- package/docs/reference/availability-and-normalization.md +40 -0
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/inventory/runtime-components.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +20 -24
- package/docs/reference/protocols.md +1 -1
- package/docs/reference/runtime-modules.md +125 -35
- package/docs/reference/sdk-api.md +23 -7
- package/examples/wasm-ai-demo/index.html +1 -0
- package/node_modules/event-pubsub/node_modules/strong-type/README.md +408 -0
- package/node_modules/event-pubsub/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/event-pubsub/node_modules/strong-type/index.js +1151 -0
- package/node_modules/event-pubsub/node_modules/strong-type/licence +21 -0
- package/node_modules/event-pubsub/node_modules/strong-type/node.js +125 -0
- package/node_modules/event-pubsub/node_modules/strong-type/package.json +61 -0
- package/node_modules/strong-type/README.md +42 -24
- package/node_modules/strong-type/benchmark/core.js +324 -0
- package/node_modules/strong-type/index.js +207 -82
- package/node_modules/strong-type/package.json +14 -6
- package/package.json +3 -3
- package/runtime/arcane/components/app-bar.html +5 -2
- package/runtime/arcane/components/assistant-panel.html +5 -2
- package/runtime/arcane/components/chart.html +15 -12
- package/runtime/arcane/components/chat.html +117 -138
- package/runtime/arcane/components/dashboard-config.html +6 -3
- package/runtime/arcane/components/data-view.html +4 -1
- package/runtime/arcane/components/directory-picker.html +5 -2
- package/runtime/arcane/components/document-inspector.html +6 -3
- package/runtime/arcane/components/file-drop.html +5 -2
- package/runtime/arcane/components/file-inspector.html +9 -6
- package/runtime/arcane/components/file-manager.html +16 -13
- package/runtime/arcane/components/local-ai-status.html +5 -2
- package/runtime/arcane/components/markdown-document.html +14 -11
- package/runtime/arcane/components/markdown-editor.html +4 -1
- package/runtime/arcane/components/modal.html +4 -1
- package/runtime/arcane/components/output-panel.html +6 -3
- package/runtime/arcane/components/preferences-form.html +4 -1
- package/runtime/arcane/components/record-timeline.html +8 -5
- package/runtime/arcane/components/relationship-board.html +9 -6
- package/runtime/arcane/components/source-code-viewer.html +12 -9
- package/runtime/arcane/components/source-explanation.html +8 -5
- package/runtime/arcane/components/speech.html +24 -21
- package/runtime/arcane/components/summary-strip.html +4 -1
- package/runtime/arcane/components/task-progress.html +6 -3
- package/runtime/arcane/components/terminal-workspace.html +4 -1
- package/runtime/arcane/components/voice-transcription.html +11 -8
- package/runtime/arcane/components/web-navigator.html +5 -2
- package/runtime/arcane/entities/ApiModelRecord.js +5 -2
- package/runtime/arcane/entities/Calculation.js +5 -2
- package/runtime/arcane/entities/Chat.js +33 -33
- package/runtime/arcane/entities/CommunicationMessage.js +4 -1
- package/runtime/arcane/entities/CommunicationThread.js +4 -1
- package/runtime/arcane/entities/File.js +1 -1
- package/runtime/arcane/entities/Image.js +8 -5
- package/runtime/arcane/entities/IntentEnvelope.js +20 -17
- package/runtime/arcane/entities/Preference.js +9 -6
- package/runtime/arcane/entities/Theme.js +5 -2
- package/runtime/arcane/entities/User.js +11 -11
- package/runtime/arcane/entities/Weather.js +5 -2
- package/runtime/arcane/modules/AI.js +491 -165
- package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
- package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
- package/runtime/arcane/modules/AIProviderRuntime.js +88 -85
- package/runtime/arcane/modules/AIResponseURLPolicy.js +148 -62
- package/runtime/arcane/modules/AIRuntimeState.js +29 -26
- package/runtime/arcane/modules/AnsiText.js +4 -1
- package/runtime/arcane/modules/ApiModelDatabase.js +22 -19
- package/runtime/arcane/modules/AppDataScope.js +8 -5
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +4 -1
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +8 -5
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +18 -15
- package/runtime/arcane/modules/AsyncBoundary.js +13 -10
- package/runtime/arcane/modules/BrowserTestSuite.js +19 -16
- package/runtime/arcane/modules/CalculatorEngine.js +5 -2
- package/runtime/arcane/modules/ChatRecords.js +18 -15
- package/runtime/arcane/modules/CommunicationAppController.js +9 -6
- package/runtime/arcane/modules/CommunicationHub.js +9 -6
- package/runtime/arcane/modules/CommunicationPreferences.js +4 -1
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +4 -1
- package/runtime/arcane/modules/ComponentContracts.js +56 -53
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +30 -27
- package/runtime/arcane/modules/ConversationActionItems.js +15 -12
- package/runtime/arcane/modules/ConversationClosingReport.js +11 -8
- package/runtime/arcane/modules/ConversationTimebox.js +28 -25
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +17 -14
- package/runtime/arcane/modules/DBLS.js +7 -4
- package/runtime/arcane/modules/DBOPFS.js +7 -7
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +34 -31
- package/runtime/arcane/modules/DevelopmentWorkspace.js +4 -1
- package/runtime/arcane/modules/DirectoryPicker.js +8 -5
- package/runtime/arcane/modules/DocumentLexicalSearch.js +11 -8
- package/runtime/arcane/modules/DocumentNavigation.js +7 -4
- package/runtime/arcane/modules/Errors.js +28 -25
- package/runtime/arcane/modules/HTMLImport.js +7 -4
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +15 -12
- package/runtime/arcane/modules/LocalAIReadiness.js +21 -18
- package/runtime/arcane/modules/LocalAIReadinessController.js +6 -3
- package/runtime/arcane/modules/MD.js +1 -1
- package/runtime/arcane/modules/Mail.js +46 -43
- package/runtime/arcane/modules/MailOutbox.mjs +48 -45
- package/runtime/arcane/modules/MailTransport.mjs +29 -26
- package/runtime/arcane/modules/MemoryRecords.js +7 -4
- package/runtime/arcane/modules/MessageAdvisory.js +6 -3
- package/runtime/arcane/modules/ModelDefinition.js +7 -4
- package/runtime/arcane/modules/Ollama.js +6 -3
- package/runtime/arcane/modules/OllamaModelIdentifier.js +4 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +18 -15
- package/runtime/arcane/modules/PersistentAIChatSession.js +33 -30
- package/runtime/arcane/modules/PreferenceStore.js +20 -17
- package/runtime/arcane/modules/PreparedSpeech.js +485 -0
- package/runtime/arcane/modules/Questionnaire.js +6 -3
- package/runtime/arcane/modules/RecordLinkIndex.js +4 -1
- package/runtime/arcane/modules/RecordPassageIndex.js +9 -6
- package/runtime/arcane/modules/RecordReviewStore.js +12 -9
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +4 -1
- package/runtime/arcane/modules/ScopedOPFSCache.js +6 -3
- package/runtime/arcane/modules/ScreenCapture.js +20 -17
- package/runtime/arcane/modules/SpeechPlayback.js +29 -26
- package/runtime/arcane/modules/StaticDocumentCatalog.js +33 -30
- package/runtime/arcane/modules/SystemAppearance.js +5 -2
- package/runtime/arcane/modules/SystemToolRegistry.js +6 -3
- package/runtime/arcane/modules/TerminalClient.js +14 -11
- package/runtime/arcane/modules/TerminalCommandRegistry.js +4 -1
- package/runtime/arcane/modules/ThemeBootstrap.js +9 -6
- package/runtime/arcane/modules/ThemeManager.js +5 -2
- package/runtime/arcane/modules/TimeGuard.js +1 -1
- package/runtime/arcane/modules/ToolCallRouter.js +11 -8
- package/runtime/arcane/modules/WaitForComponent.js +19 -16
- package/runtime/arcane/modules/YouTubeMedia.js +4 -1
- package/runtime/strong-type/index.js +1276 -352
- package/runtime/strong-type/package.json +69 -45
- package/src/app-descriptor.mjs +17 -14
- package/src/application-tests.mjs +4 -1
- package/src/cli/main.mjs +8 -5
- package/src/constants.mjs +4 -1
- package/src/dev-server.mjs +8 -5
- package/src/doctor.mjs +5 -2
- package/src/dom-event-instrumentation.mjs +24 -20
- package/src/errors.mjs +7 -3
- package/src/event-manager.mjs +111 -108
- package/src/event-queue.mjs +8 -5
- package/src/events.mjs +12 -9
- package/src/import-map.mjs +35 -27
- package/src/installed-sdk-runtime.mjs +4 -1
- package/src/integrated-provider-loader.mjs +9 -6
- package/src/mail-credentials.mjs +16 -13
- package/src/mail-server.mjs +53 -50
- package/src/mail.mjs +18 -15
- package/src/native-plan.mjs +12 -9
- package/src/native-provider-loader.mjs +7 -4
- package/src/packager/core.mjs +18 -15
- package/src/process.mjs +6 -3
- package/src/release-bundle.mjs +14 -11
- package/src/runtime.mjs +4 -1
- package/src/scaffold.mjs +9 -6
- package/src/sdk-browser-runtime.mjs +4 -1
- package/src/source-server.mjs +17 -14
- package/src/targets/index.mjs +5 -2
- package/src/templates/workspace-template.mjs +13 -6
- package/src/testing-loader.mjs +7 -4
- package/src/testing.mjs +10 -7
- package/src/toolchain.mjs +13 -10
- package/src/update-check.mjs +9 -6
- package/src/workspace-operation-lock.mjs +14 -11
- package/src/workspace-runtime.mjs +4 -1
- package/src/workspace.mjs +17 -14
- package/runtime/arcane/modules/AIResponseLength.js +0 -32
package/src/mail-server.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {createHash,randomUUID,timingSafeEqual} from 'node:crypto';
|
|
2
3
|
import http from 'node:http';
|
|
3
4
|
|
|
5
|
+
const is = new Is(false);
|
|
6
|
+
|
|
4
7
|
export const RESEND_MAIL_SERVER_PROTOCOL='arcane-resend-mail-gateway/1';
|
|
5
8
|
export const RESEND_MAIL_PATH='/v1/mail';
|
|
6
9
|
|
|
@@ -41,22 +44,22 @@ function configurationError(message){
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
function completeErrorDetails(error){
|
|
44
|
-
if(!error
|
|
47
|
+
if(!error||!is.object(error)){
|
|
45
48
|
return {message:String(error??''),name:'Error'};
|
|
46
49
|
}
|
|
47
50
|
return {
|
|
48
51
|
...error,
|
|
49
|
-
...(
|
|
50
|
-
message:
|
|
51
|
-
name:
|
|
52
|
-
...(
|
|
52
|
+
...(is.string(error.code)?{code:error.code}:{}),
|
|
53
|
+
message:is.string(error.message)?error.message:String(error),
|
|
54
|
+
name:is.string(error.name)?error.name:'Error',
|
|
55
|
+
...(is.string(error.stack)?{stack:error.stack}:{})
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
function positiveInteger(value,fallback,{label,allowZero=false}={}){
|
|
57
60
|
const resolved=value===undefined?fallback:value;
|
|
58
61
|
const minimum=allowZero?0:1;
|
|
59
|
-
if(!
|
|
62
|
+
if(!is.safeInteger(resolved)||resolved<minimum){
|
|
60
63
|
throw configurationError(`${label} must be ${allowZero?'a nonnegative':'a positive'} integer.`);
|
|
61
64
|
}
|
|
62
65
|
return resolved;
|
|
@@ -64,7 +67,7 @@ function positiveInteger(value,fallback,{label,allowZero=false}={}){
|
|
|
64
67
|
|
|
65
68
|
function optionalTimerDelay(value,label){
|
|
66
69
|
if(value===undefined||value===null) return null;
|
|
67
|
-
if(!
|
|
70
|
+
if(!is.safeInteger(value)||value<1||value>MAX_NODE_TIMER_DELAY_MS){
|
|
68
71
|
throw configurationError(
|
|
69
72
|
`${label} must be an integer from 1 through ${MAX_NODE_TIMER_DELAY_MS} `
|
|
70
73
|
+'milliseconds, the Node timer range.'
|
|
@@ -74,12 +77,12 @@ function optionalTimerDelay(value,label){
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function normalizeRetryAfter(value){
|
|
77
|
-
return
|
|
80
|
+
return is.safeInteger(value)&&value>0?value:0;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
function portNumber(value,fallback){
|
|
81
84
|
const resolved=value===undefined?fallback:value;
|
|
82
|
-
if(!
|
|
85
|
+
if(!is.safeInteger(resolved)||resolved<0||resolved>65_535){
|
|
83
86
|
throw configurationError('port must be an integer between 0 and 65535.');
|
|
84
87
|
}
|
|
85
88
|
return resolved;
|
|
@@ -93,14 +96,14 @@ function validateSignal(signal){
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
function validateApiKey(value){
|
|
96
|
-
if(
|
|
99
|
+
if(!is.string(value)||value.length<1||!/^[\x21-\x7e]+$/u.test(value)){
|
|
97
100
|
throw configurationError('Resend API key must be a nonempty printable ASCII string.');
|
|
98
101
|
}
|
|
99
102
|
return value;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
function validateAppId(value){
|
|
103
|
-
if(
|
|
106
|
+
if(!is.string(value)||!APP_ID_PATTERN.test(value)){
|
|
104
107
|
throw configurationError('Mail application identity is invalid.');
|
|
105
108
|
}
|
|
106
109
|
return value;
|
|
@@ -112,7 +115,7 @@ function appKeyDigest(value){
|
|
|
112
115
|
|
|
113
116
|
function normalizeCallerAuthentication(options){
|
|
114
117
|
const allowUnauthenticatedCaller=options.allowUnauthenticatedCaller??false;
|
|
115
|
-
if(
|
|
118
|
+
if(!is.boolean(allowUnauthenticatedCaller)){
|
|
116
119
|
throw configurationError('allowUnauthenticatedCaller must be a boolean.');
|
|
117
120
|
}
|
|
118
121
|
if(allowUnauthenticatedCaller){
|
|
@@ -126,7 +129,7 @@ function normalizeCallerAuthentication(options){
|
|
|
126
129
|
callerAuthentication:'origin-app-id-only'
|
|
127
130
|
};
|
|
128
131
|
}
|
|
129
|
-
if(
|
|
132
|
+
if(!is.string(options.appKey)||!/^[\x21-\x7e]+$/u.test(options.appKey)){
|
|
130
133
|
throw configurationError(
|
|
131
134
|
'appKey must be a nonempty printable ASCII string unless unauthenticated caller mode is explicitly enabled.'
|
|
132
135
|
);
|
|
@@ -138,7 +141,7 @@ function normalizeCallerAuthentication(options){
|
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
function normalizedEmail(value,label){
|
|
141
|
-
if(
|
|
144
|
+
if(!is.string(value)){
|
|
142
145
|
throw configurationError(`${label} must be an email address.`);
|
|
143
146
|
}
|
|
144
147
|
const normalized=value.trim().toLowerCase();
|
|
@@ -149,7 +152,7 @@ function normalizedEmail(value,label){
|
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
function validateFrom(value){
|
|
152
|
-
if(
|
|
155
|
+
if(!is.string(value)||value!==value.trim()||value.length<3||value.length>320
|
|
153
156
|
||/[\u0000-\u001f\u007f]/u.test(value)){
|
|
154
157
|
throw configurationError('Mail sender is invalid.');
|
|
155
158
|
}
|
|
@@ -165,7 +168,7 @@ function validateFrom(value){
|
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
function normalizeEmailList(value,label,{allowEmpty=false}={}){
|
|
168
|
-
if(!
|
|
171
|
+
if(!is.array(value)||(!allowEmpty&&value.length===0)){
|
|
169
172
|
throw configurationError(`${label} must contain ${allowEmpty?'zero or more':'one or more'} addresses.`);
|
|
170
173
|
}
|
|
171
174
|
const result=[];
|
|
@@ -177,7 +180,7 @@ function normalizeEmailList(value,label,{allowEmpty=false}={}){
|
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
function normalizeOrigin(value){
|
|
180
|
-
if(
|
|
183
|
+
if(!is.string(value)||!value.trim()){
|
|
181
184
|
throw configurationError('Every allowed mail origin must be a URL origin.');
|
|
182
185
|
}
|
|
183
186
|
let url;
|
|
@@ -194,7 +197,7 @@ function normalizeOrigin(value){
|
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
function normalizeOrigins(value){
|
|
197
|
-
if(!
|
|
200
|
+
if(!is.array(value)||value.length===0){
|
|
198
201
|
throw configurationError('allowedOrigins must contain one or more exact origins.');
|
|
199
202
|
}
|
|
200
203
|
const origins=[];
|
|
@@ -218,7 +221,7 @@ function validateLoopbackHost(value){
|
|
|
218
221
|
}
|
|
219
222
|
|
|
220
223
|
function normalizeConfiguration(options={}){
|
|
221
|
-
if(!options
|
|
224
|
+
if(!options||!is.object(options)||is.array(options)){
|
|
222
225
|
throw configurationError('Mail server options must be an object.');
|
|
223
226
|
}
|
|
224
227
|
const callerAuthentication=normalizeCallerAuthentication(options);
|
|
@@ -239,13 +242,13 @@ function normalizeConfiguration(options={}){
|
|
|
239
242
|
throw configurationError('Every error recipient must also be in recipientAllowlist.');
|
|
240
243
|
}
|
|
241
244
|
const fetchImpl=options.fetchImpl??globalThis.fetch;
|
|
242
|
-
if(
|
|
245
|
+
if(!is.function(fetchImpl)){
|
|
243
246
|
throw configurationError('A fetch implementation is required for Resend delivery.');
|
|
244
247
|
}
|
|
245
|
-
if(options.onEvent!==undefined
|
|
248
|
+
if(options.onEvent!==undefined&&!is.function(options.onEvent)){
|
|
246
249
|
throw configurationError('onEvent must be a function when supplied.');
|
|
247
250
|
}
|
|
248
|
-
if(options.requestIdFactory!==undefined
|
|
251
|
+
if(options.requestIdFactory!==undefined&&!is.function(options.requestIdFactory)){
|
|
249
252
|
throw configurationError('requestIdFactory must be a function when supplied.');
|
|
250
253
|
}
|
|
251
254
|
return {
|
|
@@ -277,7 +280,7 @@ function normalizeConfiguration(options={}){
|
|
|
277
280
|
function createRequestId(factory){
|
|
278
281
|
try{
|
|
279
282
|
const candidate=factory();
|
|
280
|
-
if(
|
|
283
|
+
if(is.string(candidate)&&REQUEST_ID_PATTERN.test(candidate)){
|
|
281
284
|
return candidate;
|
|
282
285
|
}
|
|
283
286
|
}catch{
|
|
@@ -292,7 +295,7 @@ function isNumericLoopback(value){
|
|
|
292
295
|
|
|
293
296
|
function headerValues(request,name){
|
|
294
297
|
const distinct=request.headersDistinct?.[name];
|
|
295
|
-
if(
|
|
298
|
+
if(is.array(distinct)){
|
|
296
299
|
return distinct.map(function stringifyDistinctHeader(value){return String(value);});
|
|
297
300
|
}
|
|
298
301
|
const values=[];
|
|
@@ -308,7 +311,7 @@ function headerValues(request,name){
|
|
|
308
311
|
if(fallback===undefined){
|
|
309
312
|
return [];
|
|
310
313
|
}
|
|
311
|
-
return
|
|
314
|
+
return is.array(fallback)?fallback.map(String):[String(fallback)];
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
function singleHeader(request,name,{required=true}={}){
|
|
@@ -340,7 +343,7 @@ function validateLoopbackRequest(request){
|
|
|
340
343
|
const hostLiteral=hostname.includes(':')?`[${hostname}]`:hostname;
|
|
341
344
|
const expectedAuthority=localPort===80?hostLiteral:`${hostLiteral}:${String(localPort)}`;
|
|
342
345
|
if(parsed.username||parsed.password||parsed.pathname!=='/'||parsed.search||parsed.hash
|
|
343
|
-
||!isNumericLoopback(hostname)||!
|
|
346
|
+
||!isNumericLoopback(hostname)||!is.safeInteger(localPort)
|
|
344
347
|
||statedPort!==localPort||rawHost!==expectedAuthority){
|
|
345
348
|
throw new MailGatewayFault('mail_invalid_host',{statusCode:421});
|
|
346
349
|
}
|
|
@@ -483,7 +486,7 @@ function createObserver(onEvent){
|
|
|
483
486
|
}catch{
|
|
484
487
|
return;
|
|
485
488
|
}
|
|
486
|
-
if(!result
|
|
489
|
+
if(!result||!is.function(result.then)){
|
|
487
490
|
return;
|
|
488
491
|
}
|
|
489
492
|
const task=Promise.resolve(result);
|
|
@@ -577,7 +580,7 @@ function readRequestBody(request,{timeoutMs,signal}){
|
|
|
577
580
|
}
|
|
578
581
|
|
|
579
582
|
function normalizedReportEmail(value){
|
|
580
|
-
if(
|
|
583
|
+
if(!is.string(value)){
|
|
581
584
|
throw new MailGatewayFault('mail_invalid_recipient',{statusCode:422});
|
|
582
585
|
}
|
|
583
586
|
const address=value.trim().toLowerCase();
|
|
@@ -588,7 +591,7 @@ function normalizedReportEmail(value){
|
|
|
588
591
|
}
|
|
589
592
|
|
|
590
593
|
function normalizeReportRecipients(report,configuration){
|
|
591
|
-
if(!
|
|
594
|
+
if(!is.array(report.to)){
|
|
592
595
|
throw new MailGatewayFault('mail_invalid_recipients',{statusCode:422});
|
|
593
596
|
}
|
|
594
597
|
const recipients=[];
|
|
@@ -609,23 +612,23 @@ function normalizeReportRecipients(report,configuration){
|
|
|
609
612
|
}
|
|
610
613
|
|
|
611
614
|
function normalizeReport(value,configuration){
|
|
612
|
-
if(!value
|
|
615
|
+
if(!value||!is.object(value)||is.array(value)){
|
|
613
616
|
throw new MailGatewayFault('mail_invalid_report',{statusCode:422});
|
|
614
617
|
}
|
|
615
618
|
if(!Object.hasOwn(value,'subject')||!Object.hasOwn(value,'to')
|
|
616
619
|
||!Object.hasOwn(value,'type')){
|
|
617
620
|
throw new MailGatewayFault('mail_invalid_report_shape',{statusCode:422});
|
|
618
621
|
}
|
|
619
|
-
if(
|
|
622
|
+
if(!is.string(value.type)||!MAIL_TYPES.has(value.type)){
|
|
620
623
|
throw new MailGatewayFault('mail_invalid_type',{statusCode:422});
|
|
621
624
|
}
|
|
622
|
-
if(
|
|
625
|
+
if(!is.string(value.subject)){
|
|
623
626
|
throw new MailGatewayFault('mail_invalid_subject',{statusCode:422});
|
|
624
627
|
}
|
|
625
628
|
const hasText=Object.hasOwn(value,'text');
|
|
626
629
|
const hasHtml=Object.hasOwn(value,'html');
|
|
627
|
-
if(!hasText&&!hasHtml||(hasText
|
|
628
|
-
||(hasHtml
|
|
630
|
+
if(!hasText&&!hasHtml||(hasText&&!is.string(value.text))
|
|
631
|
+
||(hasHtml&&!is.string(value.html))){
|
|
629
632
|
throw new MailGatewayFault('mail_content_required',{statusCode:422});
|
|
630
633
|
}
|
|
631
634
|
const recipients=normalizeReportRecipients(value,configuration);
|
|
@@ -658,7 +661,7 @@ function parseReport(serialized,configuration){
|
|
|
658
661
|
}
|
|
659
662
|
|
|
660
663
|
function parseRetryAfter(value,now=Date.now()){
|
|
661
|
-
if(
|
|
664
|
+
if(!is.string(value)||!value.trim()){
|
|
662
665
|
return 0;
|
|
663
666
|
}
|
|
664
667
|
const trimmed=value.trim();
|
|
@@ -666,7 +669,7 @@ function parseRetryAfter(value,now=Date.now()){
|
|
|
666
669
|
return normalizeRetryAfter(Math.ceil(Number(trimmed)*1000));
|
|
667
670
|
}
|
|
668
671
|
const timestamp=Date.parse(trimmed);
|
|
669
|
-
return
|
|
672
|
+
return is.finite(timestamp)
|
|
670
673
|
? normalizeRetryAfter(Math.max(0,timestamp-now))
|
|
671
674
|
: 0;
|
|
672
675
|
}
|
|
@@ -712,7 +715,7 @@ function awaitAbortable(value,signal){
|
|
|
712
715
|
function ignoreCancellationFailure(){}
|
|
713
716
|
|
|
714
717
|
function cancelProviderBody(body){
|
|
715
|
-
if(!body
|
|
718
|
+
if(!body||!is.function(body.cancel)){
|
|
716
719
|
return;
|
|
717
720
|
}
|
|
718
721
|
try{
|
|
@@ -723,7 +726,7 @@ function cancelProviderBody(body){
|
|
|
723
726
|
}
|
|
724
727
|
|
|
725
728
|
function cancelProviderReader(reader){
|
|
726
|
-
if(!reader
|
|
729
|
+
if(!reader||!is.function(reader.cancel)){
|
|
727
730
|
return;
|
|
728
731
|
}
|
|
729
732
|
try{
|
|
@@ -737,7 +740,7 @@ async function readProviderBody(response,signal){
|
|
|
737
740
|
if(response.body===null||response.body===undefined){
|
|
738
741
|
return '';
|
|
739
742
|
}
|
|
740
|
-
if(
|
|
743
|
+
if(!is.function(response.body.getReader)){
|
|
741
744
|
cancelProviderBody(response.body);
|
|
742
745
|
throw new MailGatewayFault('resend_unreadable_response',{
|
|
743
746
|
statusCode:502,
|
|
@@ -782,7 +785,7 @@ function parseProviderObject(text){
|
|
|
782
785
|
}
|
|
783
786
|
try{
|
|
784
787
|
const value=JSON.parse(text);
|
|
785
|
-
return value&&
|
|
788
|
+
return value&&is.object(value)&&!is.array(value)?value:null;
|
|
786
789
|
}catch{
|
|
787
790
|
return null;
|
|
788
791
|
}
|
|
@@ -790,7 +793,7 @@ function parseProviderObject(text){
|
|
|
790
793
|
|
|
791
794
|
function providerCode(value,statusCode){
|
|
792
795
|
const candidate=value?.name;
|
|
793
|
-
if(
|
|
796
|
+
if(is.string(candidate)&&PROVIDER_CODE_PATTERN.test(candidate)){
|
|
794
797
|
return candidate;
|
|
795
798
|
}
|
|
796
799
|
return `resend_http_${String(statusCode)}`;
|
|
@@ -883,7 +886,7 @@ async function performResendAttempt(configuration,delivery,idempotencyKey,signal
|
|
|
883
886
|
));
|
|
884
887
|
}
|
|
885
888
|
const statusCode=Number(response?.status);
|
|
886
|
-
if(!
|
|
889
|
+
if(!is.safeInteger(statusCode)||statusCode<100||statusCode>599){
|
|
887
890
|
return completeAttempt(ambiguousResult(
|
|
888
891
|
'resend_invalid_response',
|
|
889
892
|
configuration.retryableDelayMs,
|
|
@@ -912,7 +915,7 @@ async function performResendAttempt(configuration,delivery,idempotencyKey,signal
|
|
|
912
915
|
}
|
|
913
916
|
const value=parseProviderObject(text);
|
|
914
917
|
if(statusCode>=200&&statusCode<300){
|
|
915
|
-
if(!value
|
|
918
|
+
if(!value||!is.string(value.id)||!PROVIDER_ID_PATTERN.test(value.id)){
|
|
916
919
|
return completeAttempt(ambiguousResult(
|
|
917
920
|
'resend_invalid_success_response',
|
|
918
921
|
configuration.retryableDelayMs,
|
|
@@ -945,26 +948,26 @@ async function performResendAttempt(configuration,delivery,idempotencyKey,signal
|
|
|
945
948
|
report:delivery.report,
|
|
946
949
|
durationMs:Math.max(0,Date.now()-startedAt),
|
|
947
950
|
requestId,
|
|
948
|
-
providerStatus:
|
|
951
|
+
providerStatus:is.safeInteger(Number(response?.status))?Number(response.status):0
|
|
949
952
|
});
|
|
950
953
|
}
|
|
951
954
|
}
|
|
952
955
|
|
|
953
956
|
function normalizeDirectSendOptions(options){
|
|
954
|
-
if(!options
|
|
957
|
+
if(!options||!is.object(options)||is.array(options)){
|
|
955
958
|
throw configurationError('Mail send options must be an object.');
|
|
956
959
|
}
|
|
957
|
-
if(
|
|
960
|
+
if(!is.string(options.reportKey)||!IDEMPOTENCY_KEY_PATTERN.test(options.reportKey)){
|
|
958
961
|
throw configurationError('reportKey must contain safe identifier characters.');
|
|
959
962
|
}
|
|
960
963
|
const fetchImpl=options.fetchImpl??globalThis.fetch;
|
|
961
|
-
if(
|
|
964
|
+
if(!is.function(fetchImpl)){
|
|
962
965
|
throw configurationError('A fetch implementation is required for Resend delivery.');
|
|
963
966
|
}
|
|
964
|
-
if(options.onEvent!==undefined
|
|
967
|
+
if(options.onEvent!==undefined&&!is.function(options.onEvent)){
|
|
965
968
|
throw configurationError('onEvent must be a function when supplied.');
|
|
966
969
|
}
|
|
967
|
-
if(options.requestIdFactory!==undefined
|
|
970
|
+
if(options.requestIdFactory!==undefined&&!is.function(options.requestIdFactory)){
|
|
968
971
|
throw configurationError('requestIdFactory must be a function when supplied.');
|
|
969
972
|
}
|
|
970
973
|
return {
|
|
@@ -1343,7 +1346,7 @@ export async function startResendMailServer(options={}){
|
|
|
1343
1346
|
throw error;
|
|
1344
1347
|
}
|
|
1345
1348
|
const address=server.address();
|
|
1346
|
-
if(!address||
|
|
1349
|
+
if(!address||is.string(address)||!isNumericLoopback(address.address)){
|
|
1347
1350
|
await Promise.allSettled([closeHttpServer(server),requestHandler.close()]);
|
|
1348
1351
|
throw configurationError('Mail server did not bind to a numeric loopback address.');
|
|
1349
1352
|
}
|
package/src/mail.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
2
3
|
import {
|
|
3
4
|
deleteMailCredential,
|
|
@@ -7,6 +8,8 @@ import {
|
|
|
7
8
|
} from './mail-credentials.mjs';
|
|
8
9
|
import {sendResendMail,startResendMailServer} from './mail-server.mjs';
|
|
9
10
|
|
|
11
|
+
const is = new Is(false);
|
|
12
|
+
|
|
10
13
|
export const MAIL_COMMAND_ACTIONS=[
|
|
11
14
|
'key-set',
|
|
12
15
|
'key-status',
|
|
@@ -22,10 +25,10 @@ function usage(message){
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
function validateOptions(options){
|
|
25
|
-
if(!options
|
|
28
|
+
if(!options||!is.object(options)||is.array(options)){
|
|
26
29
|
usage('Mail command options must be an object.');
|
|
27
30
|
}
|
|
28
|
-
if(
|
|
31
|
+
if(!is.string(options.action)||!ACTION_SET.has(options.action)){
|
|
29
32
|
usage(`Mail action must be one of: ${MAIL_COMMAND_ACTIONS.join(', ')}.`);
|
|
30
33
|
}
|
|
31
34
|
return options;
|
|
@@ -33,26 +36,26 @@ function validateOptions(options){
|
|
|
33
36
|
|
|
34
37
|
function dependency(options,name,fallback){
|
|
35
38
|
const value=options[name]??fallback;
|
|
36
|
-
if(
|
|
39
|
+
if(!is.function(value)){
|
|
37
40
|
usage(`${name} must be a function when supplied.`);
|
|
38
41
|
}
|
|
39
42
|
return value;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
function recipientList(value,label){
|
|
43
|
-
if(value===undefined||value===null||value===''||(
|
|
46
|
+
if(value===undefined||value===null||value===''||(is.array(value)&&value.length===0)){
|
|
44
47
|
return [];
|
|
45
48
|
}
|
|
46
|
-
const entries=
|
|
49
|
+
const entries=is.array(value)
|
|
47
50
|
? value
|
|
48
|
-
:
|
|
51
|
+
: is.string(value)
|
|
49
52
|
? value.split(',')
|
|
50
53
|
: null;
|
|
51
54
|
if(!entries||entries.length===0){
|
|
52
55
|
usage(`${label} must contain at least one email address when supplied.`);
|
|
53
56
|
}
|
|
54
57
|
const result=entries.map(function normalizeMailRecipient(entry){
|
|
55
|
-
if(
|
|
58
|
+
if(!is.string(entry)||!entry.trim()){
|
|
56
59
|
usage(`${label} contains an empty email address.`);
|
|
57
60
|
}
|
|
58
61
|
return entry.trim();
|
|
@@ -66,9 +69,9 @@ function recipientList(value,label){
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
function originList(value){
|
|
69
|
-
const entries=
|
|
72
|
+
const entries=is.array(value)?value:[value];
|
|
70
73
|
if(entries.length===0||entries.some(function invalidOrigin(entry){
|
|
71
|
-
return
|
|
74
|
+
return !is.string(entry)||!entry.trim();
|
|
72
75
|
})){
|
|
73
76
|
usage('Mail serve requires at least one exact allowed origin.');
|
|
74
77
|
}
|
|
@@ -115,9 +118,9 @@ async function deleteCredential(options){
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
function withoutMailCredentials(value,seen=new WeakMap()){
|
|
118
|
-
if(!value
|
|
121
|
+
if(!value||!is.object(value))return value;
|
|
119
122
|
if(seen.has(value))return seen.get(value);
|
|
120
|
-
const copy=
|
|
123
|
+
const copy=is.array(value)?[]:{};
|
|
121
124
|
seen.set(value,copy);
|
|
122
125
|
for(const [key,entry] of Object.entries(value)){
|
|
123
126
|
if(key==='apiKey'||key==='appKey')continue;
|
|
@@ -127,7 +130,7 @@ function withoutMailCredentials(value,seen=new WeakMap()){
|
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
function completeSendFailure(result){
|
|
130
|
-
if(!result
|
|
133
|
+
if(!result||!is.object(result)||is.array(result)){
|
|
131
134
|
return {provider:'resend',result:withoutMailCredentials(result)};
|
|
132
135
|
}
|
|
133
136
|
// Complete provider detail remains visible; only credential fields are omitted.
|
|
@@ -148,7 +151,7 @@ async function sendMail(options){
|
|
|
148
151
|
`No Resend credential is configured for profile ${String(options.profile)}.`
|
|
149
152
|
);
|
|
150
153
|
}
|
|
151
|
-
if(
|
|
154
|
+
if(!is.string(apiKey)||!apiKey){
|
|
152
155
|
throw new ArcaneError(
|
|
153
156
|
ERROR_CODES.operationFailed,
|
|
154
157
|
'The configured Resend credential could not be read.'
|
|
@@ -195,7 +198,7 @@ async function serveMail(options){
|
|
|
195
198
|
`No Resend credential is configured for profile ${String(options.profile)}.`
|
|
196
199
|
);
|
|
197
200
|
}
|
|
198
|
-
if(
|
|
201
|
+
if(!is.string(apiKey)||!apiKey){
|
|
199
202
|
throw new ArcaneError(
|
|
200
203
|
ERROR_CODES.operationFailed,
|
|
201
204
|
'The configured Resend credential could not be read.'
|
|
@@ -205,7 +208,7 @@ async function serveMail(options){
|
|
|
205
208
|
try{
|
|
206
209
|
throwIfAborted(options.signal);
|
|
207
210
|
appKey=await readAppKey();
|
|
208
|
-
if(
|
|
211
|
+
if(!is.string(appKey)||!appKey){
|
|
209
212
|
throw new ArcaneError(
|
|
210
213
|
ERROR_CODES.operationFailed,
|
|
211
214
|
'The local Mail gateway app key could not be read.'
|
package/src/native-plan.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {lstat,readFile,realpath} from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {validateAppDescriptor} from './app-descriptor.mjs';
|
|
4
5
|
import {normalizeRelativePath} from './packager/core.mjs';
|
|
5
6
|
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
6
7
|
|
|
8
|
+
const is = new Is(false);
|
|
9
|
+
|
|
7
10
|
export const NATIVE_BUILD_PLAN_PROTOCOL='arcane-native-build-plan/1';
|
|
8
11
|
export const NATIVE_BUILDER_PROTOCOL='arcane-native-builder/1';
|
|
9
12
|
|
|
@@ -38,15 +41,15 @@ function fail(message,code=ERROR_CODES.policyDenied,details){
|
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
function isObject(value){
|
|
41
|
-
return value!==null&&
|
|
44
|
+
return value!==null&&is.object(value)&&!is.array(value);
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
async function emit(onEvent,event){
|
|
45
|
-
if(
|
|
48
|
+
if(is.function(onEvent))await onEvent(event);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
async function realDirectory(location,label){
|
|
49
|
-
if(
|
|
52
|
+
if(!is.string(location)||!location.trim())fail(`${label} is required.`,ERROR_CODES.usage);
|
|
50
53
|
const requested=path.resolve(location);
|
|
51
54
|
let info;
|
|
52
55
|
try{info=await lstat(requested);}
|
|
@@ -110,7 +113,7 @@ export function validateNativeBuilder(provider){
|
|
|
110
113
|
fail(`Native builder protocol must be ${NATIVE_BUILDER_PROTOCOL}.`,ERROR_CODES.targetUnavailable);
|
|
111
114
|
}
|
|
112
115
|
for(const method of ['describe','doctor','prepare','build','verify','run']){
|
|
113
|
-
if(
|
|
116
|
+
if(!is.function(provider[method])){
|
|
114
117
|
fail(`Native builder must implement ${method}().`,ERROR_CODES.targetUnavailable);
|
|
115
118
|
}
|
|
116
119
|
}
|
|
@@ -119,7 +122,7 @@ export function validateNativeBuilder(provider){
|
|
|
119
122
|
|
|
120
123
|
async function releaseSelection(releaseRoot,release,label){
|
|
121
124
|
const root=await realDirectory(releaseRoot,`${label} root`);
|
|
122
|
-
if(!isObject(release)||!
|
|
125
|
+
if(!isObject(release)||!is.array(release.files)){
|
|
123
126
|
fail(`${label} must provide its structural file inventory.`,ERROR_CODES.usage);
|
|
124
127
|
}
|
|
125
128
|
const files=release.files.map((entry,index)=>normalizeRelativePath(
|
|
@@ -150,11 +153,11 @@ async function releaseSelection(releaseRoot,release,label){
|
|
|
150
153
|
|
|
151
154
|
async function dependencySelections(value,{signal}={}){
|
|
152
155
|
if(value===undefined)return [];
|
|
153
|
-
if(!
|
|
156
|
+
if(!is.array(value))fail('dependencyReleases must be an array.',ERROR_CODES.usage);
|
|
154
157
|
const dependencies=[];
|
|
155
158
|
for(const [index,item] of value.entries()){
|
|
156
159
|
throwIfAborted(signal);
|
|
157
|
-
if(!isObject(item)
|
|
160
|
+
if(!isObject(item)||!is.string(item.appId)){
|
|
158
161
|
fail(`dependencyReleases[${index}] is malformed.`,ERROR_CODES.usage);
|
|
159
162
|
}
|
|
160
163
|
const selected=await releaseSelection(
|
|
@@ -191,10 +194,10 @@ export async function createNativeBuildPlan({
|
|
|
191
194
|
));
|
|
192
195
|
const application=await releaseSelection(appReleaseRoot,release,'application release');
|
|
193
196
|
const dependencies=await dependencySelections(dependencyReleases,{signal});
|
|
194
|
-
if(!
|
|
197
|
+
if(!is.array(protectedRoots)){
|
|
195
198
|
fail('protectedRoots must be an array.',ERROR_CODES.usage);
|
|
196
199
|
}
|
|
197
|
-
if(
|
|
200
|
+
if(!is.string(outputRoot)||!outputRoot.trim()){
|
|
198
201
|
fail('outputRoot is required.',ERROR_CODES.usage);
|
|
199
202
|
}
|
|
200
203
|
const selectedOutput=path.resolve(outputRoot);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {lstat,realpath} from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {pathToFileURL} from 'node:url';
|
|
4
5
|
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
5
6
|
import {validateNativeBuilder} from './native-plan.mjs';
|
|
6
7
|
|
|
8
|
+
const is = new Is(false);
|
|
9
|
+
|
|
7
10
|
const PROVIDER_ROOT=['machine_bundles','arcane-os-machine-bundle','tools'];
|
|
8
11
|
|
|
9
12
|
function providerPath(fileName){
|
|
@@ -55,10 +58,10 @@ async function resolveProviderLocation({
|
|
|
55
58
|
supportedTargets:Object.keys(FIXED_NATIVE_PROVIDER_PATHS)
|
|
56
59
|
});
|
|
57
60
|
}
|
|
58
|
-
if(
|
|
61
|
+
if(!is.string(arcaneRoot)||!arcaneRoot.trim()){
|
|
59
62
|
fail(`The ${target} native provider requires an Arcane OS checkout root.`);
|
|
60
63
|
}
|
|
61
|
-
if(
|
|
64
|
+
if(!is.function(inspect)||!is.function(canonicalize)){
|
|
62
65
|
fail('The Arcane native provider loader dependencies are invalid.');
|
|
63
66
|
}
|
|
64
67
|
const requestedRoot=path.resolve(arcaneRoot);
|
|
@@ -107,7 +110,7 @@ async function resolveProviderLocation({
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
export async function loadArcaneNativeProvider(options={}){
|
|
110
|
-
if(options===null
|
|
113
|
+
if(options===null||!is.object(options)||is.array(options)){
|
|
111
114
|
fail('Arcane native provider options must be an object.');
|
|
112
115
|
}
|
|
113
116
|
const {
|
|
@@ -120,7 +123,7 @@ export async function loadArcaneNativeProvider(options={}){
|
|
|
120
123
|
onEvent
|
|
121
124
|
}=options;
|
|
122
125
|
throwIfAborted(signal);
|
|
123
|
-
if(
|
|
126
|
+
if(!is.function(importModule))fail('The Arcane native provider importer is invalid.');
|
|
124
127
|
await onEvent?.({
|
|
125
128
|
type:'native.provider.load.started',
|
|
126
129
|
target,
|