arcane-os 0.18.0 → 0.20.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 +34 -0
- package/README.md +1 -1
- package/docs/architecture.md +18 -0
- package/docs/reference/cli.md +60 -33
- package/docs/reference/mail.md +65 -15
- package/docs/reference/pwa.md +16 -2
- package/docs/reference/sdk-api.md +36 -33
- package/docs/reviews/mail-server-purpose-review.md +48 -6
- package/package.json +1 -1
- package/src/cli/main.mjs +16 -17
- package/src/dev-server.mjs +10 -6
- package/src/mail-credentials.mjs +101 -632
- package/src/mail.mjs +17 -26
- package/src/packager/core.mjs +71 -26
- package/src/pwa.mjs +21 -18
- package/src/templates/workspace-template.mjs +1 -0
package/src/mail-credentials.mjs
CHANGED
|
@@ -1,670 +1,139 @@
|
|
|
1
1
|
import Is from 'strong-type';
|
|
2
|
-
import {
|
|
2
|
+
import {readFile,writeFile} from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {ArcaneError,ERROR_CODES} from './errors.mjs';
|
|
4
|
+
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
6
5
|
|
|
7
6
|
const is = new Is(false);
|
|
8
7
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const MAX_CREDENTIAL_BYTES=2_560;
|
|
14
|
-
const MAX_HELPER_INPUT_BYTES=8_192;
|
|
15
|
-
const MAX_HELPER_OUTPUT_BYTES=8_192;
|
|
16
|
-
const DEFAULT_HELPER_TIMEOUT_MS=15_000;
|
|
17
|
-
const MAX_HELPER_TIMEOUT_MS=60_000;
|
|
18
|
-
const PROFILE_PATTERN=/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
|
|
19
|
-
const SECRET_PATTERN=/^[\x21-\x7e]+$/u;
|
|
20
|
-
const BASE64_PATTERN=/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
|
|
21
|
-
|
|
22
|
-
const WINDOWS_CREDENTIAL_HELPER=String.raw`
|
|
23
|
-
$ErrorActionPreference='Stop'
|
|
24
|
-
$ProgressPreference='SilentlyContinue'
|
|
25
|
-
$VerbosePreference='SilentlyContinue'
|
|
26
|
-
$WarningPreference='SilentlyContinue'
|
|
27
|
-
Set-StrictMode -Version 3.0
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
Add-Type -Language CSharp -TypeDefinition @'
|
|
31
|
-
using System;
|
|
32
|
-
using System.ComponentModel;
|
|
33
|
-
using System.Runtime.InteropServices;
|
|
34
|
-
|
|
35
|
-
public static class ArcaneResendCredentialNative
|
|
36
|
-
{
|
|
37
|
-
private const UInt32 GenericCredential = 1;
|
|
38
|
-
private const UInt32 PersistLocalMachine = 2;
|
|
39
|
-
private const Int32 NotFound = 1168;
|
|
40
|
-
private const Int32 MaximumBlobBytes = 2560;
|
|
41
|
-
|
|
42
|
-
[StructLayout(LayoutKind.Sequential)]
|
|
43
|
-
private struct NativeFileTime
|
|
44
|
-
{
|
|
45
|
-
public UInt32 Low;
|
|
46
|
-
public UInt32 High;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
50
|
-
private struct NativeCredential
|
|
51
|
-
{
|
|
52
|
-
public UInt32 Flags;
|
|
53
|
-
public UInt32 Type;
|
|
54
|
-
[MarshalAs(UnmanagedType.LPWStr)] public String TargetName;
|
|
55
|
-
[MarshalAs(UnmanagedType.LPWStr)] public String Comment;
|
|
56
|
-
public NativeFileTime LastWritten;
|
|
57
|
-
public UInt32 CredentialBlobSize;
|
|
58
|
-
public IntPtr CredentialBlob;
|
|
59
|
-
public UInt32 Persist;
|
|
60
|
-
public UInt32 AttributeCount;
|
|
61
|
-
public IntPtr Attributes;
|
|
62
|
-
[MarshalAs(UnmanagedType.LPWStr)] public String TargetAlias;
|
|
63
|
-
[MarshalAs(UnmanagedType.LPWStr)] public String UserName;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode,
|
|
67
|
-
ExactSpelling = true, SetLastError = true)]
|
|
68
|
-
[return: MarshalAs(UnmanagedType.Bool)]
|
|
69
|
-
private static extern Boolean CredWrite(ref NativeCredential credential, UInt32 flags);
|
|
70
|
-
|
|
71
|
-
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode,
|
|
72
|
-
ExactSpelling = true, SetLastError = true)]
|
|
73
|
-
[return: MarshalAs(UnmanagedType.Bool)]
|
|
74
|
-
private static extern Boolean CredRead(String target, UInt32 type, UInt32 flags,
|
|
75
|
-
out IntPtr credential);
|
|
76
|
-
|
|
77
|
-
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode,
|
|
78
|
-
ExactSpelling = true, SetLastError = true)]
|
|
79
|
-
[return: MarshalAs(UnmanagedType.Bool)]
|
|
80
|
-
private static extern Boolean CredDelete(String target, UInt32 type, UInt32 flags);
|
|
81
|
-
|
|
82
|
-
[DllImport("Advapi32.dll", EntryPoint = "CredFree", ExactSpelling = true,
|
|
83
|
-
SetLastError = false)]
|
|
84
|
-
private static extern void CredFree(IntPtr credential);
|
|
85
|
-
|
|
86
|
-
public static void Write(String target, Byte[] value)
|
|
87
|
-
{
|
|
88
|
-
if (value == null || value.Length == 0 || value.Length > MaximumBlobBytes)
|
|
89
|
-
throw new ArgumentException("Invalid credential value.");
|
|
90
|
-
|
|
91
|
-
GCHandle pinned = default(GCHandle);
|
|
92
|
-
try
|
|
93
|
-
{
|
|
94
|
-
pinned = GCHandle.Alloc(value, GCHandleType.Pinned);
|
|
95
|
-
NativeCredential credential = new NativeCredential();
|
|
96
|
-
credential.Type = GenericCredential;
|
|
97
|
-
credential.TargetName = target;
|
|
98
|
-
credential.CredentialBlobSize = checked((UInt32)value.Length);
|
|
99
|
-
credential.CredentialBlob = pinned.AddrOfPinnedObject();
|
|
100
|
-
credential.Persist = PersistLocalMachine;
|
|
101
|
-
credential.UserName = "Arcane OS SDK";
|
|
102
|
-
if (!CredWrite(ref credential, 0))
|
|
103
|
-
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
104
|
-
}
|
|
105
|
-
finally
|
|
106
|
-
{
|
|
107
|
-
Array.Clear(value, 0, value.Length);
|
|
108
|
-
if (pinned.IsAllocated) pinned.Free();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
public static Byte[] Read(String target)
|
|
113
|
-
{
|
|
114
|
-
IntPtr pointer;
|
|
115
|
-
if (!CredRead(target, GenericCredential, 0, out pointer))
|
|
116
|
-
{
|
|
117
|
-
Int32 error = Marshal.GetLastWin32Error();
|
|
118
|
-
if (error == NotFound) return null;
|
|
119
|
-
throw new Win32Exception(error);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try
|
|
123
|
-
{
|
|
124
|
-
NativeCredential credential =
|
|
125
|
-
(NativeCredential)Marshal.PtrToStructure(pointer, typeof(NativeCredential));
|
|
126
|
-
if (credential.CredentialBlobSize == 0 ||
|
|
127
|
-
credential.CredentialBlobSize > MaximumBlobBytes)
|
|
128
|
-
throw new InvalidOperationException("Invalid credential size.");
|
|
129
|
-
Byte[] value = new Byte[credential.CredentialBlobSize];
|
|
130
|
-
Marshal.Copy(credential.CredentialBlob, value, 0, value.Length);
|
|
131
|
-
return value;
|
|
132
|
-
}
|
|
133
|
-
finally
|
|
134
|
-
{
|
|
135
|
-
CredFree(pointer);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
public static Boolean Exists(String target)
|
|
140
|
-
{
|
|
141
|
-
IntPtr pointer;
|
|
142
|
-
if (!CredRead(target, GenericCredential, 0, out pointer))
|
|
143
|
-
{
|
|
144
|
-
Int32 error = Marshal.GetLastWin32Error();
|
|
145
|
-
if (error == NotFound) return false;
|
|
146
|
-
throw new Win32Exception(error);
|
|
147
|
-
}
|
|
148
|
-
CredFree(pointer);
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
public static Boolean Delete(String target)
|
|
153
|
-
{
|
|
154
|
-
if (CredDelete(target, GenericCredential, 0)) return true;
|
|
155
|
-
Int32 error = Marshal.GetLastWin32Error();
|
|
156
|
-
if (error == NotFound) return false;
|
|
157
|
-
throw new Win32Exception(error);
|
|
8
|
+
export function mailCredentialLocation(options={}){
|
|
9
|
+
const profile=options.profile??'mail';
|
|
10
|
+
if(!is.string(profile)||!profile){
|
|
11
|
+
throw new ArcaneError(ERROR_CODES.usage,'Mail credential profile must be a nonempty string.');
|
|
158
12
|
}
|
|
13
|
+
return {
|
|
14
|
+
filePath:path.resolve(options.cwd??options.workspaceRoot??process.cwd(),'.env.json'),
|
|
15
|
+
profile,
|
|
16
|
+
setting:profile==='mail'?'RESEND_API_KEY':`MAIL_PROFILES[${JSON.stringify(profile)}].RESEND_API_KEY`
|
|
17
|
+
};
|
|
159
18
|
}
|
|
160
|
-
'@
|
|
161
|
-
|
|
162
|
-
$inputText=[Console]::In.ReadToEnd()
|
|
163
|
-
if ([Text.Encoding]::UTF8.GetByteCount($inputText) -gt 8192) {
|
|
164
|
-
throw 'Invalid helper input.'
|
|
165
|
-
}
|
|
166
|
-
$request=$inputText | ConvertFrom-Json
|
|
167
|
-
$operation=[String]$request.operation
|
|
168
|
-
$target=[String]$request.target
|
|
169
|
-
if ($target -notmatch '^ArcaneOSSDK/mail/resend/[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$') {
|
|
170
|
-
throw 'Invalid credential target.'
|
|
171
|
-
}
|
|
172
19
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
throw 'Invalid credential value.'
|
|
182
|
-
}
|
|
183
|
-
[ArcaneResendCredentialNative]::Write($target,$credentialBytes)
|
|
184
|
-
[Console]::Out.Write('{"ok":true,"configured":true}')
|
|
185
|
-
}
|
|
186
|
-
finally {
|
|
187
|
-
if ($null -ne $credentialBytes) {
|
|
188
|
-
[Array]::Clear($credentialBytes,0,$credentialBytes.Length)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
break
|
|
192
|
-
}
|
|
193
|
-
'read' {
|
|
194
|
-
$credentialBytes=$null
|
|
195
|
-
try {
|
|
196
|
-
$credentialBytes=[ArcaneResendCredentialNative]::Read($target)
|
|
197
|
-
if ($null -eq $credentialBytes) {
|
|
198
|
-
[Console]::Out.Write('{"ok":true,"found":false}')
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
$encoded=[Convert]::ToBase64String($credentialBytes)
|
|
202
|
-
$response=@{ok=$true;found=$true;secret=$encoded} | ConvertTo-Json -Compress
|
|
203
|
-
[Console]::Out.Write($response)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
finally {
|
|
207
|
-
if ($null -ne $credentialBytes) {
|
|
208
|
-
[Array]::Clear($credentialBytes,0,$credentialBytes.Length)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
break
|
|
212
|
-
}
|
|
213
|
-
'status' {
|
|
214
|
-
$configured=[ArcaneResendCredentialNative]::Exists($target)
|
|
215
|
-
$response=@{ok=$true;configured=$configured} | ConvertTo-Json -Compress
|
|
216
|
-
[Console]::Out.Write($response)
|
|
217
|
-
break
|
|
218
|
-
}
|
|
219
|
-
'delete' {
|
|
220
|
-
$deleted=[ArcaneResendCredentialNative]::Delete($target)
|
|
221
|
-
$response=@{ok=$true;deleted=$deleted;configured=$false} | ConvertTo-Json -Compress
|
|
222
|
-
[Console]::Out.Write($response)
|
|
223
|
-
break
|
|
20
|
+
async function readMailSettings(filePath,signal){
|
|
21
|
+
throwIfAborted(signal);
|
|
22
|
+
let content;
|
|
23
|
+
try{
|
|
24
|
+
content=await readFile(filePath,{encoding:'utf8',signal});
|
|
25
|
+
}catch(error){
|
|
26
|
+
if(error.code==='ENOENT'){
|
|
27
|
+
return {};
|
|
224
28
|
}
|
|
225
|
-
|
|
29
|
+
throw error;
|
|
226
30
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const WINDOWS_CREDENTIAL_HELPER_COMMAND=Buffer.from(
|
|
235
|
-
WINDOWS_CREDENTIAL_HELPER,
|
|
236
|
-
'utf16le'
|
|
237
|
-
).toString('base64');
|
|
238
|
-
|
|
239
|
-
function usageError(message){
|
|
240
|
-
return new ArcaneError(ERROR_CODES.usage,message);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function unavailableError(){
|
|
244
|
-
return new ArcaneError(
|
|
245
|
-
ERROR_CODES.targetUnavailable,
|
|
246
|
-
'Resend credential storage requires Windows Credential Manager.'
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function operationError(){
|
|
251
|
-
return new ArcaneError(
|
|
252
|
-
ERROR_CODES.operationFailed,
|
|
253
|
-
'Windows Credential Manager could not complete the Resend credential operation.'
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function cancellationError(){
|
|
258
|
-
return new ArcaneError(
|
|
259
|
-
ERROR_CODES.cancelled,
|
|
260
|
-
'The Resend credential operation was cancelled.',
|
|
261
|
-
{exitCode:130}
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function assertNotAborted(signal){
|
|
266
|
-
if(signal?.aborted)throw cancellationError();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export function validateMailCredentialProfile(profile){
|
|
270
|
-
if(!is.string(profile)||profile.length>MAX_PROFILE_LENGTH
|
|
271
|
-
||!PROFILE_PATTERN.test(profile)){
|
|
272
|
-
throw usageError(
|
|
273
|
-
'A credential profile must be 1-64 lowercase letters, digits, dots, underscores, or hyphens, and must begin and end with a letter or digit.'
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
return profile;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function mailCredentialTarget(profile){
|
|
280
|
-
return `${RESEND_CREDENTIAL_TARGET_PREFIX}${validateMailCredentialProfile(profile)}`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function validateSecret(secret){
|
|
284
|
-
if(!is.string(secret)||!SECRET_PATTERN.test(secret)
|
|
285
|
-
||Buffer.byteLength(secret,'utf8')>MAX_CREDENTIAL_BYTES){
|
|
286
|
-
throw usageError(
|
|
287
|
-
'A Resend API key must be a nonempty printable ASCII string no larger than 2,560 bytes.'
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
return secret;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function validatePlatform(platform){
|
|
294
|
-
if(platform!=='win32')throw unavailableError();
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function validateTimeout(timeoutMs){
|
|
298
|
-
if(!is.safeInteger(timeoutMs)||timeoutMs<1||timeoutMs>MAX_HELPER_TIMEOUT_MS){
|
|
299
|
-
throw usageError(
|
|
300
|
-
`Credential helper timeout must be an integer from 1 through ${String(MAX_HELPER_TIMEOUT_MS)} milliseconds.`
|
|
301
|
-
);
|
|
31
|
+
let settings;
|
|
32
|
+
try{
|
|
33
|
+
settings=JSON.parse(content);
|
|
34
|
+
}catch{
|
|
35
|
+
// A JSON parser error can quote the credential itself.
|
|
36
|
+
throw new ArcaneError(ERROR_CODES.usage,`Unable to parse ${filePath} as JSON.`);
|
|
302
37
|
}
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function powershellExecutable(systemRoot){
|
|
307
|
-
if(!is.string(systemRoot)||systemRoot.length===0
|
|
308
|
-
||systemRoot.includes('\0')||!path.win32.isAbsolute(systemRoot)){
|
|
309
|
-
throw unavailableError();
|
|
38
|
+
if(!settings||!is.object(settings)||is.array(settings)){
|
|
39
|
+
throw new ArcaneError(ERROR_CODES.usage,`${filePath} must contain a JSON object.`);
|
|
310
40
|
}
|
|
311
|
-
return
|
|
312
|
-
systemRoot,
|
|
313
|
-
'System32',
|
|
314
|
-
'WindowsPowerShell',
|
|
315
|
-
'v1.0',
|
|
316
|
-
'powershell.exe'
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function helperArguments(){
|
|
321
|
-
return [
|
|
322
|
-
'-NoLogo',
|
|
323
|
-
'-NoProfile',
|
|
324
|
-
'-NonInteractive',
|
|
325
|
-
'-ExecutionPolicy',
|
|
326
|
-
'Bypass',
|
|
327
|
-
'-EncodedCommand',
|
|
328
|
-
WINDOWS_CREDENTIAL_HELPER_COMMAND
|
|
329
|
-
];
|
|
41
|
+
return settings;
|
|
330
42
|
}
|
|
331
43
|
|
|
332
|
-
function
|
|
333
|
-
if(
|
|
334
|
-
|
|
335
|
-
throw unavailableError();
|
|
44
|
+
function mailProfileSettings(settings,location){
|
|
45
|
+
if(location.profile==='mail'){
|
|
46
|
+
return settings;
|
|
336
47
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
WINDIR:systemRoot,
|
|
340
|
-
TEMP:temporaryDirectory,
|
|
341
|
-
TMP:temporaryDirectory
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function serializeRequest(request){
|
|
346
|
-
const value=JSON.stringify(request);
|
|
347
|
-
if(Buffer.byteLength(value,'utf8')>MAX_HELPER_INPUT_BYTES){
|
|
348
|
-
throw usageError('The Resend credential request is too large.');
|
|
48
|
+
if(settings.MAIL_PROFILES===undefined){
|
|
49
|
+
return undefined;
|
|
349
50
|
}
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function parseResponse(buffer){
|
|
354
|
-
let responseText='';
|
|
355
|
-
try{
|
|
356
|
-
responseText=buffer.toString('utf8');
|
|
357
|
-
const response=JSON.parse(responseText);
|
|
358
|
-
if(response===null||!is.object(response)||is.array(response)
|
|
359
|
-
||response.ok!==true){
|
|
360
|
-
throw operationError();
|
|
361
|
-
}
|
|
362
|
-
return response;
|
|
363
|
-
}catch{
|
|
364
|
-
throw operationError();
|
|
365
|
-
}finally{
|
|
366
|
-
responseText='';
|
|
367
|
-
buffer.fill(0);
|
|
51
|
+
if(!settings.MAIL_PROFILES||!is.object(settings.MAIL_PROFILES)||is.array(settings.MAIL_PROFILES)){
|
|
52
|
+
throw new ArcaneError(ERROR_CODES.usage,`MAIL_PROFILES in ${location.filePath} must be an object.`);
|
|
368
53
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
function runCredentialProcess({
|
|
372
|
-
executable,
|
|
373
|
-
args,
|
|
374
|
-
spawnOptions,
|
|
375
|
-
stdin,
|
|
376
|
-
spawnImpl=spawn,
|
|
377
|
-
signal,
|
|
378
|
-
timeoutMs
|
|
379
|
-
}){
|
|
380
|
-
if(!is.function(spawnImpl))throw usageError('spawnImpl must be a function.');
|
|
381
|
-
let input=stdin;
|
|
382
|
-
return new Promise(function executeCredentialHelper(resolve,reject){
|
|
383
|
-
let child;
|
|
384
|
-
let settled=false;
|
|
385
|
-
let outputBytes=0;
|
|
386
|
-
let errorBytes=0;
|
|
387
|
-
let outputChunks=[];
|
|
388
|
-
let timer=null;
|
|
389
|
-
|
|
390
|
-
function wipeOutput(){
|
|
391
|
-
for(const chunk of outputChunks)chunk.fill(0);
|
|
392
|
-
outputChunks=[];
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function removeListeners(){
|
|
396
|
-
clearTimeout(timer);
|
|
397
|
-
signal?.removeEventListener('abort',onAbort);
|
|
398
|
-
child?.removeListener('error',onChildError);
|
|
399
|
-
child?.removeListener('close',onClose);
|
|
400
|
-
child?.stdin?.removeListener('error',onStdinError);
|
|
401
|
-
child?.stdout?.removeListener('data',onStdout);
|
|
402
|
-
child?.stderr?.removeListener('data',onStderr);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function finishError(error){
|
|
406
|
-
if(settled)return;
|
|
407
|
-
settled=true;
|
|
408
|
-
removeListeners();
|
|
409
|
-
wipeOutput();
|
|
410
|
-
input='';
|
|
411
|
-
reject(error);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function terminate(error){
|
|
415
|
-
try{
|
|
416
|
-
child?.kill();
|
|
417
|
-
}catch{
|
|
418
|
-
// The stable credential error below intentionally omits process details.
|
|
419
|
-
}
|
|
420
|
-
finishError(error);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function onAbort(){
|
|
424
|
-
terminate(cancellationError());
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function onTimeout(){
|
|
428
|
-
terminate(operationError());
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function onChildError(){
|
|
432
|
-
finishError(operationError());
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function onStdinError(){
|
|
436
|
-
terminate(operationError());
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function onStdout(chunk){
|
|
440
|
-
const value=Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
|
|
441
|
-
outputBytes+=value.length;
|
|
442
|
-
if(outputBytes>MAX_HELPER_OUTPUT_BYTES){
|
|
443
|
-
value.fill(0);
|
|
444
|
-
terminate(operationError());
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
outputChunks.push(value);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function onStderr(chunk){
|
|
451
|
-
errorBytes+=Buffer.byteLength(chunk);
|
|
452
|
-
if(errorBytes>MAX_HELPER_OUTPUT_BYTES)terminate(operationError());
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function onClose(code){
|
|
456
|
-
if(settled)return;
|
|
457
|
-
if(code!==0){
|
|
458
|
-
finishError(operationError());
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
const output=Buffer.concat(outputChunks,outputBytes);
|
|
462
|
-
wipeOutput();
|
|
463
|
-
settled=true;
|
|
464
|
-
removeListeners();
|
|
465
|
-
input='';
|
|
466
|
-
resolve(output);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
try{
|
|
470
|
-
child=spawnImpl(executable,args,spawnOptions);
|
|
471
|
-
if(!child?.stdin||!child?.stdout||!child?.stderr){
|
|
472
|
-
throw operationError();
|
|
473
|
-
}
|
|
474
|
-
child.once('error',onChildError);
|
|
475
|
-
child.once('close',onClose);
|
|
476
|
-
child.stdin.once('error',onStdinError);
|
|
477
|
-
child.stdout.on('data',onStdout);
|
|
478
|
-
child.stderr.on('data',onStderr);
|
|
479
|
-
signal?.addEventListener('abort',onAbort,{once:true});
|
|
480
|
-
timer=setTimeout(onTimeout,timeoutMs);
|
|
481
|
-
child.stdin.end(input,'utf8');
|
|
482
|
-
input='';
|
|
483
|
-
}catch{
|
|
484
|
-
terminate(operationError());
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
async function runWindowsCredentialHelper(request,{
|
|
490
|
-
platform=process.platform,
|
|
491
|
-
systemRoot=process.env.SystemRoot??process.env.WINDIR,
|
|
492
|
-
temporaryDirectory=process.env.TEMP??process.env.TMP,
|
|
493
|
-
spawnImpl=spawn,
|
|
494
|
-
runner=runCredentialProcess,
|
|
495
|
-
signal,
|
|
496
|
-
timeoutMs=DEFAULT_HELPER_TIMEOUT_MS
|
|
497
|
-
}={}){
|
|
498
|
-
validatePlatform(platform);
|
|
499
|
-
assertNotAborted(signal);
|
|
500
|
-
if(!is.function(runner))throw usageError('runner must be a function.');
|
|
501
|
-
const executable=powershellExecutable(systemRoot);
|
|
502
|
-
const args=helperArguments();
|
|
503
|
-
const boundedTimeout=validateTimeout(timeoutMs);
|
|
504
|
-
let requestText=serializeRequest(request);
|
|
505
|
-
const invocation={
|
|
506
|
-
executable,
|
|
507
|
-
args,
|
|
508
|
-
spawnOptions:{
|
|
509
|
-
cwd:systemRoot,
|
|
510
|
-
env:helperEnvironment(systemRoot,temporaryDirectory),
|
|
511
|
-
shell:false,
|
|
512
|
-
windowsHide:true,
|
|
513
|
-
stdio:['pipe','pipe','pipe']
|
|
514
|
-
},
|
|
515
|
-
stdin:requestText,
|
|
516
|
-
spawnImpl,
|
|
517
|
-
signal,
|
|
518
|
-
timeoutMs:boundedTimeout
|
|
519
|
-
};
|
|
520
|
-
let output;
|
|
521
|
-
try{
|
|
522
|
-
output=await runner(invocation);
|
|
523
|
-
}catch(error){
|
|
524
|
-
if(signal?.aborted||error?.code===ERROR_CODES.cancelled
|
|
525
|
-
||error?.name==='AbortError'||error?.code==='ABORT_ERR'){
|
|
526
|
-
throw cancellationError();
|
|
527
|
-
}
|
|
528
|
-
throw operationError();
|
|
529
|
-
}finally{
|
|
530
|
-
invocation.stdin='';
|
|
531
|
-
requestText='';
|
|
54
|
+
if(!Object.hasOwn(settings.MAIL_PROFILES,location.profile)){
|
|
55
|
+
return undefined;
|
|
532
56
|
}
|
|
533
|
-
|
|
534
|
-
if(!
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
57
|
+
const profileSettings=settings.MAIL_PROFILES[location.profile];
|
|
58
|
+
if(!profileSettings||!is.object(profileSettings)||is.array(profileSettings)){
|
|
59
|
+
throw new ArcaneError(
|
|
60
|
+
ERROR_CODES.usage,
|
|
61
|
+
`MAIL_PROFILES[${JSON.stringify(location.profile)}] in ${location.filePath} must be an object.`
|
|
62
|
+
);
|
|
538
63
|
}
|
|
539
|
-
return
|
|
64
|
+
return profileSettings;
|
|
540
65
|
}
|
|
541
66
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if(error?.code===ERROR_CODES.cancelled)throw cancellationError();
|
|
547
|
-
if(error?.code===ERROR_CODES.targetUnavailable)throw unavailableError();
|
|
548
|
-
if(error?.code===ERROR_CODES.usage)throw error;
|
|
549
|
-
throw operationError();
|
|
67
|
+
function configuredMailKey(settings,location){
|
|
68
|
+
const apiKey=mailProfileSettings(settings,location)?.RESEND_API_KEY;
|
|
69
|
+
if(apiKey===undefined||apiKey===null||apiKey===''){
|
|
70
|
+
return null;
|
|
550
71
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
provider:'resend',
|
|
557
|
-
storage:CREDENTIAL_STORE,
|
|
558
|
-
exists
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function validateOptions(options){
|
|
563
|
-
if(options===null||!is.object(options)||is.array(options)){
|
|
564
|
-
throw usageError('Mail credential options must be an object.');
|
|
72
|
+
if(!is.string(apiKey)){
|
|
73
|
+
throw new ArcaneError(
|
|
74
|
+
ERROR_CODES.usage,
|
|
75
|
+
`${location.setting} in ${location.filePath} must be a string.`
|
|
76
|
+
);
|
|
565
77
|
}
|
|
566
|
-
return
|
|
78
|
+
return apiKey;
|
|
567
79
|
}
|
|
568
80
|
|
|
569
|
-
function
|
|
570
|
-
return {
|
|
571
|
-
platform:options.platform,
|
|
572
|
-
systemRoot:options.systemRoot,
|
|
573
|
-
temporaryDirectory:options.temporaryDirectory,
|
|
574
|
-
spawnImpl:options.spawnImpl,
|
|
575
|
-
runner:options.runner,
|
|
576
|
-
signal:options.signal,
|
|
577
|
-
timeoutMs:options.timeoutMs
|
|
578
|
-
};
|
|
81
|
+
function mailCredentialStatus(profile,exists){
|
|
82
|
+
return {profile,provider:'resend',storage:'.env.json',exists};
|
|
579
83
|
}
|
|
580
84
|
|
|
581
85
|
export async function setMailCredential(options={}){
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
secretBytes.fill(0);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function decodeSecret(value){
|
|
607
|
-
if(!is.string(value)||value.length===0||value.length%4!==0
|
|
608
|
-
||!BASE64_PATTERN.test(value)){
|
|
609
|
-
throw operationError();
|
|
610
|
-
}
|
|
611
|
-
const bytes=Buffer.from(value,'base64');
|
|
612
|
-
try{
|
|
613
|
-
if(bytes.length===0||bytes.length>MAX_CREDENTIAL_BYTES
|
|
614
|
-
||bytes.toString('base64')!==value){
|
|
615
|
-
throw operationError();
|
|
616
|
-
}
|
|
617
|
-
const decoded=new TextDecoder('utf-8',{fatal:true}).decode(bytes);
|
|
618
|
-
validateSecret(decoded);
|
|
619
|
-
return decoded;
|
|
620
|
-
}catch{
|
|
621
|
-
throw operationError();
|
|
622
|
-
}finally{
|
|
623
|
-
bytes.fill(0);
|
|
624
|
-
}
|
|
86
|
+
const location=mailCredentialLocation(options);
|
|
87
|
+
if(!is.string(options.secret)||!options.secret){
|
|
88
|
+
throw new ArcaneError(ERROR_CODES.usage,'The Resend API key must be a nonempty string.');
|
|
89
|
+
}
|
|
90
|
+
const settings=await readMailSettings(location.filePath,options.signal);
|
|
91
|
+
const profileSettings=mailProfileSettings(settings,location);
|
|
92
|
+
const updatedProfile={...profileSettings,RESEND_API_KEY:options.secret};
|
|
93
|
+
const updatedSettings=location.profile==='mail'
|
|
94
|
+
?updatedProfile
|
|
95
|
+
:{
|
|
96
|
+
...settings,
|
|
97
|
+
MAIL_PROFILES:{...settings.MAIL_PROFILES,[location.profile]:updatedProfile}
|
|
98
|
+
};
|
|
99
|
+
throwIfAborted(options.signal);
|
|
100
|
+
await writeFile(location.filePath,`${JSON.stringify(updatedSettings,null,2)}\n`,{
|
|
101
|
+
encoding:'utf8',
|
|
102
|
+
mode:0o600,
|
|
103
|
+
signal:options.signal
|
|
104
|
+
});
|
|
105
|
+
return mailCredentialStatus(location.profile,true);
|
|
625
106
|
}
|
|
626
107
|
|
|
627
108
|
export async function readMailCredential(options={}){
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
const response=await invokeCredentialHelper(
|
|
632
|
-
{operation:'read',target},
|
|
633
|
-
helperOptions(options)
|
|
634
|
-
);
|
|
635
|
-
if(response.found===false)return null;
|
|
636
|
-
if(response.found!==true)throw operationError();
|
|
637
|
-
let encoded=response.secret;
|
|
638
|
-
try{
|
|
639
|
-
return decodeSecret(encoded);
|
|
640
|
-
}finally{
|
|
641
|
-
response.secret=null;
|
|
642
|
-
encoded='';
|
|
643
|
-
}
|
|
109
|
+
const location=mailCredentialLocation(options);
|
|
110
|
+
const settings=await readMailSettings(location.filePath,options.signal);
|
|
111
|
+
return configuredMailKey(settings,location);
|
|
644
112
|
}
|
|
645
113
|
|
|
646
114
|
export async function getMailCredentialStatus(options={}){
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
const response=await invokeCredentialHelper(
|
|
651
|
-
{operation:'status',target},
|
|
652
|
-
helperOptions(options)
|
|
653
|
-
);
|
|
654
|
-
if(!is.boolean(response.configured))throw operationError();
|
|
655
|
-
return credentialStatus(validatedProfile,response.configured);
|
|
115
|
+
const location=mailCredentialLocation(options);
|
|
116
|
+
const settings=await readMailSettings(location.filePath,options.signal);
|
|
117
|
+
return mailCredentialStatus(location.profile,configuredMailKey(settings,location)!==null);
|
|
656
118
|
}
|
|
657
119
|
|
|
658
120
|
export async function deleteMailCredential(options={}){
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
{
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
121
|
+
const location=mailCredentialLocation(options);
|
|
122
|
+
const settings=await readMailSettings(location.filePath,options.signal);
|
|
123
|
+
const profileSettings=mailProfileSettings(settings,location);
|
|
124
|
+
if(profileSettings&&Object.hasOwn(profileSettings,'RESEND_API_KEY')){
|
|
125
|
+
const {RESEND_API_KEY,...remainingSettings}=profileSettings;
|
|
126
|
+
const updatedSettings=location.profile==='mail'
|
|
127
|
+
?remainingSettings
|
|
128
|
+
:{
|
|
129
|
+
...settings,
|
|
130
|
+
MAIL_PROFILES:{...settings.MAIL_PROFILES,[location.profile]:remainingSettings}
|
|
131
|
+
};
|
|
132
|
+
throwIfAborted(options.signal);
|
|
133
|
+
await writeFile(location.filePath,`${JSON.stringify(updatedSettings,null,2)}\n`,{
|
|
134
|
+
encoding:'utf8',
|
|
135
|
+
signal:options.signal
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return mailCredentialStatus(location.profile,false);
|
|
670
139
|
}
|