Skip to content

Commit d9895a1

Browse files
fix(cli): os serve reads OS_TRUSTED_APP_SCHEMES into AuthPlugin's trustedAppSchemes (#30004)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBSxT6HBZKBXR3X4SjZA56g8)_ Fixes #29972 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** `os serve` 新增读取环境变量 `OS_TRUSTED_APP_SCHEMES`(逗号分隔,逐项 trim、丢空项、去重,读法与 `OS_TRUSTED_ORIGINS` 相同),作为 `trustedAppSchemes` 传给它构建的 `new AuthPlugin({...})`。`os dev` / `os start` 都以继承的环境派生 `serve` 子进程,走同一行。文档 `environment-variables.mdx` 在 `OS_TRUSTED_ORIGINS` 旁加一行。 - **为什么改:** 18.3.0 的移动端登录交接(`GET /api/v1/auth/one-time-token/handoff`)只给已登记的 app scheme 发 token,而 `os serve` 没有任何设置能到达这个键,所以自托管部署对每个 app 都回 400,mobile#148 卡在这里。 - **风险与回滚:** 认证边界:登记一个 scheme 即决定谁能收到登录交接。未设置 = 维持现状(不登记任何 scheme,`one-time-token` 插件不挂载,交接对所有目标回 400),没有默认值。坏值不在读取处吞掉,由 plugin-auth 的 `assertTrustedAppSchemes` 在启动时报错并点名。只加环境变量,不加 stack 键。`@objectstack/cli` patch。回滚 = revert 本 PR。 - **先看:** `packages/cli/src/commands/serve.ts` 的 `resolveTrustedAppSchemes` 与 `new AuthPlugin({` 里的 `trustedAppSchemes:` 一行;测试 `serve-auth-trusted-app-schemes.contract.test.ts`。 - **两分钟验证:** `pnpm --filter @objectstack/cli exec vitest run src/commands/serve-auth-trusted-app-schemes.contract.test.ts`,预期 `12 passed`。 - **需要你做的:** 评审本 PR 的设置读法与"未设置不挂载"(认证边界,按 #29714 的先例由维护者过目)。 ## What changes - **`packages/cli/src/commands/serve.ts`.** A new `resolveTrustedAppSchemes(env)` reads `OS_TRUSTED_APP_SCHEMES`: comma-separated, each entry trimmed, empty entries dropped, repeats folded, in the order typed. Unset, empty, or nothing but separators answers `undefined`. The `new AuthPlugin({...})` call passes `trustedAppSchemes: resolveTrustedAppSchemes(process.env)`. - **Unset stays unset.** There is no default scheme. With no scheme registered, plugin-auth mounts no `one-time-token` plugin and the hand-off route refuses every target, exactly as today. - **A bad entry still fails loudly.** The reader never validates, lower-cases or drops an entry. A bad one reaches `AuthManager`, where `assertTrustedAppSchemes` refuses it at construction, naming it. - **Every CLI door.** `serve.ts` is the only CLI file that constructs `AuthPlugin`. `os dev` (`dev.ts`, `localEnv` from `process.env`) and `os start` (`start.ts`, `localEnv` from `process.env`) spawn `serve` as a child with the inherited environment, so they read the variable through the same line. Measured below on an `os dev` boot. - **`content/docs/deployment/environment-variables.mdx`.** One row for `OS_TRUSTED_APP_SCHEMES` directly under `OS_TRUSTED_ORIGINS`: what it registers, how it is read, that a bad entry fails the boot, and that unset leaves the plugin unmounted and the hand-off answering `400`. - **Test inputs.** The new contract test reads that docs row, so `scripts/cross-package-test-inputs.mjs` and `turbo.json`'s `@objectstack/cli#test` inputs name the page (`check:cross-package-test-inputs`). `scripts/check-ci-filter-parity.mjs`'s rollback count moves 19 → 20 for that declaration, by name. - **Env variable only.** No stack key is added: that would be a protocol key, which goes contract-first. The hosted runtime's own `AuthPlugin` (cloud) is not touched. - **Declarations.** `Clause-②: no`. The new `resolveTrustedAppSchemes` and `TRUSTED_APP_SCHEMES_ENV_NAME` are named exports of `commands/serve.ts`, which the package entry does not re-export (`src/index.ts` exports only `ServeCommand`), and `serve.js` is not a published subpath (`exports`: `.`, `./console`, `./hook-body`). So the public surface does not widen. No input the CLI accepted before is refused: no reader of `OS_TRUSTED_APP_SCHEMES` existed (0 hits on main). `Metadata-protocol: none`. Changeset: `@objectstack/cli` patch. ## Tests New `packages/cli/src/commands/serve-auth-trusted-app-schemes.contract.test.ts`, 12 cases, in the style of the sibling `serve-auth-*.contract.test.ts`: - **Reader:** the variable name; unset answers `undefined`; `''`, `' '`, `','`, `' , ,, '` answer `undefined`; one scheme; padded entries, empties and repeats; a bad entry (`Https`, `AI.Objectos.Mobile`) passes through as typed. - **Reader into a real `AuthManager`:** a set value registers its schemes (`getRegisteredAppSchemes()`); unset registers none; `ai.objectos.mobile,myapp` fails construction with `invalid trustedAppSchemes: "myapp"`. - **Construction scan** (`serve.ts`, comments masked): exactly one `new AuthPlugin(`, carrying `trustedAppSchemes:resolveTrustedAppSchemes(process.env),`; no second reader of the variable in `serve.ts`; the docs row sits directly under `OS_TRUSTED_ORIGINS`. ## Acceptance notes Boot check on `examples/app-crm`, `objectstack dev --fresh -p N`, built CLI from this branch. Request: `GET /api/v1/auth/one-time-token/handoff?redirect_uri=ai.objectos.mobile%3A%2F%2Fauth%2Fcallback&code_challenge=<43 chars>&code_challenge_method=S256`. | Boot | Request | Answer | |:---|:---|:---| | `OS_TRUSTED_APP_SCHEMES=' ai.objectos.mobile , '` (padded, trailing empty) | no session | `401 UNAUTHENTICATED` | | same | signed in as the dev admin | `302`, `location: ai.objectos.mobile://auth/callback?token=…` | | same | `redirect_uri=com.other.app://cb` | `400` "is not a registered app scheme" | | unset | no session | `400` "is not a registered app scheme" | | unset | signed in | `400` "is not a registered app scheme" | | unset | `POST /api/v1/auth/one-time-token/verify` | `404` (plugin not mounted) | | `OS_TRUSTED_APP_SCHEMES='ai.objectos.mobile,MyApp'` | — | boot exits 1: `✗ [auth] invalid trustedAppSchemes: "MyApp" is not a lowercase reverse-domain scheme …` | | **ablated** (wiring line removed, CLI rebuilt), variable set | no session | `400` "is not a registered app scheme": the card's bug back | ## Verification Measured on `objectstack-ai/objectstack@9049be2` (main at branch point) plus this branch. <details><summary>Ablations (each red once, then restored)</summary> - **A1: the `trustedAppSchemes:` line removed from `new AuthPlugin({`.** 1 of 12 red: `passes trustedAppSchemes from resolveTrustedAppSchemes(process.env)` (`expected '{secret,baseUrl,appName:resolveDeploy…' to contain 'trustedAppSchemes:resolveTrustedAppSc…'`). The same ablation on a rebuilt dist answered `400` on the live boot (table above). - **A2: a default scheme when unset** (`return ['ai.objectos.mobile']`). 2 of 12 red: `unset stays unset: no default scheme` and `an unset variable registers none`. - **A3: the reader silently drops an entry that does not look like a scheme.** 2 of 12 red: `passes a bad entry through as typed` and `a bad entry fails construction loudly, naming the entry` (`expected [Function] to throw an error`). - Restored from the commit: 12/12 green; dist rebuilt and grepped (1 hit for the wiring line). </details> <details><summary>Builds, suites, lint and gates</summary> - `pnpm exec turbo run build --concurrency=2` (whole workspace, under the verify lock): exit 0. - `pnpm --filter @objectstack/cli test` (both tiers): 572 files passed, 1 skipped; 7012 tests passed, 9 skipped. - `pnpm --filter @objectstack/cli run typecheck` (`tsc --noEmit` plus `check:test-typecheck`): exit 0. The first attempt was a tsc OOM on the shared box (exit null, no diagnostics); rerun with `NODE_OPTIONS=--max-old-space-size=8192`. - `pnpm lint` (repo-wide): exit 0. - `node scripts/pm/dispatch-gates.mjs --commands` on head `3b20bb01`: 111 commands, all exit 0, recorded with `--ran`: "111 derived famil(ies) accounted for — 111 run, 0 NOT-MEASURED". Also run, from the roster block: `check:auth-mount-ledger` and `check:changeset-fixed`, both exit 0. - Two reds on the way, both fixed in this PR: - `check:cross-package-test-inputs`: the contract test reads `environment-variables.mdx`, which cli's declared inputs did not cover. The page now sits in `scripts/cross-package-test-inputs.mjs` and in `turbo.json`'s `@objectstack/cli#test` inputs, beside the `deployment/cli.mdx` the cli e2e tests already read. - `check:ci-filter-parity --self-test`: that declaration is one more glob the pre-#10015 `crosspkg` rollback uncovers, so its pinned count moves 19 → 20, recorded by name the way #14824 and #15818 recorded theirs. - `check-plugin-teardown-shape --self-test` needs its pinned fixture commit in a shallow clone; fetched, it exits 0 (48 cases). </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No spec change.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [ ] 4. One issue, one branch off an up-to-date `main`, one pull request. The branch is the harness-pinned `claude/project-thread-p09liv`, not `claude/issue-29972-*`. It is off current `main`, with one issue and one PR. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (Except the `维护者速读` section the dispatch asks for.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzipyHVcQ1pbgpD5zqV <!-- signed: domain:server#6 · os-support-ai · session_01WGufzipyHVcQ1pbgpD5zqV --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 48914fd commit d9895a1

