arcane-os 0.24.0 → 0.25.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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.0
4
+
5
+ - Automatically skip configured mail subscription verification when the actual
6
+ requester connection IP equals the server-side IP of that connection. This
7
+ lets services sharing that IP and local development send without a subscription
8
+ key. Other requester IPs, including other intranet machines, still use the
9
+ configured verification callback.
10
+ - Use native connection addresses for HTTP/1 and HTTP/2, with no environment
11
+ setting, domain lookup, forwarded-header interpretation, or special loopback rule.
12
+ Preserve CORS, report handling, provider delivery, and existing caller APIs.
13
+ - Document the IP rule and its purpose gates; update focused test source for
14
+ the exemption and the remaining subscription-verification paths.
15
+
16
+ ## 0.24.1
17
+
18
+ - Add a mail CLI parameter table with command scope, purpose, and defaults.
19
+ Explain the difference between `--app`, `X-Mail-App`, `--report-stdin`, and
20
+ the nonsecret `--report-key` used for intentional same-message retries.
21
+ - Remove redundant default-profile configuration and the alternate-account
22
+ placeholder from the main mail example. Align the CLI reference with the
23
+ shared configuration and credential files while preserving named-profile APIs.
24
+ - Clarify when subscription headers are required, how ordinary delivery failures
25
+ retain their existing report, and the separate browser callback and verification
26
+ service paths that can create feedback. Runtime behavior is unchanged.
27
+
3
28
  ## 0.24.0
4
29
 
5
30
  - Read nonsecret mail settings from `arcane.config.json.mail` and provider keys
package/README.md CHANGED
@@ -19,7 +19,7 @@ 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.24.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.25.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
 
@@ -31,7 +31,11 @@ Existing root credential, profile, and TLS settings remain supported. Browser
31
31
  mail defaults to `/v1/mail` on the current domain, and multiple applications can
32
32
  share one server with explicit allowed origins. Subscription verification is disabled
33
33
  until a `verifySubscription` callback is configured; that callback receives the
34
- application name and bearer subscription key before each provider attempt.
34
+ application name and bearer subscription key before provider attempts from other
35
+ requester IPs. Requests whose actual connection source IP equals the server-side
36
+ IP of that connection automatically skip subscription verification, including
37
+ local development. Other intranet machines still need verification. The exception
38
+ requires no environment setting or domain lookup.
35
39
  See the [method and action gate report](docs/reviews/mail-server-purpose-review.md)
36
40
  for the removals, retained responsibilities, and naming decisions.
37
41
 
@@ -812,10 +812,13 @@ npm exec -- arcane repo status
812
812
 
813
813
  ## `arcane mail`
814
814
 
815
- ### Resend credential profiles
815
+ ### Configuration and provider credentials
816
816
 
817
- The mail commands read `.arcane.env.json` from the invocation directory on Windows,
818
- Linux, and macOS. The credential subcommands select one profile in that file:
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 profile defaults
830
- to `mail`, which selects top-level `RESEND_API_KEY`. Any other exact profile
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 file is:
835
+ The minimal `.arcane.env.json` is:
834
836
 
835
837
  ```json
836
838
  {
837
- "RESEND_API_KEY": ""
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. Set and delete preserve
843
- the file's other settings and profiles. Status returns the selected profile,
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 configuration directory
848
- from `cwd ?? workspaceRoot ?? process.cwd()`. This uses ordinary Node file
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 directory. Existing deployments using
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
- Missing files or missing/empty selected keys stop `send` and `serve` with the
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 `--from` when supplied; otherwise the report or provider
894
- template supplies the sender. Direct CLI sending has
895
- no configured fallback recipients. The Resend credential comes only from the
896
- selected `.arcane.env.json` profile; omitting `--profile` selects `mail`. Neither the
897
- key nor report content is accepted through argv or process environment variables.
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` profile supplies only the server-side Resend API key;
922
- omitting `--profile` selects `mail`.
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 configuration at the top level of the same file:
952
+ Add the listener's certificate paths to `arcane.config.json`:
925
953
 
