arcane-os 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0
4
+
5
+ - Make mail hosting domain-based: use the current browser origin by default,
6
+ preserve explicit endpoints, support any application name, and remove
7
+ loopback-only binding and request admission. Keep exact configured CORS
8
+ allowlists and recipient policy for To, Cc, and Bcc. Make the CLI sender
9
+ override optional so reports and provider templates can supply their sender.
10
+ - Replace the local app-key and digest mechanism with optional
11
+ `verifySubscription({appName, subscriptionKey, signal})` configuration.
12
+ Verification is disabled until configured for the staged service setup.
13
+ A configured verifier must accept each app and bearer key before provider
14
+ delivery; rejection, service failure, and cancellation remain distinct.
15
+ - Remove redundant header reconstruction, address/origin rewriting, local
16
+ provider payload and identifier grammars, duplicate setup, manual response
17
+ length calculation, unused CLI queue options, and absent-observer work.
18
+ Preserve complete provider fields and responses, idempotency, caller-selected
19
+ deadlines, concurrent requests, and owned shutdown. Report observer and
20
+ escaped handler failures without changing provider acceptance.
21
+ - Resolve subscription credentials from the current User at HTTP delivery,
22
+ preserving explicit credentials and User replacement. Update mail references
23
+ and include the method-by-method gate report.
24
+
25
+ ## 0.15.2
26
+
27
+ - Rename the private mail configuration helper to `optionalTimeoutMs` so its
28
+ name describes the optional timeout duration it validates. Preserve accepted
29
+ values, defaults, and request lifecycle behavior.
30
+
3
31
  ## 0.15.1
4
32
 
5
33
  - Use the published `node-http-server` public lifecycle for the mail gateway.
