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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
2
|
+
import MailOutbox from './MailOutbox.mjs';
|
|
1
3
|
import {
|
|
2
4
|
DEFAULT_MAIL_REQUEST_TIMEOUT_MS,
|
|
3
5
|
sendMailReport,
|
|
@@ -5,25 +7,199 @@ import {
|
|
|
5
7
|
|
|
6
8
|
let userInstance=null;
|
|
7
9
|
|
|
8
|
-
async function loadOptionalMailDependencies(){
|
|
9
|
-
await import('./DBOPFS.js');
|
|
10
|
-
if(!userInstance){
|
|
11
|
-
const { default:UserEntity }=await import('../entities/User.js');
|
|
12
|
-
userInstance=new UserEntity();
|
|
13
|
-
}
|
|
14
|
-
return {
|
|
15
|
-
dbopfs:globalThis.dbopfs,
|
|
16
|
-
user:userInstance,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
10
|
const MAX_SUBJECT_LENGTH=160;
|
|
21
11
|
const MAX_MESSAGE_BYTES=25*1024*1024;
|
|
22
12
|
const MAX_NATIVE_REPORT_BYTES=768*1024;
|
|
23
13
|
const MAIL_TYPES=new Set(['error','report','crisis_detected']);
|
|
14
|
+
const MAIL_OUTBOX_EVENTS=Object.freeze([
|
|
15
|
+
'mail-outbox-state',
|
|
16
|
+
'mail-outbox-delivery',
|
|
17
|
+
'mail-outbox-drain'
|
|
18
|
+
]);
|
|
19
|
+
const PENDING_OUTBOX_STATES=new Set(['queued','sending','retry_wait']);
|
|
20
|
+
const NATIVE_MAIL_RESPONSE_STATUS_CODES=Object.freeze({
|
|
21
|
+
accepted:202,
|
|
22
|
+
delivery_uncertain:207,
|
|
23
|
+
partially_accepted:207
|
|
24
|
+
});
|
|
25
|
+
const NATIVE_MAIL_RESULT_KEYS=Object.freeze([
|
|
26
|
+
'partial','requestId','sent','status','statusCode','uncertain'
|
|
27
|
+
]);
|
|
28
|
+
const NATIVE_MAIL_REQUEST_ID_PATTERN=/^[A-Za-z0-9-]{8,128}$/;
|
|
29
|
+
const NATIVE_MAIL_UNCERTAIN_ERROR_CODES=new Set([
|
|
30
|
+
'ARCANE_REQUEST_TIMEOUT',
|
|
31
|
+
'MAIL_GATEWAY_RESPONSE_INVALID',
|
|
32
|
+
'MAIL_GATEWAY_RESPONSE_TOO_LARGE',
|
|
33
|
+
'MAIL_GATEWAY_UNAVAILABLE',
|
|
34
|
+
'MAIL_NATIVE_RESULT_INVALID',
|
|
35
|
+
'MAIL_SEND_TIMEOUT',
|
|
36
|
+
'NATIVE_BRIDGE_ACK_TIMEOUT'
|
|
37
|
+
]);
|
|
38
|
+
const NATIVE_MAIL_RETRYABLE_ERROR_CODES=new Set([
|
|
39
|
+
'ARCANE_BRIDGE_CALL_FAILED',
|
|
40
|
+
'ARCANE_TRANSPORT_UNAVAILABLE'
|
|
41
|
+
]);
|
|
24
42
|
const EMAIL_PATTERN=/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
25
43
|
const ARCANE_APP_ID_PATTERN=/^[a-z0-9](?:[a-z0-9-]{0,62})$/;
|
|
26
44
|
|
|
45
|
+
function codedError(message,code,ErrorType=Error){
|
|
46
|
+
const error=new ErrorType(message);
|
|
47
|
+
error.code=code;
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateMailSignal(signal){
|
|
52
|
+
if(signal===null||signal===undefined) return null;
|
|
53
|
+
if(typeof signal!=='object'
|
|
54
|
+
||typeof signal.aborted!=='boolean'
|
|
55
|
+
||typeof signal.addEventListener!=='function'
|
|
56
|
+
||typeof signal.removeEventListener!=='function'){
|
|
57
|
+
throw new TypeError('Mail signal must be an AbortSignal');
|
|
58
|
+
}
|
|
59
|
+
return signal;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function throwIfMailAborted(signal){
|
|
63
|
+
if(!signal?.aborted) return;
|
|
64
|
+
const error=codedError('Mail operation was aborted.','MAIL_OUTBOX_ABORTED');
|
|
65
|
+
error.name='AbortError';
|
|
66
|
+
if(signal.reason!==undefined) error.cause=signal.reason;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function linkedMailSignal(primary,secondary){
|
|
71
|
+
const signals=[];
|
|
72
|
+
for(const candidate of [primary,secondary]){
|
|
73
|
+
const signal=validateMailSignal(candidate);
|
|
74
|
+
if(signal&&!signals.includes(signal)) signals.push(signal);
|
|
75
|
+
}
|
|
76
|
+
if(signals.length===0){
|
|
77
|
+
return Object.freeze({signal:null,dispose:function disposeEmptyMailSignal(){}});
|
|
78
|
+
}
|
|
79
|
+
if(signals.length===1){
|
|
80
|
+
return Object.freeze({signal:signals[0],dispose:function disposeSingleMailSignal(){}});
|
|
81
|
+
}
|
|
82
|
+
const controller=new AbortController();
|
|
83
|
+
const listeners=[];
|
|
84
|
+
for(const signal of signals){
|
|
85
|
+
const listener=function forwardLinkedMailAbort(){
|
|
86
|
+
if(!controller.signal.aborted) controller.abort(signal.reason);
|
|
87
|
+
};
|
|
88
|
+
listeners.push({signal,listener});
|
|
89
|
+
if(signal.aborted) listener();
|
|
90
|
+
else signal.addEventListener('abort',listener,{once:true});
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
signal:controller.signal,
|
|
94
|
+
dispose:function disposeLinkedMailSignal(){
|
|
95
|
+
for(const entry of listeners){
|
|
96
|
+
entry.signal.removeEventListener('abort',entry.listener);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function waitForMailOperation(operation,signal){
|
|
103
|
+
const normalizedSignal=validateMailSignal(signal);
|
|
104
|
+
throwIfMailAborted(normalizedSignal);
|
|
105
|
+
if(!normalizedSignal) return operation;
|
|
106
|
+
return new Promise(function observeMailCallerAbort(resolve,reject){
|
|
107
|
+
let settled=false;
|
|
108
|
+
function finish(callback,value){
|
|
109
|
+
if(settled) return;
|
|
110
|
+
settled=true;
|
|
111
|
+
normalizedSignal.removeEventListener('abort',handleAbort);
|
|
112
|
+
callback(value);
|
|
113
|
+
}
|
|
114
|
+
function handleAbort(){
|
|
115
|
+
try{
|
|
116
|
+
throwIfMailAborted(normalizedSignal);
|
|
117
|
+
}catch(error){
|
|
118
|
+
finish(reject,error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
normalizedSignal.addEventListener('abort',handleAbort,{once:true});
|
|
122
|
+
Promise.resolve(operation).then(
|
|
123
|
+
finish.bind(null,resolve),
|
|
124
|
+
finish.bind(null,reject)
|
|
125
|
+
);
|
|
126
|
+
if(normalizedSignal.aborted) handleAbort();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function exactNativeMailResult(value){
|
|
131
|
+
if(!value||typeof value!=='object'||Array.isArray(value)
|
|
132
|
+
||Object.keys(value).sort().join(',')!==[...NATIVE_MAIL_RESULT_KEYS].sort().join(',')
|
|
133
|
+
||typeof value.requestId!=='string'
|
|
134
|
+
||!NATIVE_MAIL_REQUEST_ID_PATTERN.test(value.requestId)
|
|
135
|
+
||!Object.hasOwn(NATIVE_MAIL_RESPONSE_STATUS_CODES,value.status)
|
|
136
|
+
||value.statusCode!==NATIVE_MAIL_RESPONSE_STATUS_CODES[value.status]
|
|
137
|
+
||typeof value.sent!=='boolean'
|
|
138
|
+
||typeof value.partial!=='boolean'
|
|
139
|
+
||typeof value.uncertain!=='boolean'
|
|
140
|
+
||value.sent!==(value.status==='accepted')
|
|
141
|
+
||value.partial!==(value.status==='partially_accepted')
|
|
142
|
+
||value.uncertain!==(value.status==='delivery_uncertain')){
|
|
143
|
+
throw codedError(
|
|
144
|
+
'Native Arcane mail returned an invalid result.',
|
|
145
|
+
'MAIL_NATIVE_RESULT_INVALID'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if(value.status!=='accepted') return value;
|
|
149
|
+
return {
|
|
150
|
+
...value,
|
|
151
|
+
acceptanceAuthority:'arcane-core-mail-send-v1'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizedNativeMailError(error){
|
|
156
|
+
const uncertain=NATIVE_MAIL_UNCERTAIN_ERROR_CODES.has(error?.code);
|
|
157
|
+
const retryable=uncertain||NATIVE_MAIL_RETRYABLE_ERROR_CODES.has(error?.code);
|
|
158
|
+
if(!retryable) return error;
|
|
159
|
+
const normalized=new Error(
|
|
160
|
+
uncertain
|
|
161
|
+
?'Native Arcane mail delivery has an uncertain outcome.'
|
|
162
|
+
:'Native Arcane mail transport is temporarily unavailable.',
|
|
163
|
+
{cause:error}
|
|
164
|
+
);
|
|
165
|
+
normalized.code=error.code;
|
|
166
|
+
normalized.retryable=true;
|
|
167
|
+
normalized.uncertain=uncertain;
|
|
168
|
+
const statusCode=Number(error?.statusCode??error?.status);
|
|
169
|
+
if(Number.isSafeInteger(statusCode)&&statusCode>=100&&statusCode<=599){
|
|
170
|
+
normalized.statusCode=statusCode;
|
|
171
|
+
}
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function loadRequiredMailStorage(){
|
|
176
|
+
try{
|
|
177
|
+
await import('./DBOPFS.js');
|
|
178
|
+
}catch{
|
|
179
|
+
throw codedError(
|
|
180
|
+
'Mail outbox storage is unavailable.',
|
|
181
|
+
'MAIL_OUTBOX_STORAGE_UNAVAILABLE'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const storage=globalThis.dbopfs;
|
|
185
|
+
if(!storage||typeof storage!=='object'){
|
|
186
|
+
throw codedError(
|
|
187
|
+
'Mail outbox storage is unavailable.',
|
|
188
|
+
'MAIL_OUTBOX_STORAGE_UNAVAILABLE'
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return storage;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function loadOptionalMailUser(injectedUser){
|
|
195
|
+
if(injectedUser!==undefined) return injectedUser;
|
|
196
|
+
if(!userInstance){
|
|
197
|
+
const {default:UserEntity}=await import('../entities/User.js');
|
|
198
|
+
userInstance=new UserEntity();
|
|
199
|
+
}
|
|
200
|
+
return userInstance;
|
|
201
|
+
}
|
|
202
|
+
|
|
27
203
|
function declaredApplicationId(document=globalThis.document){
|
|
28
204
|
const value=document?.querySelector?.('meta[name="arcane-app-id"]')?.content?.trim();
|
|
29
205
|
return ARCANE_APP_ID_PATTERN.test(value||'') ? value:'';
|
|
@@ -119,7 +295,9 @@ function serializePayload(value){
|
|
|
119
295
|
|
|
120
296
|
function assertMessageSize(report){
|
|
121
297
|
const messageBytes=['text','html'].reduce(
|
|
122
|
-
(total,key)
|
|
298
|
+
function sumMessageBytes(total,key){
|
|
299
|
+
return total+utf8ByteLength(report[key]||'');
|
|
300
|
+
},
|
|
123
301
|
0
|
|
124
302
|
);
|
|
125
303
|
if(messageBytes>MAX_MESSAGE_BYTES){
|
|
@@ -127,29 +305,38 @@ function assertMessageSize(report){
|
|
|
127
305
|
}
|
|
128
306
|
}
|
|
129
307
|
|
|
130
|
-
function assertNativeReportSize(
|
|
131
|
-
|
|
132
|
-
if(utf8ByteLength(serialized)>MAX_NATIVE_REPORT_BYTES){
|
|
308
|
+
function assertNativeReportSize(serializedReport){
|
|
309
|
+
if(utf8ByteLength(serializedReport)>MAX_NATIVE_REPORT_BYTES){
|
|
133
310
|
throw new RangeError('Native Arcane mail reports cannot exceed 786,432 serialized bytes');
|
|
134
311
|
}
|
|
135
312
|
}
|
|
136
313
|
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
314
|
+
function randomReportNonce(cryptoProvider){
|
|
315
|
+
if(typeof cryptoProvider?.randomUUID==='function'){
|
|
316
|
+
try{
|
|
317
|
+
const value=cryptoProvider.randomUUID();
|
|
318
|
+
if(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)){
|
|
319
|
+
return value;
|
|
320
|
+
}
|
|
321
|
+
}catch{}
|
|
322
|
+
}
|
|
323
|
+
if(typeof cryptoProvider?.getRandomValues==='function'){
|
|
324
|
+
try{
|
|
325
|
+
const bytes=new Uint8Array(16);
|
|
326
|
+
cryptoProvider.getRandomValues(bytes);
|
|
327
|
+
return [...bytes].map(function reportNonceByte(value){
|
|
328
|
+
return value.toString(16).padStart(2,'0');
|
|
329
|
+
}).join('');
|
|
330
|
+
}catch{}
|
|
331
|
+
}
|
|
332
|
+
throw codedError(
|
|
333
|
+
'Mail requires Web Crypto to create a durable idempotency key.',
|
|
334
|
+
'MAIL_CRYPTO_UNAVAILABLE'
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function createReportKey(timestamp,cryptoProvider){
|
|
339
|
+
return `${timestamp.toString(36)}-${randomReportNonce(cryptoProvider)}-mail`;
|
|
153
340
|
}
|
|
154
341
|
|
|
155
342
|
function normalizeRecipients(values){
|
|
@@ -162,62 +349,514 @@ function normalizeRecipients(values){
|
|
|
162
349
|
|
|
163
350
|
const recipients=[];
|
|
164
351
|
for(const value of values){
|
|
165
|
-
if(!value)
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
|
|
352
|
+
if(!value) continue;
|
|
169
353
|
if(typeof value!=='string'){
|
|
170
354
|
throw new TypeError('Every mail recipient must be an email address');
|
|
171
355
|
}
|
|
172
|
-
|
|
173
356
|
const address=value.trim().toLowerCase();
|
|
174
357
|
if(address.length>254||!EMAIL_PATTERN.test(address)){
|
|
175
358
|
throw new TypeError('Mail contains an invalid recipient address');
|
|
176
359
|
}
|
|
360
|
+
if(!recipients.includes(address)) recipients.push(address);
|
|
361
|
+
}
|
|
362
|
+
return recipients;
|
|
363
|
+
}
|
|
177
364
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
365
|
+
function profileValue(user,key){
|
|
366
|
+
try{
|
|
367
|
+
const value=user?.[key];
|
|
368
|
+
return typeof value==='string'?value:'';
|
|
369
|
+
}catch{
|
|
370
|
+
return '';
|
|
181
371
|
}
|
|
372
|
+
}
|
|
182
373
|
|
|
183
|
-
|
|
374
|
+
function normalizedDeliveryResult(value){
|
|
375
|
+
if(!value||typeof value!=='object'||Array.isArray(value)) return value;
|
|
376
|
+
let status=typeof value.status==='string'?value.status:'';
|
|
377
|
+
if(!status&&value.sent===true) status='accepted';
|
|
378
|
+
let classification=typeof value.classification==='string'?value.classification:'';
|
|
379
|
+
if(!classification){
|
|
380
|
+
classification={
|
|
381
|
+
accepted:'accepted',
|
|
382
|
+
delivery_uncertain:'ambiguous',
|
|
383
|
+
retryable:'retryable',
|
|
384
|
+
partially_accepted:'permanent',
|
|
385
|
+
permanently_rejected:'permanent'
|
|
386
|
+
}[status]||'';
|
|
387
|
+
}
|
|
388
|
+
const retryAfterMilliseconds=Number(value.retryAfterMs);
|
|
389
|
+
const retryAfterSeconds=Number.isSafeInteger(value.retryAfterSeconds)
|
|
390
|
+
? value.retryAfterSeconds
|
|
391
|
+
: Number.isFinite(retryAfterMilliseconds)&&retryAfterMilliseconds>0
|
|
392
|
+
? Math.ceil(retryAfterMilliseconds/1000)
|
|
393
|
+
: null;
|
|
394
|
+
return {
|
|
395
|
+
...value,
|
|
396
|
+
...(status?{status}:{}),
|
|
397
|
+
...(classification?{classification}:{}),
|
|
398
|
+
retryAfterSeconds,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function publicSendResult(record){
|
|
403
|
+
return {
|
|
404
|
+
...(record.result||{}),
|
|
405
|
+
reportKey:record.reportKey,
|
|
406
|
+
state:record.state,
|
|
407
|
+
status:record.result?.status||record.state,
|
|
408
|
+
queued:PENDING_OUTBOX_STATES.has(record.state),
|
|
409
|
+
sent:record.state==='accepted',
|
|
410
|
+
uncertain:record.failure?.uncertain===true,
|
|
411
|
+
attempts:record.attempts,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function safeDrainDetail(summary){
|
|
416
|
+
return Object.freeze({
|
|
417
|
+
reason:summary.reason,
|
|
418
|
+
online:summary.online,
|
|
419
|
+
considered:summary.considered,
|
|
420
|
+
attempted:summary.attempted,
|
|
421
|
+
invalidRecords:summary.invalidRecords,
|
|
422
|
+
bounded:summary.bounded,
|
|
423
|
+
pending:summary.pending,
|
|
424
|
+
states:Object.freeze({...summary.states})
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function normalizeMailOptions(options){
|
|
429
|
+
if(!options||typeof options!=='object'||Array.isArray(options)){
|
|
430
|
+
throw new TypeError('Mail options must be an object');
|
|
431
|
+
}
|
|
432
|
+
const deliver=options.deliver;
|
|
433
|
+
const clock=options.clock??Date.now;
|
|
434
|
+
const isOnline=options.isOnline??function defaultMailOnlineStatus(){
|
|
435
|
+
return globalThis.navigator?.onLine!==false;
|
|
436
|
+
};
|
|
437
|
+
const onlineTarget=options.onlineTarget===undefined
|
|
438
|
+
?typeof globalThis.addEventListener==='function'?globalThis:null
|
|
439
|
+
:options.onlineTarget;
|
|
440
|
+
const includeContext=options.includeContext??false;
|
|
441
|
+
const cryptoProvider=Object.hasOwn(options,'crypto')
|
|
442
|
+
?options.crypto
|
|
443
|
+
:globalThis.crypto;
|
|
444
|
+
if(deliver!==undefined&&typeof deliver!=='function'){
|
|
445
|
+
throw new TypeError('Mail deliver must be a function');
|
|
446
|
+
}
|
|
447
|
+
if(typeof clock!=='function'||typeof isOnline!=='function'){
|
|
448
|
+
throw new TypeError('Mail clock and isOnline hooks must be functions');
|
|
449
|
+
}
|
|
450
|
+
if(onlineTarget!==null&&(
|
|
451
|
+
typeof onlineTarget?.addEventListener!=='function'
|
|
452
|
+
||typeof onlineTarget?.removeEventListener!=='function'
|
|
453
|
+
)){
|
|
454
|
+
throw new TypeError('Mail onlineTarget must be an EventTarget');
|
|
455
|
+
}
|
|
456
|
+
if(typeof includeContext!=='boolean'){
|
|
457
|
+
throw new TypeError('Mail includeContext must be boolean');
|
|
458
|
+
}
|
|
459
|
+
if(cryptoProvider!==null&&cryptoProvider!==undefined
|
|
460
|
+
&&typeof cryptoProvider!=='object'){
|
|
461
|
+
throw new TypeError('Mail crypto must be a Web Crypto provider');
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
clock,
|
|
465
|
+
cryptoProvider,
|
|
466
|
+
deliver,
|
|
467
|
+
includeContext,
|
|
468
|
+
isOnline,
|
|
469
|
+
onlineTarget,
|
|
470
|
+
outboxOptions:{
|
|
471
|
+
...(options.maxAttemptsPerDrain===undefined
|
|
472
|
+
?{}:{maxAttemptsPerDrain:options.maxAttemptsPerDrain}),
|
|
473
|
+
...(options.maxInvalidRecords===undefined
|
|
474
|
+
?{}:{maxInvalidRecords:options.maxInvalidRecords}),
|
|
475
|
+
...(options.maxRecords===undefined?{}:{maxRecords:options.maxRecords}),
|
|
476
|
+
...(options.lockManager===undefined?{}:{lockManager:options.lockManager}),
|
|
477
|
+
maxReportBytes:options.maxReportBytes??MAX_MESSAGE_BYTES,
|
|
478
|
+
},
|
|
479
|
+
storage:options.storage,
|
|
480
|
+
storageInjected:Object.hasOwn(options,'storage'),
|
|
481
|
+
user:Object.hasOwn(options,'user')?options.user:undefined
|
|
482
|
+
};
|
|
184
483
|
}
|
|
185
484
|
|
|
186
485
|
class Mail {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
486
|
+
#backgroundController=null;
|
|
487
|
+
#backgroundDrain=null;
|
|
488
|
+
#backgroundError=null;
|
|
489
|
+
#clock;
|
|
490
|
+
#cryptoProvider;
|
|
491
|
+
#deliver;
|
|
492
|
+
#disposed=false;
|
|
493
|
+
#events;
|
|
494
|
+
#includeContext=false;
|
|
495
|
+
#isOnline;
|
|
496
|
+
#lifecycleController=new AbortController();
|
|
497
|
+
#lifecycleRevision=0;
|
|
498
|
+
#onlineHandler;
|
|
499
|
+
#onlineTarget;
|
|
500
|
+
#outbox=null;
|
|
501
|
+
#outboxOptions;
|
|
502
|
+
#outboxPromise=null;
|
|
503
|
+
#startPromise=null;
|
|
504
|
+
#startRevision=null;
|
|
505
|
+
#startSummary=null;
|
|
506
|
+
#started=false;
|
|
507
|
+
#storage;
|
|
508
|
+
#storageInjected;
|
|
509
|
+
#user;
|
|
510
|
+
|
|
511
|
+
constructor(config=globalThis.arcane?.config?.mail||{},options={}) {
|
|
512
|
+
const resolved=resolveMailConfig(config);
|
|
513
|
+
const normalizedOptions=normalizeMailOptions(options);
|
|
514
|
+
const existing=globalThis.window?.mail;
|
|
515
|
+
if(existing instanceof Mail){
|
|
516
|
+
if(!existing.disposed){
|
|
517
|
+
if(arguments.length>0){
|
|
518
|
+
existing.#applyConfiguration(resolved,normalizedOptions,{reconfigure:true});
|
|
519
|
+
}
|
|
520
|
+
return existing;
|
|
521
|
+
}
|
|
522
|
+
if(globalThis.window.mail===existing) globalThis.window.mail=null;
|
|
523
|
+
}else if(existing){
|
|
524
|
+
throw codedError(
|
|
525
|
+
'window.mail is already owned by an incompatible implementation.',
|
|
526
|
+
'MAIL_SINGLETON_CONFLICT'
|
|
527
|
+
);
|
|
190
528
|
}
|
|
191
529
|
|
|
192
|
-
|
|
530
|
+
this.#applyConfiguration(resolved,normalizedOptions);
|
|
531
|
+
this.#onlineHandler=this.#handleOnline.bind(this);
|
|
532
|
+
this.#events=createArcaneEventSource(this,{
|
|
533
|
+
source:'mail-outbox',
|
|
534
|
+
eventTypes:MAIL_OUTBOX_EVENTS
|
|
535
|
+
});
|
|
536
|
+
if(globalThis.window&&!globalThis.window.mail) globalThis.window.mail=this;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
#applyConfiguration(resolved,options,{reconfigure=false}={}){
|
|
540
|
+
if(reconfigure&&(this.#started||this.#startPromise||this.#outbox||this.#outboxPromise)){
|
|
541
|
+
throw codedError(
|
|
542
|
+
'Mail configuration is locked after its durable lifecycle begins.',
|
|
543
|
+
'MAIL_CONFIGURATION_LOCKED'
|
|
544
|
+
);
|
|
545
|
+
}
|
|
193
546
|
this.appName=resolved.appName;
|
|
194
547
|
this.appKey=resolved.appKey;
|
|
195
548
|
this.endpoint=resolved.endpoint;
|
|
196
549
|
this.requestTimeout=resolved.requestTimeout;
|
|
550
|
+
this.#storageInjected=options.storageInjected;
|
|
551
|
+
this.#storage=options.storage;
|
|
552
|
+
this.#deliver=options.deliver;
|
|
553
|
+
this.#clock=options.clock;
|
|
554
|
+
this.#cryptoProvider=options.cryptoProvider;
|
|
555
|
+
this.#includeContext=options.includeContext;
|
|
556
|
+
this.#isOnline=options.isOnline;
|
|
557
|
+
this.#onlineTarget=options.onlineTarget;
|
|
558
|
+
this.#outboxOptions=options.outboxOptions;
|
|
559
|
+
this.#user=options.user;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
get started(){return this.#started;}
|
|
563
|
+
get disposed(){return this.#disposed;}
|
|
564
|
+
get events(){return this.#events;}
|
|
565
|
+
get invalidOutboxRecords(){
|
|
566
|
+
return this.#outbox?.invalidRecords??Object.freeze([]);
|
|
567
|
+
}
|
|
568
|
+
get lastBackgroundError(){return this.#backgroundError;}
|
|
569
|
+
|
|
570
|
+
#assertActive(){
|
|
571
|
+
if(this.#disposed){
|
|
572
|
+
throw codedError('Mail has been disposed.','MAIL_DISPOSED');
|
|
573
|
+
}
|
|
197
574
|
}
|
|
198
575
|
|
|
199
576
|
#assertConfigured(){
|
|
200
|
-
|
|
201
|
-
|
|
577
|
+
const hasTransport=this.#deliver!==undefined
|
|
578
|
+
||typeof globalThis.Arcane?.mail?.send==='function'
|
|
579
|
+
||Boolean(this.endpoint);
|
|
580
|
+
if(!this.appName||!hasTransport){
|
|
581
|
+
throw codedError('Mail transport is not configured.','MAIL_NOT_CONFIGURED');
|
|
202
582
|
}
|
|
203
583
|
}
|
|
204
584
|
|
|
205
|
-
|
|
206
|
-
if(
|
|
207
|
-
|
|
208
|
-
return globalThis.Arcane.mail.send({report,reportKey});
|
|
585
|
+
#activeLifecycleSignal(){
|
|
586
|
+
if(this.#lifecycleController.signal.aborted){
|
|
587
|
+
this.#lifecycleController=new AbortController();
|
|
209
588
|
}
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
589
|
+
return this.#lifecycleController.signal;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#time(){
|
|
593
|
+
const value=this.#clock();
|
|
594
|
+
const milliseconds=value instanceof Date?value.getTime():Number(value);
|
|
595
|
+
if(!Number.isSafeInteger(milliseconds)||milliseconds<0
|
|
596
|
+
||Number.isNaN(new Date(milliseconds).getTime())){
|
|
597
|
+
throw codedError('Mail clock returned an invalid time.','MAIL_CLOCK_INVALID');
|
|
598
|
+
}
|
|
599
|
+
return milliseconds;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#publish(type,detail,operationId=null){
|
|
603
|
+
if(this.#disposed||this.#events.disposed) return;
|
|
604
|
+
try{
|
|
605
|
+
this.#events.dispatch(type,detail,{operationId,publicDetail:detail});
|
|
606
|
+
}catch{}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#publishState(detail,operationId=null){
|
|
610
|
+
this.#publish('mail-outbox-state',Object.freeze({...detail}),operationId);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#publishRecord(record){
|
|
614
|
+
const stateDetail=Object.freeze({
|
|
615
|
+
reportKey:record.reportKey,
|
|
616
|
+
state:record.state,
|
|
617
|
+
attempts:record.attempts,
|
|
618
|
+
pending:PENDING_OUTBOX_STATES.has(record.state)
|
|
217
619
|
});
|
|
620
|
+
this.#publish('mail-outbox-state',stateDetail,record.reportKey);
|
|
621
|
+
if(record.attempts<1||record.state==='queued'||record.state==='sending') return;
|
|
622
|
+
const outcome={
|
|
623
|
+
accepted:'accepted',
|
|
624
|
+
retry_wait:'retry',
|
|
625
|
+
failed:'failed',
|
|
626
|
+
reconciliation_required:'reconciliation_required'
|
|
627
|
+
}[record.state];
|
|
628
|
+
if(!outcome) return;
|
|
629
|
+
this.#publish('mail-outbox-delivery',Object.freeze({
|
|
630
|
+
reportKey:record.reportKey,
|
|
631
|
+
state:record.state,
|
|
632
|
+
outcome,
|
|
633
|
+
uncertain:record.failure?.uncertain===true
|
|
634
|
+
}),record.reportKey);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
#handleOutboxRecord(record){
|
|
638
|
+
this.#publishRecord(record);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async #transportDelivery(request){
|
|
642
|
+
if(this.#deliver) return this.#deliver(request);
|
|
643
|
+
if(this.endpoint){
|
|
644
|
+
return sendMailReport({
|
|
645
|
+
appKey:this.appKey,
|
|
646
|
+
appName:this.appName,
|
|
647
|
+
endpoint:this.endpoint,
|
|
648
|
+
report:request.report,
|
|
649
|
+
reportKey:request.reportKey,
|
|
650
|
+
requestTimeout:this.requestTimeout,
|
|
651
|
+
serializedReport:request.serializedReport,
|
|
652
|
+
...(request.signal?{signal:request.signal}:{})
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
assertNativeReportSize(request.serializedReport);
|
|
656
|
+
try{
|
|
657
|
+
const result=await globalThis.Arcane.mail.send({
|
|
658
|
+
report:request.report,
|
|
659
|
+
reportKey:request.reportKey
|
|
660
|
+
});
|
|
661
|
+
return exactNativeMailResult(result);
|
|
662
|
+
}catch(error){
|
|
663
|
+
throw normalizedNativeMailError(error);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async #deliverOutboxRecord(request){
|
|
668
|
+
return normalizedDeliveryResult(await this.#transportDelivery(request));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async #createOutbox(){
|
|
672
|
+
let storage=this.#storage;
|
|
673
|
+
if(!this.#storageInjected) storage=await loadRequiredMailStorage();
|
|
674
|
+
const outbox=new MailOutbox({
|
|
675
|
+
storage,
|
|
676
|
+
deliver:this.#deliverOutboxRecord.bind(this),
|
|
677
|
+
clock:this.#clock,
|
|
678
|
+
isOnline:this.#isOnline,
|
|
679
|
+
onRecordCommitted:this.#handleOutboxRecord.bind(this),
|
|
680
|
+
onlineTarget:null,
|
|
681
|
+
...this.#outboxOptions
|
|
682
|
+
});
|
|
683
|
+
this.#outbox=outbox;
|
|
684
|
+
return outbox;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async #getOutbox(){
|
|
688
|
+
this.#assertActive();
|
|
689
|
+
if(this.#outbox) return this.#outbox;
|
|
690
|
+
if(!this.#outboxPromise) this.#outboxPromise=this.#createOutbox();
|
|
691
|
+
try{
|
|
692
|
+
return await this.#outboxPromise;
|
|
693
|
+
}catch(error){
|
|
694
|
+
this.#outboxPromise=null;
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async #performStart(revision,signal){
|
|
700
|
+
const outbox=await this.#getOutbox();
|
|
701
|
+
let summary;
|
|
702
|
+
try{
|
|
703
|
+
summary=await outbox.start({signal});
|
|
704
|
+
}catch(error){
|
|
705
|
+
outbox.stop();
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
if(this.#disposed||revision!==this.#lifecycleRevision){
|
|
709
|
+
outbox.stop();
|
|
710
|
+
throw codedError('Mail start was cancelled.','MAIL_START_CANCELLED');
|
|
711
|
+
}
|
|
712
|
+
this.#onlineTarget?.addEventListener('online',this.#onlineHandler);
|
|
713
|
+
this.#started=true;
|
|
714
|
+
this.#startSummary=summary;
|
|
715
|
+
this.#publish('mail-outbox-drain',safeDrainDetail(summary));
|
|
716
|
+
this.#publishState({lifecycle:'started',started:true});
|
|
717
|
+
return summary;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async #handleOnline(){
|
|
721
|
+
if(!this.#started||this.#disposed||this.#backgroundDrain) return;
|
|
722
|
+
const controller=new AbortController();
|
|
723
|
+
this.#backgroundController=controller;
|
|
724
|
+
const operation=this.drain({reason:'online',signal:controller.signal});
|
|
725
|
+
this.#backgroundDrain=operation;
|
|
726
|
+
try{
|
|
727
|
+
await operation;
|
|
728
|
+
this.#backgroundError=null;
|
|
729
|
+
}catch(error){
|
|
730
|
+
if(error?.name!=='AbortError'&&error?.code!=='MAIL_OUTBOX_ABORTED'){
|
|
731
|
+
this.#backgroundError=error;
|
|
732
|
+
this.#publishState({lifecycle:'background-drain-failed',started:this.#started});
|
|
733
|
+
}
|
|
734
|
+
}finally{
|
|
735
|
+
if(this.#backgroundDrain===operation) this.#backgroundDrain=null;
|
|
736
|
+
if(this.#backgroundController===controller) this.#backgroundController=null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async #optionalProfile(){
|
|
741
|
+
const fallback={email:'',language:'',phone:'',username:''};
|
|
742
|
+
try{
|
|
743
|
+
const user=await loadOptionalMailUser(this.#user);
|
|
744
|
+
if(!user) return fallback;
|
|
745
|
+
try{
|
|
746
|
+
await user.load?.();
|
|
747
|
+
}catch{
|
|
748
|
+
return fallback;
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
email:profileValue(user,'email'),
|
|
752
|
+
language:profileValue(user,'language'),
|
|
753
|
+
phone:profileValue(user,'phone'),
|
|
754
|
+
username:profileValue(user,'username')
|
|
755
|
+
};
|
|
756
|
+
}catch{
|
|
757
|
+
return fallback;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async start({signal=null}={}){
|
|
762
|
+
this.#assertActive();
|
|
763
|
+
this.#assertConfigured();
|
|
764
|
+
const callerSignal=validateMailSignal(signal);
|
|
765
|
+
throwIfMailAborted(callerSignal);
|
|
766
|
+
if(this.#started) return this.#startSummary;
|
|
767
|
+
const revision=this.#lifecycleRevision;
|
|
768
|
+
if(this.#startPromise&&this.#startRevision===revision){
|
|
769
|
+
return waitForMailOperation(this.#startPromise,callerSignal);
|
|
770
|
+
}
|
|
771
|
+
const previous=this.#startPromise;
|
|
772
|
+
const linked=linkedMailSignal(this.#activeLifecycleSignal(),callerSignal);
|
|
773
|
+
const operation=(previous
|
|
774
|
+
?previous.catch(function ignoreCancelledStartGeneration(){})
|
|
775
|
+
:Promise.resolve()
|
|
776
|
+
).then(this.#performStart.bind(this,revision,linked.signal));
|
|
777
|
+
this.#startPromise=operation;
|
|
778
|
+
this.#startRevision=revision;
|
|
779
|
+
try{
|
|
780
|
+
return await operation;
|
|
781
|
+
}finally{
|
|
782
|
+
linked.dispose();
|
|
783
|
+
if(this.#startPromise===operation){
|
|
784
|
+
this.#startPromise=null;
|
|
785
|
+
this.#startRevision=null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async drain({reason='manual',signal=null}={}){
|
|
791
|
+
this.#assertActive();
|
|
792
|
+
this.#assertConfigured();
|
|
793
|
+
const linked=linkedMailSignal(this.#activeLifecycleSignal(),signal);
|
|
794
|
+
try{
|
|
795
|
+
const outbox=await this.#getOutbox();
|
|
796
|
+
const summary=await outbox.drain({reason,signal:linked.signal});
|
|
797
|
+
this.#publish('mail-outbox-drain',safeDrainDetail(summary));
|
|
798
|
+
return summary;
|
|
799
|
+
}finally{
|
|
800
|
+
linked.dispose();
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async listOutbox(){
|
|
805
|
+
return (await this.#getOutbox()).list();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async auditOutbox(){
|
|
809
|
+
return (await this.#getOutbox()).audit();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async getOutboxRecord(reportKey){
|
|
813
|
+
return (await this.#getOutbox()).get(reportKey);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async deleteInvalidOutbox(fileName){
|
|
817
|
+
return (await this.#getOutbox()).deleteInvalid(fileName);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async repairInvalidOutbox(fileName,replacement){
|
|
821
|
+
return (await this.#getOutbox()).repairInvalid(fileName,replacement);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async quarantineInvalidOutbox(options){
|
|
825
|
+
return (await this.#getOutbox()).quarantineInvalid(options);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
stop(){
|
|
829
|
+
if(this.#disposed) return this;
|
|
830
|
+
this.#lifecycleRevision+=1;
|
|
831
|
+
this.#lifecycleController.abort(
|
|
832
|
+
codedError('Mail lifecycle was stopped.','MAIL_LIFECYCLE_STOPPED')
|
|
833
|
+
);
|
|
834
|
+
this.#backgroundController?.abort(
|
|
835
|
+
codedError('Mail background drain was stopped.','MAIL_BACKGROUND_DRAIN_STOPPED')
|
|
836
|
+
);
|
|
837
|
+
this.#outbox?.stop();
|
|
838
|
+
if(this.#started){
|
|
839
|
+
this.#onlineTarget?.removeEventListener('online',this.#onlineHandler);
|
|
840
|
+
this.#started=false;
|
|
841
|
+
this.#startSummary=null;
|
|
842
|
+
this.#publishState({lifecycle:'stopped',started:false});
|
|
843
|
+
}
|
|
844
|
+
return this;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
dispose(){
|
|
848
|
+
if(this.#disposed) return false;
|
|
849
|
+
this.stop();
|
|
850
|
+
this.#disposed=true;
|
|
851
|
+
this.#events.dispose();
|
|
852
|
+
try{
|
|
853
|
+
if(globalThis.window?.mail===this) globalThis.window.mail=null;
|
|
854
|
+
}catch{}
|
|
855
|
+
return true;
|
|
218
856
|
}
|
|
219
857
|
|
|
220
858
|
async send(to=[], subject='', payload={}, messageStyle='', messageType='') {
|
|
859
|
+
this.#assertActive();
|
|
221
860
|
const normalizedSubject=typeof subject==='string' ? subject.trim():'';
|
|
222
861
|
if(!normalizedSubject||normalizedSubject.length>MAX_SUBJECT_LENGTH||/[\u0000-\u001f\u007f]/.test(normalizedSubject)){
|
|
223
862
|
throw new TypeError(`Mail subject must contain 1-${MAX_SUBJECT_LENGTH} characters without line breaks`);
|
|
@@ -236,17 +875,24 @@ class Mail {
|
|
|
236
875
|
}
|
|
237
876
|
|
|
238
877
|
this.#assertConfigured();
|
|
239
|
-
|
|
240
878
|
const recipients=normalizeRecipients(to);
|
|
241
879
|
if(messageType!=='error'&&recipients.length===0){
|
|
242
880
|
throw new TypeError('Report and crisis mail require at least one recipient');
|
|
243
881
|
}
|
|
244
882
|
|
|
245
|
-
|
|
883
|
+
await this.start();
|
|
884
|
+
this.#assertActive();
|
|
885
|
+
const signal=this.#lifecycleController.signal;
|
|
886
|
+
throwIfMailAborted(signal);
|
|
887
|
+
const outbox=await this.#getOutbox();
|
|
888
|
+
const timestamp=this.#time();
|
|
889
|
+
const reportKey=createReportKey(timestamp,this.#cryptoProvider);
|
|
246
890
|
const reportPayload={
|
|
247
891
|
...clonePayload(payload),
|
|
248
|
-
source_at:new Date().toISOString(),
|
|
249
|
-
|
|
892
|
+
source_at:new Date(timestamp).toISOString(),
|
|
893
|
+
...(this.#includeContext
|
|
894
|
+
?{source_path:globalThis.location?.pathname||''}
|
|
895
|
+
:{}),
|
|
250
896
|
subject:normalizedSubject,
|
|
251
897
|
type:messageType,
|
|
252
898
|
};
|
|
@@ -259,38 +905,25 @@ class Mail {
|
|
|
259
905
|
type:messageType,
|
|
260
906
|
};
|
|
261
907
|
assertMessageSize(reportPayload.report);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
let reportStorage=null;
|
|
268
|
-
let profile={ email:'',language:'',phone:'',username:'' };
|
|
269
|
-
try{
|
|
270
|
-
const dependencies=await loadOptionalMailDependencies();
|
|
271
|
-
reportStorage=dependencies.dbopfs;
|
|
272
|
-
try{
|
|
273
|
-
await dependencies.user.load();
|
|
274
|
-
}catch(error){
|
|
275
|
-
console.warn('Unable to load the user profile for mail; continuing with available values.',error);
|
|
276
|
-
}
|
|
277
|
-
profile={
|
|
278
|
-
email:dependencies.user.email,
|
|
279
|
-
language:dependencies.user.language,
|
|
280
|
-
phone:dependencies.user.phone,
|
|
281
|
-
username:dependencies.user.username,
|
|
282
|
-
};
|
|
283
|
-
}catch(error){
|
|
284
|
-
console.warn('Optional mail formatting dependencies are unavailable; using a deterministic fallback.',error);
|
|
908
|
+
const record=await outbox.enqueue(
|
|
909
|
+
{report:reportPayload.report,reportKey},
|
|
910
|
+
{signal}
|
|
911
|
+
);
|
|
912
|
+
return publicSendResult(record);
|
|
285
913
|
}
|
|
286
914
|
|
|
915
|
+
const profile=this.#includeContext
|
|
916
|
+
?await this.#optionalProfile()
|
|
917
|
+
:{email:'',language:'',phone:'',username:''};
|
|
287
918
|
const wantsHtml=/\bhtml\b/i.test(messageStyle);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
919
|
+
if(this.#includeContext){
|
|
920
|
+
Object.assign(reportPayload,{
|
|
921
|
+
email:profile.email,
|
|
922
|
+
language:profile.language,
|
|
923
|
+
phone:profile.phone,
|
|
924
|
+
source_user:profile.username || 'username not specified',
|
|
925
|
+
});
|
|
926
|
+
}
|
|
294
927
|
|
|
295
928
|
// Email formatting stays local and deterministic. The supplied style
|
|
296
929
|
// is presentation intent only; it is never sent to an AI provider and
|
|
@@ -300,7 +933,7 @@ class Mail {
|
|
|
300
933
|
? `<pre>${escapeHtml(serialized)}</pre>`
|
|
301
934
|
: serialized;
|
|
302
935
|
|
|
303
|
-
const sourceUser=String(reportPayload.source_user)
|
|
936
|
+
const sourceUser=String(reportPayload.source_user||'')
|
|
304
937
|
.replace(/[\u0000-\u001f\u007f]/g,' ')
|
|
305
938
|
.trim()
|
|
306
939
|
.slice(0,80);
|
|
@@ -315,38 +948,28 @@ class Mail {
|
|
|
315
948
|
type:messageType,
|
|
316
949
|
};
|
|
317
950
|
if(wantsHtml){
|
|
318
|
-
reportPayload.report.html
|
|
951
|
+
reportPayload.report.html=this.#includeContext?`${generatedMessage}
|
|
319
952
|
<hr>
|
|
320
|
-
<p>Phone: ${escapeHtml(profile.phone || 'not provided')}<br>Email: ${escapeHtml(profile.email || 'not provided')}</p
|
|
953
|
+
<p>Phone: ${escapeHtml(profile.phone || 'not provided')}<br>Email: ${escapeHtml(profile.email || 'not provided')}</p>`
|
|
954
|
+
:generatedMessage;
|
|
321
955
|
}else{
|
|
322
|
-
reportPayload.report.text
|
|
956
|
+
reportPayload.report.text=this.#includeContext?`${generatedMessage}
|
|
323
957
|
|
|
324
958
|
Phone: ${profile.phone || 'not provided'}
|
|
325
|
-
Email: ${profile.email || 'not provided'}
|
|
959
|
+
Email: ${profile.email || 'not provided'}`
|
|
960
|
+
:generatedMessage;
|
|
326
961
|
}
|
|
327
962
|
|
|
328
963
|
assertMessageSize(reportPayload.report);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}catch(error){
|
|
336
|
-
console.warn('Unable to store the generated mail report; continuing with delivery.',error);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const delivery=await this.#deliver(reportPayload.report,reportKey);
|
|
340
|
-
|
|
341
|
-
return {
|
|
342
|
-
...delivery,
|
|
343
|
-
reportKey,
|
|
344
|
-
};
|
|
964
|
+
throwIfMailAborted(signal);
|
|
965
|
+
const record=await outbox.enqueue(
|
|
966
|
+
{report:reportPayload.report,reportKey},
|
|
967
|
+
{signal}
|
|
968
|
+
);
|
|
969
|
+
return publicSendResult(record);
|
|
345
970
|
}
|
|
346
971
|
}
|
|
347
972
|
|
|
348
|
-
if(globalThis.window
|
|
349
|
-
globalThis.window.mail=new Mail();
|
|
350
|
-
}
|
|
973
|
+
if(globalThis.window) new Mail();
|
|
351
974
|
|
|
352
975
|
export default Mail;
|