Skip to content

Commit 85edd61

Browse files
feat(spec): the stack definition admits appName and the non-secret email / sms members; their credentials are refused by name (#22778)
Fixes #22748 Clause-②: yes (widening: the stack definition admits `appName`, the non-secret members of `email`, and `sms` carrying `retries` alone, which it refused as unrecognized keys) The mail and SMS readers already read `config.email`, `config.sms` and `config.appName`, but the stack definition refused all three keys, so only a config `defineStack` did not build could reach them. This PR gives the stack a door for them. It follows the triage direction (`6104830037`), claim `6105299924` and the seat's patch-round order `6106350788`: - `appName` and the non-secret members of `email` are admitted, typed from the existing schemas. - `sms` carries `retries` alone. - The credentials and the SMS provider are refused **by name**, each with the setting that replaces it. **Status: draft.** The diff touches `skills/objectstack-platform/SKILL.md`, a governed Tier H surface: `check:skill-top-level-keys` reconciles that page's top-level key list against the schema, so it cannot stay out of this PR. Landing therefore needs an authorized approval (see the maintainer summary below). The contract review at `CONTRACT_REVIEW_TIER` is owed on the head named in the evidence section. ## Patch round 1 (seat order `6106350788`) - **`main` was merged in** (`a2e94c2a05`) as a merge commit, through `scripts/pm/os-regen-merge.sh`. - `type-alias-convention.pin.test.ts` conflicted on content. Both sides' entries were kept, and the merged pin was green at 777. - The reference index, the one generated artifact the driver deferred, was regenerated with `gen:schema` then `gen:docs` in its own commit (`3a68830032`). - **Open question 1 → A.** The `strictObject` guidance form stays as it was. - **Open question 2 → B.** `sms.provider` is now refused by name, with the `OS_SMS_PROVIDER` / Settings → SMS Delivery prescription, exactly as `sms.providerOptions` is. - **`sms` keeps `retries` alone, and is not refused whole.** `retries` is the SMS service's own attempt budget, and the service keeps it across the settings namespace's transport swap: `setTransport` replaces the transport and nothing else. So it applies to whichever transport delivers. It has no environment or settings carrier, so this block is its only authoring door, and refusing `sms` whole would make a live knob unreachable. A `retries`-only block does not mislead, because a `provider` written beside it is refused by name, with the right setting. - **`SmsProviderSchema` leaves with its only consumer.** It is gone, together with its isomorphic-alias pin (the pin file is back to `main`'s bytes) and the cli parity half. It was never released. It comes back, with the key, in the change that makes `SmsServicePlugin` honour a constructor provider. ## Reader-key census (the admitted shape is this table) Each key the readers read off the config, not the environment, is classified. No reader needs a secret from the config file: every secret has a carrier outside it. So the triage stop rule did not fire. | Reader | Key | Secret? | Carrier outside the file | Stack definition | |---|---|---|---|---| | `resolveEmailCapabilityArg` | `provider` | no | `OS_EMAIL_PROVIDER` | admitted | | | `apiKey` | **yes** | `OS_EMAIL_API_KEY` | **refused by name** | | | `defaultFrom` | no | `OS_EMAIL_FROM` | admitted | | | `retries` | no | `OS_EMAIL_RETRIES` | admitted | | | `queueDelivery` | no | `OS_EMAIL_QUEUE_ENABLED` | admitted | | | `persist` | no | `OS_EMAIL_PERSIST_ENABLED` | admitted | | | `appName` | no | `OS_APP_NAME` | admitted | | | `defaultTemplateContext` | no (template render context) | none (`OS_APP_NAME` for its `appName`) | admitted | | | `options.host` / `.port` / `.secure` / `.user` | no | `OS_EMAIL_SMTP_HOST` / `_PORT` / `_SECURE` / `_USER` | admitted (strict block) | | | `options.messageStream` | no | none | admitted | | | `options.password` | **yes** | `OS_EMAIL_SMTP_PASSWORD` | **refused by name** | | | `options.apiKey` | **yes** (`makeTransport` spreads `options` over the key for resend / postmark) | `OS_EMAIL_API_KEY` | **refused by name** | | `resolveCapabilityArgument` (core), `resolveDeploymentAppName` | top-level `appName` | no | `OS_APP_NAME` | admitted | | `resolveSmsCapabilityArg` | `retries` | no | none | admitted | | | `provider` | no, but **not delivered from a stack** (measured, below) | `OS_SMS_PROVIDER` / Settings → SMS Delivery | **refused by name** | | | `providerOptions` | **yes**: every non-`log` transport requires a credential inside it (Twilio `authToken`, Aliyun `accessKeySecret`) | the `sms` settings namespace overrides (`OS_SMS_TWILIO_*`, `OS_SMS_ALIYUN_*`) | **refused by name, whole** | ### Why the SMS provider settings stop at the door - **`sms.provider` cannot select the transport that delivers.** Measured through `bootStack`: - A `defineStack`-built `sms: { provider: 'twilio' }`, with `OS_SMS_TWILIO_ACCOUNT_SID` / `_AUTH_TOKEN` / `_FROM_NUMBER` set, boots with `sms.isConfigured() === false`. The log says "provider='twilio' selected but transport build failed … falling back to LogSmsTransport". - Control: the same, plus `OS_SMS_PROVIDER=twilio`, logs "transport rebuilt from settings (provider=twilio)" and gives `isConfigured() === true`. - The reason: `applySmsSettings` builds the delivering transport from the settings namespace's own `provider`. The constructor's provider has no credential to build with, because a stack carries none. - **`sms.providerOptions` is refused whole, while `email.options` is narrowed.** This too was measured. - The mail reader layers `OS_EMAIL_SMTP_*` over `config.email.options` key by key, so host and user from the file compose with the password from the environment. - The SMS reader hands `providerOptions` to the transport as written. Non-secret members alone gave `isConfigured=false` for Twilio and for Aliyun, with the environment credential set; the control with `authToken` gave `true`. ## What changes - **`packages/spec/src/system/email-config.zod.ts`** - `StackEmailConfigSchema` is **derived** from `EmailServiceConfigSchema`: it `.omit()`s `apiKey` and `options`, then adds `options: StackEmailProviderOptionsSchema`. A spec pin asserts the difference is exactly `['apiKey']`. - `StackEmailProviderOptionsSchema` is a strict block holding `host` / `port` / `secure` / `user` / `messageStream`. - The layer-1 header says which members a config file may carry, and names `resolveEmailCapabilityArg` (`@objectstack/plugin-email`) as the reader, not `serve.ts`. - **`packages/spec/src/system/sms-config.zod.ts` (new).** `packages/spec` had no SMS configuration schema, so `sms` is typed from what `resolveSmsCapabilityArg` reads. `StackSmsConfigSchema` is `{ retries }`, strict, and refuses `provider`, `providerOptions` and a flat `authToken` / `accessKeySecret` by name. - **`packages/spec/src/stack.zod.ts`** declares `appName`, `email` and `sms` on the stack. - In the compose table (`COMPOSE_KEY_DISPOSITIONS`) all three are `'single'`: one product name, one mail configuration and one SMS configuration per booted stack. - Because they are not `concat` keys, they stay out of the assembled package body. - **The by-name refusal** is a `strictObject` `guidance` entry, the stack's own idiom for keys at the wrong layer (seat ruling on open question 1). The first sentence names the key, and the prescription bullet names the setting. Through the public door, `os validate` exits 1 on a stack carrying `email.apiKey` and on one carrying `sms.provider`. Each refusal names its key and its setting. The admitted shape exits 0. ``` ✗ email: Unrecognized key(s) on the stack `email` block: `apiKey`. • Not authorable here. A provider API key is a credential, and a stack definition is compiled into the published artifact — set OS_EMAIL_API_KEY in the deployment environment instead (required for provider resend / postmark). … ✗ sms: Unrecognized key(s) on the stack `sms` block: `provider`. • Not authorable here. The SMS provider and its credentials are chosen by the sms settings namespace, which builds the delivering transport from its own settings, never from a stack definition (…). Configure them in Setup → Settings → SMS Delivery, or through that namespace's environment overrides: OS_SMS_PROVIDER plus OS_SMS_TWILIO_ACCOUNT_SID / … (Aliyun). … ``` The admitted shape, `appName` plus `email: { provider, defaultFrom, persist }` plus `sms: { retries }`, exits 0. `sms.retries` reaches the transport that delivers. Measured through `bootStack` with `sms: { retries: 2 }`, plus `OS_SMS_PROVIDER=twilio` and the Twilio credentials in the environment: - the log reads "transport rebuilt from settings (provider=twilio)"; - `isConfigured() === true`; - `service.options.retries === 2`, on a `TwilioSmsTransport`. ## Pins - **`packages/spec/src/stack-email-sms.test.ts`** (16 cases): - Accept: `email: { provider, defaultFrom }` builds through `defineStack`. So do every non-secret mail member, `appName` and `sms: { retries }`. - The derivation pin. - Refuse: `email.apiKey`, `email.options.password`, `email.options.apiKey`, **`sms.provider`**, `sms.providerOptions` and a flat `sms.authToken` are each refused. The pin asserts the `STACK_SCHEMA_INVALID` / 422 envelope, the issue path and key, and the setting in the prescription. - Control: a stack without the keys is unchanged by the parse, and neighbouring keys still refuse. - Compose: an identical value passes, and a different one is refused by key. - **`packages/cli/src/commands/serve-stack-email-sms-door.contract.test.ts`** (4 cases). It drives the call `serve` makes for each provider, `resolveCapabilityArgument` with the provider module, on a stack `defineStack` returned: - The `email` block reaches the email provider, with `OS_EMAIL_SMTP_PASSWORD` layered over the file's host and user. - `appName` reaches the template context and AuthPlugin's name. - `sms.retries` reaches the SMS provider, beside the provider `OS_SMS_PROVIDER` names. - `OS_EMAIL_*` still wins per key. - **CONTROL:** with no block, env-only configuration builds exactly what the reader builds from no config. ## Ablation (two legs, through `scripts/ablation-replace.mjs`) The spec pins import `./stack.zod` from `src/`, so no `dist/` sits between the mutation and the reading. Both legs ran on `34286f646a`. Each was restored by `ablation-replace`, and a trap restored from `HEAD` as a fallback. | Leg | Mutation | Reading | |---|---|---| | Control (unmutated) | none | `stack-email-sms.test.ts`: 16 / 16 passed | | `email.apiKey` refusal removed | `guidance: { apiKey: STACK_EMAIL_API_KEY_PRESCRIPTION },` → `guidance: {},` (anchor 1 → 0, replacement 0 → 1, blob `e9d625633b26` → `d5c0f4a5cb8b`) | 1 failed / 15 passed. Exactly the `email.apiKey` pin went red: the message became "Unrecognized key(s) on the stack email block: apiKey. This block is strict from birth…", with no `OS_EMAIL_API_KEY` | | `sms.provider` refusal removed | `provider: STACK_SMS_PROVIDER_SETTINGS_PRESCRIPTION,` → `provider_ABLATED: …` (anchor 1 → 0, replacement 0 → 1, blob `0015c421bb5b` → `8ae310bea6c3`) | 1 failed / 15 passed. Exactly the `sms.provider` pin went red: the message became "Unrecognized key(s) on the stack sms block: provider. This block is strict from birth…", with no `OS_SMS_PROVIDER` | | Restore, each leg | `ablation-replace` restore | blob equals the `HEAD` blob (`e9d625633b26`, `0015c421bb5b`), and `git diff HEAD` is empty | The direction was the expected one: red. The key is still refused, but without its prescription. ## Evidence Builds and tests ran through `scripts/pm/os-verify-lock.sh` on head `34286f646a`. The closure was built first: `pnpm turbo run build --filter=@objectstack/cli... --filter=@objectstack/example-showcase^... …`, 68 successful. | Command | Result | |---|---| | spec `--project local` over the touched and adjacent files: `stack-email-sms`, `assembled-package-body`, `type-alias-convention.pin`, `compose-key-dispositions-export.pin`, `shared/alias-integrity`, `shared/strict-object`, `system/email-config` | 7 files / 115 tests passed | | `pnpm --filter @objectstack/spec typecheck` | exit 0; test layer 52 files / 246 errors / 135 pinned signatures held, unchanged | | `pnpm --filter @objectstack/cli typecheck` | exit 0; test layer 3 files / 28 errors / 6 signatures, unchanged | | cli `--project unit`: the rider (4 cases), `serve-auth-app-name`, `serve-email-capability`, `serve-sms-capability`, `test/option-b-reader-acceptance.pin` | 5 files / 44 tests passed | | Consumers of the keys: core `capability-composition`, plugin-email `capability-arg.config-parity.contract`, runtime `artifact-collections`, verify `harness.served-composition` | 12 / 12, 8 / 8, 22 / 22, 7 / 7 passed | | `pnpm --filter @objectstack/spec check:generated` | all 14 generated artifacts current | | `os validate --json` on `examples/app-crm`, `app-multi-package`, `app-showcase` and `app-todo` | all four `valid: true`. The payloads minus `duration` are byte-identical to round 1's pre-change reading on the base dist: no example app changed | Round 1's full spec suites ran on the pre-merge tree. `--project local` gave 644 files and 19228 passed, 1 todo; `--project repo` gave 54 of 55, the one red being the concurrent-regeneration flake, 20 / 20 when re-run alone. **Gates, all on head `34286f646a`** (`git rev-parse --short HEAD`, working tree clean): - **Derived set.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, with no paths, derived 123 commands. Every one ran with its exit code captured before any pipe, and **all 123 exited 0**. The changeset gates `check-changeset-no-major` and `check-adr-0087-registration` are among them and read the new `Clause-②` line. - **Reconciliation.** `node scripts/pm/dispatch-gates.mjs --ran ran.list` reports "123 derived, 123 run, 0 NOT-MEASURED, 0 UNRUN". - **Roster gates** for the directories this diff writes in, each exit 0: `check:meta-url-spelling`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`, `check-changeset-fixed`, `check:published-readme-exports`, `check:stack-collection-maps`. - **Lint, narrowed and proven.** - ESLint (`--no-inline-config --format json`) on the 8 touched script files reports 8 files, 0 errors, 0 warnings. - The narrowing excludes nothing: `eslint.config.mjs` enables no type-aware linting, so this diff cannot move the verdict on any untouched file. - **Left to CI:** the repo-wide `pnpm lint` and the workspace type-check lanes. ## The published skill: two readings, as the governed-skills rule requires These are unchanged by this round. The page names `sms`, never `sms.provider`. - `skills/objectstack-platform/SKILL.md`: **489 → 488 lines**, 23322 → 23301 bytes, **5831 → 5826 tokens** (ceiling 5833). - Whole catalog, the sum of all ten `skills/*/SKILL.md`: **4410 → 4409 lines**, 210541 → 210520 bytes. - The three keys are appended to the existing enumeration line. They are paid for by deleting a restatement ("; the input type is `ObjectStackDefinitionInput`") that the section's opening sentence already makes. Nothing is re-wrapped. ## Deviations from the claim's file surface, each forced by a gate or a measurement - `skills/objectstack-platform/SKILL.md` (governed, Tier H): `check:skill-top-level-keys`. - `packages/cli/test/option-b-reader-acceptance.pin.test.ts` and `packages/spec/src/assembled-package-body.test.ts`: their envelope-key lists. - `packages/spec/llms.txt` (`check:llms-txt`): 203 → 204 schemas; system 34 → 35. - `content/docs/getting-started/quick-reference.mdx` (`check:quick-reference-counts`): System, 34 → 35 reference pages. - `content/docs/getting-started/quick-start.mdx`: its "Not in the table, and why" list names the three keys. ## Acceptance notes (not filed here) - **`sms.provider` comes back with the fix that honours it.** The seat files the services-lane card from the `bootStack` measurement above. That change re-admits the key, with its provider vocabulary. - **The mail reader's refusal text** for a missing API key still says "set OS_EMAIL_API_KEY (or config.email.apiKey)". A built stack refuses the second form by name and prescribes the first. The text lives in `@objectstack/plugin-email`. - **`transportOptions`, `timeout` and `endpoint`** are read by the transports but declared by no spec text, so the strict `email.options` block does not admit them. `transportOptions` can carry credentials of its own. - **Two pre-existing slips in sentences this PR touched were corrected with them.** The `llms.txt` system row named a retired schema ("Compliance"), and the quick-start list lacked `devHint` / `devLogins`. - **Docs-drift check.** Its five hand-written pages naming `appName` were re-read. None states that the key cannot be set in a config file, or names `serve.ts` as the reader. ## 维护者速读(草稿) **改了什么**:`objectstack.config.ts`(`defineStack`)现在可以写 `appName`、`email`(发件方、重试、是否落库、队列投递、模板上下文、SMTP 主机/端口/TLS/用户名等)和 `sms`(仅 `retries` 重试次数)。过去这三个键会被当成"未知键"拒绝,运行时却一直在读它们。所有密钥(邮件 API Key、SMTP 密码、短信供应商凭据)以及短信供应商 `sms.provider` 都不能写进配置文件,写了会被点名拒绝,并告诉作者改用哪个环境变量,或在 Setup → 设置 → 短信投递 中配置。已发布技能 `skills/objectstack-platform/SKILL.md` 的顶层键清单补上这三个键,所以本 PR 属于 Tier H。 **为什么改**:配置文件的这一半一直有人读,却没有入口可写。配置文件会被 `objectstack build` 编译进制品,制品会被发布,所以密钥一律不进制品。`sms.provider` 实测从配置文件选不到真正投递的通道(投递通道由设置页 / `OS_SMS_PROVIDER` 决定),所以按"声明即兑现"原则先点名拒绝,待短信插件支持后再开放。 **风险与代价(含回滚)**:只放宽,不收紧,已有的合法配置不受影响(四个示例应用的 `os validate` 前后一致)。技能文件为凑 token 余量删去了一处重复说明。回滚:revert 本 PR 即可,无数据迁移。 **席位意见**: **你要做的**:审阅并批准(Tier H,因触及 `skills/**`)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0984817 commit 85edd61

25 files changed

Lines changed: 858 additions & 27 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): the stack definition admits `appName`, the non-secret members of `email`, and `sms.retries`; credentials and the SMS provider are refused by name
6+
7+
Clause-②: yes (widening: the stack definition admits `appName`, the non-secret members of `email`, and `sms` carrying `retries` alone, which it refused as unrecognized keys)
8+
9+
- **What you can write now.** `defineStack()` accepts three top-level keys it used to refuse as unrecognized. The mail and SMS readers already read them, and `objectstack serve` and `bootStack` construct the email and SMS providers through those readers:
10+
- `appName`: the deployment's product name. It is the last configured rung of the name chain (`OS_APP_NAME` > `email.appName` > `email.defaultTemplateContext.appName` > `appName` > `'ObjectStack'`), and it reaches the email templates and AuthPlugin.
11+
- `email` (`StackEmailConfigSchema`): every member of `EmailServiceConfigSchema` except `apiKey`. That is `provider`, `defaultFrom`, `retries`, `persist`, `queueDelivery`, `appName` and `defaultTemplateContext`, plus `options` narrowed to `host` / `port` / `secure` / `user` / `messageStream`. `OS_EMAIL_*` still overrides it per key.
12+
- `sms` (`StackSmsConfigSchema`): `retries`, the SMS service's attempt budget. It applies to whichever transport the `sms` settings namespace selects.
13+
- **What stays elsewhere, and is refused by name.** Each refusal names the key and the setting that replaces it:
14+
- `email.apiKey` and `email.options.apiKey`: a credential, and a stack definition is compiled into the published artifact. Set `OS_EMAIL_API_KEY`.
15+
- `email.options.password`: a credential. Set `OS_EMAIL_SMTP_PASSWORD`; the reader layers it over the host / port / secure / user the file carries.
16+
- `sms.provider`, `sms.providerOptions` (and a flat `authToken` / `accessKeySecret`): the delivering SMS transport is built by the `sms` settings namespace from its own provider and credentials, never from a stack definition. Configure them in Setup → Settings → SMS Delivery, or through that namespace's overrides (`OS_SMS_PROVIDER`, `OS_SMS_TWILIO_*`, `OS_SMS_ALIYUN_*`).
17+
- **Composition.** All three keys compose as single values. `composeStacks` passes an identical value through and refuses two different ones by key.
18+
- **Nothing to migrate.** No stack that was accepted before is refused now. A configuration that set these keys only in an object `defineStack` did not build (the one `objectstack serve` boots) reads the same as before. To build it, move any credential, and the SMS provider, into the environment or Settings.
19+
- New exports from `@objectstack/spec/system`: `StackEmailConfigSchema`, `StackEmailProviderOptionsSchema`, `StackSmsConfigSchema`, and their `z.input` / `z.infer` types.