package/README.md CHANGED
@@ -19,10 +19,18 @@ 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.15.1` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.16.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 the configured host and defaults
27
+ browser mail to `/v1/mail` on the current domain. Multiple applications can share
28
+ one server with explicit allowed origins. Subscription verification is disabled
29
+ until a `verifySubscription` callback is configured; that callback receives the
30
+ application name and bearer subscription key before each provider attempt.
31
+ See the [method and action gate report](docs/reviews/mail-server-purpose-review.md)
32
+ for the removals, retained responsibilities, and naming decisions.
33
+
26
34
  Applications can enable [PWA installation and offline resources](docs/reference/pwa.md)
27
35
  through their app descriptor. The SDK generates manifests, an independent
28
36
  registration module and a service worker, with clean browser resource URLs,
@@ -34,7 +34,7 @@ and exits nonzero on failure. Machine output is defined by
34
34
  | `arcane repo status\|pull\|push` | Runs one selected repository operation for the current app workspace. |
35
35
  | `arcane mail key set\|status\|delete` | Manages one server-only Resend API-key profile in Windows Credential Manager. |
36
36
  | `arcane mail send` | Performs one explicit, idempotency-keyed Resend attempt from a complete JSON report on redirected stdin. |
37
- | `arcane mail serve` | Starts one credential-protected numeric-loopback Arcane-to-Resend gateway for the selected app, Origin, and recipients. |
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
 
39
39
  ## Parser-wide options
40
40
 
@@ -46,9 +46,9 @@ meaning and cardinality rules:
46
46
  | `--path` | directory | `new` |
47
47
  | `--display-name` | string | `new`, `init` |
48
48
  | `--workspace` | directory | Commands that select an external or integrated workspace; defaults to `.`. |
49
- | `--app` | app id | Workspace/app operations except shared scope and `verify-bundle`; also the exact `mail serve` caller id. |
49
+ | `--app` | app id or label | Workspace/app operations except shared scope and `verify-bundle`; optional diagnostic label for `mail serve`. |
50
50
  | `--arcane-root` | directory | `doctor`, native `build`/`run`, `native-doctor`, `native-prepare` |
51
- | `--host` / `--port` | host / integer 0–65535 | Browser `dev`/`run` default to HTTPS at `127.0.0.1:8000`; `mail serve` defaults to HTTP at `127.0.0.1:8025` and admits numeric loopback only. |
51
+ | `--host` / `--port` | host / integer 0–65535 | Browser `dev`/`run` default to HTTPS at `127.0.0.1:8000`; `mail serve` defaults to HTTP at `0.0.0.0:8025` and accepts an explicit bind host. |
52
52
  | `--http-port` | integer 0–65535 | Browser `dev`/`run` HTTP redirect listener; defaults to `0`, which selects an available port. |
53
53
  | `--public` | flag | `dev`; binds to `0.0.0.0` unless `--host` explicitly selects another address. |
54
54
  | `--http` | flag | `dev` only; serves source and PWA routes on one HTTP listener selected by `--port`, without TLS. |
@@ -61,10 +61,10 @@ meaning and cardinality rules:
61
61
  | `--test-file` | repository-relative `.test.mjs` | `test --scope shared` only |
62
62
  | `--artifact` | bundle path | `bundle`, `verify-bundle` |
63
63
  | `--profile` | credential profile id | `mail send`, `mail serve` |
64
- | `--from` | verified sender | `mail send`, `mail serve` |
65
- | `--origin` | exact browser origin | `mail serve` |
64
+ | `--from` | optional sender override | `mail send`, `mail serve`; otherwise the report or provider template supplies the sender. |
65
+ | `--origin` | optional exact browser origin | `mail serve`; selects an explicit CORS allowed origin. |
66
66
  | `--allow-to` | optional comma-separated addresses | `mail serve` |
67
- | `--report-key` | nonempty safe-character string | `mail send`; caller-owned stable Resend idempotency key |
67
+ | `--report-key` | nonempty string | `mail send`; caller-owned stable Resend idempotency key, forwarded unchanged |
68
68
  | `--request-timeout` | optional integer from 1 through 2147483647 milliseconds | `mail send`, `mail serve` |
69
69
  | `--output` | `human`, `json`, `ndjson` | Every invocation; the final occurrence wins. |
70
70
  | `--git` | flag | `new` |
@@ -73,7 +73,6 @@ meaning and cardinality rules:
73
73
  | `--require-local-ai` | flag | `doctor` |
74
74
  | `--overwrite` | flag | `bundle` only |
75
75
  | `--secret-stdin` | flag | `mail key set`; requires redirected input |
76
- | `--app-key-stdin` | flag | `mail serve`; requires redirected input |
77
76
  | `--report-stdin` | flag | `mail send`; requires redirected JSON input |
78
77
  | `--help`, `-h` | flag | Prints help and exits zero. |
79
78
  | `--version`, `-v` | flag | Prints the exact SDK version and exits zero. |
@@ -835,10 +834,10 @@ or positional values.
835
834
  ### One-shot provider send
836
835
 
837
836
  `mail send` performs exactly one Resend provider attempt without starting a
838
- loopback server:
837
+ server:
839
838
 
840
839
  ```text
841
- arcane mail send --profile <profile> --from <verified-sender> --report-key <id> --report-stdin [--request-timeout <ms>]
840
+ arcane mail send --profile <profile> [--from <verified-sender>] --report-key <id> --report-stdin [--request-timeout <ms>]
842
841
  ```
843
842
 
844
843
  `--report-stdin` is mandatory and rejects a terminal before attaching input
@@ -854,15 +853,16 @@ shape:
854
853
  }
855
854
  ```
856
855
 
857
- The required fields are `type`, `to`, `subject`, and at least one of `text` or
858
- `html`; additional JSON-compatible provider fields are preserved. Direct CLI
859
- sending requires at least one explicit recipient, including for `error`
860
- reports. The Resend credential comes only from the selected Windows Credential
861
- Manager profile; neither it nor report content is accepted through argv or
862
- environment variables.
856
+ The CLI forwards the complete provider fields, including template requests.
857
+ Resend owns their accepted shape. The adapter removes the application-only
858
+ `type` field and applies `--from` when supplied; otherwise the report or provider
859
+ template supplies the sender. Direct CLI sending has
860
+ no configured fallback recipients. The Resend credential comes only from the
861
+ selected Windows Credential Manager profile; neither it nor report content is
862
+ accepted through argv or environment variables.
863
863
 