7 files changed

Lines changed: 204 additions & 3 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os serve` reads `OS_TRUSTED_APP_SCHEMES` into AuthPlugin's `trustedAppSchemes`, so a deployment can register its mobile app for the sign-in hand-off
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `serve` constructed `AuthPlugin` with no `trustedAppSchemes`, and no setting a deployment could make reached the key. The `one-time-token` plugin never mounted and `GET /api/v1/auth/one-time-token/handoff` answered `400 … is not a registered app scheme` for every app, whatever the operator set.
11+
- **Now.** `OS_TRUSTED_APP_SCHEMES` is read the way `OS_TRUSTED_ORIGINS` is: comma-separated, each entry trimmed, empty entries dropped, repeats folded. The list is passed as `trustedAppSchemes`. `os dev` and `os start` run `serve` as a child with the inherited environment, so they read it through the same line.
12+
- **Unset stays unset.** No default scheme is registered: unset, empty or only separators, the plugin stays unmounted and the hand-off refuses every target, as before.
13+
- **A bad entry still fails the boot.** Entries are not validated, lower-cased or dropped by the reader; plugin-auth's `assertTrustedAppSchemes` refuses a bad one at construction, naming it.
14+
- Environment variable only: no stack key is added. The hosted runtime's own `AuthPlugin` is not changed. Listed in `content/docs/deployment/environment-variables.mdx` beside `OS_TRUSTED_ORIGINS`.

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Read by `@objectstack/service-print` when `PrintServicePlugin` is composed; see
108108
| `OS_OIDC_PROVIDER_ENABLED` | boolean | tracks MCP | When `true`, expose this instance as an OIDC identity provider. When unset it follows the MCP server surface (`OS_MCP_SERVER_ENABLED`, on by default) — the MCP human-client track is OAuth 2.1, so every MCP-enabled deployment is its own authorization server. |
109109
| `OS_COOKIE_DOMAIN` | string | — | Cookie domain for cross-subdomain session sharing (e.g. `.example.com`). |
110110
| `OS_TRUSTED_ORIGINS` | csv | — | Comma-separated list of origins permitted for auth callbacks / CSRF. |
111+
| `OS_TRUSTED_APP_SCHEMES` | csv | — | Comma-separated custom URL schemes of the native apps the mobile sign-in hand-off (`GET /api/v1/auth/one-time-token/handoff`) may redirect a one-time token to, e.g. `ai.objectos.mobile`. Read by `os serve`, and so by the `serve` that `os dev` and `os start` run; entries are trimmed and empty ones dropped. Each entry must be a lowercase reverse-domain scheme with no `:` suffix: a bad one fails the boot, naming it. Unset, no app is registered, the one-time-token plugin is not mounted, and the hand-off answers `400` for every target. |
111112
| `OS_TERMS_URL` | url | — | URL shown next to the "Terms of Service" link on the sign-up form. Empty string hides the link. |
112113
| `OS_PRIVACY_URL` | url | — | URL shown next to the "Privacy Policy" link on the sign-up form. Empty string hides the link. |
113114
| `OS_TURNSTILE_SITE_KEY` / `OS_TURNSTILE_SECRET_KEY` | string | — | Opt-in Cloudflare Turnstile on email sign-up. Set **both** and `/sign-up/email` (the register page and the first-run setup) requires a verified Turnstile token, the Console renders the challenge, and its content-security-policy admits `https://challenges.cloudflare.com`; sign-in and password reset are not challenged. Only the site key is served to the browser (`features.turnstileSiteKey`); the secret never leaves the server. With either unset nothing changes (one boot `warn` names the missing one). |
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// #29972 — `objectstack serve` hands the deployment's registered app schemes
4+
// to AuthPlugin, from `OS_TRUSTED_APP_SCHEMES`.
5+
//
6+
// The mobile sign-in hand-off (`GET /api/v1/auth/one-time-token/handoff`)
7+
// redirects a one-time token only to a scheme `AuthConfig.trustedAppSchemes`
8+
// registers, and plugin-auth mounts the `one-time-token` plugin only when that
9+
// list is non-empty. `serve` used to construct AuthPlugin with no
10+
// `trustedAppSchemes` and nothing a deployment could set reached the key, so
11+
// the hand-off refused every app (`400 … is not a registered app scheme`).
12+
//
13+
// Three halves. The reader half drives `resolveTrustedAppSchemes` over the
14+
// shapes an operator types: unset stays unset (no default scheme), entries are
15+
// trimmed, empties dropped and repeats folded the way `OS_TRUSTED_ORIGINS` is
16+
// read, and nothing is validated or dropped. The refusal half hands what the
17+
// reader answers for a bad entry to a real `AuthManager` (what AuthPlugin
18+
// builds at init) and shows the boot still fails loudly, naming the entry. The
19+
// construction half scans `serve.ts` with comments masked, as the sibling
20+
// `serve-auth-*.contract.test.ts` files do: AuthPlugin is imported through a
21+
// variable specifier, so the type checker cannot see the literal.
22+
23+
import { describe, it, expect } from 'vitest';
24+
import { readFileSync } from 'node:fs';
25+
import path from 'node:path';
26+
import { fileURLToPath } from 'node:url';
27+
import { AuthManager } from '@objectstack/plugin-auth';
28+
import { resolveTrustedAppSchemes, TRUSTED_APP_SCHEMES_ENV_NAME } from './serve.js';
29+
import { maskComments } from '../../../../scripts/js-comment-mask.mjs';
30+
31+
const HERE = path.dirname(fileURLToPath(import.meta.url));
32+
const SERVE_SOURCE = maskComments(readFileSync(path.resolve(HERE, 'serve.ts'), 'utf8'));
33+
const ENV_DOC = readFileSync(
34+
path.resolve(HERE, '../../../../content/docs/deployment/environment-variables.mdx'),
35+
'utf8',
36+
);
37+
38+
/** The argument text of every `new AuthPlugin(` call. */
39+
function authPluginConstructions(source: string): string[] {
40+
const out: string[] = [];
41+
const needle = 'new AuthPlugin(';
42+
for (let i = source.indexOf(needle); i !== -1; i = source.indexOf(needle, i + 1)) {
43+
const open = i + needle.length - 1;
44+
let depth = 0;
45+
for (let j = open; j < source.length; j++) {
46+
if (source[j] === '(') depth++;
47+
else if (source[j] === ')' && --depth === 0) {
48+
out.push(source.slice(open + 1, j));
49+
break;
50+
}
51+
}
52+
}
53+
return out;
54+
}
55+
56+
const normalise = (text: string) => text.replace(/\s+/g, '');
57+
const env = (value?: string): NodeJS.ProcessEnv =>
58+
value === undefined ? {} : { [TRUSTED_APP_SCHEMES_ENV_NAME]: value };
59+
60+
const managerWith = (trustedAppSchemes: string[] | undefined) =>
61+
new AuthManager({
62+
secret: 'a-test-secret-that-is-at-least-32-chars-long',
63+
baseUrl: 'http://localhost:3000',
64+
trustedAppSchemes,
65+
} as any);
66+
67+
describe('resolveTrustedAppSchemes reads OS_TRUSTED_APP_SCHEMES', () => {
68+
it('reads the variable named OS_TRUSTED_APP_SCHEMES', () => {
69+
expect(TRUSTED_APP_SCHEMES_ENV_NAME).toBe('OS_TRUSTED_APP_SCHEMES');
70+
});
71+
72+
it('unset stays unset: no default scheme', () => {
73+
expect(resolveTrustedAppSchemes(env())).toBeUndefined();
74+
});
75+
76+
it('set but empty, or only separators and spaces, registers nothing', () => {
77+
for (const value of ['', ' ', ',', ' , ,, ']) {
78+
expect(resolveTrustedAppSchemes(env(value)), JSON.stringify(value)).toBeUndefined();
79+
}
80+
});
81+
82+
it('one scheme', () => {
83+
expect(resolveTrustedAppSchemes(env('ai.objectos.mobile'))).toEqual(['ai.objectos.mobile']);
84+
});
85+
86+
it('trims entries, drops empties and folds repeats, in the order typed', () => {
87+
expect(
88+
resolveTrustedAppSchemes(env(' ai.objectos.mobile , ,com.example.app,ai.objectos.mobile, ')),
89+
).toEqual(['ai.objectos.mobile', 'com.example.app']);
90+
});
91+
92+
it('passes a bad entry through as typed: never validated, lower-cased or dropped here', () => {
93+
expect(resolveTrustedAppSchemes(env('ai.objectos.mobile, Https,AI.Objectos.Mobile'))).toEqual([
94+
'ai.objectos.mobile',
95+
'Https',
96+
'AI.Objectos.Mobile',
97+
]);
98+
});
99+
});
100+
101+
describe('what the reader answers is what AuthManager registers or refuses', () => {
102+
it('a set variable registers its schemes', () => {
103+
const manager = managerWith(resolveTrustedAppSchemes(env('ai.objectos.mobile, com.example.app')));
104+
expect(manager.getRegisteredAppSchemes()).toEqual(['ai.objectos.mobile', 'com.example.app']);
105+
});
106+
107+
it('an unset variable registers none', () => {
108+
expect(managerWith(resolveTrustedAppSchemes(env())).getRegisteredAppSchemes()).toEqual([]);
109+
});
110+
111+
it('a bad entry fails construction loudly, naming the entry', () => {
112+
expect(() => managerWith(resolveTrustedAppSchemes(env('ai.objectos.mobile,myapp')))).toThrow(
113+
/invalid trustedAppSchemes: "myapp"/,
114+
);
115+
});
116+
});
117+
118+
describe('serve constructs AuthPlugin with the schemes OS_TRUSTED_APP_SCHEMES names', () => {
119+
it('passes trustedAppSchemes from resolveTrustedAppSchemes(process.env)', () => {
120+
const constructions = authPluginConstructions(SERVE_SOURCE);
121+
// Absence must be loud: a scan that finds no construction proves nothing.
122+
expect(constructions.length, 'serve.ts must construct AuthPlugin exactly once').toBe(1);
123+
expect(normalise(constructions[0])).toContain('trustedAppSchemes:resolveTrustedAppSchemes(process.env),');
124+
});
125+
126+
it('reads the variable nowhere else in serve.ts', () => {
127+
// One reader: a second `process.env.OS_TRUSTED_APP_SCHEMES` could disagree
128+
// with the one AuthPlugin is built from.
129+
expect(SERVE_SOURCE).not.toMatch(/process\.env\.OS_TRUSTED_APP_SCHEMES|\['OS_TRUSTED_APP_SCHEMES'\]/);
130+
});
131+
132+
it('is listed in the environment-variables reference next to OS_TRUSTED_ORIGINS', () => {
133+
const rows = ENV_DOC.split('\n');
134+
const origins = rows.findIndex((row) => row.startsWith('| `OS_TRUSTED_ORIGINS` |'));
135+
expect(origins, 'the OS_TRUSTED_ORIGINS row').toBeGreaterThan(-1);
136+
expect(rows[origins + 1]).toMatch(/^\| `OS_TRUSTED_APP_SCHEMES` \| csv \|/);
137+
});
138+
});