926
954
  ```json
927
955
  {
928
- "RESEND_API_KEY": "",
929
- "MAIL_TLS_CERT_PATH": ".arcane/mail/fullchain.pem",
930
- "MAIL_TLS_KEY_PATH": ".arcane/mail/private-key.pem"
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 `.arcane.env.json`, or may be absolute. They are shared across provider
936
- profiles. Missing TLS settings name the fields to fill in before a listener
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,30 +982,50 @@ 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 for this initial service setup. The
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
- enables verification before each provider attempt. It must resolve to `true`
988
+ enables verification for requests whose actual connection source IP differs
989
+ from the server-side IP of that connection. The gateway compares the native
990
+ `request.socket.remoteAddress` with `request.socket.localAddress`. A nonempty,
991
+ equal requester IP skips subscription verification and needs neither a bearer
992
+ key nor an application header for that check. No environment setting or domain
993
+ lookup is involved. Local development qualifies under the same rule; another
994
+ machine on an intranet still needs verification. Missing connection metadata
995
+ does not qualify. CORS and the remaining mail requirements still apply.
996
+
997
+ For requests requiring verification, the callback must resolve to `true`
956
998
  to accept the request. An invalid subscription receives 401; verifier service
957
999
  failure receives retryable 503; cancellation stops verification before sending.
958
1000
  The callback connects the actual TWiN Stripe endpoint when its contract is
959
1001
  ready. There is no guessed URL, response schema, or command-line endpoint flag.
960
1002
 
961
- The listener defaults to `0.0.0.0`; `--host` selects another bind host. Browser
962
- mail defaults to `/v1/mail` on the current domain. `--origin` is optional and
963
- selects an explicit CORS allowlist when supplied. `--allow-to` optionally
964
- supplies a comma-separated recipient allowlist. CLI parsing preserves supplied
965
- address spelling and repeated entries. Programmatic `errorTo` selects fallback
966
- recipients for error reports; when omitted, the selected `allowTo` list supplies
967
- that fallback. `--request-timeout`
968
- adds a caller-selected provider-attempt timeout from 1 through 2147483647
969
- milliseconds, the Node timer range. When it is omitted, the SDK adds no
970
- provider timeout.
1003
+ The listener defaults to `0.0.0.0:4433`; `mail.host` and `mail.port` configure
1004
+ the listener, and explicit `--host` and `--port` override them. Browser mail
1005
+ defaults to `/v1/mail` on the current domain. `mail.origins` supplies an exact
1006
+ origin array; `--origin` replaces it with one origin. Repeated `--origin`
1007
+ options keep the last value. An absent or empty list selects the current
1008
+ request authority. Other Origins receive `403 mail_origin_not_allowed`;
1009
+ requests without an Origin continue normally. This CORS configuration does
1010
+ not filter client IPs or give loopback special treatment.
1011
+
1012
+ `mail.recipientAllowlist` supplies the recipient list; `--allow-to` replaces it
1013
+ with a comma-separated list. CLI parsing preserves supplied address spelling
1014
+ and repeated entries. Programmatic `errorTo` or `mail.errorRecipients` selects
1015
+ fallback recipients for error reports. When neither is supplied, the effective
1016
+ recipient allowlist supplies that fallback; an explicit empty array supplies
1017
+ none. The [Mail CLI parameters](mail.md#mail-cli-parameters) reference also
1018
+ documents programmatic aliases and list-replacement precedence.
1019
+
1020
+ `--request-timeout` overrides `mail.providerTimeoutMs` and accepts 1 through
1021
+ 2147483647 milliseconds, the Node timer range. An absent effective timeout
1022
+ adds no provider deadline; programmatic/configured null also selects no
1023
+ deadline. `mail.bodyTimeoutMs` selects the optional request-body deadline.
971
1024
 
972
1025
  After binding, `server.ready` reports lifecycle fields such as
973
1026
  protocol, optional app label, bind address, port, URL, and `callerAuthentication`
974
1027
  (`none` or `subscription`). Human output states whether verification is disabled
975
- or configured.
1028
+ or configured; configured verification includes the automatic same-IP exception.
976
1029
  The command owns the server until its lifecycle ends or `SIGINT`/`SIGTERM`
977
1030
  cancels it. The server's Resend credential remains outside results and events;
978
1031
  per-request observer events preserve the complete delivery, report, provider
@@ -115,7 +115,8 @@ An absent key, `null`, or an empty string omits the Authorization header; the
115
115
  transport does not block initial setup because a key is missing. Explicit
116
116
  `subscriptionKey: null` or `subscriptionKey: ''` also skips User lookup.
117
117
  A supplied value of another type is rejected. A gateway with subscription
118
- verification configured owns rejection of requests without a usable key.
118
+ verification configured owns rejection of requests without a usable key,
119
+ subject to its [automatic same-IP exception](#automatic-same-ip-exception).
119
120
 
120
121
  For a caller-owned key and shared endpoint:
121
122
 
@@ -272,6 +273,40 @@ committed acceptance result.
272
273
 
273
274
  ## Operate the CLI and gateway
274
275
 
276
+ ### Mail CLI parameters
277
+
278
+ `mail serve` starts the shared mail server. `mail send` makes one direct Resend
279
+ request. `mail key set|status|delete` manages the provider credential.
280
+
281
+ | Parameter | Command | What it does | When omitted |
282
+ | --- | --- | --- | --- |
283
+ | `--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. |
284
+ | `--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. |
285
+ | `--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. |
286
+ | `--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. |
287
+ | `--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`. |
288
+ | `--port <number>` | `mail serve` | Selects the HTTPS listener port. Explicit `0` asks the operating system for an available port. | Uses `mail.port`, then `4433`. |
289
+ | `--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. |
290
+ | `--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. |
291
+ | `--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. |
292
+ | `--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. |
293
+ | `[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`. |
294
+ | `--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. |
295
+ | `--output human\|json\|ndjson` | All mail commands | Selects readable console output, one JSON result, or newline-delimited JSON events. | Uses `human`. |
296
+ | `--help` | CLI | Displays CLI help instead of running the mail operation. | Runs the requested operation. |
297
+ | `--version` | CLI | Displays the SDK version instead of running the mail operation. | Runs the requested operation. |
298
+
299
+ There is no `--report` flag. `--report-stdin` supplies the message;
300
+ `--report-key` identifies that same message across attempts. Browser `Mail.send()`
301
+ generates and retains its report key automatically.
302
+
303
+ Mail reads configuration from the directory in which the command runs;
304
+ `--workspace` does not relocate these mail files. Mail HTTPS uses `mail.certPath`
305
+ and `mail.keyPath` in JSON. The `--cert`, `--key`, `--https`, and `--http-port`
306
+ flags belong to browser `dev`/`run`, and `--http` belongs to `dev`.
307
+
308
+ ### Mail configuration
309
+
275
310
  Keep app-supplied SDK settings under named capability members, starting with
276
311
  `mail`. Put the nonsecret mail settings in `arcane.config.json` in the directory
277
312
  from which the command runs:
@@ -285,7 +320,6 @@ from which the command runs:
285
320
  "https://dragons.example",
286
321
  "https://www.dragons.example"
287
322
  ],