864
- The caller owns `--report-key`. It must contain one or more ASCII letters, digits,
865
- periods, underscores, colons, or hyphens. Reuse the same key only with the same
864
+ The caller owns the nonempty `--report-key`, which is forwarded unchanged.
865
+ Reuse the same key only with the same
866
866
  logical report when deliberately reconciling or retrying an
867
867
  ambiguous attempt. The CLI never retries automatically.
868
868
 
@@ -875,32 +875,46 @@ available request and outcome detail. Cancellation before the
875
875
  provider attempt exits 130 without sending; cancellation, timeout, or transport
876
876
  loss after the attempt begins is ambiguous because Resend may have accepted it.
877
877
 
878
- ### Authenticated local gateway
878
+ ### Mail gateway
879
879
 
880
880
  `mail serve` starts one owned Node HTTP gateway:
881
881
 
882
882
  ```text
883
- arcane mail serve --profile <profile> --from <verified-sender> --app <id> --origin <exact-origin> [--allow-to <addresses>] [--app-key-stdin] [--host 127.0.0.1] [--port 8025] [--request-timeout <ms>]
883
+ arcane mail serve --profile <profile> [--from <verified-sender>] [--app <label>] [--origin <exact-origin>] [--allow-to <addresses>] [--host 0.0.0.0] [--port 8025] [--request-timeout <ms>]
884
884
  ```
885
885
 
886
886
  The selected credential profile supplies only the server-side Resend API key.
887
- A separate local mail app key is read through a hidden prompt. Structured
888
- output requires `--app-key-stdin` with redirected input; the app key is never an
889
- argv value or part of the server result. The browser must use the same value as
890
- `arcane.config.mail.appKey`.
891
-
892
- The CLI admits only numeric loopback host values accepted by the gateway. The
893
- gateway also binds the exact app id, Origin, and sender, and it requires the
894
- separate app key by default. `--allow-to` optionally supplies a comma-separated
895
- recipient allowlist with no fixed recipient-count ceiling. `--request-timeout`
887
+ The CLI does not read a browser app key. Its optional `--app` value labels the
888
+ server; the incoming request's `X-Mail-App` identifies the application for
889
+ subscription verification. The HTTP authentication contract pairs that
890
+ application with `Authorization: Bearer <subscription_key>`.
891
+
892
+ Subscription verification is disabled for this initial service setup. The
893
+ programmatic `createToolchain().mail({action: 'serve', ...})` path accepts
894
+ `verifySubscription({appName, subscriptionKey, signal})`; supplying that callback
895
+ enables verification before each provider attempt. It must resolve to `true`
896
+ to accept the request. An invalid subscription receives 401; verifier service
897
+ failure receives retryable 503; cancellation stops verification before sending.
898
+ The callback connects the actual TWiN Stripe endpoint when its contract is
899
+ ready. There is no guessed URL, response schema, or command-line endpoint flag.
900
+
901
+ The listener defaults to `0.0.0.0`; `--host` selects another bind host. Browser
902
+ mail defaults to `/v1/mail` on the current domain. `--origin` is optional and
903
+ selects an explicit CORS allowlist when supplied. `--allow-to` optionally
904
+ supplies a comma-separated recipient allowlist. CLI parsing preserves supplied
905
+ address spelling and repeated entries. Programmatic `errorTo` selects fallback
906
+ recipients for error reports; when omitted, the selected `allowTo` list supplies
907
+ that fallback. `--request-timeout`
896
908
  adds a caller-selected provider-attempt timeout from 1 through 2147483647
897
909
  milliseconds, the Node timer range. When it is omitted, the SDK adds no
898
910
  provider timeout.
899
911
 
900
912
  After binding, `server.ready` reports lifecycle fields such as
901
- protocol, app id, loopback address, port, URL, and caller-authentication mode.
913
+ protocol, optional app label, bind address, port, URL, and `callerAuthentication`
914
+ (`none` or `subscription`). Human output states whether verification is disabled
915
+ or configured.
902
916
  The command owns the server until its lifecycle ends or `SIGINT`/`SIGTERM`
