Skip to content

Commit 001b617

Browse files
committed
Merge origin/main (carries #30368, the liveness fix) into claude/project-thread-8y2k2m
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZ1rxZPzain2p5bv5x
2 parents 2f621a5 + 38717d3 commit 001b617

34 files changed

Lines changed: 2442 additions & 111 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the liveness ledger's `query.searchFields` row cites `engine-find.ts#expandSearchOnAst`, where the hand-off lives, so `check:liveness` is green again
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- The row cited `engine.ts#expandSearchOnAst`. The body has lived in `engine-find.ts` since the `engine.ts` codemod split, and the last mention of `searchFields` in `engine.ts`, a private docblock, moved out with the first slice of the hand-series docblock move, so the check found no mention of the key in the cited file.
11+
- Nothing to do on upgrade: only the shipped `liveness/query.json` evidence text changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/rest': minor
4+
---
5+
6+
fix(metadata-protocol,rest): a `.custom` pair's declared override of a page, dataset, app, book, dashboard, view or report is labelled in the ruled order on every `/meta` read — the pair's own translation, then the override's own label, then the base's translation (#30062)
7+
8+
Clause-②: yes (widening) — `ObjectStackProtocolImplementation` gains six public methods: `getPackagedPageBase`, `getPackagedDatasetBase`, `getPackagedAppBase`, `getPackagedBookBase`, `servedOverrideBaseOf` and `getPairTranslationBundle` (measured on `packages/metadata-protocol/dist/index.d.ts`); `@objectstack/rest`'s exported `MetaBookTreeSources` accepts one more optional key, `resolveProtocol` (measured on `packages/rest/dist/index.d.ts`). No accepted key, value or answer is taken away.
9+
Metadata-protocol: semantic
10+
11+
- **Now.** A changed label on a declared override reads the pair's own translation of that key when the pair ships one, else the override's own text, in every locale, on `GET /meta/:type/:name`, `GET /meta/:type` and `GET /meta/book/:name/tree`. An unchanged label still reads the base's translation. Before, a page, dataset, app or book override read the base's translation over its own changed label, and no type read the pair's translation first.
12+
- **The pair's translations.** They are the pair's own active `translation` rows (`package_id` = the pair). A `.custom` package has no code bundle, so it has no code translation source. A package-less authored row is in the deployment catalog but is not the pair's, so it does not rank first.
13+
- **Object.** An object is extended, never replaced (ADR-0146 D10), so it has no declared override and no pair bundle: seven types take one.
14+
- **The book tree.** `GET /meta/book/:name/tree` now judges a book against its packaged base as the list does, so the docs sidebar and the book list serve the same label (#26690).
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/service-settings': patch
4+
---
5+
6+
fix(plugin-auth)!: saving the auth settings refuses to turn off the last way to sign in (#30202)
7+
8+
Clause-②: yes (narrowing) — a save an administrator could make before is now refused at the settings save door with the existing `SETTINGS_VALIDATION` / `invalid_value` field error: a save after which email and password is off and nothing else signs anyone in, judged on the whole save, including the Google change that removes the last provider while email and password is off. No new error code or settings key. The widening half: `SettingsWritePredicateInput`, exported from `@objectstack/service-settings`, gains an optional `batch` field, the whole write a predicate is asked about.
9+
Metadata-protocol: none
10+
Skills-docs: none — no file under `skills/**` teaches `auth.email_password_enabled` or the settings write predicate; `content/docs/deployment/environment-variables.mdx` already names the `OS_AUTH_EMAIL_PASSWORD_ENABLED` override used for recovery below.
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: auth.email_password_enabled and the auth.google_* keys keep their declarations, and the save door now refuses only the save that leaves no way to sign in. The predicate input's new batch field is optional and additive. A value stored before is not rewritten (the existing OS_AUTH_EMAIL_PASSWORD_ENABLED override recovers it), so there is nothing at rest for a migration to rewrite. -->
13+
14+
**BREAKING** accept-set narrowing at the auth settings save door (`PUT /api/settings/auth`, `SettingsService.setMany('auth', …)`): a save that would leave no way to sign in is refused, where it was saved before.
15+
16+
- **Before.** Setup → Settings → Authentication, "Enable email/password login" off, save: "Settings saved", with no social provider and SSO off. After the next sign-out nobody could sign in, the administrator included.
17+
- **Now.** A save after which email and password is off and no other way in is in force answers `400 SETTINGS_VALIDATION`. The field error sits at `email_password_enabled` (`code: invalid_value`, `constraint.lastSignInMethod: email_password`), and its message, in the caller's language (en, zh-CN, ja-JP, es-ES), says to keep or set up another sign-in method before or in that save. Nothing in that batch is written, and email and password stays on.
18+
- **The whole save is judged.** `{ email_password_enabled: false, google_enabled: false }` (or with `google_client_secret` cleared) on a deployment signing in with email and password plus Google is refused, with a field error at each of the two keys. Google filled in (switch, client ID and secret) in the same save that turns email and password off is accepted, and so is the switch alone over a client ID and secret already stored.
19+
- **What counts as another way in.** A social or OIDC provider in force whose client ID and client secret are both set; enterprise SSO switched on with at least one identity provider registered (the switch alone does not count); phone-number sign-in; a host sign-in handoff the host declared. `ssoOnlyMode` alone, magic link, passkeys, and a provider listed without its credentials do not count.
20+
- **The other direction.** While email and password is off, `google_enabled: false`, or clearing `google_client_id` / `google_client_secret` when no environment variable or stack config supplies it, is refused (`constraint.lastSignInMethod: google`) if Google is the last way in.
21+
- **Already locked out.** Set `OS_AUTH_EMAIL_PASSWORD_ENABLED=true` and restart: the environment override wins over the stored `false`, and email and password sign-in comes back. Then set up another method before turning it off again.
22+
- **For a write predicate's author (`@objectstack/service-settings`).** `SettingsWritePredicateInput.batch` holds every key the write sets in the namespace, the asked key among them, with the value it would store (`null` clears). `setMany` always passes it, frozen. A rule over several keys judges the state after the whole write from it instead of one key against the rest as they stand. Existing predicates need no change.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
'@objectstack/cli': patch
4+
---
5+
6+
fix(cloud-connection,cli): the install-local ledger follows `OS_HOME`, so `os dev --fresh` and a second instance under another `OS_HOME` start without an earlier run's installs
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
- **Now.** With no `storageDir`, the install-local ledger (`LocalManifestSource`, and so `MarketplaceInstallLocalPlugin`) lives at `$OS_HOME/installed-packages` when `OS_HOME` is set, and at `CWD/.objectstack/installed-packages` when it is not. Before, it was always the working directory, so `os dev --fresh` (a fresh `OS_HOME` and database) rehydrated a package an earlier run installed, and so did a second instance under another `OS_HOME`.
12+
- **Unchanged.** `os dev` without `OS_HOME` keeps the ledger in the project, beside its database. `os start` in project mode sets `OS_HOME` to `CWD/.objectstack`, which is the same directory as before. An explicit `storageDir` is used as given.
13+
- **Existing installs.** When the ledger moved (an `OS_HOME` other than `CWD/.objectstack`) and its new directory does not exist yet, the boot copies the `.json` files of the working-directory ledger into it once, and logs a warning naming both directories. The old files are left in place and are not read again; remove them. A copy that fails is logged as an error, and the next boot tries again. Under `os dev --fresh` nothing is copied.
14+
- **`os doctor`** reads the same directory the runtime does, `OS_HOME` included.
15+
- **The widening.** `@objectstack/cloud-connection` exports `resolveInstalledPackagesDir({ env?, cwd? })` (returns `{ dir, legacyDir }`), `INSTALLED_PACKAGES_HOME_SUBDIR`, and `LocalManifestSource.legacyDir` / `adoptLegacyEntries()`. `os doctor` takes the ledger directory from `resolveInstalledPackagesDir`; a `@objectstack/cloud-connection` without it is reported as a reader that does not declare the ledger directory. Align the two packages' versions.

‎content/docs/ui/translations.mdx‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,21 @@ with the same string in the item as the package shipped it:
217217
- **A widget, global filter or bulk action the package never shipped counts as
218218
edited**: the org authored it, so its text is served as written.
219219
- **An edited string is served in the language it was written in, to every
220-
reader.** The bundle entry for that string no longer applies to it.
220+
reader**, unless the base's paired `.custom` package translates it (below).
221+
The bundle entry for that string no longer applies to it.
221222
- **Resetting the overlay restores the shipped string and its translation.**
222223

224+
A declared override in the base's paired `<base>.custom` package
225+
([ADR-0146](/docs/concepts/metadata-lifecycle#customizing-an-installed-package-the-paired-custom-override-adr-0146))
226+
is compared with the base's own item the same way, for a page, dataset, app
227+
and book as well as a dashboard, view and report, and the pair's own
228+
translations rank first. A string the override changed is served in the
229+
pair's translation of its key for that locale, else as written. A string it
230+
left alone takes the pair's translation, then the base's, locale by locale.
231+
Only `translation` items saved in the pair count, not an authored translation
232+
in no package. `GET /meta/:type/:name`, `GET /meta/:type` and the book tree
233+
`GET /meta/book/:name/tree` serve the same label.
234+
223235
Objects follow the same rule for their `label`, `pluralLabel` and
224236
`description`: a value that differs from the owning package's declaration (a
225237
label an [object extension](/docs/data-modeling/object-extensions) contributes,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import { FRESH_HOME_CHANNEL_ENV as CONSUMER_SPELLING } from '@objectstack/cloud-connection';
5+
import { applyFreshHomeChannel, FRESH_HOME_CHANNEL_ENV } from './dev.js';
6+
7+
/**
8+
* #30217 — `os dev --fresh` tells its `serve` child which `OS_HOME` is the
9+
* run's ephemeral one, so the install ledger there adopts nothing from the
10+
* project's working-directory ledger and the run starts without an earlier
11+
* run's installs. The parent owns the variable: without `--fresh` it is
12+
* removed, so an inherited copy never speaks for this run.
13+
*/
14+
describe('#30217 `os dev` owns the fresh-home channel on its serve child', () => {
15+
it('is spelled exactly as @objectstack/cloud-connection reads it', () => {
16+
expect(FRESH_HOME_CHANNEL_ENV).toBe(CONSUMER_SPELLING);
17+
});
18+
19+
it('names the ephemeral home under --fresh', () => {
20+
const env: NodeJS.ProcessEnv = { OS_HOME: '/tmp/objectstack-dev-abc' };
21+
applyFreshHomeChannel(env, '/tmp/objectstack-dev-abc');
22+
expect(env[FRESH_HOME_CHANNEL_ENV]).toBe('/tmp/objectstack-dev-abc');
23+
});
24+
25+
it('removes an inherited copy when the run is not --fresh', () => {
26+
const env: NodeJS.ProcessEnv = { [FRESH_HOME_CHANNEL_ENV]: '/tmp/objectstack-dev-old' };
27+
applyFreshHomeChannel(env, undefined);
28+
expect(FRESH_HOME_CHANNEL_ENV in env).toBe(false);
29+
});
30+
});

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,28 @@ export function forwardSeedSettledToParent(msg: unknown): boolean {
251251
return true;
252252
}
253253

254+
/**
255+
* The internal channel `@objectstack/cloud-connection` reads as
256+
* `FRESH_HOME_CHANNEL_ENV` (#30217): the ephemeral `OS_HOME` a `--fresh` run
257+
* created. Spelled here rather than imported because oclif loads every command
258+
* module on every invocation; `dev-fresh-home-channel.test.ts` holds the two
259+
* spellings equal.
260+
*/
261+
export const FRESH_HOME_CHANNEL_ENV = 'OS_INTERNAL_FRESH_HOME';
262+
263+
/**
264+
* Set or clear {@link FRESH_HOME_CHANNEL_ENV} on the serve child's env
265+
* (#30217). Under `--fresh` it names the run's ephemeral `OS_HOME`, so the
266+
* install ledger there adopts nothing from the project's working-directory
267+
* ledger — a fresh run starts without an earlier run's installs. Otherwise it is
268+
* removed: the parent owns the variable, so an inherited copy never speaks for
269+
* a home this run did not create. Not an operator setting.
270+
*/
271+
export function applyFreshHomeChannel(childEnv: NodeJS.ProcessEnv, freshHome: string | undefined): void {
272+
if (freshHome) childEnv[FRESH_HOME_CHANNEL_ENV] = freshHome;
273+
else delete childEnv[FRESH_HOME_CHANNEL_ENV];
274+
}
275+
254276
/**
255277
* Whether this `os dev` boot runs the watch-recompile loop (#20681).
256278
*
@@ -676,6 +698,9 @@ export default class Dev extends Command {
676698
// (OS_HOME → <home>/data/objectstack.db, published as OS_DATABASE_URL),
677699
// - the storage-service uploads root, published on the settings
678700
// service's own env name OS_STORAGE_LOCAL_ROOT (#4968),
701+
// - the install-local ledger, which follows OS_HOME (#30217); the
702+
// child is told this home is ephemeral (applyFreshHomeChannel), so
703+
// it adopts nothing from the project's working-directory ledger,
679704
// - any other state a plugin keys off OS_HOME.
680705
// In one sentence: framework-owned, OS_HOME-keyed state plus the env
681706
// channels this block publishes below. Auto-deleted on exit.
@@ -810,6 +835,7 @@ export default class Dev extends Command {
810835
...(flags['database-auth-token'] ? { OS_DATABASE_AUTH_TOKEN: flags['database-auth-token'] } : {}),
811836
...(flags['auth-secret'] ? { OS_AUTH_SECRET: flags['auth-secret'] } : {}),
812837
};
838+
applyFreshHomeChannel(localEnv, freshHome);
813839
// The data key's file: the ONE answer `os start` takes too (#29934), so a
814840
// secret saved here reads back under `os start` over the same project —
815841
// and the file an earlier `os start` kept in the project is still read.

‎packages/cli/src/commands/doctor-ledger-dir-authority.test.ts‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
*
66
* ── What this file pins, honestly ────────────────────────────────────────
77
*
8-
* Not a live defect. `DEFAULT_INSTALLED_PACKAGES_DIR` is an export of
8+
* Not a live defect. The directory authority (`DEFAULT_INSTALLED_PACKAGES_DIR`,
9+
* `resolveInstalledPackagesDir` since #30217) is an export of
910
* `@objectstack/cloud-connection` in every version ever shipped, so the `??`
1011
* fallback this change deletes had never fired. What is being hardened is a
1112
* DIAGNOSIS BOUNDARY — the last cell of the edge #5644 carved. That issue
@@ -114,10 +115,10 @@ describe('installedPackageLedgerDirAuthorityMissingCheck — loaded but unrecogn
114115
expect(check.message).not.toContain('\n');
115116
});
116117

117-
it('names the received value when the export exists but is not a string', () => {
118+
it('names the received value when the export exists but is not a function', () => {
118119
const check = installedPackageLedgerDirAuthorityMissingCheck(42);
119120

120-
expect(check.message).toContain('not a string');
121+
expect(check.message).toContain('not a function');
121122
expect(check.message).toContain('got number: 42');
122123
});
123124

@@ -136,7 +137,7 @@ describe('installedPackageLedgerDirAuthorityMissingCheck — loaded but unrecogn
136137
// it from the #5644 row one cell over. Without both, the reader's first
137138
// move would be to rebuild a package that builds and loads fine.
138139
expect(fix).toContain('IS installed here and it DID load');
139-
expect(fix).toContain('authority on what the ledger directory is called');
140+
expect(fix).toContain('authority on where the ledger directory is');
140141
// And it names the remedy: the two ends of this contract disagree.
141142
expect(fix).toContain('Align their versions');
142143
});
@@ -145,7 +146,7 @@ describe('installedPackageLedgerDirAuthorityMissingCheck — loaded but unrecogn
145146
const check = installedPackageLedgerDirAuthorityMissingCheck(undefined);
146147

147148
// The one name doctor had for the ledger is the export that came back
148-
// non-string. A path in the row — the old `??` guess included — would be
149+
// as no function. A path in the row — the old `??` guess included — would be
149150
// doctor asserting what it cannot know (#5644's option-B rejection, one
150151
// cell over).
151152
expect(check.message).not.toContain('.objectstack');
@@ -182,7 +183,7 @@ describe('installedPackageLedgerSkippedEntriesCheck — names the directory it a
182183
*
183184
* Simulated through the loader seam, like the #5644 cause case next door: the
184185
* mock hands doctor a module that LOADED (`state: 'loaded'`) and carries no
185-
* `DEFAULT_INSTALLED_PACKAGES_DIR`. No preflight (`assertLedgerReaderIsBuilt`)
186+
* `resolveInstalledPackagesDir`. No preflight (`assertLedgerReaderIsBuilt`)
186187
* in this describe, and that is not a rollback of #5612: nothing here reads
187188
* the real package at all — the seam is mocked — so there is no accident for
188189
* a preflight to catch.
@@ -271,7 +272,7 @@ describe('os doctor, end to end, against a reader that declares no ledger direct
271272
it('reports the named row and reads NO directory — guessed or otherwise', async () => {
272273
const ctorCalls: string[] = [];
273274
const fakeModule = {
274-
// Deliberately NO `DEFAULT_INSTALLED_PACKAGES_DIR` — the state under test.
275+
// Deliberately NO `resolveInstalledPackagesDir` — the state under test.
275276
LocalManifestSource: class {
276277
constructor(dir: string) {
277278
ctorCalls.push(dir);
@@ -312,13 +313,13 @@ describe('os doctor, end to end, against a reader that declares no ledger direct
312313
expect(verboseRun.exitCode).toBeUndefined();
313314
}, 120_000);
314315

315-
it('a non-string export is the same fact — never a path.join over it', async () => {
316+
it('a non-function export is the same fact — never a call of it', async () => {
316317
const ctorCalls: string[] = [];
317318
const fakeModule = {
318-
// Present, and not a string: the shape whose `path.join` throw the old
319-
// code mis-reported through the config `catch` (see the function's
320-
// header in doctor.ts).
321-
DEFAULT_INSTALLED_PACKAGES_DIR: 42,
319+
// Present, and not a function: the shape a bare call would throw on, which
320+
// the old `path.join` over the string authority mis-reported through the
321+
// config `catch` (see the function's header in doctor.ts).
322+
resolveInstalledPackagesDir: 42,
322323
LocalManifestSource: class {
323324
constructor(dir: string) {
324325
ctorCalls.push(dir);
@@ -333,7 +334,7 @@ describe('os doctor, end to end, against a reader that declares no ledger direct
333334

334335
expect(ctorCalls).toHaveLength(0);
335336
expect(run.out).toContain(AUTHORITY_HEADLINE);
336-
expect(run.out).toContain('not a string');
337+
expect(run.out).toContain('not a function');
337338
expect(run.out).toContain('got number: 42');
338339
expect(run.out).not.toContain(CLEAN_BILL);
339340
expect(run.out).not.toContain('invoice.code');

0 commit comments

Comments
 (0)