288
- "profile": "mail",
289
323
  "certPath": "certificates/fullchain.pem",
290
324
  "keyPath": "certificates/private-key.pem"
291
325
  }
@@ -333,24 +367,16 @@ injected providers, `fetchImpl`, `onEvent`, `requestIdFactory`,
333
367
  runtime inputs, not JSON settings. Reports and their idempotency keys remain
334
368
  inputs to each send operation.
335
369
 
336
- The default credential profile `mail` selects `.arcane.env.json.mail.apiKey`.
337
- Other profile names select exact entries in `.arcane.env.json.mail.profiles`:
370
+ The single-key configuration above needs no `profile` or `profiles` member.
371
+ Omitting the selection uses `.arcane.env.json.mail.apiKey`; multiple apps and
372
+ domains can share that provider key.
338
373
 
339
- ```json
340
- {
341
- "mail": {
342
- "apiKey": "",
343
- "profiles": {
344
- "another-provider-account": {
345
- "apiKey": ""
346
- }
347
- }
348
- }
349
- }
350
- ```
351
-
352
- An absent named profile does not fall back to the default key. The profile
353
- selects Resend provider credentials; it is separate from the incoming
374
+ For callers that already select an alternate credential, named-profile support
375
+ remains available: `--profile <name>` or `mail.profile` selects
376
+ `.arcane.env.json.mail.profiles[name].apiKey`. An absent named profile does not
377
+ fall back to the default key. Selection happens once when send or serve starts;
378
+ it does not map incoming apps or domains to different provider accounts.
379
+ The profile selects Resend provider credentials, separately from the incoming
354
380
  application name and subscriber key. Existing top-level `RESEND_API_KEY` and