903
- cancels it. Resend and local app credentials never appear in results or events;
917
+ cancels it. The server's Resend credential remains outside results and events;
904
918
  per-request observer events preserve the complete delivery, report, provider
905
919
  outcome, and failure detail available to the gateway.
906
920
 
@@ -1,8 +1,8 @@
1
1
  # Mail gateway and durable outbox
2
2
 
3
3
  Arcane Mail is a pure-JavaScript SDK path. It does not use WebAssembly: mail is
4
- network and durable-state work, while the Resend credential belongs in the
5
- local Node gateway rather than in browser or WebAssembly state.
4
+ network and durable-state work. The Resend credential belongs in the Node
5
+ gateway and is never included in browser or WebAssembly state.
6
6
 
7
7
  ## Ownership and availability
8
8
 
@@ -12,7 +12,7 @@ local Node gateway rather than in browser or WebAssembly state.
12
12
  | `MailOutbox.mjs` | Browser or compatible injected storage | Stores exact requests in the `mail_outbox` DBOPFS table before delivery and normalizes terminal, retry, and reconciliation states. |
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
- | `arcane mail serve` | Node on the local machine | Authenticates the local caller, protects the provider credential, applies any explicitly configured recipient policy, and makes the single server-side Resend request. |
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
16
  | `arcane mail key ...` | Node on Windows | Stores, inspects, or deletes a Resend API key in Windows Credential Manager. |
17
17
 
18
18
  The browser never receives the Resend API key. The gateway never writes that
@@ -49,52 +49,81 @@ boundary; ordinary operators use `arcane mail send`, `arcane mail serve`, and
49
49
  `arcane mail key ...`. This keeps Node credential and server authority out of a
50
50
  browser import while preserving one shared CLI/toolchain implementation.
51
51
 
52
- ## Two separate credential boundaries
52
+ ## Provider and subscription credentials
53
53
 
54
54
  Arcane Mail deliberately separates two credentials:
55
55
 
56
56
  - The **Resend API key** is provider authority. `arcane mail key set <profile>`
57
57
  stores it in Windows Credential Manager. `mail send --profile <profile>` and
58
58
  `mail serve --profile <profile>` read it only inside the owning Node process.
59
- - The **mail app key** authenticates one browser/application caller to the
60
- loopback gateway. It is supplied to `mail serve` through hidden terminal
61
- input, or through redirected input with `--app-key-stdin`, and must match the
62
- browser's `arcane.config.mail.appKey`. It is a nonempty printable ASCII
63
- bearer-like local authentication value, never the Resend API key.
59
+ - The **subscription key** is the application user's subscription credential.
60
+ When present, the browser sends it as `Authorization: Bearer <subscriptionKey>`,
61
+ with the exact application name in `X-Mail-App`. The application name identifies the
62
+ subscription account to verify; it is a separate field from the key and the
63
+ report content.
64
64
 
65
65
  Do not put either secret on the command line. Command-line arguments may be
66
- recorded by the operating system or shell history. Structured CLI output
67
- requires the matching explicit redirected-input flag and rejects TTY input so
68
- the terminal cannot echo a secret.
69
-
70
- The mail app key is not confidential from scripts executing in the same page:
71
- same-runtime script or XSS can read browser configuration and issue the same
72
- request. Inject it at runtime, never hardcode it in shipped assets, protect the
73
- page's script boundary, restrict the exact gateway destination, and rotate it
74
- when page or process trust is lost. It protects the loopback server from
75
- unadmitted local callers; it is not provider authority or a replacement for
76
- browser application security.
66
+ recorded by the operating system or shell history. The credential-store
67
+ command uses hidden terminal input or explicitly selected redirected input;
68
+ subscription credentials are supplied through runtime configuration.
69
+
70
+ Subscription credentials stay in runtime configuration or the canonical User
71
+ entity. They are not added to reports, outbox records, delivery events, or
72
+ provider payloads. Supply credentials at runtime; do not hardcode them in
73
+ shipped source or fixtures.
77
74
 
78
75
  ## Configure the browser runtime
79
76
 