‎content/docs/getting-started/quick-reference.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Plugin architecture, manifests, and kernel runtime.
8585
| **[Metadata Loader](/docs/references/kernel/metadata-loader)** | `metadata-loader.zod.ts` | MetadataLoaderContract | Metadata loading |
8686
| **[Package Registry](/docs/references/kernel/package-registry)** | `package-registry.zod.ts` | InstalledPackage, InstallPackageRequest | Package resolution |
8787

88-
## System Protocol (16 of 34 schemas)
88+
## System Protocol (16 of 35 schemas)
8989

9090
Runtime environment, logging, jobs, caching, and observability.
9191

‎content/docs/getting-started/quick-start.mdx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,10 @@ schema page is the authority until one lands.
232232
**Not in the table, and why.** `viewItems` and `runtimeModule` are not authorable
233233
at all (`viewItems` is `z.never()`, the machine-assembled channel for
234234
runtime-assembled manifests; `runtimeModule` is written by `objectstack build`);
235-
`manifest`, `i18n`, `api`, `server`, `requires`, `tiers`, `plugins` and
236-
`devPlugins` configure the stack rather than declare metadata items; and
235+
`manifest`, `i18n`, `api`, `server`, `appName`, `email`, `sms`, `requires`,
236+
`tiers`, `plugins`, `devPlugins`, `devHint` and `devLogins` configure the stack
237+
rather than declare metadata items (`email` and `sms` carry no credentials —
238+
those stay in the environment or in Settings); and
237239
`functions` and `onEnable` are code the runtime calls, not metadata it stores.
238240