355
381
  `MAIL_PROFILES[profile].RESEND_API_KEY` remain supported. A nested selected
356
382
  `apiKey` takes precedence when the property exists, including null or an empty
@@ -449,8 +475,8 @@ arcane mail send --from "Arcane <verified@example.com>" --report-key <stable-id>
449
475
  The redirected UTF-8 JSON object is read completely. Its fields and values
450
476
  are retained; the Resend adapter removes the SDK's `type` routing field,
451
477
  uses `from` when configured (otherwise the report or provider template supplies
452
- the sender), and applies configured error-recipient fallback
453
- when applicable. Resend evaluates its own required provider fields. Message
478
+ the sender). Direct sending supplies no fallback recipients. Resend evaluates
479
+ its own required provider fields. Message
454
480
  content is not accepted in argv. Programmatic results and observer events
455
481
  preserve the complete report, provider request, provider response, and error detail while
456
482
  never exposing either credential.
@@ -561,10 +587,40 @@ Subscription verification is disabled during initial setup when
561
587
  subscription key. Starting the ordinary CLI gateway uses this mode. This does
562
588
  not claim that a subscription was checked.
563
589
 
590
+ ### Automatic same-IP exception
591
+
592
+ The gateway automatically skips subscription verification when the incoming
593
+ connection's nonempty `request.socket.remoteAddress` equals
594
+ `request.socket.localAddress`. This also applies when a verifier is configured
595
+ and the request has no subscription key or supplies an invalid one. The
596
+ verification callback is not called for that request.
597
+
598
+ No `.arcane.env.json` setting or domain allowlist is needed. The comparison uses
599
+ the actual source and destination IP addresses exposed by Node's request socket
600
+ for HTTP/1.1 and HTTP/2. It adds no DNS lookup, configuration read, cache, or
601
+ interface scan. `Origin`, `Host`, `Forwarded`, and `X-Forwarded-For` do not
602
+ establish the connection's IP equality.
603
+
604
+ | Caller connection | Subscription behavior with a verifier configured |
605
+ | --- | --- |
606
+ | The verification service connects to mail using the same source and destination IP | Skips subscription checking, allowing its service-generated emails without a subscriber key. |
607
+ | A local development request has equal source and destination IPs, including an ordinary loopback connection | Uses the same automatic exception. There is no special loopback rule. |
608
+ | Another machine on the same intranet connects from a different IP | Uses the configured subscription verifier. Sharing a network or subnet supplies no exception. |
609
+ | Two services on one machine connect through different interface IPs | Uses the configured subscription verifier when the actual addresses differ. Machine identity alone supplies no exception. |
610
+ | The request has no usable socket source address | Uses the configured subscription verifier; absent addresses do not count as equal. |
611
+
612
+ The exception skips only subscription checking. Existing CORS, route, method,
613
+ idempotency-key, report, recipient, and provider handling still apply. The gateway
614
+ continues to require its Resend provider credential and reports acceptance only
615
+ after the provider accepts the email.
616
+
617
+ ### Verification callback contract
618
+
564
619
  The hosting process enables verification by supplying the programmatic
565
620
  `verifySubscription` function through
566
621
  `createToolchain().mail({action:'serve', verifySubscription, ...options})`.
567
- The server then reports `callerAuthentication: 'subscription'`. The callback
622
+ The server then reports `callerAuthentication: 'subscription'`, identifying the
623
+ configured mode while retaining the same-IP exception above. The callback
568
624
  contract is:
569
625
 