80
- One application declares an exact app id, gateway endpoint, and local app key:
77
+ Configure Mail before its durable lifecycle starts:
81
78
 
82
79
  ```javascript
83
80
  globalThis.arcane = globalThis.arcane || {};
84
81
  globalThis.arcane.config = globalThis.arcane.config || {};
85
82
  globalThis.arcane.config.mail = {
86
- appName: 'arcane-dev',
87
- appKey: localMailAppKey,
88
- endpoint: 'http://127.0.0.1:8025/v1/mail'
83
+ appName: 'My application'
89
84
  };
90
85
  ```
91
86
 
92
- The transport endpoint must be HTTPS or loopback HTTP at `localhost`,
93
- `127.0.0.1`, or `[::1]`. The SDK CLI gateway itself binds numeric loopback only. An
94
- explicit HTTPS endpoint receives the app key as `X-Mail-Key`, so its ownership
95
- and trust must be verified before configuration. A hosted default is derived
96
- only when the page declares an admitted Arcane mail base domain; otherwise
97
- configuration fails rather than selecting an arbitrary remote host.
87
+ `appName` is an arbitrary nonempty string, preserved as supplied. `BOSS` and
88
+ `TWiN` are examples, not an enum or an admission list. When `appName` is omitted,
89
+ Mail reads the page's `arcane-app-id` metadata without changing its value.
90
+
91
+ On an HTTP or HTTPS page, the default endpoint is `/v1/mail` on the current
92
+ origin, including its port. Mail does not infer a base domain, select a mail
93
+ subdomain, or treat loopback addresses specially. An explicit `endpoint` may
94
+ select a shared server on another domain. The transport resolves it through
95
+ the URL parser and accepts HTTP or HTTPS; relative URLs and query strings are
96
+ supported. The gateway routes by pathname. Its configured CORS origins must
97
+ include a caller on another origin.
98
+
99
+ An explicit `endpoint: ''` disables HTTP delivery and selects the existing
100
+ native `Arcane.mail.send` fallback when available. Pages with a non-HTTP origin
101
+ also have no default HTTP endpoint. Configured HTTP delivery takes precedence
102
+ over an available native bridge.
103
+
104
+ An explicit nonempty string `subscriptionKey` supplies the HTTP credential.
105
+ When it is omitted, Mail resolves the current canonical User entity at the actual HTTP delivery,
106
+ waits for its existing `load()` operation, and reads `subscription_key`.
107
+ An injected `options.user` retains precedence, including an explicit `null`.
108
+ Mail does not retain a separate User instance, so later deliveries observe a
109
+ replacement canonical User. This adds no page-startup wait, polling, new
110
+ credential storage, or migration of saved data.
111
+
112
+ An absent key, `null`, or an empty string omits the Authorization header; the
113
+ transport does not block initial setup because a key is missing. Explicit
114
+ `subscriptionKey: null` or `subscriptionKey: ''` also skips User lookup.
115
+ A supplied value of another type is rejected. A gateway with subscription
116
+ verification configured owns rejection of requests without a usable key.
117
+
118
+ For a caller-owned key and shared endpoint:
119
+
120
+ ```javascript
121
+ globalThis.arcane.config.mail = {
122
+ appName: 'Another application',
123
+ subscriptionKey: currentSubscriptionKey,
124
+ endpoint: 'https://mail.example.test/v1/mail'
125
+ };
126
+ ```
98
127
 
99
128
  The ordinary browser transport has no automatic request deadline and reads the
100
129
  complete gateway response. A caller may explicitly supply a positive
@@ -107,7 +136,7 @@ accepted the request.
107
136
  `Mail.send(to, subject, payload, messageStyle, messageType)` preserves the
108
137
  existing signature. `messageType` is `error`, `report`, or `crisis_detected`.
109
138
  Report and crisis mail require at least one recipient; error mail may use the
110
- gateway's configured allowlisted fallback recipients.
139
+ gateway's configured fallback recipients.
111
140
 
112
141
  In a browser, the module owns the one `window.mail` singleton. An explicit
113
142
  `new Mail(config, options)` may configure that owned singleton only before its
