arcane-os 0.15.2 → 0.16.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 +22 -0
- package/README.md +9 -1
- package/docs/reference/cli.md +44 -30
- package/docs/reference/mail.md +166 -67
- package/docs/reviews/mail-server-purpose-review.md +350 -0
- package/package.json +1 -1
- package/runtime/arcane/modules/Mail.js +42 -71
- package/runtime/arcane/modules/MailOutbox.mjs +5 -5
- package/runtime/arcane/modules/MailTransport.mjs +12 -21
- package/src/cli/main.mjs +14 -46
- package/src/mail-server.mjs +333 -617
- package/src/mail.mjs +50 -110
package/src/mail.mjs
CHANGED
|
@@ -20,65 +20,38 @@ export const MAIL_COMMAND_ACTIONS=[
|
|
|
20
20
|
|
|
21
21
|
const ACTION_SET=new Set(MAIL_COMMAND_ACTIONS);
|
|
22
22
|
|
|
23
|
-
function
|
|
23
|
+
function throwMailUsageError(message){
|
|
24
24
|
throw new ArcaneError(ERROR_CODES.usage,message);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function
|
|
27
|
+
function validateMailCommandOptions(options){
|
|
28
28
|
if(!options||!is.object(options)||is.array(options)){
|
|
29
|
-
|
|
29
|
+
throwMailUsageError('Mail command options must be an object.');
|
|
30
30
|
}
|
|
31
31
|
if(!is.string(options.action)||!ACTION_SET.has(options.action)){
|
|
32
|
-
|
|
32
|
+
throwMailUsageError(`Mail action must be one of: ${MAIL_COMMAND_ACTIONS.join(', ')}.`);
|
|
33
33
|
}
|
|
34
34
|
return options;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function
|
|
37
|
+
function resolveMailCommandDependency(options,name,fallback){
|
|
38
38
|
const value=options[name]??fallback;
|
|
39
39
|
if(!is.function(value)){
|
|
40
|
-
|
|
40
|
+
throwMailUsageError(`${name} must be a function when supplied.`);
|
|
41
41
|
}
|
|
42
42
|
return value;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function
|
|
46
|
-
if(value===undefined||value===null||value===''
|
|
45
|
+
function mailRecipientOptions(value,label){
|
|
46
|
+
if(value===undefined||value===null||value===''){
|
|
47
47
|
return [];
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
? value.split(',')
|
|
53
|
-
: null;
|
|
54
|
-
if(!entries||entries.length===0){
|
|
55
|
-
usage(`${label} must contain at least one email address when supplied.`);
|
|
56
|
-
}
|
|
57
|
-
const result=entries.map(function normalizeMailRecipient(entry){
|
|
58
|
-
if(!is.string(entry)||!entry.trim()){
|
|
59
|
-
usage(`${label} contains an empty email address.`);
|
|
60
|
-
}
|
|
61
|
-
return entry.trim();
|
|
62
|
-
});
|
|
63
|
-
if(new Set(result.map(function lowercaseRecipient(entry){
|
|
64
|
-
return entry.toLowerCase();
|
|
65
|
-
})).size!==result.length){
|
|
66
|
-
usage(`${label} must not contain duplicate email addresses.`);
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function originList(value){
|
|
72
|
-
const entries=is.array(value)?value:[value];
|
|
73
|
-
if(entries.length===0||entries.some(function invalidOrigin(entry){
|
|
74
|
-
return !is.string(entry)||!entry.trim();
|
|
75
|
-
})){
|
|
76
|
-
usage('Mail serve requires at least one exact allowed origin.');
|
|
77
|
-
}
|
|
78
|
-
return entries.map(function normalizeAllowedOrigin(entry){return entry.trim();});
|
|
49
|
+
if(is.array(value))return [...value];
|
|
50
|
+
if(is.string(value))return value.split(',');
|
|
51
|
+
throwMailUsageError(`${label} must be an address array or comma-separated string.`);
|
|
79
52
|
}
|
|
80
53
|
|
|
81
|
-
function
|
|
54
|
+
function mailCredentialOptions(options){
|
|
82
55
|
return {
|
|
83
56
|
profile:options.profile,
|
|
84
57
|
platform:options.platform,
|
|
@@ -91,60 +64,40 @@ function credentialOptions(options){
|
|
|
91
64
|
};
|
|
92
65
|
}
|
|
93
66
|
|
|
94
|
-
async function
|
|
95
|
-
const readSecret=
|
|
96
|
-
const store=
|
|
67
|
+
async function setMailCredentialFromInput(options){
|
|
68
|
+
const readSecret=resolveMailCommandDependency(options,'readSecret',null);
|
|
69
|
+
const store=resolveMailCommandDependency(options,'setCredential',setMailCredential);
|
|
97
70
|
throwIfAborted(options.signal);
|
|
98
71
|
let secret='';
|
|
99
72
|
try{
|
|
100
73
|
secret=await readSecret();
|
|
101
74
|
throwIfAborted(options.signal);
|
|
102
|
-
return await store({...
|
|
75
|
+
return await store({...mailCredentialOptions(options),secret});
|
|
103
76
|
}finally{
|
|
104
77
|
secret='';
|
|
105
78
|
}
|
|
106
79
|
}
|
|
107
80
|
|
|
108
|
-
async function
|
|
109
|
-
const status=
|
|
81
|
+
async function readMailCredentialStatus(options){
|
|
82
|
+
const status=resolveMailCommandDependency(options,'getCredentialStatus',getMailCredentialStatus);
|
|
110
83
|
throwIfAborted(options.signal);
|
|
111
|
-
return status(
|
|
84
|
+
return status(mailCredentialOptions(options));
|
|
112
85
|
}
|
|
113
86
|
|
|
114
|
-
async function
|
|
115
|
-
const remove=
|
|
87
|
+
async function deleteMailCredentialProfile(options){
|
|
88
|
+
const remove=resolveMailCommandDependency(options,'deleteCredential',deleteMailCredential);
|
|
116
89
|
throwIfAborted(options.signal);
|
|
117
|
-
return remove(
|
|
90
|
+
return remove(mailCredentialOptions(options));
|
|
118
91
|
}
|
|
119
92
|
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
seen.set(value,copy);
|
|
125
|
-
for(const [key,entry] of Object.entries(value)){
|
|
126
|
-
if(key==='apiKey'||key==='appKey')continue;
|
|
127
|
-
copy[key]=withoutMailCredentials(entry,seen);
|
|
128
|
-
}
|
|
129
|
-
return copy;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function completeSendFailure(result){
|
|
133
|
-
if(!result||!is.object(result)||is.array(result)){
|
|
134
|
-
return {provider:'resend',result:withoutMailCredentials(result)};
|
|
135
|
-
}
|
|
136
|
-
// Complete provider detail remains visible; only credential fields are omitted.
|
|
137
|
-
return withoutMailCredentials(result);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function sendMail(options){
|
|
141
|
-
const readReport=dependency(options,'readReport',null);
|
|
142
|
-
const readCredential=dependency(options,'readCredential',readMailCredential);
|
|
143
|
-
const send=dependency(options,'sendMail',sendResendMail);
|
|
93
|
+
async function sendMailFromReport(options){
|
|
94
|
+
const readReport=resolveMailCommandDependency(options,'readReport',null);
|
|
95
|
+
const readCredential=resolveMailCommandDependency(options,'readCredential',readMailCredential);
|
|
96
|
+
const send=resolveMailCommandDependency(options,'sendMail',sendResendMail);
|
|
144
97
|
throwIfAborted(options.signal);
|
|
145
98
|
const report=await readReport();
|
|
146
99
|
throwIfAborted(options.signal);
|
|
147
|
-
let apiKey=await readCredential(
|
|
100
|
+
let apiKey=await readCredential(mailCredentialOptions(options));
|
|
148
101
|
if(apiKey===null){
|
|
149
102
|
throw new ArcaneError(
|
|
150
103
|
ERROR_CODES.prerequisiteMissing,
|
|
@@ -173,9 +126,11 @@ async function sendMail(options){
|
|
|
173
126
|
signal:options.signal
|
|
174
127
|
});
|
|
175
128
|
if(result?.classification==='accepted'&&result.status==='accepted'){
|
|
176
|
-
return
|
|
129
|
+
return result;
|
|
177
130
|
}
|
|
178
|
-
const details=
|
|
131
|
+
const details=result&&is.object(result)&&!is.array(result)
|
|
132
|
+
?result
|
|
133
|
+
:{provider:'resend',result};
|
|
179
134
|
throw new ArcaneError(
|
|
180
135
|
ERROR_CODES.operationFailed,
|
|
181
136
|
`Resend did not authoritatively accept the mail request (${details.classification||'unknown'}).`,
|
|
@@ -186,12 +141,11 @@ async function sendMail(options){
|
|
|
186
141
|
}
|
|
187
142
|
}
|
|
188
143
|
|
|
189
|
-
async function
|
|
190
|
-
const readCredential=
|
|
191
|
-
const
|
|
192
|
-
const startServer=dependency(options,'startServer',startResendMailServer);
|
|
144
|
+
async function serveMailGateway(options){
|
|
145
|
+
const readCredential=resolveMailCommandDependency(options,'readCredential',readMailCredential);
|
|
146
|
+
const startServer=resolveMailCommandDependency(options,'startServer',startResendMailServer);
|
|
193
147
|
throwIfAborted(options.signal);
|
|
194
|
-
let apiKey=await readCredential(
|
|
148
|
+
let apiKey=await readCredential(mailCredentialOptions(options));
|
|
195
149
|
if(apiKey===null){
|
|
196
150
|
throw new ArcaneError(
|
|
197
151
|
ERROR_CODES.prerequisiteMissing,
|
|
@@ -204,60 +158,46 @@ async function serveMail(options){
|
|
|
204
158
|
'The configured Resend credential could not be read.'
|
|
205
159
|
);
|
|
206
160
|
}
|
|
207
|
-
let appKey='';
|
|
208
161
|
try{
|
|
209
162
|
throwIfAborted(options.signal);
|
|
210
|
-
|
|
211
|
-
if(!is.string(appKey)||!appKey){
|
|
212
|
-
throw new ArcaneError(
|
|
213
|
-
ERROR_CODES.operationFailed,
|
|
214
|
-
'The local Mail gateway app key could not be read.'
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
throwIfAborted(options.signal);
|
|
218
|
-
const recipientAllowlist=recipientList(options.allowTo,'allowTo');
|
|
163
|
+
const recipientAllowlist=mailRecipientOptions(options.allowTo,'allowTo');
|
|
219
164
|
const errorRecipients=options.errorTo===undefined
|
|
220
165
|
? recipientAllowlist
|
|
221
|
-
:
|
|
166
|
+
: mailRecipientOptions(options.errorTo,'errorTo');
|
|
222
167
|
return await startServer({
|
|
223
168
|
apiKey,
|
|
224
|
-
appKey,
|
|
225
169
|
appId:options.appId,
|
|
226
|
-
allowedOrigins:
|
|
227
|
-
|
|
170
|
+
allowedOrigins:options.origin===undefined
|
|
171
|
+
?[]
|
|
172
|
+
:is.array(options.origin)?[...options.origin]:[options.origin],
|
|
228
173
|
bodyTimeoutMs:options.bodyTimeoutMs,
|
|
229
174
|
errorRecipients,
|
|
230
175
|
fetchImpl:options.fetchImpl,
|
|
231
176
|
from:options.from,
|
|
232
|
-
host:options.host,
|
|
233
|
-
maxConcurrentBodyReads:options.maxConcurrentBodyReads,
|
|
234
|
-
maxConcurrentSends:options.maxConcurrentSends,
|
|
235
|
-
maxQueuedBodyReads:options.maxQueuedBodyReads,
|
|
236
|
-
maxQueuedSends:options.maxQueuedSends,
|
|
177
|
+
host:options.host??'0.0.0.0',
|
|
237
178
|
onEvent:options.onEvent,
|
|
238
179
|
port:options.port,
|
|
239
180
|
providerTimeoutMs:options.requestTimeout,
|
|
240
181
|
recipientAllowlist,
|
|
241
182
|
requestIdFactory:options.requestIdFactory,
|
|
242
183
|
retryableDelayMs:options.retryableDelayMs,
|
|
243
|
-
|
|
244
|
-
|
|
184
|
+
signal:options.signal,
|
|
185
|
+
verifySubscription:options.verifySubscription
|
|
245
186
|
});
|
|
246
187
|
}finally{
|
|
247
188
|
apiKey='';
|
|
248
|
-
appKey='';
|
|
249
189
|
}
|
|
250
190
|
}
|
|
251
191
|
|
|
252
192
|
export async function executeMailCommand(rawOptions={}){
|
|
253
|
-
const options=
|
|
193
|
+
const options=validateMailCommandOptions(rawOptions);
|
|
254
194
|
switch(options.action){
|
|
255
|
-
case 'key-set':return
|
|
256
|
-
case 'key-status':return
|
|
257
|
-
case 'key-delete':return
|
|
258
|
-
case 'send':return
|
|
259
|
-
case 'serve':return
|
|
260
|
-
default:
|
|
195
|
+
case 'key-set':return setMailCredentialFromInput(options);
|
|
196
|
+
case 'key-status':return readMailCredentialStatus(options);
|
|
197
|
+
case 'key-delete':return deleteMailCredentialProfile(options);
|
|
198
|
+
case 'send':return sendMailFromReport(options);
|
|
199
|
+
case 'serve':return serveMailGateway(options);
|
|
200
|
+
default:throwMailUsageError('Unsupported Mail action.');
|
|
261
201
|
}
|
|
262
202
|
}
|
|
263
203
|
|