570
626
  ```javascript
@@ -577,7 +633,15 @@ The gateway also treats any other returned value as invalid. `appName` is the
577
633
  exact incoming `X-Mail-App` value, `subscriptionKey` is the incoming Bearer
578
634
  key, and `signal` follows the request lifecycle. These control fields stay
579
635
  separate from the mail report and Resend payload. The verifier runs for each
580
- POST request before any provider attempt; results are not cached.
636
+ POST request outside the same-IP exception before any provider attempt; results
637
+ are not cached.
638
+
639
+ `X-Mail-App` identifies the originating app so the verifier can select that
640
+ app's subscription account, such as the appropriate Stripe account for `BOSS`
641
+ or `TWiN`. It is separate from the gateway's optional `--app` event label.
642
+ The missing-header and Bearer errors below apply only when `verifySubscription`
643
+ is configured and the same-IP exception does not apply. Ordinary `mail serve`
644
+ does not require either header.
581
645
 
582
646
  | Configured-verifier outcome | Gateway response |
583
647
  | --- | --- |
@@ -602,6 +666,36 @@ CLI startup output says `Subscription verification: disabled` or
602
666
  `Subscription verification: configured`. Structured `server.ready` output
603
667
  includes the corresponding `callerAuthentication` value.
604
668
 
669
+ ## Error reports and retries
670
+
671
+ An incoming report with `type: 'error'` is an email supplied by its caller.
672
+ The gateway's `errorRecipients` setting only supplies fallback recipients for
673
+ that report when its `to` array is empty. A gateway or provider failure returns
674
+ an HTTP error and diagnostic events; it does not generate another error email.
675
+ Gateway event-handler failures are written to the console. Each POST and each
676
+ direct CLI send performs at most one provider attempt, with no automatic server retry.
677
+
678
+ The browser outbox retains the same report and key after a retryable delivery
679
+ failure. Each drain attempts each eligible record once; a later startup,
680
+ connectivity event, or explicit drain can retry it. Browser `Errors` notification
681
+ failure retains the same pending occurrence and logs a warning without scheduling
682
+ itself again.
683
+
684
+ A separate browser callback path can feed back into error reporting: a mail-event
685
+ subscriber that repeatedly throws can reach global error capture, create another
686
+ error report, and trigger another mail event. Subscriber failures cannot change
687
+ an already committed mail result, though global capture can report them as new
688
+ errors. This is a source-level possibility, not evidence of a deployed loop.
689
+
690
+ For a verification service's own emails sent through the same IP, the gateway
691
+ skips its subscription callback and proceeds to provider delivery. That removes
692
+ the circular dependency `mail -> verify subscription -> send mail -> verify
693
+ subscription` for this connection path. Connections from a different IP still
694
+ use the configured callback, whose owning service must preserve a verification
695
+ path that can return without recursively depending on another verification.
696
+ The exception does not change the separate browser event-subscriber behavior
697
+ described above.
698
+
605
699
  ## Gateway request lifecycle
606
700
 
607
701
  The gateway uses the published `node-http-server` instance lifecycle. Its raw
@@ -1,5 +1,10 @@
1
1
  [Roshi's Codex PRIME] The gateway review and its portable credential follow-up apply the same three purpose gates. The gateway removals preserve complete delivery outcomes, configured CORS and recipient policy, and the optional subscription verifier. Mail credentials now belong in the user-selected `.arcane.env.json`, with one portable Node implementation for Windows, Linux, and macOS and a compatible Node host as the Android adaptation boundary. The original gateway inventory below uses baseline `f055e05`; the credential follow-up reviews the Windows storage implementation present at SDK `0.18.0`. Commit, package, publication, and actual platform execution outcomes belong to the delivery record rather than to this source-review report.
2
2
 
3
+ The [same-IP subscription exception follow-up](#same-ip-subscription-exception-follow-up)
4
+ records the later user-selected change to when the optional verifier runs. The
5
+ earlier inventories retain their stated historical scope; current verification
6
+ skips requests whose actual connection source and destination IPs are equal.
7
+
3
8
  | Decision | Behavior | Why it matters | Source status at this review |
4
9
  | --- | --- | --- | --- |
5
10
  | Remove | Loopback-only admission, manual header reconstruction, address/origin normalization, local payload grammar, manual content-length scan, dead queue knobs | These restrict supported use, rewrite supplied values, or repeat work already owned elsewhere. | Removed from the inspected source; naming-only changes carry no runtime claim. |
@@ -480,7 +485,7 @@ files. Their explicit precedence and credential compatibility rules are in the
480
485
  | `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
486
  | 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
487
  | `origins` string array and list replacement | Y/Y/N | Keep one exact list for multiple caller domains. Explicit options replace the file list, including empty arrays. Existing `origin`, `allowTo`, `errorTo`, and `requestTimeout` aliases remain compatible and win over their canonical programmatic names when both are supplied. No normalization or list-merging helper is needed. |