@@ -124,9 +153,10 @@ option `{includeContext:true}`, every message also captures
124
153
  current User entity and add its `username`, `email`, `language`, and `phone`
125
154
  values to the locally rendered content. Those fields are then stored and sent
126
155
  unencrypted as part of the message, so the application owns consent, purpose,
127
- recipient scope, retention, and disclosure. Without that option, Mail neither
128
- loads the User profile nor adds the path/profile fields. The generated
129
- `source_at` timestamp, caller-supplied payload, subject, type, and recipients
156
+ recipient scope, retention, and disclosure. Without that option, Mail adds no
157
+ path/profile fields to the report. HTTP subscription lookup may still load
158
+ the User at delivery without copying profile fields into the report. The
159
+ generated `source_at` timestamp, caller-supplied payload, subject, type, and recipients
130
160
  remain part of the requested report in either mode.
131
161
 
132
162
  The public `Mail` integration requires its compatible DBOPFS adapter. Before the
@@ -145,8 +175,9 @@ durable claims then belong to that adapter's implemented `get`, `set`,
145
175
  default uses `navigator.locks`; when that cross-context authority is absent,
146
176
  the outbox reports `MAIL_OUTBOX_LOCK_UNAVAILABLE` and does not start the drain.
147
177
 
148
- Call `await mail.start()` during application startup so pre-existing records
149
- are scanned even when the application does not send a new report. The first
178
+ Start `mail.start()` during application startup so pre-existing records are
179
+ scanned even when the application does not send a new report. Observe its
180
+ completion or error without holding page rendering behind the drain. The first
150
181
  `send()` also starts the lifecycle if needed.
151
182
 
152
183
  | Mail method/property | Contract |
@@ -218,9 +249,14 @@ boundary.
218
249
  Mail publishes complete semantic events through the SDK singleton event
219
250
  authority. Public detail includes the full mutable durable record, serialized
220
251
  report, result or failure, provider details, and complete drain inventory
221
- available at that transition. It never includes the Resend API key or mail app
222
- key. Listener exceptions are observational and cannot change a committed mail
223
- operation result.
252
+ available at that transition. It never includes the Resend API key or
253
+ subscription key. Listener exceptions are observational and cannot change a
254
+ committed mail operation result.
255
+
256
+ Provider IDs, provider codes, and nonempty failure codes retain their complete
257
+ string values through delivery, persistence, and retrieval. They have no SDK
258
+ character grammar. Missing values retain the existing fallback behavior.
259
+ Request IDs and outbox filenames retain their existing contracts.
224
260
 
225
261
  When both an explicit endpoint and native `Arcane.mail.send` exist, Mail uses
226
262
  the configured HTTP endpoint so the authenticated SDK gateway can return its
@@ -232,7 +268,7 @@ unavailability is a non-ambiguous retryable failure. Once a valid accepted
232
268
  result has returned, a racing lifecycle cancellation cannot erase that
233
269
  committed acceptance result.
234
270
 
235
- ## Operate the CLI and local gateway
271
+ ## Operate the CLI and gateway
236
272
 
237
273
  Store one Resend key under a local profile:
238
274
 
@@ -251,19 +287,20 @@ Perform one provider attempt directly from the SDK CLI:
251
287
  arcane mail send --profile arcane-dev --from "Arcane <verified@example.com>" --report-key <stable-id> --report-stdin