‎packages/cli/src/commands/serve.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4363,6 +4363,12 @@ export default class Serve extends Command {
43634363
appName: resolveDeploymentAppName((config as any).email ?? {}, process.env, (config as any).appName),
43644364
socialProviders: Object.keys(socialProviders).length > 0 ? socialProviders : undefined,
43654365
trustedOrigins: trustedOrigins.length ? trustedOrigins : undefined,
4366+
// The native app schemes the mobile sign-in hand-off may redirect a
4367+
// token to, from `OS_TRUSTED_APP_SCHEMES` (#29972). Unset stays
4368+
// unset: no scheme registered, the `one-time-token` plugin is not
4369+
// mounted and the hand-off refuses every target. A bad entry is
4370+
// refused by AuthPlugin at construction, never dropped.
4371+
trustedAppSchemes: resolveTrustedAppSchemes(process.env),
43664372
// The stack's `server.trustProxy`, the same authored value the
43674373
// dispatcher's inbound rate limiter receives below: whether the
43684374
// ADR-0069 D5 IP allow-list and the D7 login audit may believe
@@ -6897,6 +6903,35 @@ export function formatUnusableAuthBaseUrlDiagnostic(
68976903
+ ' OS_BASE_URL / http://localhost:<port>.';
68986904
}
68996905

6906+
/** The variable `serve` reads the deployment's registered app schemes from (#29972). */
6907+
export const TRUSTED_APP_SCHEMES_ENV_NAME = 'OS_TRUSTED_APP_SCHEMES';
6908+
6909+
/**
6910+
* The app schemes this deployment registers for the mobile sign-in hand-off
6911+
* (`AuthConfig.trustedAppSchemes`), read from `OS_TRUSTED_APP_SCHEMES` in the
6912+
* shape `OS_TRUSTED_ORIGINS` is read: comma-separated, each entry trimmed,
6913+
* empty entries dropped, repeats folded (#29972).
6914+
*
6915+
* Unset (or nothing but separators and spaces) answers `undefined`: no scheme
6916+
* is registered, so plugin-auth mounts no `one-time-token` plugin and the
6917+
* hand-off route refuses every target. There is no default scheme — the
6918+
* hand-off is opt-in per deployment.
6919+
*
6920+
* ⛔ An entry is never validated, lower-cased or dropped here. A bad one goes
6921+
* through to `new AuthPlugin(...)` as typed, where `assertTrustedAppSchemes`
6922+
* refuses it at construction naming the entry, so a misspelt scheme fails the
6923+
* boot instead of booting a hand-off that refuses the deployment's own app.
6924+
*/
6925+
export function resolveTrustedAppSchemes(env: NodeJS.ProcessEnv): string[] | undefined {
6926+
const raw = env[TRUSTED_APP_SCHEMES_ENV_NAME];
6927+
if (raw === undefined) return undefined;
6928+
const schemes: string[] = [];
6929+
for (const entry of raw.split(',').map((s) => s.trim()).filter(Boolean)) {
6930+
if (!schemes.includes(entry)) schemes.push(entry);
6931+
}
6932+
return schemes.length ? schemes : undefined;
6933+
}
6934+
69006935
// [#22301] The constructor arguments a served boot hands the `storage`, `email`
69016936
// and `sms` providers are read by ONE rule, `resolveCapabilityArgument`
69026937
// (`@objectstack/core`), which `@objectstack/verify`'s `bootStack` reads too —

‎scripts/check-ci-filter-parity.mjs‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,10 +1294,15 @@ export async function selfTest() {
12941294
// rollback keeps leaves the count alone, one under a new root moves it and
12951295
// is recorded here by name -- and a root `core` starts covering moves it the
12961296
// other way, recorded the same way.
1297+
// Plus, since #29972, the one doc page that card declared for
1298+
// @objectstack/cli: the `OS_TRUSTED_APP_SCHEMES` contract test holds the
1299+
// variable's row beside `OS_TRUSTED_ORIGINS` in
1300+
// `deployment/environment-variables.mdx`, covered only through the same
1301+
// `content/**` root, like #14824's three and #15818's two. Twenty.
12971302
const preFix = judge(fixtureWorkflow({ core: real.filters?.core, crosspkg: ['scripts/**'] }), CROSS_PACKAGE_TEST_INPUTS);
12981303
assert(
1299-
new Set(uncoveredGlobs(preFix)).size === 19,
1300-
`rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three plus #15818's two plus #18650's one, minus the three under the \`skills/**\` build input \`core\` now covers -- got ${new Set(uncoveredGlobs(preFix)).size}`,
1304+
new Set(uncoveredGlobs(preFix)).size === 20,
1305+
`rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three plus #15818's two plus #18650's one plus #29972's one, minus the three under the \`skills/**\` build input \`core\` now covers -- got ${new Set(uncoveredGlobs(preFix)).size}`,
13011306
);
13021307
assert(
13031308
!uncoveredGlobs(preFix).includes('skills/**'),
@@ -1333,6 +1338,10 @@ export async function selfTest() {
13331338
`-- and #14824 added the \`os create plugin\` scaffold-listing page ${page}, by name`,
13341339
);
13351340
}
1341+
assert(
1342+
uncoveredGlobs(preFix).includes('content/docs/deployment/environment-variables.mdx'),
1343+
`-- and #29972 added the OS_TRUSTED_APP_SCHEMES reference page, by name`,
1344+
);
13361345
for (const page of [
13371346
'content/docs/deployment/troubleshooting.mdx',
13381347
'content/docs/getting-started/index.mdx',
@@ -1885,7 +1894,7 @@ export async function selfTest() {
18851894
`same-root-different-file case observed failing and then covered by naming the file, a glob covered by ` +
18861895
`\`core\`, one covered only by \`crosspkg\` and one covered by neither judged separately in one table, the ` +
18871896
`stale-entry direction, seven refusals over subjects that could not be read, the checked-in ci.yml, the ` +
1888-
`pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three plus #15818's two plus #18650's one, minus the three under the \`skills/**\` build input \`core\` now covers, ` +
1897+
`pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three plus #15818's two plus #18650's one plus #29972's one, minus the three under the \`skills/**\` build input \`core\` now covers, ` +
18891898
`and the CI wiring read out of lint.yml; ` +
18901899
`and Build Core's build inputs: a build input dropped from \`core\` observed red by path and declaring task, ` +
18911900
`turbo.json itself, a globalDependencies entry and a new-root input each required, test-task and negated inputs ` +

‎scripts/cross-package-test-inputs.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,9 @@ export const CROSS_PACKAGE_TEST_INPUTS = {
542542
'examples/app-showcase/src/data/objects/semantic-zoo.object.ts',
543543
'examples/app-showcase/src/ui/pages/task-triage.page.ts',
544544
'content/docs/deployment/cli.mdx',
545+
// src/commands/serve-auth-trusted-app-schemes.contract.test.ts holds the
546+
// OS_TRUSTED_APP_SCHEMES row beside OS_TRUSTED_ORIGINS on this page (#29972).
547+
'content/docs/deployment/environment-variables.mdx',
545548
'content/docs/deployment/index.mdx',
546549
'content/docs/deployment/troubleshooting.mdx',
547550
'content/docs/getting-started/index.mdx',

‎turbo.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@
283283
"$TURBO_ROOT$/examples/app-showcase/src/data/objects/semantic-zoo.object.ts",
284284
"$TURBO_ROOT$/examples/app-showcase/src/ui/pages/task-triage.page.ts",
285285
"$TURBO_ROOT$/content/docs/deployment/cli.mdx",
286+
"$TURBO_ROOT$/content/docs/deployment/environment-variables.mdx",
286287
"$TURBO_ROOT$/content/docs/deployment/index.mdx",
287288
"$TURBO_ROOT$/content/docs/deployment/troubleshooting.mdx",
288289
"$TURBO_ROOT$/content/docs/getting-started/index.mdx",

0 commit comments

Comments
 (0)