arcane-os 0.23.0 → 0.24.1
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 +24 -0
- package/README.md +8 -6
- package/docs/reference/cli.md +82 -38
- package/docs/reference/mail.md +213 -50
- package/docs/reviews/mail-server-purpose-review.md +84 -0
- package/package.json +1 -1
- package/src/cli/main.mjs +5 -5
- package/src/mail-credentials.mjs +99 -39
- package/src/mail.mjs +43 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.1
|
|
4
|
+
|
|
5
|
+
- Add a mail CLI parameter table with command scope, purpose, and defaults.
|
|
6
|
+
Explain the difference between `--app`, `X-Mail-App`, `--report-stdin`, and
|
|
7
|
+
the nonsecret `--report-key` used for intentional same-message retries.
|
|
8
|
+
- Remove redundant default-profile configuration and the alternate-account
|
|
9
|
+
placeholder from the main mail example. Align the CLI reference with the
|
|
10
|
+
shared configuration and credential files while preserving named-profile APIs.
|
|
11
|
+
- Clarify when subscription headers are required, how ordinary delivery failures
|
|
12
|
+
retain their existing report, and the separate browser callback and verification
|
|
13
|
+
service paths that can create feedback. Runtime behavior is unchanged.
|
|
14
|
+
|
|
15
|
+
## 0.24.0
|
|
16
|
+
|
|
17
|
+
- Read nonsecret mail settings from `arcane.config.json.mail` and provider keys
|
|
18
|
+
from `.arcane.env.json.mail`. Keep existing root keys, exact named profiles,
|
|
19
|
+
and TLS path settings working without migrating or rewriting either file.
|
|
20
|
+
- Let explicit CLI/API options override configuration, replace origin lists,
|
|
21
|
+
and apply listener defaults after file settings. Share configured profile,
|
|
22
|
+
sender and provider deadlines with `mail send` without requiring TLS paths.
|
|
23
|
+
- Preserve unrelated settings during credential updates and remove both selected
|
|
24
|
+
key representations on explicit deletion. Document configuration precedence,
|
|
25
|
+
origin rejection, startup, platform behavior and the purpose-gate review.
|
|
26
|
+
|
|
3
27
|
## 0.23.0
|
|
4
28
|
|
|
5
29
|
- Rename the mail configuration file to `.arcane.env.json`. Upgrade existing
|
package/README.md
CHANGED
|
@@ -19,15 +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.1` 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
|
-
|
|
30
|
-
|
|
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
|
|
31
33
|
until a `verifySubscription` callback is configured; that callback receives the
|
|
32
34
|
application name and bearer subscription key before each provider attempt.
|
|
33
35
|
See the [method and action gate report](docs/reviews/mail-server-purpose-review.md)
|
package/docs/reference/cli.md
CHANGED
|
@@ -812,10 +812,13 @@ npm exec -- arcane repo status
|
|
|
812
812
|
|
|
813
813
|
## `arcane mail`
|
|
814
814
|
|
|
815
|
-
###
|
|
815
|
+
### Configuration and provider credentials
|
|
816
816
|
|
|
817
|
-
|
|
818
|
-
|
|
817
|
+
Mail reads nonsecret settings from `arcane.config.json.mail` and the Resend key
|
|
818
|
+
from `.arcane.env.json.mail.apiKey` in the invocation directory on Windows,
|
|
819
|
+
Linux, and macOS. See [Mail CLI parameters](mail.md#mail-cli-parameters) for the
|
|
820
|
+
complete parameter, configuration-field, default, and precedence reference.
|
|
821
|
+
The credential subcommands manage the provider key:
|
|
819
822
|
|
|
820
823
|
```text
|
|
821
824
|
arcane mail key set [profile] [--secret-stdin]
|
|
@@ -826,26 +829,43 @@ arcane mail key delete [profile]
|
|
|
826
829
|
`key set` reads the Resend API key from a hidden terminal prompt. The
|
|
827
830
|
`--secret-stdin` form is for deliberately redirected non-interactive input and
|
|
828
831
|
rejects a TTY before reading. The key is written to `.arcane.env.json` and is never
|
|
829
|
-
accepted in argv or returned in status output. The optional
|
|
830
|
-
to `mail`,
|
|
831
|
-
selects `MAIL_PROFILES[profile].RESEND_API_KEY`, with no default-key fallback.
|
|
832
|
+
accepted in argv or returned in status output. The optional credential-command
|
|
833
|
+
profile defaults to `mail`, independently of the send/serve configuration.
|
|
832
834
|
|
|
833
|
-
The minimal
|
|
835
|
+
The minimal `.arcane.env.json` is:
|
|
834
836
|
|
|
835
837
|
```json
|
|
836
838
|
{
|
|
837
|
-
"
|
|
839
|
+
"mail": {
|
|
840
|
+
"apiKey": ""
|
|
841
|
+
}
|
|
838
842
|
}
|
|
839
843
|
```
|
|
840
844
|
|
|
841
845
|
Fill in the key before starting mail, and add `.arcane.env.json` to the project's
|
|
842
|
-
`.gitignore`; the SDK repository already ignores it.
|
|
843
|
-
|
|
846
|
+
`.gitignore`; the SDK repository already ignores it. Keep nonsecret listener
|
|
847
|
+
settings, including certificate file paths, in `arcane.config.json.mail`.
|
|
848
|
+
|
|
849
|
+
Named profiles remain an optional CLI/toolchain compatibility feature for
|
|
850
|
+
selecting another stored Resend key. An exact name selects
|
|
851
|
+
`mail.profiles[name].apiKey`; the name is independent of incoming application
|
|
852
|
+
names, domains, and subscription keys. One server process uses one selected
|
|
853
|
+
Resend key for all its requests. An absent named key never falls back to the
|
|
854
|
+
default account. Existing root `RESEND_API_KEY` and
|
|
855
|
+
`MAIL_PROFILES[name].RESEND_API_KEY` remain fallbacks when the corresponding
|
|
856
|
+
nested key property is absent. A nested property containing null or an empty
|
|
857
|
+
string means the selected key is absent and takes precedence over a legacy key.
|
|
858
|
+
|
|
859
|
+
Set and delete preserve the file's other settings and profile containers. New
|
|
860
|
+
keys are written to the nested mail member. Existing legacy keys are updated
|
|
861
|
+
in place unless the selected nested key property exists; delete removes both
|
|
862
|
+
representations of only the selected key. Status returns the selected profile,
|
|
844
863
|
`provider:'resend'`, `storage:'.arcane.env.json'`, and `exists`. Delete returns
|
|
845
864
|
`exists:false` for both a removed and an already-absent credential.
|
|
846
865
|
|
|
847
|
-
Programmatic `createToolchain().mail(...)` resolves the
|
|
848
|
-
|
|
866
|
+
Programmatic `createToolchain().mail(...)` resolves both files from the directory
|
|
867
|
+
selected by `cwd ?? workspaceRoot ?? process.cwd()`, with no upward search.
|
|
868
|
+
This uses ordinary Node file
|
|
849
869
|
access rather than platform-specific credential processes. An Android host
|
|
850
870
|
supplies a compatible Node runtime and an accessible configuration directory.
|
|
851
871
|
Existing Windows Credential Manager records remain untouched; the JSON reader
|
|
@@ -854,11 +874,13 @@ populate or depend on process environment variables for this key.
|
|
|
854
874
|
|
|
855
875
|
The SDK's installation directory does not affect this location. With an SDK
|
|
856
876
|
under `my-site/arcane-os-sdk/`, run the command from `my-site/` and keep
|
|
857
|
-
`my-site/.arcane.env.json` alongside that
|
|
877
|
+
`my-site/arcane.config.json` and `my-site/.arcane.env.json` alongside that
|
|
878
|
+
directory. Existing deployments using
|
|
858
879
|
SDK 0.22.1 or earlier must rename `.env.json` to `.arcane.env.json` while
|
|
859
880
|
preserving its contents; the loader reads only the new name.
|
|
860
881
|
|
|
861
|
-
|
|
882
|
+
Each file is an optional configuration source. Missing or empty selected keys
|
|
883
|
+
stop `send` and `serve` with the
|
|
862
884
|
configuration path and exact JSON setting to fill in. Invalid JSON and file
|
|
863
885
|
access failures remain observable without printing credential content.
|
|
864
886
|
|
|
@@ -890,11 +912,16 @@ shape:
|
|
|
890
912
|
|
|
891
913
|
The CLI forwards the complete provider fields, including template requests.
|
|
892
914
|
Resend owns their accepted shape. The adapter removes the application-only
|
|
893
|
-
`type` field and applies
|
|
894
|
-
template supplies the sender. Direct
|
|
895
|
-
no configured fallback recipients
|
|
896
|
-
|
|
897
|
-
|
|
915
|
+
`type` field and applies the shared sender from `--from` or `mail.from` when
|
|
916
|
+
supplied; otherwise the report or provider template supplies the sender. Direct
|
|
917
|
+
CLI sending has no configured fallback recipients and requires no listener TLS
|
|
918
|
+
paths. It consumes the configured provider timeout and retry guidance.
|
|
919
|
+
|
|
920
|
+
The Resend credential comes from the selected `.arcane.env.json` entry.
|
|
921
|
+
An explicit `--profile` overrides `arcane.config.json.mail.profile`; omitting
|
|
922
|
+
both selects the default `mail.apiKey`, with the legacy fallback described
|
|
923
|
+
above. Neither the key nor report content is accepted through argv or process
|
|
924
|
+
environment variables.
|
|
898
925
|
|
|
899
926
|
The caller owns the nonempty `--report-key`, which is forwarded unchanged.
|
|
900
927
|
Reuse the same key only with the same
|
|
@@ -918,22 +945,28 @@ loss after the attempt begins is ambiguous because Resend may have accepted it.
|
|
|
918
945
|
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>]
|
|
919
946
|
```
|
|
920
947
|
|
|
921
|
-
The selected `.arcane.env.json`
|
|
922
|
-
|
|
948
|
+
The selected `.arcane.env.json` entry supplies only the server-side Resend API
|
|
949
|
+
key. An explicit `--profile` overrides `arcane.config.json.mail.profile`;
|
|
950
|
+
omitting both selects the default key.
|
|
923
951
|
|
|
924
|
-
Add the listener's certificate
|
|
952
|
+
Add the listener's certificate paths to `arcane.config.json`:
|
|
925
953
|
|
|
926
954
|
```json
|
|
927
955
|
{
|
|
928
|
-
"
|
|
929
|
-
|
|
930
|
-
|
|
956
|
+
"mail": {
|
|
957
|
+
"certPath": ".arcane/mail/fullchain.pem",
|
|
958
|
+
"keyPath": ".arcane/mail/private-key.pem"
|
|
959
|
+
}
|
|
931
960
|
}
|
|
932
961
|
```
|
|
933
962
|
|
|
934
963
|
Supply an existing PEM certificate chain and its private key. Paths resolve
|
|
935
|
-
relative to
|
|
936
|
-
|
|
964
|
+
relative to the selected configuration directory, or may be absolute. They
|
|
965
|
+
belong to the listener regardless of the provider key selected. Legacy root
|
|
966
|
+
`MAIL_TLS_CERT_PATH` and `MAIL_TLS_KEY_PATH` in `.arcane.env.json` remain
|
|
967
|
+
fallbacks for omitted config paths. Explicit programmatic `certPath` and
|
|
968
|
+
`keyPath` options override those files. Missing TLS settings name the fields
|
|
969
|
+
to fill in before a listener
|
|
937
970
|
opens; the TLS owner reports PEM file errors. Keep private-key material outside
|
|
938
971
|
tracked source. The SDK repository already ignores `.arcane/` and `.arcane.env.json`.
|
|
939
972
|
|
|
@@ -949,7 +982,7 @@ server; the incoming request's `X-Mail-App` identifies the application for
|
|
|
949
982
|
subscription verification. The HTTP authentication contract pairs that
|
|
950
983
|
application with `Authorization: Bearer <subscription_key>`.
|
|
951
984
|
|
|
952
|
-
Subscription verification is disabled
|
|
985
|
+
Subscription verification is disabled when no callback is configured. The
|
|
953
986
|
programmatic `createToolchain().mail({action: 'serve', ...})` path accepts
|
|
954
987
|
`verifySubscription({appName, subscriptionKey, signal})`; supplying that callback
|
|
955
988
|
enables verification before each provider attempt. It must resolve to `true`
|
|
@@ -958,16 +991,27 @@ failure receives retryable 503; cancellation stops verification before sending.
|
|
|
958
991
|
The callback connects the actual TWiN Stripe endpoint when its contract is
|
|
959
992
|
ready. There is no guessed URL, response schema, or command-line endpoint flag.
|
|
960
993
|
|
|
961
|
-
The listener defaults to `0.0.0.0`;
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
994
|
+
The listener defaults to `0.0.0.0:4433`; `mail.host` and `mail.port` configure
|
|
995
|
+
the listener, and explicit `--host` and `--port` override them. Browser mail
|
|
996
|
+
defaults to `/v1/mail` on the current domain. `mail.origins` supplies an exact
|
|
997
|
+
origin array; `--origin` replaces it with one origin. Repeated `--origin`
|
|
998
|
+
options keep the last value. An absent or empty list selects the current
|
|
999
|
+
request authority. Other Origins receive `403 mail_origin_not_allowed`;
|
|
1000
|
+
requests without an Origin continue normally. This CORS configuration does
|
|
1001
|
+
not filter client IPs or give loopback special treatment.
|
|
1002
|
+
|
|
1003
|
+
`mail.recipientAllowlist` supplies the recipient list; `--allow-to` replaces it
|
|
1004
|
+
with a comma-separated list. CLI parsing preserves supplied address spelling
|
|
1005
|
+
and repeated entries. Programmatic `errorTo` or `mail.errorRecipients` selects
|
|
1006
|
+
fallback recipients for error reports. When neither is supplied, the effective
|
|
1007
|
+
recipient allowlist supplies that fallback; an explicit empty array supplies
|
|
1008
|
+
none. The [Mail CLI parameters](mail.md#mail-cli-parameters) reference also
|
|
1009
|
+
documents programmatic aliases and list-replacement precedence.
|
|
1010
|
+
|
|
1011
|
+
`--request-timeout` overrides `mail.providerTimeoutMs` and accepts 1 through
|
|
1012
|
+
2147483647 milliseconds, the Node timer range. An absent effective timeout
|
|
1013
|
+
adds no provider deadline; programmatic/configured null also selects no
|
|
1014
|
+
deadline. `mail.bodyTimeoutMs` selects the optional request-body deadline.
|
|
971
1015
|
|
|
972
1016
|
After binding, `server.ready` reports lifecycle fields such as
|
|
973
1017
|
protocol, optional app label, bind address, port, URL, and `callerAuthentication`
|
package/docs/reference/mail.md
CHANGED
|
@@ -272,51 +272,154 @@ committed acceptance result.
|
|
|
272
272
|
|
|
273
273
|
## Operate the CLI and gateway
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
### Mail CLI parameters
|
|
276
|
+
|
|
277
|
+
`mail serve` starts the shared mail server. `mail send` makes one direct Resend
|
|
278
|
+
request. `mail key set|status|delete` manages the provider credential.
|
|
279
|
+
|
|
280
|
+
| Parameter | Command | What it does | When omitted |
|
|
281
|
+
| --- | --- | --- | --- |
|
|
282
|
+
| `--app <label>` | `mail serve` | Supplies a fallback application label for server events. An incoming `X-Mail-App` supplies the request's label. This option does not choose a Stripe account or limit which apps may send mail. | Uses `mail.appId` when configured; otherwise adds no fallback label. |
|
|
283
|
+
| `--from <sender>` | `mail serve`, `mail send` | Overrides the sender, for example `"Dragon Dispatch <dispatch@example.com>"`. On the server, the override applies to every report. Resend must accept that sender. | Uses `mail.from` when configured, then the report's `from` or provider template default. |
|
|
284
|
+
| `--report-stdin` | `mail send` | Reads one complete UTF-8 report JSON object from redirected standard input, including recipients, subject, body, and other provider fields. | Required; the command reports a usage error. |
|
|
285
|
+
| `--report-key <id>` | `mail send` | Identifies one intended email and is forwarded unchanged as Resend's `Idempotency-Key`. Use a new value for a new email; reuse the value and unchanged report for an intentional retry. It is not a credential. | Required; the command reports a usage error. |
|
|
286
|
+
| `--host <address>` | `mail serve` | Selects the listener bind address. `0.0.0.0` accepts connections through all IPv4 interfaces. | Uses `mail.host`, then `0.0.0.0`. |
|
|
287
|
+
| `--port <number>` | `mail serve` | Selects the HTTPS listener port. Explicit `0` asks the operating system for an available port. | Uses `mail.port`, then `4433`. |
|
|
288
|
+
| `--origin <origin>` | `mail serve` | Replaces the configured CORS origins with one exact browser origin, including scheme and any nondefault port. Use the JSON `origins` array for multiple origins; repeated flags keep only the last value. | Uses `mail.origins`; an absent or empty list uses the current request authority. Requests without an `Origin` header continue normally. |
|
|
289
|
+
| `--allow-to <addresses>` | `mail serve` | Replaces the recipient allowlist with a comma-separated list. Applies to `to`, `cc`, and `bcc`, and supplies error-report fallback recipients unless `mail.errorRecipients` is configured separately. | Uses `mail.recipientAllowlist`; an absent or empty list leaves recipients unrestricted. |
|
|
290
|
+
| `--request-timeout <ms>` | `mail serve`, `mail send` | Sets the deadline for one Resend request, in milliseconds. A timeout after sending begins leaves delivery uncertain; it does not schedule a retry. | Uses `mail.providerTimeoutMs`; absent or null adds no provider deadline. |
|
|
291
|
+
| `--profile <name>` | `mail serve`, `mail send` | Selects an alternate saved Resend credential for this invocation. One listener uses that selected provider key for all callers. | Uses `mail.profile` if configured, otherwise the normal `mail.apiKey`. The single-key setup needs no profile setting. |
|
|
292
|
+
| `[profile]` | `mail key set\|status\|delete` | Optional positional name of the credential to manage; it is not a flag. | Manages the normal `mail.apiKey`, independently of `mail.profile`. |
|
|
293
|
+
| `--secret-stdin` | `mail key set` | Reads the Resend API key from deliberately redirected standard input instead of the hidden terminal prompt. | Prompts with hidden input. Machine-output mode requires redirected input. |
|
|
294
|
+
| `--output human\|json\|ndjson` | All mail commands | Selects readable console output, one JSON result, or newline-delimited JSON events. | Uses `human`. |
|
|
295
|
+
| `--help` | CLI | Displays CLI help instead of running the mail operation. | Runs the requested operation. |
|
|
296
|
+
| `--version` | CLI | Displays the SDK version instead of running the mail operation. | Runs the requested operation. |
|
|
297
|
+
|
|
298
|
+
There is no `--report` flag. `--report-stdin` supplies the message;
|
|
299
|
+
`--report-key` identifies that same message across attempts. Browser `Mail.send()`
|
|
300
|
+
generates and retains its report key automatically.
|
|
301
|
+
|
|
302
|
+
Mail reads configuration from the directory in which the command runs;
|
|
303
|
+
`--workspace` does not relocate these mail files. Mail HTTPS uses `mail.certPath`
|
|
304
|
+
and `mail.keyPath` in JSON. The `--cert`, `--key`, `--https`, and `--http-port`
|
|
305
|
+
flags belong to browser `dev`/`run`, and `--http` belongs to `dev`.
|
|
306
|
+
|
|
307
|
+
### Mail configuration
|
|
308
|
+
|
|
309
|
+
Keep app-supplied SDK settings under named capability members, starting with
|
|
310
|
+
`mail`. Put the nonsecret mail settings in `arcane.config.json` in the directory
|
|
311
|
+
from which the command runs:
|
|
277
312
|
|
|
278
313
|
```json
|
|
279
314
|
{
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
|
|
315
|
+
"mail": {
|
|
316
|
+
"host": "0.0.0.0",
|
|
317
|
+
"port": 4433,
|
|
318
|
+
"origins": [
|
|
319
|
+
"https://dragons.example",
|
|
320
|
+
"https://www.dragons.example"
|
|
321
|
+
],
|
|
322
|
+
"certPath": "certificates/fullchain.pem",
|
|
323
|
+
"keyPath": "certificates/private-key.pem"
|
|
324
|
+
}
|
|
283
325
|
}
|
|
284
326
|
```
|
|
285
327
|
|
|
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`.
|
|
289
|
-
|
|
290
|
-
The default profile is `mail`, which selects top-level `RESEND_API_KEY`.
|
|
291
|
-
The explicit `--profile mail` form selects the same setting. Other profile names
|
|
292
|
-
select exact entries under `MAIL_PROFILES`:
|
|
328
|
+
Put the Resend provider key in the separate `.arcane.env.json`:
|
|
293
329
|
|
|
294
330
|
```json
|
|
295
331
|
{
|
|
296
|
-
"
|
|
297
|
-
|
|
298
|
-
"another-provider-account": {
|
|
299
|
-
"RESEND_API_KEY": ""
|
|
300
|
-
}
|
|
332
|
+
"mail": {
|
|
333
|
+
"apiKey": ""
|
|
301
334
|
}
|
|
302
335
|
}
|
|
303
336
|
```
|
|
304
337
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
338
|
+
The SDK repository ignores `.arcane.env.json`. Keep the same entry in a consuming
|
|
339
|
+
project's `.gitignore`. `arcane.config.json` contains settings suitable for source
|
|
340
|
+
control; its certificate fields contain file paths, never PEM contents or provider
|
|
341
|
+
keys. Both files are read directly as JSON without copying values into
|
|
342
|
+
`process.env`. Other top-level capability members remain untouched. The portable
|
|
343
|
+
browser `arcane-os/mail` import does not read these Node-side files.
|
|
344
|
+
|
|
345
|
+
The supported `arcane.config.json.mail` fields are:
|
|
308
346
|
|
|
309
|
-
|
|
347
|
+
| Field | Type | Purpose and default |
|
|
348
|
+
| --- | --- | --- |
|
|
349
|
+
| `host` | string | Listener bind address; defaults to `0.0.0.0`. |
|
|
350
|
+
| `port` | integer | Listener port; defaults to `4433`. Explicit `0` selects an available port. |
|
|
351
|
+
| `origins` | string array | Exact allowed browser origins. An absent or empty list uses the current request authority as described below. |
|
|
352
|
+
| `profile` | string | Resend credential profile for send/serve; defaults to `mail`. |
|
|
353
|
+
| `from` | string | Optional shared sender override. Omit it to retain each report's sender or provider template default. |
|
|
354
|
+
| `appId` | string | Optional server event label; does not restrict incoming application names. |
|
|
355
|
+
| `recipientAllowlist` | string array | Optional allowed recipients; absent or empty means unrestricted recipients. |
|
|
356
|
+
| `errorRecipients` | string array | Error-report fallback recipients; defaults to the effective recipient allowlist. An explicit empty array supplies no fallback. |
|
|
357
|
+
| `bodyTimeoutMs` | integer or null | Optional request-body deadline in milliseconds; absent or null adds no deadline. |
|
|
358
|
+
| `providerTimeoutMs` | integer or null | Optional provider deadline in milliseconds; absent or null adds no deadline. |
|
|
359
|
+
| `retryableDelayMs` | positive integer | Retry guidance in a retryable result; defaults to `1000`. It does not schedule a retry. |
|
|
360
|
+
| `certPath` | string | PEM certificate-chain file path, required for gateway HTTPS. |
|
|
361
|
+
| `keyPath` | string | PEM private-key file path, required for gateway HTTPS. |
|
|
362
|
+
|
|
363
|
+
Resend is the supported provider; no provider selector is needed. Keep callbacks,
|
|
364
|
+
injected providers, `fetchImpl`, `onEvent`, `requestIdFactory`,
|
|
365
|
+
`verifySubscription`, and `AbortSignal` values in programmatic options. They are
|
|
366
|
+
runtime inputs, not JSON settings. Reports and their idempotency keys remain
|
|
367
|
+
inputs to each send operation.
|
|
368
|
+
|
|
369
|
+
The single-key configuration above needs no `profile` or `profiles` member.
|
|
370
|
+
Omitting the selection uses `.arcane.env.json.mail.apiKey`; multiple apps and
|
|
371
|
+
domains can share that provider key.
|
|
372
|
+
|
|
373
|
+
For callers that already select an alternate credential, named-profile support
|
|
374
|
+
remains available: `--profile <name>` or `mail.profile` selects
|
|
375
|
+
`.arcane.env.json.mail.profiles[name].apiKey`. An absent named profile does not
|
|
376
|
+
fall back to the default key. Selection happens once when send or serve starts;
|
|
377
|
+
it does not map incoming apps or domains to different provider accounts.
|
|
378
|
+
The profile selects Resend provider credentials, separately from the incoming
|
|
379
|
+
application name and subscriber key. Existing top-level `RESEND_API_KEY` and
|
|
380
|
+
`MAIL_PROFILES[profile].RESEND_API_KEY` remain supported. A nested selected
|
|
381
|
+
`apiKey` takes precedence when the property exists, including null or an empty
|
|
382
|
+
string, which means the selected key is absent. Only an absent nested key
|
|
383
|
+
property permits fallback to the corresponding legacy key.
|
|
384
|
+
|
|
385
|
+
Programmatic operations resolve both files from `options.cwd`, then
|
|
310
386
|
`options.workspaceRoot`, then `process.cwd()`, choosing the first supplied
|
|
311
387
|
directory. The CLI uses its invocation directory. There is no upward directory
|
|
312
388
|
search or dependency on a Windows installation directory or temporary-directory
|
|
313
|
-
environment variable.
|
|
389
|
+
environment variable. Missing files are optional configuration sources; send and
|
|
390
|
+
serve still report their missing required values before attempting delivery or
|
|
391
|
+
binding. Malformed or unreadable files produce an error.
|
|
392
|
+
|
|
393
|
+
Configuration precedence is explicit:
|
|
394
|
+
|
|
395
|
+
1. A CLI/API option overrides its file setting when its value is not `undefined`.
|
|
396
|
+
An explicit null retains the option's existing meaning; it does not select
|
|
397
|
+
the file value again.
|
|
398
|
+
2. `arcane.config.json.mail` supplies nonsecret settings absent from those options.
|
|
399
|
+
3. Legacy `.arcane.env.json` root `MAIL_TLS_CERT_PATH` and `MAIL_TLS_KEY_PATH`
|
|
400
|
+
supply certificate paths absent from the selected options and config member.
|
|
401
|
+
4. Remaining settings use the defaults above.
|
|
402
|
+
|
|
403
|
+
The existing programmatic aliases `origin`, `allowTo`, `errorTo`, and
|
|
404
|
+
`requestTimeout` take precedence over their corresponding canonical options
|
|
405
|
+
`origins`, `recipientAllowlist`, `errorRecipients`, and `providerTimeoutMs` when
|
|
406
|
+
both are supplied. The CLI continues to expose `--origin`, `--allow-to`, and
|
|
407
|
+
`--request-timeout`. `origin` accepts a string or an array; the recipient aliases
|
|
408
|
+
accept address arrays or comma-separated strings. Lists replace the lower-priority
|
|
409
|
+
list completely. The configuration reader does not concatenate or deduplicate
|
|
410
|
+
lists, rewrite case, or automatically add local addresses.
|
|
411
|
+
|
|
412
|
+
Use the JSON `origins` array for multiple origins. Repeating the current
|
|
413
|
+
`--origin` option keeps only its last value; there is no `--origins` CLI option.
|
|
414
|
+
The CLI leaves omitted host, port, and send/serve profile options unset until
|
|
415
|
+
configuration resolves, so its defaults do not mask file settings.
|
|
314
416
|
|
|
315
417
|
Keep the configuration in the deployment directory even when the SDK is nested
|
|
316
418
|
below it:
|
|
317
419
|
|
|
318
420
|
```text
|
|
319
421
|
my-site/
|
|
422
|
+
├── arcane.config.json
|
|
320
423
|
├── .arcane.env.json
|
|
321
424
|
└── arcane-os-sdk/
|
|
322
425
|
└── bin/arcane.mjs
|
|
@@ -325,13 +428,15 @@ my-site/
|
|
|
325
428
|
Run from `my-site`, for example:
|
|
326
429
|
|
|
327
430
|
```sh
|
|
328
|
-
node ./arcane-os-sdk/bin/arcane.mjs mail serve
|
|
431
|
+
node ./arcane-os-sdk/bin/arcane.mjs mail serve
|
|
329
432
|
```
|
|
330
433
|
|
|
331
434
|
The SDK directory does not choose the configuration location. When upgrading
|
|
332
435
|
from SDK 0.22.1 or earlier, rename the existing `.env.json` to
|
|
333
436
|
`.arcane.env.json` in the invocation directory, preserving its contents.
|
|
334
|
-
The loader reads only `.arcane.env.json
|
|
437
|
+
The secret loader reads only `.arcane.env.json`; it does not read the old filename.
|
|
438
|
+
Existing root key, profile, and TLS fields can remain in that file. Adopting the
|
|
439
|
+
capability members does not perform an automatic rewrite or migration.
|
|
335
440
|
|
|
336
441
|
The existing key commands manage the same file:
|
|
337
442
|
|
|
@@ -343,8 +448,14 @@ arcane mail key delete
|
|
|
343
448
|
|
|
344
449
|
`key set` prompts with hidden input. `--secret-stdin` is the explicit
|
|
345
450
|
non-interactive alternative and rejects a TTY. Each command accepts an optional
|
|
346
|
-
profile argument, defaulting to `mail
|
|
347
|
-
settings and profiles; status reports
|
|
451
|
+
profile argument, defaulting to `mail` independently of `arcane.config.json.mail.profile`.
|
|
452
|
+
Set and delete preserve other JSON settings and profiles; status reports
|
|
453
|
+
existence without returning the key. A new credential is written to the nested
|
|
454
|
+
mail member. An existing legacy credential is updated at its existing location
|
|
455
|
+
unless the selected nested key property exists, in which case set updates that
|
|
456
|
+
nested property. Delete removes both representations of only the selected key,
|
|
457
|
+
so an older key cannot reappear through fallback. Other settings and profile
|
|
458
|
+
containers remain intact.
|
|
348
459
|
Results identify `storage: '.arcane.env.json'`. An already-absent deletion succeeds
|
|
349
460
|
with `exists: false`.
|
|
350
461
|
|
|
@@ -363,8 +474,8 @@ arcane mail send --from "Arcane <verified@example.com>" --report-key <stable-id>
|
|
|
363
474
|
The redirected UTF-8 JSON object is read completely. Its fields and values
|
|
364
475
|
are retained; the Resend adapter removes the SDK's `type` routing field,
|
|
365
476
|
uses `from` when configured (otherwise the report or provider template supplies
|
|
366
|
-
the sender)
|
|
367
|
-
|
|
477
|
+
the sender). Direct sending supplies no fallback recipients. Resend evaluates
|
|
478
|
+
its own required provider fields. Message
|
|
368
479
|
content is not accepted in argv. Programmatic results and observer events
|
|
369
480
|
preserve the complete report, provider request, provider response, and error detail while
|
|
370
481
|
never exposing either credential.
|
|
@@ -379,19 +490,28 @@ cancellation after the provider attempt begins is returned as an ambiguous
|
|
|
379
490
|
nonzero outcome because the provider may already have accepted the request.
|
|
380
491
|
Cancellation before the attempt exits 130 without sending.
|
|
381
492
|
For both CLI mail operations, `--request-timeout` accepts 1 through 2147483647
|
|
382
|
-
milliseconds, the Node timer range.
|
|
383
|
-
|
|
493
|
+
milliseconds, the Node timer range. The same range applies to configured body
|
|
494
|
+
and provider deadlines. When no provider timeout is selected in options or
|
|
495
|
+
configuration, the SDK adds no provider deadline.
|
|
496
|
+
|
|
497
|
+
`mail send` consumes the selected profile, sender, provider timeout, and retry
|
|
498
|
+
guidance from the same configuration. It does not require gateway TLS paths.
|
|
499
|
+
Send and serve read each required JSON file once, concurrently when both are
|
|
500
|
+
needed, before consuming their settings. An injected `readCredential` remains
|
|
501
|
+
the credential owner and reads once. With that injection, send reads only
|
|
502
|
+
`arcane.config.json`; serve also reads `.arcane.env.json` for legacy TLS paths
|
|
503
|
+
without interpreting its unused file credential.
|
|
384
504
|
|
|
385
505
|
Start the gateway:
|
|
386
506
|
|
|
387
507
|
```text
|
|
388
|
-
npm exec -- arcane mail serve
|
|
508
|
+
npm exec -- arcane mail serve
|
|
389
509
|
```
|
|
390
510
|
|
|
391
|
-
The default listener is `0.0.0.0:4433`;
|
|
392
|
-
address and port
|
|
393
|
-
|
|
394
|
-
|
|
511
|
+
The default listener is `0.0.0.0:4433`; `mail.host` and `mail.port` select its
|
|
512
|
+
configured bind address and port, and explicit `--host` / `--port` override them.
|
|
513
|
+
The server can serve callers from multiple domains on the same machine.
|
|
514
|
+
Configure the caller's endpoint to reach that listener.
|
|
395
515
|
|
|
396
516
|
`mail serve` uses HTTPS with HTTP/2 on that selected port. The published
|
|
397
517
|
`node-http-server` PEM API owns TLS and negotiates HTTP/2 or HTTP/1.1 on the
|
|
@@ -399,25 +519,29 @@ same listener. It creates no additional plain-HTTP listener. The returned URL
|
|
|
399
519
|
uses `https://`; `0.0.0.0` is the bind address, so callers use the deployed
|
|
400
520
|
domain, for example `https://mail.example.com:4433/v1/mail`.
|
|
401
521
|
|
|
402
|
-
Set `
|
|
403
|
-
to its PEM private-key file. These
|
|
404
|
-
and apply regardless of the selected provider profile.
|
|
405
|
-
|
|
522
|
+
Set `arcane.config.json.mail.certPath` to the PEM certificate chain and
|
|
523
|
+
`mail.keyPath` to its PEM private-key file. These settings belong to the listener
|
|
524
|
+
and apply regardless of the selected provider profile. The legacy root
|
|
525
|
+
`MAIL_TLS_CERT_PATH` and `MAIL_TLS_KEY_PATH` fields in `.arcane.env.json` remain
|
|
526
|
+
fallbacks. Relative certificate paths resolve from the selected configuration
|
|
527
|
+
directory, including explicit programmatic path options; absolute paths are also accepted.
|
|
406
528
|
The certificate must cover the hostname callers use. One certificate may
|
|
407
529
|
cover multiple names; the gateway does not require one certificate per calling
|
|
408
530
|
application. Keep private-key files outside tracked source, such as in the
|
|
409
531
|
already-ignored `.arcane/` directory or an existing host certificate directory.
|
|
410
532
|
|
|
411
|
-
Startup reads
|
|
533
|
+
Startup reads each JSON file once, reports missing TLS settings before
|
|
412
534
|
binding, and lets the TLS owner report unreadable or unusable PEM files. It
|
|
413
535
|
does not generate certificates, modify system trust, or add a renewal watcher.
|
|
536
|
+
No JSON configuration file is reread for an incoming HTTP request.
|
|
414
537
|
Restart the gateway after the configured certificate files are renewed.
|
|
415
538
|
The same Node file and TLS APIs are used on Windows, Linux, and macOS; Android
|
|
416
539
|
requires a compatible Node host and accessible configuration and certificate
|
|
417
540
|
paths. These platform contracts are separate from actual platform execution.
|
|
418
541
|
|
|
419
542
|
The public `createToolchain().mail({action: 'serve', ...options})` operation
|
|
420
|
-
uses the same
|
|
543
|
+
uses the same settings with explicit options taking precedence. Internally,
|
|
544
|
+
`startResendMailServer` accepts
|
|
421
545
|
`certPath` and `keyPath` and retains its existing HTTP behavior when neither
|
|
422
546
|
is supplied. That internal function is not an npm package export.
|
|
423
547
|
|
|
@@ -429,18 +553,25 @@ The existing listener remains running; this launch does not retry or select
|
|
|
429
553
|
another port.
|
|
430
554
|
|
|
431
555
|
`--app` is an optional server event label and does not restrict incoming
|
|
432
|
-
application names. `--from`
|
|
433
|
-
preserve each report's sender or its provider template's default.
|
|
434
|
-
|
|
435
|
-
programmatic `origin`
|
|
436
|
-
no origins configured, the gateway accepts an Origin matching its request
|
|
556
|
+
application names. `--from` and `mail.from` are optional shared sender overrides;
|
|
557
|
+
omit both to preserve each report's sender or its provider template's default.
|
|
558
|
+
`mail.origins` supplies an array of exact allowed caller origins. An explicit
|
|
559
|
+
`--origin` replaces that array; the programmatic `origin` alias also accepts an
|
|
560
|
+
array. With no origins configured, the gateway accepts an Origin matching its request
|
|
437
561
|
authority (`:authority` for HTTP/2, `Host` for HTTP/1.1) using HTTP or HTTPS.
|
|
438
|
-
|
|
562
|
+
An Origin outside the configured list or current-authority default receives
|
|
563
|
+
`403 mail_origin_not_allowed`. Origin strings are compared exactly: include the
|
|
564
|
+
scheme and any nondefault port, with no path or trailing slash. There are no
|
|
565
|
+
wildcards, subdomain expansion, normalization, or loopback exceptions.
|
|
566
|
+
Requests without an `Origin` header continue normally. This is a declared-origin
|
|
567
|
+
CORS list, not a client-IP or connecting-machine allowlist; ordinary
|
|
568
|
+
server-to-server requests commonly omit Origin.
|
|
439
569
|
Cross-origin preflight permits `Content-Type`, `Idempotency-Key`, `X-Mail-App`,
|
|
440
|
-
and `Authorization`.
|
|
570
|
+
and `Authorization`.
|
|
441
571
|
|
|
442
|
-
`--allow-to`
|
|
443
|
-
|
|
572
|
+
`mail.recipientAllowlist` or explicit `--allow-to` limits recipients when
|
|
573
|
+
configured. An absent or empty effective list imposes no recipient allowlist.
|
|
574
|
+
When configured, the list applies to
|
|
444
575
|
every recipient in the resolved `to`, `cc`, and `bcc` fields, whether supplied
|
|
445
576
|
as a string or an array. Sender, recipient, subject, and body
|
|
446
577
|
values are not trimmed, lowercased, or filtered by an SDK email grammar at
|
|
@@ -473,6 +604,12 @@ key, and `signal` follows the request lifecycle. These control fields stay
|
|
|
473
604
|
separate from the mail report and Resend payload. The verifier runs for each
|
|
474
605
|
POST request before any provider attempt; results are not cached.
|
|
475
606
|
|
|
607
|
+
`X-Mail-App` identifies the originating app so the verifier can select that
|
|
608
|
+
app's subscription account, such as the appropriate Stripe account for `BOSS`
|
|
609
|
+
or `TWiN`. It is separate from the gateway's optional `--app` event label.
|
|
610
|
+
The missing-header and Bearer errors below apply only when `verifySubscription`
|
|
611
|
+
is configured. Ordinary `mail serve` does not require either header.
|
|
612
|
+
|
|
476
613
|
| Configured-verifier outcome | Gateway response |
|
|
477
614
|
| --- | --- |
|
|
478
615
|
| Missing or empty `X-Mail-App` | `400 mail_invalid_headers` |
|
|
@@ -496,6 +633,32 @@ CLI startup output says `Subscription verification: disabled` or
|
|
|
496
633
|
`Subscription verification: configured`. Structured `server.ready` output
|
|
497
634
|
includes the corresponding `callerAuthentication` value.
|
|
498
635
|
|
|
636
|
+
## Error reports and retries
|
|
637
|
+
|
|
638
|
+
An incoming report with `type: 'error'` is an email supplied by its caller.
|
|
639
|
+
The gateway's `errorRecipients` setting only supplies fallback recipients for
|
|
640
|
+
that report when its `to` array is empty. A gateway or provider failure returns
|
|
641
|
+
an HTTP error and diagnostic events; it does not generate another error email.
|
|
642
|
+
Gateway event-handler failures are written to the console. Each POST and each
|
|
643
|
+
direct CLI send performs at most one provider attempt, with no automatic server retry.
|
|
644
|
+
|
|
645
|
+
The browser outbox retains the same report and key after a retryable delivery
|
|
646
|
+
failure. Each drain attempts each eligible record once; a later startup,
|
|
647
|
+
connectivity event, or explicit drain can retry it. Browser `Errors` notification
|
|
648
|
+
failure retains the same pending occurrence and logs a warning without scheduling
|
|
649
|
+
itself again.
|
|
650
|
+
|
|
651
|
+
A separate browser callback path can feed back into error reporting: a mail-event
|
|
652
|
+
subscriber that repeatedly throws can reach global error capture, create another
|
|
653
|
+
error report, and trigger another mail event. Subscriber failures cannot change
|
|
654
|
+
an already committed mail result, though global capture can report them as new
|
|
655
|
+
errors. This is a source-level possibility, not evidence of a deployed loop.
|
|
656
|
+
|
|
657
|
+
For a verification service that sends its own emails, its host integration must
|
|
658
|
+
also avoid the circular request path `mail -> verify subscription -> send mail ->
|
|
659
|
+
verify subscription`. The SDK supplies a callback for verification; it does not
|
|
660
|
+
choose that service's caller flow or enable it in ordinary CLI startup.
|
|
661
|
+
|
|
499
662
|
## Gateway request lifecycle
|
|
500
663
|
|
|
501
664
|
The gateway uses the published `node-http-server` instance lifecycle. Its raw
|
|
@@ -457,3 +457,87 @@ payloads, profiles, other settings and lifecycle remain unchanged.
|
|
|
457
457
|
|
|
458
458
|
Existing credential and CLI test fixtures now use the selected name. Source
|
|
459
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 supported inputs 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. Named-profile deployment use is unverified; the retained behavior is a compatibility contract. |
|
|
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.
|
|
510
|
+
|
|
511
|
+
## Parameter examples and error-report path clarification
|
|
512
|
+
|
|
513
|
+
The ordinary shared mail server needs one Resend key. Its current-domain
|
|
514
|
+
behavior, origin list, and incoming application/subscription verification do
|
|
515
|
+
not select among provider profiles. A named profile selects one stored key
|
|
516
|
+
for an entire send or server invocation. Source inspection found named CLI and
|
|
517
|
+
toolchain inputs plus synthetic tests; it did not establish a deployment that
|
|
518
|
+
uses named mail profiles.
|
|
519
|
+
|
|
520
|
+
| Method or action | Gates | Decision, callers, and concrete purpose |
|
|
521
|
+
| --- | --- | --- |
|
|
522
|
+
| Explicit default `profile: 'mail'` in setup examples | N/N/Y | Remove the redundant setting. Omission already selects the default key when no named configuration is supplied. CLI send/serve still honor an explicitly selected profile or `mail.profile`. |
|
|
523
|
+
| Extra `another-provider-account` credential placeholder | N/N/Y | Remove it from ordinary setup. It adds an unused account choice to a one-key configuration. Existing optional named-profile behavior remains documented without presenting another account as a setup requirement. |
|
|
524
|
+
| Named-profile CLI/toolchain capability and credential CRUD | Y/Y/N for compatibility | Retain the supported optional profile argument and exact named-key operations. Concrete application/deployment use is unverified. This is an account-selection contract, separate from removing redundant default arguments; its retention makes no claim that the nominal deployment needs it. |
|
|
525
|
+
| Full CLI parameter explanation | Y/Y/N | Keep one canonical [Mail CLI parameters](../reference/mail.md#mail-cli-parameters) table. The CLI reference links to it and describes the current split configuration, selected values, defaults, aliases, and provider behavior without another competing table. |
|
|
526
|
+
| Claim that mail errors can never produce an error-report loop | N/N/Y | Replace the absolute claim with the source-path qualification below. Ordinary server failures do not send another mail; a repeatedly failing browser event subscriber can produce new global error incidents during error-mail delivery. |
|
|
527
|
+
|
|
528
|
+
The Node gateway reports ordinary request/provider failures through responses,
|
|
529
|
+
observer results, or its console logger. Its error reporter does not call the
|
|
530
|
+
mail sender. In the browser, `Errors` owns global `error` and
|
|
531
|
+
`unhandledrejection` capture and sends error notifications through Mail. A
|
|
532
|
+
failed notification remains pending with a warning rather than immediately
|
|
533
|
+
creating another notification from that send failure.
|
|
534
|
+
|
|
535
|
+
There is a separate conditional path: a subscriber that repeatedly throws
|
|
536
|
+
while handling Mail events reaches the event authority's listener-failure
|
|
537
|
+
reporter. In browsers with `reportError`, that failure can become a new global
|
|
538
|
+
error incident. Sending that incident through Mail can invoke the same failing
|
|
539
|
+
subscriber again. A loop therefore depends on that subscriber and the active
|
|
540
|
+
global error handler; it is not an automatic server response to an ordinary
|
|
541
|
+
mail failure. This is a static source-path finding, not an observed production
|
|
542
|
+
incident or an executed reproduction. This documentation clarification changes
|
|
543
|
+
no error, subscription, retry, or delivery behavior.
|
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
|
@@ -41,38 +41,63 @@ 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;
|
|
@@ -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
|
});
|