252
288
  ```
253
289
 
254
- The redirected UTF-8 JSON input is read completely. A report requires `type`,
255
- `to`, `subject`, and at least one of `text` or `html`; additional
256
- JSON-compatible provider fields are preserved. Direct CLI sends require at
257
- least one explicit recipient, including for `error` reports. Message content is
258
- not accepted in argv. Programmatic results and observer events preserve the
259
- complete report, provider request, provider response, and error detail while
290
+ The redirected UTF-8 JSON object is read completely. Its fields and values
291
+ are retained; the Resend adapter removes the SDK's `type` routing field,
292
+ uses `from` when configured (otherwise the report or provider template supplies
293
+ the sender), and applies configured error-recipient fallback
294
+ when applicable. Resend evaluates its own required provider fields. Message
295
+ content is not accepted in argv. Programmatic results and observer events
296
+ preserve the complete report, provider request, provider response, and error detail while
260
297
  never exposing either credential.
261
298
 
262
- The caller must create and retain a safe-character `--report-key` before the
299
+ The caller must create and retain a nonempty `--report-key` before the
263
300
  attempt. It is the Resend idempotency key and may be reused only with the same
264
301
  serialized report content for an intentional retry or reconciliation. The
265
302
  CLI performs exactly one attempt and never retries automatically. Exit zero
266
- requires a successful Resend response containing a valid provider id; that is
303
+ requires a successful Resend response containing a nonempty string provider id; that is
267
304
  provider acceptance, not an inbox-delivery claim. Timeout, connection loss, or
268
305
  cancellation after the provider attempt begins is returned as an ambiguous
269
306
  nonzero outcome because the provider may already have accepted the request.
@@ -272,16 +309,80 @@ For both CLI mail operations, `--request-timeout` accepts 1 through 2147483647
272
309
  milliseconds, the Node timer range. When omitted, the SDK adds no provider
273
310
  deadline.
274
311
 
275
- Start the authenticated gateway:
312
+ Start the gateway:
276
313
 
277
314
  ```text
