arcane-os 0.17.0 → 0.19.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.
@@ -1,670 +1,139 @@
1
1
  import Is from 'strong-type';
2
- import {spawn} from 'node:child_process';
2
+ import {readFile,writeFile} from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import {TextDecoder} from 'node:util';
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 const RESEND_CREDENTIAL_TARGET_PREFIX='ArcaneOSSDK/mail/resend/';
10
-
11
- const CREDENTIAL_STORE='windows-credential-manager';
12
- const MAX_PROFILE_LENGTH=64;
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
- switch ($operation) {
174
- 'set' {
175
- $credentialBytes=$null
176
- try {
177
- $encoded=[String]$request.secret
178
- if ([String]::IsNullOrEmpty($encoded)) { throw 'Invalid credential value.' }
179
- $credentialBytes=[Convert]::FromBase64String($encoded)
180
- if ($credentialBytes.Length -eq 0 -or $credentialBytes.Length -gt 2560) {
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
- default { throw 'Invalid credential operation.' }
29
+ throw error;
226
30
  }
227
- }
228
- catch {
229
- [Console]::Error.Write('ARCANE_CREDENTIAL_HELPER_FAILED')
230
- [Environment]::Exit(1)
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
- return timeoutMs;
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 path.win32.join(
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 helperEnvironment(systemRoot,temporaryDirectory){
333
- if(!is.string(temporaryDirectory)||temporaryDirectory.length===0
334
- ||temporaryDirectory.includes('\0')||!path.win32.isAbsolute(temporaryDirectory)){
335
- throw unavailableError();
44
+ function mailProfileSettings(settings,location){
45
+ if(location.profile==='mail'){
46
+ return settings;
336
47
  }
337
- return {
338
- SystemRoot:systemRoot,
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
- return value;
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
- if(is.string(output))output=Buffer.from(output,'utf8');
534
- if(!Buffer.isBuffer(output))throw operationError();
535
- if(output.length>MAX_HELPER_OUTPUT_BYTES){
536
- output.fill(0);
537
- throw operationError();
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 parseResponse(output);
64
+ return profileSettings;
540
65
  }
541
66
 
542
- async function invokeCredentialHelper(request,options){
543
- try{
544
- return await runWindowsCredentialHelper(request,options);
545
- }catch(error){
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
- function credentialStatus(profile,exists){
554
- return {
555
- profile,
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 options;
78
+ return apiKey;
567
79
  }
568
80
 
569
- function helperOptions(options){
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
- validateOptions(options);
583
- const {profile,secret}=options;
584
- const validatedProfile=validateMailCredentialProfile(profile);
585
- validateSecret(secret);
586
- const target=mailCredentialTarget(validatedProfile);
587
- const secretBytes=Buffer.from(secret,'utf8');
588
- let encoded='';
589
- const request={operation:'set',target,secret:''};
590
- try{
591
- encoded=secretBytes.toString('base64');
592
- request.secret=encoded;
593
- const response=await invokeCredentialHelper(
594
- request,
595
- helperOptions(options)
596
- );
597
- if(response.configured!==true)throw operationError();
598
- return credentialStatus(validatedProfile,true);
599
- }finally{
600
- request.secret='';
601
- encoded='';
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
- validateOptions(options);
629
- const validatedProfile=validateMailCredentialProfile(options.profile);
630
- const target=mailCredentialTarget(validatedProfile);
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
- validateOptions(options);
648
- const validatedProfile=validateMailCredentialProfile(options.profile);
649
- const target=mailCredentialTarget(validatedProfile);
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
- validateOptions(options);
660
- const validatedProfile=validateMailCredentialProfile(options.profile);
661
- const target=mailCredentialTarget(validatedProfile);
662
- const response=await invokeCredentialHelper(
663
- {operation:'delete',target},
664
- helperOptions(options)
665
- );
666
- if(!is.boolean(response.deleted)||response.configured!==false){
667
- throw operationError();
668
- }
669
- return credentialStatus(validatedProfile,false);
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
  }