arcane-os 0.22.1 → 0.24.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 +23 -0
- package/README.md +8 -5
- package/docs/reference/cli.md +15 -9
- package/docs/reference/mail.md +172 -45
- package/docs/reviews/mail-server-purpose-review.md +77 -6
- package/package.json +1 -1
- package/src/cli/main.mjs +5 -5
- package/src/mail-credentials.mjs +101 -41
- package/src/mail.mjs +43 -27
- package/src/templates/workspace-template.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
- Read nonsecret mail settings from `arcane.config.json.mail` and provider keys
|
|
6
|
+
from `.arcane.env.json.mail`. Keep existing root keys, exact named profiles,
|
|
7
|
+
and TLS path settings working without migrating or rewriting either file.
|
|
8
|
+
- Let explicit CLI/API options override configuration, replace origin lists,
|
|
9
|
+
and apply listener defaults after file settings. Share configured profile,
|
|
10
|
+
sender and provider deadlines with `mail send` without requiring TLS paths.
|
|
11
|
+
- Preserve unrelated settings during credential updates and remove both selected
|
|
12
|
+
key representations on explicit deletion. Document configuration precedence,
|
|
13
|
+
origin rejection, startup, platform behavior and the purpose-gate review.
|
|
14
|
+
|
|
15
|
+
## 0.23.0
|
|
16
|
+
|
|
17
|
+
- Rename the mail configuration file to `.arcane.env.json`. Upgrade existing
|
|
18
|
+
deployments by renaming `.env.json` in the directory where the mail command
|
|
19
|
+
runs, preserving its contents. Mail commands now read only the selected new
|
|
20
|
+
name and report `storage: '.arcane.env.json'`.
|
|
21
|
+
- Keep configuration independent of the SDK installation directory, including
|
|
22
|
+
an SDK nested beneath the site root. Preserve provider profiles, relative TLS
|
|
23
|
+
paths, other JSON settings and HTTPS/HTTP2 port 4433. Update help, references,
|
|
24
|
+
the purpose-gate report and generated-workspace Git ignores.
|
|
25
|
+
|
|
3
26
|
## 0.22.1
|
|
4
27
|
|
|
5
28
|
- Change the mail gateway's default HTTPS/HTTP2 port from 8025 to 4433 in
|
package/README.md
CHANGED
|
@@ -19,14 +19,17 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
|
|
20
20
|
event, cancellation, and browser run contracts.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.
|
|
22
|
+
This checkout defines the `0.24.0` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
26
|
-
The [mail gateway](docs/reference/mail.md) serves HTTPS with HTTP/2 on port 4433
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
The [mail gateway](docs/reference/mail.md) serves HTTPS with HTTP/2 on port 4433
|
|
27
|
+
by default. Configure its host, port, origin list, certificate paths, and other
|
|
28
|
+
mail settings in `arcane.config.json.mail`; keep provider keys in the ignored
|
|
29
|
+
`.arcane.env.json.mail`. Both files belong in the command's working directory.
|
|
30
|
+
Existing root credential, profile, and TLS settings remain supported. Browser
|
|
31
|
+
mail defaults to `/v1/mail` on the current domain, and multiple applications can
|
|
32
|
+
share one server with explicit allowed origins. Subscription verification is disabled
|
|
30
33
|
until a `verifySubscription` callback is configured; that callback receives the
|
|
31
34
|
application name and bearer subscription key before each provider attempt.
|
|
32
35
|
See the [method and action gate report](docs/reviews/mail-server-purpose-review.md)
|
package/docs/reference/cli.md
CHANGED
|
@@ -32,7 +32,7 @@ and exits nonzero on failure. Machine output is defined by
|
|
|
32
32
|
| `arcane update-check` | Performs one explicit, read-only npm dist-tag query for the installed SDK version. |
|
|
33
33
|
| `arcane targets` | Lists target ids, declared status, formats, architectures, signing profiles, methods, and pairing reason. |
|
|
34
34
|
| `arcane repo status\|pull\|push` | Runs one selected repository operation for the current app workspace. |
|
|
35
|
-
| `arcane mail key set\|status\|delete` | Manages one server-only Resend API-key profile in `.env.json`. |
|
|
35
|
+
| `arcane mail key set\|status\|delete` | Manages one server-only Resend API-key profile in `.arcane.env.json`. |
|
|
36
36
|
| `arcane mail send` | Performs one explicit, idempotency-keyed Resend attempt from a complete JSON report on redirected stdin. |
|
|
37
37
|
| `arcane mail serve` | Starts one Arcane-to-Resend gateway with a server-only provider profile, a selected listener, and optional CORS and recipient configuration. |
|
|
38
38
|
|
|
@@ -814,7 +814,7 @@ npm exec -- arcane repo status
|
|
|
814
814
|
|
|
815
815
|
### Resend credential profiles
|
|
816
816
|
|
|
817
|
-
The mail commands read `.env.json` from the invocation directory on Windows,
|
|
817
|
+
The mail commands read `.arcane.env.json` from the invocation directory on Windows,
|
|
818
818
|
Linux, and macOS. The credential subcommands select one profile in that file:
|
|
819
819
|
|
|
820
820
|
```text
|
|
@@ -825,7 +825,7 @@ arcane mail key delete [profile]
|
|
|
825
825
|
|
|
826
826
|
`key set` reads the Resend API key from a hidden terminal prompt. The
|
|
827
827
|
`--secret-stdin` form is for deliberately redirected non-interactive input and
|
|
828
|
-
rejects a TTY before reading. The key is written to `.env.json` and is never
|
|
828
|
+
rejects a TTY before reading. The key is written to `.arcane.env.json` and is never
|
|
829
829
|
accepted in argv or returned in status output. The optional profile defaults
|
|
830
830
|
to `mail`, which selects top-level `RESEND_API_KEY`. Any other exact profile
|
|
831
831
|
selects `MAIL_PROFILES[profile].RESEND_API_KEY`, with no default-key fallback.
|
|
@@ -838,10 +838,10 @@ The minimal file is:
|
|
|
838
838
|
}
|
|
839
839
|
```
|
|
840
840
|
|
|
841
|
-
Fill in the key before starting mail, and add `.env.json` to the project's
|
|
841
|
+
Fill in the key before starting mail, and add `.arcane.env.json` to the project's
|
|
842
842
|
`.gitignore`; the SDK repository already ignores it. Set and delete preserve
|
|
843
843
|
the file's other settings and profiles. Status returns the selected profile,
|
|
844
|
-
`provider:'resend'`, `storage:'.env.json'`, and `exists`. Delete returns
|
|
844
|
+
`provider:'resend'`, `storage:'.arcane.env.json'`, and `exists`. Delete returns
|
|
845
845
|
`exists:false` for both a removed and an already-absent credential.
|
|
846
846
|
|
|
847
847
|
Programmatic `createToolchain().mail(...)` resolves the configuration directory
|
|
@@ -852,6 +852,12 @@ Existing Windows Credential Manager records remain untouched; the JSON reader
|
|
|
852
852
|
does not migrate or fall back to them. Mail reads JSON directly and does not
|
|
853
853
|
populate or depend on process environment variables for this key.
|
|
854
854
|
|
|
855
|
+
The SDK's installation directory does not affect this location. With an SDK
|
|
856
|
+
under `my-site/arcane-os-sdk/`, run the command from `my-site/` and keep
|
|
857
|
+
`my-site/.arcane.env.json` alongside that directory. Existing deployments using
|
|
858
|
+
SDK 0.22.1 or earlier must rename `.env.json` to `.arcane.env.json` while
|
|
859
|
+
preserving its contents; the loader reads only the new name.
|
|
860
|
+
|
|
855
861
|
Missing files or missing/empty selected keys stop `send` and `serve` with the
|
|
856
862
|
configuration path and exact JSON setting to fill in. Invalid JSON and file
|
|
857
863
|
access failures remain observable without printing credential content.
|
|
@@ -887,7 +893,7 @@ Resend owns their accepted shape. The adapter removes the application-only
|
|
|
887
893
|
`type` field and applies `--from` when supplied; otherwise the report or provider
|
|
888
894
|
template supplies the sender. Direct CLI sending has
|
|
889
895
|
no configured fallback recipients. The Resend credential comes only from the
|
|
890
|
-
selected `.env.json` profile; omitting `--profile` selects `mail`. Neither the
|
|
896
|
+
selected `.arcane.env.json` profile; omitting `--profile` selects `mail`. Neither the
|
|
891
897
|
key nor report content is accepted through argv or process environment variables.
|
|
892
898
|
|
|
893
899
|
The caller owns the nonempty `--report-key`, which is forwarded unchanged.
|
|
@@ -912,7 +918,7 @@ loss after the attempt begins is ambiguous because Resend may have accepted it.
|
|
|
912
918
|
arcane mail serve [--profile <profile>] [--from <verified-sender>] [--app <label>] [--origin <exact-origin>] [--allow-to <addresses>] [--host 0.0.0.0] [--port 4433] [--request-timeout <ms>]
|
|
913
919
|
```
|
|
914
920
|
|
|
915
|
-
The selected `.env.json` profile supplies only the server-side Resend API key;
|
|
921
|
+
The selected `.arcane.env.json` profile supplies only the server-side Resend API key;
|
|
916
922
|
omitting `--profile` selects `mail`.
|
|
917
923
|
|
|
918
924
|
Add the listener's certificate configuration at the top level of the same file:
|
|
@@ -926,10 +932,10 @@ Add the listener's certificate configuration at the top level of the same file:
|
|
|
926
932
|
```
|
|
927
933
|
|
|
928
934
|
Supply an existing PEM certificate chain and its private key. Paths resolve
|
|
929
|
-
relative to `.env.json`, or may be absolute. They are shared across provider
|
|
935
|
+
relative to `.arcane.env.json`, or may be absolute. They are shared across provider
|
|
930
936
|
profiles. Missing TLS settings name the fields to fill in before a listener
|
|
931
937
|
opens; the TLS owner reports PEM file errors. Keep private-key material outside
|
|
932
|
-
tracked source. The SDK repository already ignores `.arcane/` and `.env.json`.
|
|
938
|
+
tracked source. The SDK repository already ignores `.arcane/` and `.arcane.env.json`.
|
|
933
939
|
|
|
934
940
|
The selected `node-http-server` module negotiates HTTP/2 with HTTP/1.1 fallback
|
|
935
941
|
on the same HTTPS port, default `4433`, with no plain-HTTP listener. Callers use
|
package/docs/reference/mail.md
CHANGED
|
@@ -13,7 +13,7 @@ gateway and is never included in browser or WebAssembly state.
|
|
|
13
13
|
| `MailTransport.mjs` | Browser, WebView, or compatible Fetch host | Sends one already-persisted request to the configured Arcane gateway with the stable report key as its idempotency key. |
|
|
14
14
|
| `arcane mail send` | Node on the local machine | Reads one complete provider-neutral report from redirected stdin and performs one explicit Resend attempt with a caller-owned idempotency key. |
|
|
15
15
|
| `arcane mail serve` | Node on the configured host | Owns caller verification, protects the provider credential, applies explicitly configured recipient and origin settings, and makes one server-side Resend request. |
|
|
16
|
-
| `arcane mail key ...` | Node on Windows, Linux, or macOS | Stores, inspects, or deletes a Resend API key in the selected `.env.json`. |
|
|
16
|
+
| `arcane mail key ...` | Node on Windows, Linux, or macOS | Stores, inspects, or deletes a Resend API key in the selected `.arcane.env.json`. |
|
|
17
17
|
|
|
18
18
|
The browser never receives the Resend API key. The gateway never writes that
|
|
19
19
|
key to source, argv, logs, events, fixtures, browser storage, or its public
|
|
@@ -56,7 +56,7 @@ browser import while preserving one shared CLI/toolchain implementation.
|
|
|
56
56
|
Arcane Mail deliberately separates two credentials:
|
|
57
57
|
|
|
58
58
|
- The **Resend API key** is provider authority. The Node process reads it from
|
|
59
|
-
`.env.json`. `arcane mail key set [profile]` can store it there through hidden
|
|
59
|
+
`.arcane.env.json`. `arcane mail key set [profile]` can store it there through hidden
|
|
60
60
|
input; `mail send` and `mail serve` read the selected profile inside that process.
|
|
61
61
|
- The **subscription key** is the application user's subscription credential.
|
|
62
62
|
When present, the browser sends it as `Authorization: Bearer <subscriptionKey>`,
|
|
@@ -272,31 +272,78 @@ committed acceptance result.
|
|
|
272
272
|
|
|
273
273
|
## Operate the CLI and gateway
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
Keep app-supplied SDK settings under named capability members, starting with
|
|
276
|
+
`mail`. Put the nonsecret mail settings in `arcane.config.json` in the directory
|
|
277
|
+
from which the command runs:
|
|
277
278
|
|
|
278
279
|
```json
|
|
279
280
|
{
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
"mail": {
|
|
282
|
+
"host": "0.0.0.0",
|
|
283
|
+
"port": 4433,
|
|
284
|
+
"origins": [
|
|
285
|
+
"https://dragons.example",
|
|
286
|
+
"https://www.dragons.example"
|
|
287
|
+
],
|
|
288
|
+
"profile": "mail",
|
|
289
|
+
"certPath": "certificates/fullchain.pem",
|
|
290
|
+
"keyPath": "certificates/private-key.pem"
|
|
291
|
+
}
|
|
283
292
|
}
|
|
284
293
|
```
|
|
285
294
|
|
|
286
|
-
|
|
287
|
-
project's `.gitignore`. This is a JSON configuration file; the mail commands
|
|
288
|
-
read it directly without copying its contents into `process.env`.
|
|
295
|
+
Put the Resend provider key in the separate `.arcane.env.json`:
|
|
289
296
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"mail": {
|
|
300
|
+
"apiKey": ""
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The SDK repository ignores `.arcane.env.json`. Keep the same entry in a consuming
|
|
306
|
+
project's `.gitignore`. `arcane.config.json` contains settings suitable for source
|
|
307
|
+
control; its certificate fields contain file paths, never PEM contents or provider
|
|
308
|
+
keys. Both files are read directly as JSON without copying values into
|
|
309
|
+
`process.env`. Other top-level capability members remain untouched. The portable
|
|
310
|
+
browser `arcane-os/mail` import does not read these Node-side files.
|
|
311
|
+
|
|
312
|
+
The supported `arcane.config.json.mail` fields are:
|
|
313
|
+
|
|
314
|
+
| Field | Type | Purpose and default |
|
|
315
|
+
| --- | --- | --- |
|
|
316
|
+
| `host` | string | Listener bind address; defaults to `0.0.0.0`. |
|
|
317
|
+
| `port` | integer | Listener port; defaults to `4433`. Explicit `0` selects an available port. |
|
|
318
|
+
| `origins` | string array | Exact allowed browser origins. An absent or empty list uses the current request authority as described below. |
|
|
319
|
+
| `profile` | string | Resend credential profile for send/serve; defaults to `mail`. |
|
|
320
|
+
| `from` | string | Optional shared sender override. Omit it to retain each report's sender or provider template default. |
|
|
321
|
+
| `appId` | string | Optional server event label; does not restrict incoming application names. |
|
|
322
|
+
| `recipientAllowlist` | string array | Optional allowed recipients; absent or empty means unrestricted recipients. |
|
|
323
|
+
| `errorRecipients` | string array | Error-report fallback recipients; defaults to the effective recipient allowlist. An explicit empty array supplies no fallback. |
|
|
324
|
+
| `bodyTimeoutMs` | integer or null | Optional request-body deadline in milliseconds; absent or null adds no deadline. |
|
|
325
|
+
| `providerTimeoutMs` | integer or null | Optional provider deadline in milliseconds; absent or null adds no deadline. |
|
|
326
|
+
| `retryableDelayMs` | positive integer | Retry guidance in a retryable result; defaults to `1000`. It does not schedule a retry. |
|
|
327
|
+
| `certPath` | string | PEM certificate-chain file path, required for gateway HTTPS. |
|
|
328
|
+
| `keyPath` | string | PEM private-key file path, required for gateway HTTPS. |
|
|
329
|
+
|
|
330
|
+
Resend is the supported provider; no provider selector is needed. Keep callbacks,
|
|
331
|
+
injected providers, `fetchImpl`, `onEvent`, `requestIdFactory`,
|
|
332
|
+
`verifySubscription`, and `AbortSignal` values in programmatic options. They are
|
|
333
|
+
runtime inputs, not JSON settings. Reports and their idempotency keys remain
|
|
334
|
+
inputs to each send operation.
|
|
335
|
+
|
|
336
|
+
The default credential profile `mail` selects `.arcane.env.json.mail.apiKey`.
|
|
337
|
+
Other profile names select exact entries in `.arcane.env.json.mail.profiles`:
|
|
293
338
|
|
|
294
339
|
```json
|
|
295
340
|
{
|
|
296
|
-
"
|
|
297
|
-
|
|
298
|
-
"
|
|
299
|
-
"
|
|
341
|
+
"mail": {
|
|
342
|
+
"apiKey": "",
|
|
343
|
+
"profiles": {
|
|
344
|
+
"another-provider-account": {
|
|
345
|
+
"apiKey": ""
|
|
346
|
+
}
|
|
300
347
|
}
|
|
301
348
|
}
|
|
302
349
|
}
|
|
@@ -304,13 +351,67 @@ select exact entries under `MAIL_PROFILES`:
|
|
|
304
351
|
|
|
305
352
|
An absent named profile does not fall back to the default key. The profile
|
|
306
353
|
selects Resend provider credentials; it is separate from the incoming
|
|
307
|
-
application name and subscriber key.
|
|
354
|
+
application name and subscriber key. Existing top-level `RESEND_API_KEY` and
|
|
355
|
+
`MAIL_PROFILES[profile].RESEND_API_KEY` remain supported. A nested selected
|
|
356
|
+
`apiKey` takes precedence when the property exists, including null or an empty
|
|
357
|
+
string, which means the selected key is absent. Only an absent nested key
|
|
358
|
+
property permits fallback to the corresponding legacy key.
|
|
308
359
|
|
|
309
|
-
Programmatic operations resolve
|
|
360
|
+
Programmatic operations resolve both files from `options.cwd`, then
|
|
310
361
|
`options.workspaceRoot`, then `process.cwd()`, choosing the first supplied
|
|
311
362
|
directory. The CLI uses its invocation directory. There is no upward directory
|
|
312
363
|
search or dependency on a Windows installation directory or temporary-directory
|
|
313
|
-
environment variable.
|
|
364
|
+
environment variable. Missing files are optional configuration sources; send and
|
|
365
|
+
serve still report their missing required values before attempting delivery or
|
|
366
|
+
binding. Malformed or unreadable files produce an error.
|
|
367
|
+
|
|
368
|
+
Configuration precedence is explicit:
|
|
369
|
+
|
|
370
|
+
1. A CLI/API option overrides its file setting when its value is not `undefined`.
|
|
371
|
+
An explicit null retains the option's existing meaning; it does not select
|
|
372
|
+
the file value again.
|
|
373
|
+
2. `arcane.config.json.mail` supplies nonsecret settings absent from those options.
|
|
374
|
+
3. Legacy `.arcane.env.json` root `MAIL_TLS_CERT_PATH` and `MAIL_TLS_KEY_PATH`
|
|
375
|
+
supply certificate paths absent from the selected options and config member.
|
|
376
|
+
4. Remaining settings use the defaults above.
|
|
377
|
+
|
|
378
|
+
The existing programmatic aliases `origin`, `allowTo`, `errorTo`, and
|
|
379
|
+
`requestTimeout` take precedence over their corresponding canonical options
|
|
380
|
+
`origins`, `recipientAllowlist`, `errorRecipients`, and `providerTimeoutMs` when
|
|
381
|
+
both are supplied. The CLI continues to expose `--origin`, `--allow-to`, and
|
|
382
|
+
`--request-timeout`. `origin` accepts a string or an array; the recipient aliases
|
|
383
|
+
accept address arrays or comma-separated strings. Lists replace the lower-priority
|
|
384
|
+
list completely. The configuration reader does not concatenate or deduplicate
|
|
385
|
+
lists, rewrite case, or automatically add local addresses.
|
|
386
|
+
|
|
387
|
+
Use the JSON `origins` array for multiple origins. Repeating the current
|
|
388
|
+
`--origin` option keeps only its last value; there is no `--origins` CLI option.
|
|
389
|
+
The CLI leaves omitted host, port, and send/serve profile options unset until
|
|
390
|
+
configuration resolves, so its defaults do not mask file settings.
|
|
391
|
+
|
|
392
|
+
Keep the configuration in the deployment directory even when the SDK is nested
|
|
393
|
+
below it:
|
|
394
|
+
|
|
395
|
+
```text
|
|
396
|
+
my-site/
|
|
397
|
+
├── arcane.config.json
|
|
398
|
+
├── .arcane.env.json
|
|
399
|
+
└── arcane-os-sdk/
|
|
400
|
+
└── bin/arcane.mjs
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Run from `my-site`, for example:
|
|
404
|
+
|
|
405
|
+
```sh
|
|
406
|
+
node ./arcane-os-sdk/bin/arcane.mjs mail serve
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The SDK directory does not choose the configuration location. When upgrading
|
|
410
|
+
from SDK 0.22.1 or earlier, rename the existing `.env.json` to
|
|
411
|
+
`.arcane.env.json` in the invocation directory, preserving its contents.
|
|
412
|
+
The secret loader reads only `.arcane.env.json`; it does not read the old filename.
|
|
413
|
+
Existing root key, profile, and TLS fields can remain in that file. Adopting the
|
|
414
|
+
capability members does not perform an automatic rewrite or migration.
|
|
314
415
|
|
|
315
416
|
The existing key commands manage the same file:
|
|
316
417
|
|
|
@@ -322,9 +423,15 @@ arcane mail key delete
|
|
|
322
423
|
|
|
323
424
|
`key set` prompts with hidden input. `--secret-stdin` is the explicit
|
|
324
425
|
non-interactive alternative and rejects a TTY. Each command accepts an optional
|
|
325
|
-
profile argument, defaulting to `mail
|
|
326
|
-
settings and profiles; status reports
|
|
327
|
-
|
|
426
|
+
profile argument, defaulting to `mail` independently of `arcane.config.json.mail.profile`.
|
|
427
|
+
Set and delete preserve other JSON settings and profiles; status reports
|
|
428
|
+
existence without returning the key. A new credential is written to the nested
|
|
429
|
+
mail member. An existing legacy credential is updated at its existing location
|
|
430
|
+
unless the selected nested key property exists, in which case set updates that
|
|
431
|
+
nested property. Delete removes both representations of only the selected key,
|
|
432
|
+
so an older key cannot reappear through fallback. Other settings and profile
|
|
433
|
+
containers remain intact.
|
|
434
|
+
Results identify `storage: '.arcane.env.json'`. An already-absent deletion succeeds
|
|
328
435
|
with `exists: false`.
|
|
329
436
|
|
|
330
437
|
Existing Windows Credential Manager records remain untouched. The JSON path
|
|
@@ -358,19 +465,28 @@ cancellation after the provider attempt begins is returned as an ambiguous
|
|
|
358
465
|
nonzero outcome because the provider may already have accepted the request.
|
|
359
466
|
Cancellation before the attempt exits 130 without sending.
|
|
360
467
|
For both CLI mail operations, `--request-timeout` accepts 1 through 2147483647
|
|
361
|
-
milliseconds, the Node timer range.
|
|
362
|
-
|
|
468
|
+
milliseconds, the Node timer range. The same range applies to configured body
|
|
469
|
+
and provider deadlines. When no provider timeout is selected in options or
|
|
470
|
+
configuration, the SDK adds no provider deadline.
|
|
471
|
+
|
|
472
|
+
`mail send` consumes the selected profile, sender, provider timeout, and retry
|
|
473
|
+
guidance from the same configuration. It does not require gateway TLS paths.
|
|
474
|
+
Send and serve read each required JSON file once, concurrently when both are
|
|
475
|
+
needed, before consuming their settings. An injected `readCredential` remains
|
|
476
|
+
the credential owner and reads once. With that injection, send reads only
|
|
477
|
+
`arcane.config.json`; serve also reads `.arcane.env.json` for legacy TLS paths
|
|
478
|
+
without interpreting its unused file credential.
|
|
363
479
|
|
|
364
480
|
Start the gateway:
|
|
365
481
|
|
|
366
482
|
```text
|
|
367
|
-
npm exec -- arcane mail serve
|
|
483
|
+
npm exec -- arcane mail serve
|
|
368
484
|
```
|
|
369
485
|
|
|
370
|
-
The default listener is `0.0.0.0:4433`;
|
|
371
|
-
address and port
|
|
372
|
-
|
|
373
|
-
|
|
486
|
+
The default listener is `0.0.0.0:4433`; `mail.host` and `mail.port` select its
|
|
487
|
+
configured bind address and port, and explicit `--host` / `--port` override them.
|
|
488
|
+
The server can serve callers from multiple domains on the same machine.
|
|
489
|
+
Configure the caller's endpoint to reach that listener.
|
|
374
490
|
|
|
375
491
|
`mail serve` uses HTTPS with HTTP/2 on that selected port. The published
|
|
376
492
|
`node-http-server` PEM API owns TLS and negotiates HTTP/2 or HTTP/1.1 on the
|
|
@@ -378,25 +494,29 @@ same listener. It creates no additional plain-HTTP listener. The returned URL
|
|
|
378
494
|
uses `https://`; `0.0.0.0` is the bind address, so callers use the deployed
|
|
379
495
|
domain, for example `https://mail.example.com:4433/v1/mail`.
|
|
380
496
|
|
|
381
|
-
Set `
|
|
382
|
-
to its PEM private-key file. These
|
|
383
|
-
and apply regardless of the selected provider profile.
|
|
384
|
-
|
|
497
|
+
Set `arcane.config.json.mail.certPath` to the PEM certificate chain and
|
|
498
|
+
`mail.keyPath` to its PEM private-key file. These settings belong to the listener
|
|
499
|
+
and apply regardless of the selected provider profile. The legacy root
|
|
500
|
+
`MAIL_TLS_CERT_PATH` and `MAIL_TLS_KEY_PATH` fields in `.arcane.env.json` remain
|
|
501
|
+
fallbacks. Relative certificate paths resolve from the selected configuration
|
|
502
|
+
directory, including explicit programmatic path options; absolute paths are also accepted.
|
|
385
503
|
The certificate must cover the hostname callers use. One certificate may
|
|
386
504
|
cover multiple names; the gateway does not require one certificate per calling
|
|
387
505
|
application. Keep private-key files outside tracked source, such as in the
|
|
388
506
|
already-ignored `.arcane/` directory or an existing host certificate directory.
|
|
389
507
|
|
|
390
|
-
Startup reads
|
|
508
|
+
Startup reads each JSON file once, reports missing TLS settings before
|
|
391
509
|
binding, and lets the TLS owner report unreadable or unusable PEM files. It
|
|
392
510
|
does not generate certificates, modify system trust, or add a renewal watcher.
|
|
511
|
+
No JSON configuration file is reread for an incoming HTTP request.
|
|
393
512
|
Restart the gateway after the configured certificate files are renewed.
|
|
394
513
|
The same Node file and TLS APIs are used on Windows, Linux, and macOS; Android
|
|
395
514
|
requires a compatible Node host and accessible configuration and certificate
|
|
396
515
|
paths. These platform contracts are separate from actual platform execution.
|
|
397
516
|
|
|
398
517
|
The public `createToolchain().mail({action: 'serve', ...options})` operation
|
|
399
|
-
uses the same
|
|
518
|
+
uses the same settings with explicit options taking precedence. Internally,
|
|
519
|
+
`startResendMailServer` accepts
|
|
400
520
|
`certPath` and `keyPath` and retains its existing HTTP behavior when neither
|
|
401
521
|
is supplied. That internal function is not an npm package export.
|
|
402
522
|
|
|
@@ -408,18 +528,25 @@ The existing listener remains running; this launch does not retry or select
|
|
|
408
528
|
another port.
|
|
409
529
|
|
|
410
530
|
`--app` is an optional server event label and does not restrict incoming
|
|
411
|
-
application names. `--from`
|
|
412
|
-
preserve each report's sender or its provider template's default.
|
|
413
|
-
|
|
414
|
-
programmatic `origin`
|
|
415
|
-
no origins configured, the gateway accepts an Origin matching its request
|
|
531
|
+
application names. `--from` and `mail.from` are optional shared sender overrides;
|
|
532
|
+
omit both to preserve each report's sender or its provider template's default.
|
|
533
|
+
`mail.origins` supplies an array of exact allowed caller origins. An explicit
|
|
534
|
+
`--origin` replaces that array; the programmatic `origin` alias also accepts an
|
|
535
|
+
array. With no origins configured, the gateway accepts an Origin matching its request
|
|
416
536
|
authority (`:authority` for HTTP/2, `Host` for HTTP/1.1) using HTTP or HTTPS.
|
|
417
|
-
|
|
537
|
+
An Origin outside the configured list or current-authority default receives
|
|
538
|
+
`403 mail_origin_not_allowed`. Origin strings are compared exactly: include the
|
|
539
|
+
scheme and any nondefault port, with no path or trailing slash. There are no
|
|
540
|
+
wildcards, subdomain expansion, normalization, or loopback exceptions.
|
|
541
|
+
Requests without an `Origin` header continue normally. This is a declared-origin
|
|
542
|
+
CORS list, not a client-IP or connecting-machine allowlist; ordinary
|
|
543
|
+
server-to-server requests commonly omit Origin.
|
|
418
544
|
Cross-origin preflight permits `Content-Type`, `Idempotency-Key`, `X-Mail-App`,
|
|
419
|
-
and `Authorization`.
|
|
545
|
+
and `Authorization`.
|
|
420
546
|
|
|
421
|
-
`--allow-to`
|
|
422
|
-
|
|
547
|
+
`mail.recipientAllowlist` or explicit `--allow-to` limits recipients when
|
|
548
|
+
configured. An absent or empty effective list imposes no recipient allowlist.
|
|
549
|
+
When configured, the list applies to
|
|
423
550
|
every recipient in the resolved `to`, `cc`, and `bcc` fields, whether supplied
|
|
424
551
|
as a string or an array. Sender, recipient, subject, and body
|
|
425
552
|
values are not trimmed, lowercased, or filtered by an SDK email grammar at
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
[Roshi's Codex PRIME] The gateway review and its portable credential follow-up apply the same three purpose gates. The gateway removals preserve complete delivery outcomes, configured CORS and recipient policy, and the optional subscription verifier. Mail credentials now belong in the user-selected `.env.json`, with one portable Node implementation for Windows, Linux, and macOS and a compatible Node host as the Android adaptation boundary. The original gateway inventory below uses baseline `f055e05`; the credential follow-up reviews the Windows storage implementation present at SDK `0.18.0`. Commit, package, publication, and actual platform execution outcomes belong to the delivery record rather than to this source-review report.
|
|
1
|
+
[Roshi's Codex PRIME] The gateway review and its portable credential follow-up apply the same three purpose gates. The gateway removals preserve complete delivery outcomes, configured CORS and recipient policy, and the optional subscription verifier. Mail credentials now belong in the user-selected `.arcane.env.json`, with one portable Node implementation for Windows, Linux, and macOS and a compatible Node host as the Android adaptation boundary. The original gateway inventory below uses baseline `f055e05`; the credential follow-up reviews the Windows storage implementation present at SDK `0.18.0`. Commit, package, publication, and actual platform execution outcomes belong to the delivery record rather than to this source-review report.
|
|
2
2
|
|
|
3
3
|
| Decision | Behavior | Why it matters | Source status at this review |
|
|
4
4
|
| --- | --- | --- | --- |
|
|
5
5
|
| Remove | Loopback-only admission, manual header reconstruction, address/origin normalization, local payload grammar, manual content-length scan, dead queue knobs | These restrict supported use, rewrite supplied values, or repeat work already owned elsewhere. | Removed from the inspected source; naming-only changes carry no runtime claim. |
|
|
6
6
|
| Remove and replace | Local app-key hashes and X-Mail-Key authentication | The selected application plus bearer subscription contract needs an actual verifier, not a local shared-key comparison. | Removed. Optional `verifySubscription` implements the local integration contract; the remote endpoint adapter remains future work. Initial setup without that callback performs no caller authentication. |
|
|
7
|
-
| Remove and replace | Windows-only credential process, native interop, helper timers, environment prerequisites, encoding and input/output limits | They prevent the required portable startup and perform work absent from the selected JSON storage path. | Replaced by direct `.env.json` access. Existing Windows credential records remain untouched. Default and named credential operations remain useful and are preserved. |
|
|
7
|
+
| Remove and replace | Windows-only credential process, native interop, helper timers, environment prerequisites, encoding and input/output limits | They prevent the required portable startup and perform work absent from the selected JSON storage path. | Replaced by direct `.arcane.env.json` access. Existing Windows credential records remain untouched. Default and named credential operations remain useful and are preserved. |
|
|
8
8
|
| Simplify | Exact CORS policy, one configuration resolution, one provider request construction/serialization, optional observation | Preserve the intended result while eliminating repeated parsing, copies, policy structures, and absent-observer work. | Present in inspected source. Explicit nonempty origin lists remain authoritative; otherwise the nominal-domain default applies. |
|
|
9
9
|
| Keep | Configured subscription authentication, explicit recipient policy/fallback, stable idempotency, real cancellation/deadlines, complete provider outcomes, concurrent requests and owned shutdown | These have concrete callers and determine whether mail is sent, retried, reported honestly, or stopped correctly. | Retained. Only a verifier result of `true` permits authenticated delivery; failures and cancellation prevent the provider attempt. |
|
|
10
10
|
|
|
@@ -35,7 +35,7 @@ The latest user-selected boundaries govern this review:
|
|
|
35
35
|
- Explicit recipient allowlists and error-recipient fallback remain. A configured nonempty allowlist covers resolved `to`, `cc`, and `bcc` recipients; allowing additional provider fields must not bypass the selected recipient policy. With no allowlist, the policy performs no recipient scan. Per-address trimming, lowercasing, and local email grammar do not remain.
|
|
36
36
|
- Caller-selected deadlines, the cancellation adapter, complete provider outcomes, retry classification, stable idempotency, and independently constructed snapshots for emitted observations remain. Shared mutable provider snapshots have not been selected.
|
|
37
37
|
- Remove manual response content-length scanning, duplicate configuration/CORS construction, unobserved event payload creation, unused `allowZero`, and unused CLI queue options.
|
|
38
|
-
- All SDK mail operations require a functional Windows, Linux, and macOS path, with Android adaptation at the host boundary. Read provider configuration directly from an ignored `.env.json`; an operating-system limitation notice does not satisfy the portable requirement. Default profile `mail` uses `RESEND_API_KEY`; other exact names use `MAIL_PROFILES[profile].RESEND_API_KEY`. Missing configuration must identify the file and setting to fill in. Existing Windows records remain untouched, without an automatic read or migration.
|
|
38
|
+
- All SDK mail operations require a functional Windows, Linux, and macOS path, with Android adaptation at the host boundary. Read provider configuration directly from an ignored `.arcane.env.json`; an operating-system limitation notice does not satisfy the portable requirement. Default profile `mail` uses `RESEND_API_KEY`; other exact names use `MAIL_PROFILES[profile].RESEND_API_KEY`. Missing configuration must identify the file and setting to fill in. Existing Windows records remain untouched, without an automatic read or migration.
|
|
39
39
|
|
|
40
40
|
Source inspection during this report's handoff shows local app/email/sender/API-key format validators and loopback admission removed; origin values retained in one Set; complete provider JSON values or raw response text preserved; template-shaped provider requests no longer required to supply local text/HTML fields; and configuration passed once into `createConfiguredMailHandler`. Observer objects, pending Sets, callback closures, event payload reconstruction, and drain waits are absent when `onEvent` is omitted. An already-cancelled direct send stops before provider payload construction or serialization. Response writing no longer calculates content length. The fixed `/v1/mail` route accepts its query-bearing form, matching the portable endpoint parser. These are source observations, not executed behavior.
|
|
41
41
|
|
|
@@ -311,7 +311,7 @@ The coupled CLI/browser actions below are necessary to avoid leaving contradicto
|
|
|
311
311
|
|
|
312
312
|
| Coupled path/action | Gates and decision | Required result and evidence boundary |
|
|
313
313
|
| --- | --- | --- |
|
|
314
|
-
| `src/mail.mjs` `serveMail` old hidden app-key input and local auth options | Y/Y/Y — Reconciled with the optional verifier contract. | Obsolete shared-app-key prompt/options are removed. Current `serveMailGateway` forwards `options.verifySubscription` to the server and keeps provider credential operations and the provider API key separate. The portable follow-up resolves that provider credential from `.env.json`. |
|
|
314
|
+
| `src/mail.mjs` `serveMail` old hidden app-key input and local auth options | Y/Y/Y — Reconciled with the optional verifier contract. | Obsolete shared-app-key prompt/options are removed. Current `serveMailGateway` forwards `options.verifySubscription` to the server and keeps provider credential operations and the provider API key separate. The portable follow-up resolves that provider credential from `.arcane.env.json`. |
|
|
315
315
|
| `src/mail.mjs` `recipientList` and origin list preparation | Y/Y/Y — Remove redundant rewriting, preserve configured policy values. | Gateway and CLI must not disagree about preserved recipients/origins. An exact allowlist is still an intentional policy. |
|
|
316
316
|
| `src/mail.mjs` `withoutMailCredentials` | N/N/Y for recursive payload-key redaction — Remove. | It recursively traversed/copied complete results and deleted any content property named `apiKey` or `appKey`, including ordinary user payload fields. Keep actual transport credentials outside constructed public results at their owning boundary; do not rewrite the report based on property names. Source inspection shows this traversal removed. |
|
|
317
317
|
| `src/mail.mjs` `completeSendFailure` | Y/Y/Y — Remove helper after preserving the complete result directly. | Failure output still contains the provider result, or a narrow wrapper for a non-object injected result. No recursive copying/redaction pass remains. |
|
|
@@ -378,7 +378,7 @@ Existing native credential records are neither read nor changed.
|
|
|
378
378
|
| `setMailCredential`, `readMailCredential`, `getMailCredentialStatus`, `deleteMailCredential` | Y/Y/Y | Preserve all four operations and replace their implementation. Explicit set/delete update only the selected credential field while preserving other settings and profiles; read returns the selected key or null; status reports existence. File access uses portable Node APIs. |
|
|
379
379
|
| `mailCredentialOptions`, `readMailProviderKey` | Y/Y/Y | Keep shared command configuration and consolidate the real send/serve duplicate key-read error path. Remove Windows runner, helper directory, and helper timeout forwarding. Missing configuration names the exact JSON setting and file before opening a listener or attempting delivery. |
|
|
380
380
|
| CLI required profile flag and omitted invocation directory | Y/Y/Y | Make the profile optional with default `mail`, retaining explicit profiles. Forward the invocation directory through every mail operation, so the CLI reads the operator's selected file. Preserve hidden/stdin key entry and existing send/serve lifecycle. |
|
|
381
|
-
| Repository and workspace-template `.gitignore` entry | Y/Y/N | Keep `.env.json` out of source control in this checkout and newly generated workspaces. The populated local file is outside the committed and published change. |
|
|
381
|
+
| Repository and workspace-template `.gitignore` entry | Y/Y/N | Keep `.arcane.env.json` out of source control in this checkout and newly generated workspaces. The populated local file is outside the committed and published change. |
|
|
382
382
|
|
|
383
383
|
The credential path performs one file read per selected operation and one write
|
|
384
384
|
only for an explicit set or a deletion that finds a credential. Startup reads
|
|
@@ -402,7 +402,7 @@ subscription callback, sender selection, recipient configuration and cancellatio
|
|
|
402
402
|
|
|
403
403
|
| Method or action | Gates | Decision, callers, and concrete purpose |
|
|
404
404
|
| --- | --- | --- |
|
|
405
|
-
| `readMailServerSettings` | Y/Y/N | Read the same `.env.json` once for server startup, select the existing provider profile and resolve the two shared PEM path settings relative to that file. This avoids a second configuration read and platform-specific credential or certificate processes. |
|
|
405
|
+
| `readMailServerSettings` | Y/Y/N | Read the same `.arcane.env.json` once for server startup, select the existing provider profile and resolve the two shared PEM path settings relative to that file. This avoids a second configuration read and platform-specific credential or certificate processes. |
|
|
406
406
|
| `readMailProviderKey` and `serveMailGateway` | Y/Y/N | Preserve the existing credential-injection interface and missing-provider error while consuming the already-read startup key. Require the selected TLS pair before binding; forward only the paths to the listener. Direct provider sending and key CRUD remain unchanged. |
|
|
407
407
|
| `MAIL_TLS_CERT_PATH` and `MAIL_TLS_KEY_PATH` | Y/Y/N | Clear top-level names identify certificate and private-key file paths for one listener, independently of any provider profile or calling application. Missing settings and unusable JSON values name the field without printing its content. |
|
|
408
408
|
| `startResendMailServer` TLS options | Y/Y/N | Add `certPath` and `keyPath` through the module's published PEM API. HTTPS-only mode creates one listener with HTTP/2 and HTTP/1.1 negotiation on the selected port. This internal source integration retains its existing HTTP behavior when neither path is supplied; it is not an npm package export. The public toolchain mail operation requires the JSON pair. |
|
|
@@ -436,3 +436,74 @@ defaults adds no runtime work or helper. TLS negotiation, the returned endpoint
|
|
|
436
436
|
and the occupied-port message already use the selected port. Existing test
|
|
437
437
|
source was updated for the omitted-port path; explicit-port cases were retained.
|
|
438
438
|
No local tests, checks or server launch were performed for this increment.
|
|
439
|
+
|
|
440
|
+
## Deployment configuration filename follow-up
|
|
441
|
+
|
|
442
|
+
The user selected `.arcane.env.json` in the command's invocation directory,
|
|
443
|
+
independently of a nested SDK directory. `mailCredentialLocation` remains the
|
|
444
|
+
single filename owner for key operations, direct sending and server startup;
|
|
445
|
+
`mailCredentialStatus` reports the same new name. The directory precedence and
|
|
446
|
+
relative certificate-path resolution remain unchanged.
|
|
447
|
+
|
|
448
|
+
The filename change passes Y/Y/N: its name identifies Arcane configuration,
|
|
449
|
+
deployment ownership makes the location useful, and removing that configuration
|
|
450
|
+
would lose the provider and TLS settings. Additional filename fallbacks, upward
|
|
451
|
+
searches and automatic migrations fail N/N/Y and are omitted. This rename adds
|
|
452
|
+
no read, timer, helper or per-request work. Existing deployments rename their
|
|
453
|
+
file with its contents intact when adopting SDK 0.23.0. Repository and generated
|
|
454
|
+
workspace ignores include the new name and retain the old entry for existing
|
|
455
|
+
private files. Only the intended filename and status contract change; mail
|
|
456
|
+
payloads, profiles, other settings and lifecycle remain unchanged.
|
|
457
|
+
|
|
458
|
+
Existing credential and CLI test fixtures now use the selected name. Source
|
|
459
|
+
and diff were reviewed; local tests, checks and server execution were not run.
|
|
460
|
+
|
|
461
|
+
## Capability configuration and secret separation follow-up
|
|
462
|
+
|
|
463
|
+
The selected outcome is one app-authored mail configuration in
|
|
464
|
+
`arcane.config.json.mail`, with Resend credentials in ignored
|
|
465
|
+
`.arcane.env.json.mail`. The SDK owns reading and applying these settings through
|
|
466
|
+
the existing CLI/toolchain mail operation. Domains, senders, certificates, and
|
|
467
|
+
provider accounts remain deployment-owned data. This increment begins the
|
|
468
|
+
named-capability configuration convention with mail; it does not migrate other
|
|
469
|
+
capabilities or consumer files.
|
|
470
|
+
|
|
471
|
+
The source audit found no existing `arcane.config.json` reader to reuse. The
|
|
472
|
+
workspace loader owns `arcane-packager.json`, descriptors, and application
|
|
473
|
+
layout. Requiring that loader to start a mail listener would add unrelated work.
|
|
474
|
+
The existing mail JSON reader is the reusable boundary for the two selected
|
|
475
|
+
files. Their explicit precedence and credential compatibility rules are in the
|
|
476
|
+
[mail reference](../reference/mail.md#operate-the-cli-and-gateway).
|
|
477
|
+
|
|
478
|
+
| Method or action | Gates | Decision, callers, and concrete purpose |
|
|
479
|
+
| --- | --- | --- |
|
|
480
|
+
| `readMailConfiguration` | Y/Y/N | Keep the cohesive configuration at the SDK owner. Send/serve read each required file once, in parallel when both are needed. A send with an injected credential reader skips the unused secrets file; incoming HTTP requests use the resolved configuration without file reads. Other capability settings stay untouched. |
|
|
481
|
+
| CLI's early default host, port, and send/serve profile values | Y/Y/Y | Remove premature default assignment. The same defaults remain after file configuration resolves, while explicit CLI values retain priority. Assigning defaults before reading JSON would conceal the deployment's chosen settings. |
|
|
482
|
+
| `origins` string array and list replacement | Y/Y/N | Keep one exact list for multiple caller domains. Explicit options replace the file list, including empty arrays. Existing `origin`, `allowTo`, `errorTo`, and `requestTimeout` aliases remain compatible and win over their canonical programmatic names when both are supplied. No normalization or list-merging helper is needed. |
|
|
483
|
+
| Root provider-key, named-profile, and TLS compatibility | Y/Y/N | Preserve live deployments using `RESEND_API_KEY`, `MAIL_PROFILES`, `MAIL_TLS_CERT_PATH`, and `MAIL_TLS_KEY_PATH`. Nested selected key presence takes priority even when null or empty. Named profiles never select a different account's default key. |
|
|
484
|
+
| Key set/status/delete operations | Y/Y/N | Preserve credential management and secret-free status. New keys use the nested member; existing legacy keys are updated in place unless a nested key exists. Delete removes both selected representations to prevent an old credential reappearing, preserving other keys, settings, and profile containers. Key commands still default to `mail` independently of the serving profile. |
|
|
485
|
+
| Send configuration | Y/Y/N | Use the same profile, sender, provider-timeout, and retry-guidance settings for direct sending. A send needs provider authority and its report, so listener certificates remain a serve-only prerequisite. Reports and idempotency keys remain per-operation inputs. |
|
|
486
|
+
| Explicit runtime dependencies and callbacks | Y/Y/N | Preserve `readCredential`, provider injection, observation, cancellation, and subscription callbacks as programmatic inputs. A function or signal is not JSON configuration. The portable browser mail import retains its existing dependency boundary. |
|
|
487
|
+
| Shared sender omission | Y/Y/N | Preserve per-report `from` and provider-template sender selection when no shared override is configured. One listener can therefore serve independent application sender identities. |
|
|
488
|
+
| Origin rejection behavior and explanation | Y/Y/N | Document the existing `403 mail_origin_not_allowed`, exact current-authority default, acceptance without Origin, and absence of IP filtering or loopback exceptions. This clarifies the retained behavior without adding admission policy. |
|
|
489
|
+
| New provider selector, per-field helper hierarchy, consumer parser, CLI-generation layer, automatic migration, watcher, and request-time config reads | N/N/Y | Add none. Resend is the sole implemented provider, the selected settings fit one owner, and existing CLI/API options express overrides. These additions would increase work without changing the requested outcome. |
|
|
490
|
+
|
|
491
|
+
The operation graph is at most two asynchronous JSON reads and one selected
|
|
492
|
+
credential per send/serve invocation, followed by its existing provider attempt
|
|
493
|
+
or listener lifecycle. A send with an injected credential reader reads only
|
|
494
|
+
`arcane.config.json` and calls that reader once. Key operations retain one
|
|
495
|
+
credential-file read and a write only when
|
|
496
|
+
the requested set/delete needs one. Certificate contents are still loaded by
|
|
497
|
+
the published HTTP server module. No dependency, process, polling loop, cache,
|
|
498
|
+
or application/host fan-out is introduced. These are source-level work counts,
|
|
499
|
+
not measured performance claims.
|
|
500
|
+
|
|
501
|
+
The compatibility audit traces the CLI into `executeMailCommand`, the public
|
|
502
|
+
`createToolchain().mail(...)` / `executeOperation('mail', ...)` entry points,
|
|
503
|
+
credential CRUD, direct send, and serve. The complete report and provider-result
|
|
504
|
+
paths are preserved. Configuration paths use the same portable Node filesystem
|
|
505
|
+
contract on Windows, Linux, and macOS, with a compatible Node host as the Android
|
|
506
|
+
adaptation. Local tests, checks, server launches, live mail sends, and platform
|
|
507
|
+
execution were not performed by this documentation author. Selected package
|
|
508
|
+
verification and publication outcomes belong to the release owner's delivery
|
|
509
|
+
record.
|
package/package.json
CHANGED
package/src/cli/main.mjs
CHANGED
|
@@ -86,7 +86,7 @@ Usage:
|
|
|
86
86
|
${CLI_NAME} mail key delete [profile]
|
|
87
87
|
${CLI_NAME} mail send [--profile <profile>] [--from <address>] --report-key <id> --report-stdin [--request-timeout <ms>]
|
|
88
88
|
${CLI_NAME} mail serve [--profile <profile>] [--from <address>] [--app <label>] [--origin <origin>] [--allow-to <addresses>] [--host 0.0.0.0] [--port 4433] [--request-timeout <ms>]
|
|
89
|
-
HTTPS/HTTP2; .
|
|
89
|
+
HTTPS/HTTP2; arcane.config.json.mail supplies settings; .arcane.env.json supplies keys.
|
|
90
90
|
|
|
91
91
|
Development:
|
|
92
92
|
--public Bind dev to all IPv4 interfaces (0.0.0.0) and print network URLs.
|
|
@@ -692,13 +692,13 @@ function operationOptions(command,parsed,cwd){
|
|
|
692
692
|
return {
|
|
693
693
|
action:'serve',
|
|
694
694
|
cwd,
|
|
695
|
-
profile:values.profile
|
|
695
|
+
profile:values.profile,
|
|
696
696
|
from:values.from,
|
|
697
697
|
appId:values.app,
|
|
698
698
|
origin:values.origin,
|
|
699
699
|
allowTo:values['allow-to'],
|
|
700
|
-
host:values.host
|
|
701
|
-
port:readPort(values.port
|
|
700
|
+
host:values.host,
|
|
701
|
+
port:readPort(values.port),
|
|
702
702
|
requestTimeout:readMailRequestTimeout(values['request-timeout']),
|
|
703
703
|
};
|
|
704
704
|
}
|
|
@@ -721,7 +721,7 @@ function operationOptions(command,parsed,cwd){
|
|
|
721
721
|
return {
|
|
722
722
|
action:'send',
|
|
723
723
|
cwd,
|
|
724
|
-
profile:values.profile
|
|
724
|
+
profile:values.profile,
|
|
725
725
|
from:values.from,
|
|
726
726
|
reportKey:values['report-key'],
|
|
727
727
|
reportStdin:true,
|
package/src/mail-credentials.mjs
CHANGED
|
@@ -11,7 +11,7 @@ export function mailCredentialLocation(options={}){
|
|
|
11
11
|
throw new ArcaneError(ERROR_CODES.usage,'Mail credential profile must be a nonempty string.');
|
|
12
12
|
}
|
|
13
13
|
return {
|
|
14
|
-
filePath:path.resolve(options.cwd??options.workspaceRoot??process.cwd(),'.env.json'),
|
|
14
|
+
filePath:path.resolve(options.cwd??options.workspaceRoot??process.cwd(),'.arcane.env.json'),
|
|
15
15
|
profile,
|
|
16
16
|
setting:profile==='mail'?'RESEND_API_KEY':`MAIL_PROFILES[${JSON.stringify(profile)}].RESEND_API_KEY`
|
|
17
17
|
};
|
|
@@ -41,45 +41,70 @@ async function readMailSettings(filePath,signal){
|
|
|
41
41
|
return settings;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function mailProfileSettings(settings,location){
|
|
44
|
+
function mailProfileSettings(settings,location,nested=false){
|
|
45
|
+
const source=nested?settings.mail:settings;
|
|
46
|
+
if(source===undefined)return undefined;
|
|
47
|
+
if(!source||!is.object(source)||is.array(source)){
|
|
48
|
+
throw new ArcaneError(ERROR_CODES.usage,`mail in ${location.filePath} must be an object.`);
|
|
49
|
+
}
|
|
45
50
|
if(location.profile==='mail'){
|
|
46
|
-
return
|
|
51
|
+
return source;
|
|
47
52
|
}
|
|
48
|
-
|
|
53
|
+
const profiles=nested?source.profiles:source.MAIL_PROFILES;
|
|
54
|
+
const label=nested?'mail.profiles':'MAIL_PROFILES';
|
|
55
|
+
if(profiles===undefined){
|
|
49
56
|
return undefined;
|
|
50
57
|
}
|
|
51
|
-
if(!
|
|
52
|
-
throw new ArcaneError(ERROR_CODES.usage
|
|
58
|
+
if(!profiles||!is.object(profiles)||is.array(profiles)){
|
|
59
|
+
throw new ArcaneError(ERROR_CODES.usage,`${label} in ${location.filePath} must be an object.`);
|
|
53
60
|
}
|
|
54
|
-
if(!Object.hasOwn(
|
|
61
|
+
if(!Object.hasOwn(profiles,location.profile)){
|
|
55
62
|
return undefined;
|
|
56
63
|
}
|
|
57
|
-
const profileSettings=
|
|
64
|
+
const profileSettings=profiles[location.profile];
|
|
58
65
|
if(!profileSettings||!is.object(profileSettings)||is.array(profileSettings)){
|
|
59
66
|
throw new ArcaneError(
|
|
60
67
|
ERROR_CODES.usage,
|
|
61
|
-
|
|
68
|
+
`${label}[${JSON.stringify(location.profile)}] in ${location.filePath} must be an object.`
|
|
62
69
|
);
|
|
63
70
|
}
|
|
64
71
|
return profileSettings;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
function mailCredentialEntry(settings, location) {
|
|
75
|
+
const nestedSettings = mailProfileSettings(settings, location, true);
|
|
76
|
+
const nestedEntry = {
|
|
77
|
+
profileSettings: nestedSettings,
|
|
78
|
+
key: 'apiKey',
|
|
79
|
+
setting: location.profile === 'mail'
|
|
80
|
+
? 'mail.apiKey'
|
|
81
|
+
: `mail.profiles[${JSON.stringify(location.profile)}].apiKey`
|
|
82
|
+
};
|
|
83
|
+
if (nestedSettings && Object.hasOwn(nestedSettings, 'apiKey')) return nestedEntry;
|
|
84
|
+
const legacySettings = mailProfileSettings(settings, location);
|
|
85
|
+
if (legacySettings && Object.hasOwn(legacySettings, 'RESEND_API_KEY')) {
|
|
86
|
+
return {profileSettings: legacySettings, key: 'RESEND_API_KEY', setting: location.setting};
|
|
87
|
+
}
|
|
88
|
+
return nestedEntry;
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
function configuredMailKey(settings,location){
|
|
68
|
-
const
|
|
92
|
+
const entry=mailCredentialEntry(settings,location);
|
|
93
|
+
const apiKey=entry.profileSettings?.[entry.key];
|
|
69
94
|
if(apiKey===undefined||apiKey===null||apiKey===''){
|
|
70
95
|
return null;
|
|
71
96
|
}
|
|
72
97
|
if(!is.string(apiKey)){
|
|
73
98
|
throw new ArcaneError(
|
|
74
99
|
ERROR_CODES.usage,
|
|
75
|
-
`${
|
|
100
|
+
`${entry.setting} in ${location.filePath} must be a string.`
|
|
76
101
|
);
|
|
77
102
|
}
|
|
78
103
|
return apiKey;
|
|
79
104
|
}
|
|
80
105
|
|
|
81
106
|
function mailCredentialStatus(profile,exists){
|
|
82
|
-
return {profile,provider:'resend',storage:'.env.json',exists};
|
|
107
|
+
return {profile,provider:'resend',storage:'.arcane.env.json',exists};
|
|
83
108
|
}
|
|
84
109
|
|
|
85
110
|
export async function setMailCredential(options={}){
|
|
@@ -88,16 +113,22 @@ export async function setMailCredential(options={}){
|
|
|
88
113
|
throw new ArcaneError(ERROR_CODES.usage,'The Resend API key must be a nonempty string.');
|
|
89
114
|
}
|
|
90
115
|
const settings=await readMailSettings(location.filePath,options.signal);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
116
|
+
const entry=mailCredentialEntry(settings,location);
|
|
117
|
+
if(entry.profileSettings){
|
|
118
|
+
entry.profileSettings[entry.key]=options.secret;
|
|
119
|
+
}else{
|
|
120
|
+
settings.mail??={};
|
|
121
|
+
if(location.profile==='mail'){
|
|
122
|
+
settings.mail.apiKey=options.secret;
|
|
123
|
+
}else{
|
|
124
|
+
settings.mail.profiles={
|
|
125
|
+
...settings.mail.profiles,
|
|
126
|
+
[location.profile]:{apiKey:options.secret}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
99
130
|
throwIfAborted(options.signal);
|
|
100
|
-
await writeFile(location.filePath,`${JSON.stringify(
|
|
131
|
+
await writeFile(location.filePath,`${JSON.stringify(settings,null,2)}\n`,{
|
|
101
132
|
encoding:'utf8',
|
|
102
133
|
mode:0o600,
|
|
103
134
|
signal:options.signal
|
|
@@ -111,30 +142,58 @@ export async function readMailCredential(options={}){
|
|
|
111
142
|
return configuredMailKey(settings,location);
|
|
112
143
|
}
|
|
113
144
|
|
|
114
|
-
export async function
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
export async function readMailConfiguration(options = {}) {
|
|
146
|
+
const directory = path.resolve(options.cwd ?? options.workspaceRoot ?? process.cwd());
|
|
147
|
+
const configPath = path.join(directory, 'arcane.config.json');
|
|
148
|
+
const envPath = path.join(directory, '.arcane.env.json');
|
|
149
|
+
const readCredentialFromFile = (options.readCredential ?? null) === null;
|
|
150
|
+
const [config, secrets] = await Promise.all(
|
|
151
|
+
[
|
|
152
|
+
readMailSettings(configPath, options.signal),
|
|
153
|
+
options.action !== 'send' || readCredentialFromFile
|
|
154
|
+
? readMailSettings(envPath, options.signal)
|
|
155
|
+
: {}
|
|
156
|
+
]
|
|
157
|
+
);
|
|
158
|
+
const mailSettings = config.mail === undefined ? {} : config.mail;
|
|
159
|
+
if (!mailSettings || !is.object(mailSettings) || is.array(mailSettings)) {
|
|
160
|
+
throw new ArcaneError(ERROR_CODES.usage, `mail in ${configPath} must be an object.`);
|
|
161
|
+
}
|
|
162
|
+
const location = mailCredentialLocation(
|
|
163
|
+
{...options, profile: options.profile !== undefined ? options.profile : mailSettings.profile}
|
|
164
|
+
);
|
|
165
|
+
const configuration = {profile: location.profile};
|
|
166
|
+
for (const name of [
|
|
167
|
+
'host', 'port', 'origins', 'from', 'appId', 'recipientAllowlist',
|
|
168
|
+
'errorRecipients', 'bodyTimeoutMs', 'providerTimeoutMs', 'retryableDelayMs'
|
|
169
|
+
]) {
|
|
170
|
+
if (mailSettings[name] !== undefined) configuration[name] = mailSettings[name];
|
|
171
|
+
}
|
|
172
|
+
if (readCredentialFromFile) {
|
|
173
|
+
configuration.apiKey = configuredMailKey(secrets, location);
|
|
174
|
+
}
|
|
175
|
+
// Sending a report does not consume listener certificate configuration.
|
|
176
|
+
if (options.action === 'send') return configuration;
|
|
120
177
|
const tlsSettings = {
|
|
121
178
|
MAIL_TLS_CERT_PATH: 'certPath',
|
|
122
179
|
MAIL_TLS_KEY_PATH: 'keyPath'
|
|
123
180
|
};
|
|
124
181
|
for (const [setting, option] of Object.entries(tlsSettings)) {
|
|
125
|
-
const value =
|
|
182
|
+
const value = options[option] !== undefined
|
|
183
|
+
? options[option]
|
|
184
|
+
: mailSettings[option] !== undefined ? mailSettings[option] : secrets[setting];
|
|
126
185
|
if (value === undefined || value === null || value === '') {
|
|
127
186
|
continue;
|
|
128
187
|
}
|
|
129
188
|
if (!is.string(value)) {
|
|
130
189
|
throw new ArcaneError(
|
|
131
190
|
ERROR_CODES.usage,
|
|
132
|
-
|
|
191
|
+
`mail.${option} (${setting}) must be a PEM file path string.`
|
|
133
192
|
);
|
|
134
193
|
}
|
|
135
|
-
|
|
194
|
+
configuration[option] = path.resolve(directory, value);
|
|
136
195
|
}
|
|
137
|
-
return
|
|
196
|
+
return configuration;
|
|
138
197
|
}
|
|
139
198
|
|
|
140
199
|
export async function getMailCredentialStatus(options={}){
|
|
@@ -146,17 +205,18 @@ export async function getMailCredentialStatus(options={}){
|
|
|
146
205
|
export async function deleteMailCredential(options={}){
|
|
147
206
|
const location=mailCredentialLocation(options);
|
|
148
207
|
const settings=await readMailSettings(location.filePath,options.signal);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
208
|
+
let changed=false;
|
|
209
|
+
for(const nested of [true,false]){
|
|
210
|
+
const profileSettings=mailProfileSettings(settings,location,nested);
|
|
211
|
+
const key=nested?'apiKey':'RESEND_API_KEY';
|
|
212
|
+
if(profileSettings&&Object.hasOwn(profileSettings,key)){
|
|
213
|
+
delete profileSettings[key];
|
|
214
|
+
changed=true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if(changed){
|
|
158
218
|
throwIfAborted(options.signal);
|
|
159
|
-
await writeFile(location.filePath,`${JSON.stringify(
|
|
219
|
+
await writeFile(location.filePath,`${JSON.stringify(settings,null,2)}\n`,{
|
|
160
220
|
encoding:'utf8',
|
|
161
221
|
signal:options.signal
|
|
162
222
|
});
|
package/src/mail.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getMailCredentialStatus,
|
|
6
6
|
mailCredentialLocation,
|
|
7
7
|
readMailCredential,
|
|
8
|
-
|
|
8
|
+
readMailConfiguration,
|
|
9
9
|
setMailCredential
|
|
10
10
|
} from './mail-credentials.mjs';
|
|
11
11
|
import {sendResendMail,startResendMailServer} from './mail-server.mjs';
|
|
@@ -62,6 +62,12 @@ function mailCredentialOptions(options){
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function configuredMailOption(options, settings, name, alias = name) {
|
|
66
|
+
if (options[alias] !== undefined) return options[alias];
|
|
67
|
+
if (options[name] !== undefined) return options[name];
|
|
68
|
+
return settings[name];
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
async function setMailCredentialFromInput(options){
|
|
66
72
|
const readSecret=resolveMailCommandDependency(options,'readSecret',null);
|
|
67
73
|
const store=resolveMailCommandDependency(options,'setCredential',setMailCredential);
|
|
@@ -94,20 +100,23 @@ async function sendMailFromReport(options){
|
|
|
94
100
|
throwIfAborted(options.signal);
|
|
95
101
|
const report=await readReport();
|
|
96
102
|
throwIfAborted(options.signal);
|
|
97
|
-
|
|
103
|
+
const readConfiguration=resolveMailCommandDependency(options,'readServerSettings',readMailConfiguration);
|
|
104
|
+
const mailSettings=await readConfiguration(options);
|
|
105
|
+
throwIfAborted(options.signal);
|
|
106
|
+
let apiKey=await readMailProviderKey(options,mailSettings);
|
|
98
107
|
try{
|
|
99
108
|
throwIfAborted(options.signal);
|
|
100
109
|
const result=await send({
|
|
101
110
|
apiKey,
|
|
102
111
|
appId:'arcane-cli',
|
|
103
112
|
fetchImpl:options.fetchImpl,
|
|
104
|
-
from:options
|
|
113
|
+
from:configuredMailOption(options,mailSettings,'from'),
|
|
105
114
|
onEvent:options.onEvent,
|
|
106
|
-
providerTimeoutMs:options
|
|
115
|
+
providerTimeoutMs:configuredMailOption(options,mailSettings,'providerTimeoutMs','requestTimeout'),
|
|
107
116
|
report,
|
|
108
117
|
reportKey:options.reportKey,
|
|
109
118
|
requestIdFactory:options.requestIdFactory,
|
|
110
|
-
retryableDelayMs:options
|
|
119
|
+
retryableDelayMs:configuredMailOption(options,mailSettings,'retryableDelayMs'),
|
|
111
120
|
signal:options.signal
|
|
112
121
|
});
|
|
113
122
|
if(result?.classification==='accepted'&&result.status==='accepted'){
|
|
@@ -126,16 +135,21 @@ async function sendMailFromReport(options){
|
|
|
126
135
|
}
|
|
127
136
|
}
|
|
128
137
|
|
|
129
|
-
async function readMailProviderKey(options,
|
|
130
|
-
const credentialOptions=mailCredentialOptions(
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
async function readMailProviderKey(options, mailSettings){
|
|
139
|
+
const credentialOptions=mailCredentialOptions(
|
|
140
|
+
{...options,profile:options.profile??mailSettings?.profile}
|
|
141
|
+
);
|
|
142
|
+
const apiKey = mailSettings !== undefined && (options.readCredential ?? null) === null
|
|
143
|
+
? mailSettings.apiKey
|
|
133
144
|
: await resolveMailCommandDependency(options, 'readCredential', readMailCredential)(credentialOptions);
|
|
134
145
|
if(apiKey===null){
|
|
135
146
|
const location=mailCredentialLocation(credentialOptions);
|
|
147
|
+
const nestedSetting = location.profile === 'mail'
|
|
148
|
+
? 'mail.apiKey'
|
|
149
|
+
: `mail.profiles[${JSON.stringify(location.profile)}].apiKey`;
|
|
136
150
|
throw new ArcaneError(
|
|
137
151
|
ERROR_CODES.prerequisiteMissing,
|
|
138
|
-
`Missing ${location.setting} in ${location.filePath}.`
|
|
152
|
+
`Missing ${location.setting} or ${nestedSetting} in ${location.filePath}.`
|
|
139
153
|
);
|
|
140
154
|
}
|
|
141
155
|
if(!is.string(apiKey)||!apiKey){
|
|
@@ -149,11 +163,9 @@ async function readMailProviderKey(options, serverSettings){
|
|
|
149
163
|
|
|
150
164
|
async function serveMailGateway(options){
|
|
151
165
|
const startServer=resolveMailCommandDependency(options,'startServer',startResendMailServer);
|
|
152
|
-
const readServerSettings = resolveMailCommandDependency(options, 'readServerSettings',
|
|
166
|
+
const readServerSettings = resolveMailCommandDependency(options, 'readServerSettings', readMailConfiguration);
|
|
153
167
|
throwIfAborted(options.signal);
|
|
154
|
-
const serverSettings = await readServerSettings(
|
|
155
|
-
{...mailCredentialOptions(options), readCredential: options.readCredential}
|
|
156
|
-
);
|
|
168
|
+
const serverSettings = await readServerSettings(options);
|
|
157
169
|
throwIfAborted(options.signal);
|
|
158
170
|
let apiKey=await readMailProviderKey(options, serverSettings);
|
|
159
171
|
try{
|
|
@@ -165,32 +177,36 @@ async function serveMailGateway(options){
|
|
|
165
177
|
const location = mailCredentialLocation(options);
|
|
166
178
|
throw new ArcaneError(
|
|
167
179
|
ERROR_CODES.prerequisiteMissing,
|
|
168
|
-
`Missing ${missingSettings.join(', ')} in ${location.filePath}. Mail HTTPS requires a certificate and private key.`
|
|
180
|
+
`Missing ${missingSettings.join(', ')} in ${location.filePath}, or the corresponding mail.certPath/mail.keyPath in arcane.config.json. Mail HTTPS requires a certificate and private key.`
|
|
169
181
|
);
|
|
170
182
|
}
|
|
171
|
-
const recipientAllowlist=mailRecipientOptions(
|
|
172
|
-
|
|
183
|
+
const recipientAllowlist=mailRecipientOptions(
|
|
184
|
+
configuredMailOption(options,serverSettings,'recipientAllowlist','allowTo'),'recipientAllowlist'
|
|
185
|
+
);
|
|
186
|
+
const errorTo=configuredMailOption(options,serverSettings,'errorRecipients','errorTo');
|
|
187
|
+
const errorRecipients=errorTo===undefined
|
|
173
188
|
? recipientAllowlist
|
|
174
|
-
: mailRecipientOptions(
|
|
189
|
+
: mailRecipientOptions(errorTo,'errorRecipients');
|
|
190
|
+
const origins=configuredMailOption(options,serverSettings,'origins','origin');
|
|
175
191
|
return await startServer({
|
|
176
192
|
apiKey,
|
|
177
|
-
appId:options
|
|
178
|
-
allowedOrigins:
|
|
193
|
+
appId:configuredMailOption(options,serverSettings,'appId'),
|
|
194
|
+
allowedOrigins:origins===undefined
|
|
179
195
|
?[]
|
|
180
|
-
:is.array(
|
|
181
|
-
bodyTimeoutMs:options
|
|
196
|
+
:is.array(origins)?[...origins]:[origins],
|
|
197
|
+
bodyTimeoutMs:configuredMailOption(options,serverSettings,'bodyTimeoutMs'),
|
|
182
198
|
certPath:serverSettings.certPath,
|
|
183
199
|
errorRecipients,
|
|
184
200
|
fetchImpl:options.fetchImpl,
|
|
185
|
-
from:options
|
|
186
|
-
host:options
|
|
201
|
+
from:configuredMailOption(options,serverSettings,'from'),
|
|
202
|
+
host:configuredMailOption(options,serverSettings,'host')??'0.0.0.0',
|
|
187
203
|
keyPath:serverSettings.keyPath,
|
|
188
204
|
onEvent:options.onEvent,
|
|
189
|
-
port:options
|
|
190
|
-
providerTimeoutMs:options
|
|
205
|
+
port:configuredMailOption(options,serverSettings,'port'),
|
|
206
|
+
providerTimeoutMs:configuredMailOption(options,serverSettings,'providerTimeoutMs','requestTimeout'),
|
|
191
207
|
recipientAllowlist,
|
|
192
208
|
requestIdFactory:options.requestIdFactory,
|
|
193
|
-
retryableDelayMs:options
|
|
209
|
+
retryableDelayMs:configuredMailOption(options,serverSettings,'retryableDelayMs'),
|
|
194
210
|
signal:options.signal,
|
|
195
211
|
verifySubscription:options.verifySubscription
|
|
196
212
|
});
|