483
- | Root provider-key, named-profile, and TLS compatibility | Y/Y/N | Preserve live deployments using `RESEND_API_KEY`, `MAIL_PROFILES`, `MAIL_TLS_CERT_PATH`, and `MAIL_TLS_KEY_PATH`. Nested selected key presence takes priority even when null or empty. Named profiles never select a different account's default key. |
488
+ | 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
489
  | 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
490
  | 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
491
  | 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. |
@@ -507,3 +512,92 @@ adaptation. Local tests, checks, server launches, live mail sends, and platform
507
512
  execution were not performed by this documentation author. Selected package
508
513
  verification and publication outcomes belong to the release owner's delivery
509
514
  record.
515
+
516
+ ## Parameter examples and error-report path clarification
517
+
518
+ The ordinary shared mail server needs one Resend key. Its current-domain
519
+ behavior, origin list, and incoming application/subscription verification do
520
+ not select among provider profiles. A named profile selects one stored key
521
+ for an entire send or server invocation. Source inspection found named CLI and
522
+ toolchain inputs plus synthetic tests; it did not establish a deployment that
523
+ uses named mail profiles.
524
+
525
+ | Method or action | Gates | Decision, callers, and concrete purpose |
526
+ | --- | --- | --- |
527
+ | 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`. |
528
+ | 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. |
529
+ | 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. |
530
+ | 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. |
531
+ | 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. |
532
+
533
+ The Node gateway reports ordinary request/provider failures through responses,
534
+ observer results, or its console logger. Its error reporter does not call the
535
+ mail sender. In the browser, `Errors` owns global `error` and
536
+ `unhandledrejection` capture and sends error notifications through Mail. A
537
+ failed notification remains pending with a warning rather than immediately
538
+ creating another notification from that send failure.
539
+
540
+ There is a separate conditional path: a subscriber that repeatedly throws
541
+ while handling Mail events reaches the event authority's listener-failure
542
+ reporter. In browsers with `reportError`, that failure can become a new global
543
+ error incident. Sending that incident through Mail can invoke the same failing
544
+ subscriber again. A loop therefore depends on that subscriber and the active
545
+ global error handler; it is not an automatic server response to an ordinary
546
+ mail failure. This is a static source-path finding, not an observed production
547
+ incident or an executed reproduction. This documentation clarification changes
548
+ no error, subscription, retry, or delivery behavior.
549
+
550
+ ## Same-IP subscription exception follow-up
551
+
552
+ The user selected an automatic same-IP exception so the verification service can
553
+ send its own emails without a subscriber key. This supersedes the proposed
554
+ domain exception in `.arcane.env.json`; neither an environment setting nor a
555
+ domain allowlist is needed. The reusable decision belongs in the SDK's existing
556
+ `handleMailRequest`, immediately at its subscription-verification branch.
557
+ Application-owned messages, sender choices, subscription accounts, and the
558
+ verification callback remain with their existing owners.
559
+
560
+ The condition is a nonempty `request.socket.remoteAddress` equal to
561
+ `request.socket.localAddress`. Matching requests skip `verifyMailSubscription`
562
+ and the configured `verifySubscription` callback, including when the request
563
+ has no subscriber key or supplies an invalid one. Requests with different or
564
+ unavailable socket addresses retain the configured verification path. Omitting
565
+ the callback continues to select the existing no-verification setup.
566
+
567
+ | Method or action | Gate 1: Do we care? | Gate 2: Why is it worth the work? | Gate 3: Can we remove it without losing the required result? | Decision and concrete effect |
568
+ | --- | --- | --- | --- | --- |
569
+ | Automatic source/destination IP equality at `handleMailRequest` | Yes. The verifier needs to send service mail without a subscriber key. | It identifies the exact connection case the user selected using already-available socket fields. | No. Removing the exception would restore the verifier's dependency on subscription checking for its own mail. | Keep one inline equality decision with a nonempty source address. No new helper or configuration owner. |
570
+ | Subscription checking for a matching connection | The subscription-verification capability matters; this particular call does not. | Requiring it for the verifier's own email adds an unnecessary dependency and can create a circular service call. | Yes. The user expressly permits these senders regardless of their subscription. | Skip the callback and its asynchronous wait for matching requests. A callback that contacts the verification service makes no such request on this path. |
571
+ | Proposed `.arcane.env.json` domain exception, DNS resolution, address cache, and interface enumeration | No. The selected rule is actual IP equality and requires no setup. | These would introduce configuration, I/O, retained state, or a broader machine/domain interpretation absent from the selected outcome. | Yes. Native request socket fields already supply both values. | Add none. Secret files, named provider profiles, and current configuration loading remain unchanged. |
572
+ | `Origin`, `Host`, `Forwarded`, or `X-Forwarded-For` as evidence of matching connection IPs | No. These request fields do not define the native socket endpoints. | Reading them for this exception would change its meaning and duplicate unrelated routing or CORS concerns. | Yes. The actual socket exposes the relevant addresses directly. | Keep header handling at its existing owner; none of these headers selects the exception. |
573
+ | Special loopback, intranet, subnet, or same-machine rules | No. The selected condition is equal IPs. | A larger address policy would grant a different exception and add work. | Yes. Equal local-development addresses already use the ordinary equality path. | Add none. Another intranet machine with a different IP still uses configured verification. Services on one machine using different interface IPs may also require it. |
574
+ | CORS, routing, methods, idempotency, report/recipient handling, provider result, and cancellation | Yes. These continue to determine the actual mail outcome. | Skipping subscription checking does not change the requested delivery or lifecycle contracts. | No. Their retained behavior supports complete delivery and honest results. | Preserve these owners and their current ordering. Provider acceptance remains separate from the subscription decision. |
575
+
576
+ The operation graph adds one synchronous connection-address comparison at the
577
+ existing request owner. A matching request avoids subscription header checking,
578
+ callback invocation, and its asynchronous verification wait, then uses the
579
+ ordinary provider path. There is no new file read, DNS request, timer, polling,
580
+ cache, helper, shared-state lock, startup barrier, or per-platform process.
581
+ Requests continue independently under the existing handler lifecycle. These are
582
+ source-level operation changes, not measured timing improvements.
583
+
584
+ The selected `node-http-server` raw-request hook supplies the native Node request.
585
+ HTTP/1.1 and HTTP/2 both expose the needed address properties; the same Node
586
+ contract applies on Windows, Linux, and macOS, with Android requiring a compatible
587
+ Node host. The local socket address is the actual destination used by the
588
+ connection, including when the listener binds `0.0.0.0`.
589
+ [Node socket addresses](https://nodejs.org/api/net.html#socketlocaladdress) and
590
+ [HTTP/2 request sockets](https://nodejs.org/api/http2.html#requestsocket)
591
+ document these properties.
592
+
593
+ For equal connection IPs, the verification service's own mail proceeds without
594
+ calling that service again. This removes its circular subscription dependency
595
+ on this path. It does not claim that arbitrary callback code or a repeatedly
596
+ failing browser mail-event subscriber cannot create a separate feedback loop.
597
+ The existing error and retry distinctions remain documented in the
598
+ [mail reference](../reference/mail.md#error-reports-and-retries).
599
+
600
+ This follow-up records the selected behavior and source review. No local test,
601
+ check, build, server launch, live mail send, or platform execution was performed
602
+ by this documentation author. Release and runtime evidence remain with the
603
+ corresponding operation's owner.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -899,8 +899,16 @@ function createConfiguredMailHandler(configuration){
899
899
  if(request.method!=='POST'){
900
900
  throw new MailGatewayFault('mail_method_not_allowed',{statusCode:405});
901
901
  }
902
- if(configuration.verifySubscription){
903
- appId=await verifyMailSubscription(request,configuration,requestController.signal);
902
+ if (configuration.verifySubscription) {
903
+ const requesterIp = request.socket?.remoteAddress;
904
+ const serverIp = request.socket?.localAddress;
905
+ if (!requesterIp || requesterIp !== serverIp) {
906
+ appId = await verifyMailSubscription(
907
+ request,
908
+ configuration,
909
+ requestController.signal
910
+ );
911
+ }
904
912
  }
905
913
  idempotencyKey=requireRequestHeader(request,'idempotency-key');
906
914
  const requestText=await readRequestBodyText(request,{