278
- arcane mail serve --profile arcane-dev --from "Arcane <verified@example.com>" --app arcane-dev --origin http://127.0.0.1:8000 --allow-to recipient@example.com
315
+ arcane mail serve --profile arcane-dev --from "Arcane <verified@example.com>"
279
316
  ```
280
317
 
281
- Human output prompts for the separate mail app key with hidden input.
282
- Non-interactive structured output requires `--app-key-stdin` and redirected
283
- stdin. The server binds numeric loopback only; the default is
284
- `127.0.0.1:8025/v1/mail`.
318
+ The default listener is `0.0.0.0:8025`; `--host` and `--port` select its bind
319
+ address and port. The server can serve callers from multiple domains on the
320
+ same machine. Route the page's `/v1/mail` to this listener, or configure an
321
+ explicit shared endpoint in the caller.
322
+
323
+ `--app` is an optional server event label and does not restrict incoming
324
+ application names. `--from` is an optional shared sender override; omit it to
325
+ preserve each report's sender or its provider template's default.
326
+ `--origin` selects an exact allowed caller origin; the
327
+ programmatic `origin` option also accepts an array for multiple origins. With
328
+ no origins configured, the gateway accepts an Origin matching its request
329
+ Host using HTTP or HTTPS. Requests without Origin continue normally.
330
+ Cross-origin preflight permits `Content-Type`, `Idempotency-Key`, `X-Mail-App`,
331
+ and `Authorization`. This is origin configuration, not a loopback policy.
332
+
333
+ `--allow-to` explicitly limits recipients when supplied. With it omitted, the
334
+ gateway imposes no recipient allowlist. When configured, the list applies to
335
+ every recipient in the resolved `to`, `cc`, and `bcc` fields, whether supplied
336
+ as a string or an array. Sender, recipient, subject, and body
337
+ values are not trimmed, lowercased, or filtered by an SDK email grammar at
338
+ the gateway. Error reports with an empty `to` array use configured fallback
339
+ recipients; other report content is preserved.
340
+
341
+ ## Configure subscription verification
342
+
343
+ Subscription verification is disabled during initial setup when
344
+ `verifySubscription` is omitted. The server reports
345
+ `callerAuthentication: 'none'` and can perform mail delivery without a
346
+ subscription key. Starting the ordinary CLI gateway uses this mode. This does
347
+ not claim that a subscription was checked.
348
+
349
+ The hosting process enables verification by supplying the programmatic
350
+ `verifySubscription` function through
351
+ `createToolchain().mail({action:'serve', verifySubscription, ...options})`.
352
+ The server then reports `callerAuthentication: 'subscription'`. The callback
353
+ contract is:
354
+
355
+ ```javascript
356
+ verifySubscription({appName, subscriptionKey, signal})
357
+ ```
358
+
359
+ It may return a promise. Return exactly `true` for a valid subscription;
360
+ return `false` for an invalid one, and throw when the verifier service fails.
361
+ The gateway also treats any other returned value as invalid. `appName` is the
362
+ exact incoming `X-Mail-App` value, `subscriptionKey` is the incoming Bearer
363
+ key, and `signal` follows the request lifecycle. These control fields stay
364
+ separate from the mail report and Resend payload. The verifier runs for each
365
+ POST request before any provider attempt; results are not cached.
366
+
367
+ | Configured-verifier outcome | Gateway response |
368
+ | --- | --- |
369
+ | Missing, empty, or repeated `X-Mail-App` | `400 mail_invalid_headers` |
370
+ | Missing or malformed Bearer credential | `401 mail_subscription_required` |
371
+ | Callback returns anything except `true` | `401 mail_subscription_invalid` |
372
+ | Callback throws | `503 mail_subscription_verification_failed`, retryable with the configured `retryableDelayMs` |
373
+ | Request cancelled while verifying | `408 mail_request_cancelled`, with no provider attempt |
374
+
375
+ The `401` responses include `WWW-Authenticate: Bearer`. A successful callback
376
+ permits the existing mail-delivery path; it is not itself a mail-acceptance
377
+ result. The SDK supplies no default verification URL or built-in Stripe
378
+ endpoint adapter. Connecting the actual subscription endpoint is a separate
379
+ hosting integration.
380
+
381
+ CLI startup output says `Subscription verification: disabled` or
382
+ `Subscription verification: configured`. Structured `server.ready` output
383
+ includes the corresponding `callerAuthentication` value.
384
+
385
+ ## Gateway request lifecycle
285
386
 
286
387
  The gateway uses the published `node-http-server` instance lifecycle. Its raw
287
388
  request hook hands the original request and response directly to the mail
@@ -289,17 +390,15 @@ handler before body parsing or static routing. Socket inactivity timeout remains
289
390
  disabled; caller-selected mail deadlines, cancellation, and complete responses
290
391
  remain owned by the mail handler.
291
392
 
292
- The gateway protects the provider credential by requiring:
293
-
294
- - its exact numeric-loopback `Host` authority and `/v1/mail` route;
295
- - an exact configured `Origin`, app id, and constant-time app-key match;
296
- - complete JSON requests with at least one recipient, or configured fallback
297
- recipients for an `error` report;
298
- - an optional explicit recipient allowlist when one is configured; and
299
- - one fixed Resend endpoint with the stable Arcane report key forwarded as
300
- `Idempotency-Key`.
393
+ The gateway handles POST requests to `/v1/mail` and the corresponding OPTIONS
394
+ preflight. JSON parsing owns request readability; the gateway does not reject
395
+ a parseable body because of its Content-Type spelling. One nonempty
396
+ `Idempotency-Key` is forwarded unchanged to the fixed Resend endpoint. The
397
+ browser and gateway preserve complete request and response content without
398
+ body-size gates. With no event observer, the gateway does not construct event
399
+ payloads or parse a second provider-request representation for observation.
301
400
 
302
- The gateway returns `202` only after Resend returns a valid provider id.
401
+ The gateway returns `202` only after Resend returns a nonempty string provider id.
303
402
  Transport loss, an explicit caller-selected timeout, an invalid success body,
304
403
  or an unreadable provider response returns an explicit uncertain result and never claims
305
404
  delivery. Rate limits, concurrent idempotency requests, permanent validation
@@ -308,8 +407,8 @@ results with the complete available provider response or error detail.
308
407
 
309
408
  ## Operational verification
310
409
 
311
- The focused SDK tests use only synthetic keys, addresses, responses, storage,
312
- and loopback requests. They do not contact Resend or send email. A live
410
+ The focused SDK tests use synthetic keys, addresses, responses, storage,
411
+ and local HTTP requests. They do not contact Resend or send email. A live
313
412
  acceptance send is a separate operational boundary: use a disposable message,
314
- the real allowlist, and the selected credential profile, then verify both the
413
+ the selected recipient configuration and credential profile, then verify both the
315
414
  gateway's provider-acceptance id and the intended inbox outcome.