239241
## How you verify

‎content/docs/references/index.mdx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Protocol reference — every schema by module
33
navTitle: Protocol Reference
4-
description: Every schema published by @objectstack/spec — 1517 schemas across 14 protocol modules
4+
description: Every schema published by @objectstack/spec — 1520 schemas across 14 protocol modules
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -32,9 +32,9 @@ counts are sums of the rows they head. Regenerate with
3232
| [Security Protocol](/docs/references/security) | 5 | 30 | Permission sets, row-level security, sharing rules, tenancy posture. |
3333
| [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
3434
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
35-
| [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
35+
| [System Protocol](/docs/references/system) | 35 | 278 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3636
| [UI Protocol](/docs/references/ui) | 16 | 166 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37-
| **Total** | **195** | **1517** | 14 protocol modules |
37+
| **Total** | **196** | **1520** | 14 protocol modules |
3838

3939
---
4040

@@ -317,7 +317,7 @@ Studio designer metadata — the authoring surfaces for the protocols above.
317317

318318
## System Protocol
319319

320-
**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **34 pages, 275 schemas**
320+
**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **35 pages, 278 schemas**
321321

322322
The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance.
323323

@@ -333,7 +333,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a
333333
| [`dev-login.zod.ts`](/docs/references/system/dev-login) | `DevLogin` |
334334
| [`disaster-recovery.zod.ts`](/docs/references/system/disaster-recovery) | `BackupConfig`, `BackupRetention`, `BackupStrategy`, `DisasterRecoveryPlan`, `FailoverConfig`, `FailoverMode`, `RPO`, `RTO` |
335335
| [`doc.zod.ts`](/docs/references/system/doc) | `Doc` |
336-
| [`email-config.zod.ts`](/docs/references/system/email-config) | `EmailAddressConfig`, `EmailProvider`, `EmailServiceConfig` |
336+
| [`email-config.zod.ts`](/docs/references/system/email-config) | `EmailAddressConfig`, `EmailProvider`, `EmailServiceConfig`, `StackEmailConfig`, `StackEmailProviderOptions` |
337337
| [`email-template.zod.ts`](/docs/references/system/email-template) | `EmailTemplateDefinition`, `EmailTemplateDefinitionCategory`, `EmailTemplateDefinitionVariable` |
338338
| [`encryption.zod.ts`](/docs/references/system/encryption) | `EncryptionAlgorithm`, `EncryptionConfig`, `FieldEncryption`, `KeyManagementProvider`, `KeyRotationPolicy` |
339339
| [`environment-artifact.zod.ts`](/docs/references/system/environment-artifact) | `Sha256Digest` |
@@ -351,6 +351,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a
351351
| [`security-context.zod.ts`](/docs/references/system/security-context) | `ComplianceAuditRequirement`, `ComplianceEncryptionRequirement`, `ComplianceFramework`, `DataClassification`, `DataClassificationPolicy`, `MaskingVisibilityRule`, `SecurityContextConfig`, `SecurityEventCorrelation` |
352352
| [`settings-client.zod.ts`](/docs/references/system/settings-client) | `SettingsChangeEvent` |
353353
| [`settings-manifest.zod.ts`](/docs/references/system/settings-manifest) | `ResolvedSettingValue`, `SettingsActionResult`, `SettingsManifest`, `SettingsNamespacePayload`, `Specifier`, `SpecifierHandler`, `SpecifierOption`, `SpecifierScope`, `SpecifierType`, `SpecifierValueDomain` |
354+
| [`sms-config.zod.ts`](/docs/references/system/sms-config) | `StackSmsConfig` |
354355
| [`stack-server.zod.ts`](/docs/references/system/stack-server) | `ServerRateLimitConfig`, `StackServerConfig`, `StackServerSecurity` |
355356
| [`supplier-security.zod.ts`](/docs/references/system/supplier-security) | `SupplierAssessmentStatus`, `SupplierRiskLevel`, `SupplierSecurityAssessment`, `SupplierSecurityPolicy`, `SupplierSecurityRequirement` |
356357
| [`tenant.zod.ts`](/docs/references/system/tenant) | `DatabaseLevelIsolationStrategy`, `DatabaseProvider`, `QuotaEnforcementResult`, `RowLevelIsolationStrategy`, `SchemaLevelIsolationStrategy`, `Tenant`, `TenantConnectionConfig`, `TenantIsolationConfig`, `TenantIsolationLevel`, `TenantQuota`, `TenantSecurityPolicy`, `TenantUsage` |

‎content/docs/references/system/email-config.mdx‎

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,17 @@ transport for the EmailServicePlugin. Provider is a `provider` tag
1313
+ provider-specific settings; concrete `IEmailTransport`
1414
implementations live in `@objectstack/plugin-email/transports/*`.
1515

16-
Resolution order in `serve.ts`:
17-
1. `config.email.*` from objectstack.config.ts
18-
2. `OS_EMAIL_*` environment variables (override per setting)
16+
Resolution order in `resolveEmailCapabilityArg` (`@objectstack/plugin-email`,
17+
the one reader; `objectstack serve` and `bootStack` both construct the email
18+
provider through it):
19+
1. `config.email.*` from objectstack.config.ts — the NON-SECRET members
20+
only. A config file is compiled into the published artifact, so the
21+
stack definition carries `StackEmailConfigSchema`: every member
22+
below except `apiKey`, with `options` narrowed to host / port / secure /
23+
user / messageStream. `apiKey` and the SMTP `options.password` are
24+
refused there by name; they are environment-only.
25+
2. `OS_EMAIL_*` environment variables (override per setting) — the only
26+
carrier of `OS_EMAIL_API_KEY` and `OS_EMAIL_SMTP_PASSWORD`
1927
3. Default → provider='log' (LogTransport, no real send)
2028

2129
`appName` is the one key whose env layer is not `OS_EMAIL_*` — it is
@@ -32,7 +40,9 @@ defaultTemplateContext), both times with the spec on the lagging side.
3240
SMTP delivery is built in (ADR-0012): select it with provider='smtp'
3341
and supply the connection through `options` (host / port / secure /
3442
user / password) or the matching OS_EMAIL_SMTP_HOST / _PORT /
35-
_SECURE / _USER / _PASSWORD environment variables.
43+
_SECURE / _USER / _PASSWORD environment variables. From a config file the
44+
password always arrives through OS_EMAIL_SMTP_PASSWORD, which the reader
45+
layers over the host / port / secure / user the file carries.
3646

3747
<Callout type="info">
3848
**Source:** `packages/spec/src/system/email-config.zod.ts`
@@ -41,8 +51,8 @@ _SECURE / _USER / _PASSWORD environment variables.
4151
## TypeScript Usage
4252

4353
```typescript
44-
import { EmailAddressConfigSchema, EmailProviderSchema, EmailServiceConfigSchema } from '@objectstack/spec/system';
45-
import type { EmailAddressConfig, EmailProvider, EmailServiceConfig } from '@objectstack/spec/system';
54+
import { EmailAddressConfigSchema, EmailProviderSchema, EmailServiceConfigSchema, StackEmailConfigSchema, StackEmailProviderOptionsSchema } from '@objectstack/spec/system';
55+
import type { EmailAddressConfig, EmailProvider, EmailServiceConfig, StackEmailConfig, StackEmailProviderOptions } from '@objectstack/spec/system';
4656

4757
// Validate data
4858
const result = EmailAddressConfigSchema.parse(data);
@@ -100,3 +110,53 @@ const result = EmailAddressConfigSchema.parse(data);
100110

101111
---
102112

113+
## StackEmailConfig
114+
115+
### Properties
116+
117+
| Property | Type | Required | Description |
118+
| :--- | :--- | :--- | :--- |
119+
| **provider** | `Enum<'log' \| 'resend' \| 'postmark' \| 'smtp'>` | optional (default: `"log"`) | Transport to deliver through (OS_EMAIL_PROVIDER env). Default log — boots, sends nothing |
120+
| **defaultFrom** | `{ name?: string; address: string }` | optional | |
121+
| **retries** | `integer` | optional | Retry attempts on transport throw |
122+
| **persist** | `boolean` | optional | Persist to sys_email (default true) |
123+
| **queueDelivery** | `boolean` | optional | Deliver through the durable sys_job_queue instead of inline (or OS_EMAIL_QUEUE_ENABLED env). Default false. Reuses `retries` as the queue attempt budget; requires a queue service and sys_email persistence, else the boot fails |
124+
| **appName** | `string` | optional | Product name templates interpolate as the appName variable — OS_APP_NAME env wins, then this, then defaultTemplateContext.appName, then the top-level config appName, then "ObjectStack". Also seeds the placeholder no-reply sender when no defaultFrom is configured |
125+
| **defaultTemplateContext** | `Record<string, any>` | optional | Free-form render context merged into every sendTemplate() call, under the per-call data. Passed through unchanged except appName, which is resolved by its own chain — OS_APP_NAME and the appName key both override the value written here |
126+
| **options** | `{ host?: string; port?: integer; secure?: boolean; user?: string; … }` | optional | Provider-specific extras, non-secret members only. smtp: host / port / secure / user, mirroring OS_EMAIL_SMTP_HOST / _PORT / _SECURE / _USER (the password is OS_EMAIL_SMTP_PASSWORD only). postmark: messageStream |
127+
128+
### Nested Shape: `StackEmailConfig.defaultFrom`
129+
130+
| Property | Type | Required | Description |
131+
| :--- | :--- | :--- | :--- |
132+
| **name** | `string` | optional | Display name (e.g. "Acme CRM") |
133+
| **address** | `string` | ✅ | RFC-5322 address |
134+
135+
### Nested Shape: `StackEmailConfig.options`
136+
137+
| Property | Type | Required | Description |
138+
| :--- | :--- | :--- | :--- |
139+
| **host** | `string` | optional | SMTP server hostname (OS_EMAIL_SMTP_HOST env wins). Required for provider smtp unless the env supplies it |
140+
| **port** | `integer` | optional | SMTP port (OS_EMAIL_SMTP_PORT env wins). Default 587; 465 selects implicit TLS |
141+
| **secure** | `boolean` | optional | Require TLS (OS_EMAIL_SMTP_SECURE env wins). Default true |
142+
| **user** | `string` | optional | SMTP AUTH username (OS_EMAIL_SMTP_USER env wins). Its password is environment-only: OS_EMAIL_SMTP_PASSWORD |
143+
| **messageStream** | `string` | optional | Postmark message stream. Default outbound |
144+
145+
146+
---
147+
148+
## StackEmailProviderOptions
149+
150+
### Properties
151+
152+
| Property | Type | Required | Description |
153+
| :--- | :--- | :--- | :--- |
154+
| **host** | `string` | optional | SMTP server hostname (OS_EMAIL_SMTP_HOST env wins). Required for provider smtp unless the env supplies it |
155+
| **port** | `integer` | optional | SMTP port (OS_EMAIL_SMTP_PORT env wins). Default 587; 465 selects implicit TLS |
156+
| **secure** | `boolean` | optional | Require TLS (OS_EMAIL_SMTP_SECURE env wins). Default true |
157+
| **user** | `string` | optional | SMTP AUTH username (OS_EMAIL_SMTP_USER env wins). Its password is environment-only: OS_EMAIL_SMTP_PASSWORD |
158+
| **messageStream** | `string` | optional | Postmark message stream. Default outbound |
159+
160+
161+
---
162+

‎content/docs/references/system/index.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: System Protocol — complete schema reference
33
navTitle: System Protocol
4-
description: "The ObjectStack System Protocol in 34 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example."
4+
description: "The ObjectStack System Protocol in 35 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example."
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -37,6 +37,7 @@ This section contains all protocol schemas for the system layer of ObjectStack.
3737
<Card href="/docs/references/system/security-context" title="Security Context" description="Source: packages/spec/src/system/security-context.zod.ts" />
3838
<Card href="/docs/references/system/settings-client" title="Settings Client" description="Source: packages/spec/src/system/settings-client.zod.ts" />
3939
<Card href="/docs/references/system/settings-manifest" title="Settings Manifest" description="Source: packages/spec/src/system/settings-manifest.zod.ts" />
40+
<Card href="/docs/references/system/sms-config" title="Sms Config" description="Source: packages/spec/src/system/sms-config.zod.ts" />
4041
<Card href="/docs/references/system/stack-server" title="Stack Server" description="Source: packages/spec/src/system/stack-server.zod.ts" />
4142
<Card href="/docs/references/system/supplier-security" title="Supplier Security" description="Source: packages/spec/src/system/supplier-security.zod.ts" />
4243
<Card href="/docs/references/system/tenant" title="Tenant" description="Source: packages/spec/src/system/tenant.zod.ts" />

‎content/docs/references/system/meta.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"doc",
4141
"---More---",
4242
"dev-login",
43+
"sms-config",
4344
"stack-server"
4445
]
4546
}

0 commit comments

Comments
 (0)