Skip to content

Commit 1cdd9d1

Browse files
committed
Merge origin/main: #23835 landed the 11 lead lines, which drop out of this diff
Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude <noreply@anthropic.com>
2 parents f2b4cd5 + 66d456d commit 1cdd9d1

25 files changed

Lines changed: 716 additions & 75 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the liveness ledger reads `app.branding.logo` as `live`, citing the console's `AppMark` reader (#23394)
6+
7+
Clause-②: no
8+
9+
**What was wrong.** The liveness ledger shipped with `@objectstack/spec` graded `app.branding.logo` `planned`, with a note saying no mounted component rendered it. The console now does: objectui's `AppMark` draws `branding.logo` as the app's square icon on the top-bar app pill, on each App Switcher row and in the phone drawer header.
10+
11+
**What changes.**
12+
13+
- `liveness/app.json`: `branding.logo` reads `live`, citing `AppMark` and the two shell components that mount it at the `.objectui-sha` pin. The designer pages that write the key stay cited as its `producer`. The `branding` container's note says the same.
14+
- `liveness/state-counts/app.md` is regenerated: `app` moves from `live 50 · planned 1` to `live 51 · planned 0`.
15+
16+
Unchanged: what `AppSchema` accepts and refuses (an app-level `logo` is still refused, and the refusal still points at `branding`), and every other ledger row.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/metadata-protocol': patch
4+
'@objectstack/runtime': patch
5+
---
6+
7+
docs: the default-record-page remedies name the other package's page, not the whole package
8+
9+
Clause-②: no
10+
11+
A second `isDefault: true` record page for one object is refused with `DEFAULT_RECORD_PAGE_CONFLICT`. The package registry's refusal already told the author to keep one default or "compose the deployment without the other package's page". Three sibling texts still told them to drop the whole package, though a deployment can keep a package and leave out the one page it ships:
12+
13+
- `@objectstack/metadata-protocol`: the metadata save door's refusal now reads "a page a package ships stays the default unless the deployment is composed without that package's page." Only the remedy clause changed; `code`, `status`, `object` and `holders` are unchanged.
14+
- `@objectstack/spec`: `PageSchema.isDefault`'s TSDoc and the error-code ledger's comment on `DEFAULT_RECORD_PAGE_CONFLICT` say the same.
15+
- `@objectstack/runtime`: `AppPlugin`'s private `bundle` field now documents the three readers outside the class that read it structurally, so a rename is not made blind.
16+
17+
No behaviour changes; nothing to migrate.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os serve`'s refusal of a `memory://` or `mingo://` `OS_DATABASE_URL` names the URL's scheme and never prints the URL, so a password or token in it no longer reaches the console
6+
7+
Clause-②: no
8+
9+
This is the `os serve` path a host config takes (one whose `plugins` hold plugin instances), and the one `OS_MODE=off|none|legacy` takes. `os dev` and `os start` reach it through their `serve` child. Both schemes name the retired in-memory (mingo) engine, and both accept anything after the `//`, a `user:password@` and a query string included. The refusal interpolated the whole URL, and the CLI prints it as it exits 1.
10+
11+
With `OS_DATABASE_URL=memory://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN`:
12+
13+
- FROM: `✗ OS_DATABASE_URL "memory://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN" selects the in-memory (mingo) engine, …`
14+
- TO: `✗ An OS_DATABASE_URL with the "memory://" scheme selects the in-memory (mingo) engine, …`
15+
16+
`mingo://` reads the same way, with `"mingo://"`. These are the runtime host's words for the same URL (`os dev` / `os start` over a plain config, and `os migrate`), so the two hosts print one sentence. The rest of the sentence, which names the SQLite replacements (`os dev --fresh`, and the URL `:memory:`), is unchanged.
17+
18+
What does not change: both schemes are still refused, with the same error class and exit code, in dev and production; every other URL resolves as before, to the same driver.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/metadata-core': patch
3+
---
4+
5+
fix(metadata-core): the package installs on npm 10 again, and into projects that use vitest 5, because it no longer declares an optional `vitest` peer
6+
7+
Clause-②: no
8+
9+
On npm 10, the npm that Node 22 bundles, `npm install @objectstack/metadata-core` exited 1 with `Cannot read properties of null (reading 'edgesOut')`. So did installing a package that depends on it, such as `@objectstack/objectql`. The cause was this package's optional peer `vitest: ^4.0.0`. It led npm 10 to vitest 4.1.12, whose own peer declarations contradict each other, and npm 10's resolver crashes on that. The same peer also made npm refuse (`ERESOLVE`) to install the package into a project that already uses vitest 5.
10+
11+
The package's main entry never imports `vitest`. Only the test-only `@objectstack/metadata-core/testing` entry does, so the peer is removed. Nothing the package exports changes.
12+
13+
If you import `@objectstack/metadata-core/testing`, list `vitest` in your own `devDependencies`, as that entry's documentation already says. This package runs that suite on vitest 4. In a pnpm workspace with hoisting turned off (`hoist=false`), that entry can no longer find `vitest` from inside a workspace package. Declare the peer back in your `pnpm-workspace.yaml`:
14+
15+
```yaml
16+
packageExtensions:
17+
'@objectstack/metadata-core':
18+
peerDependencies:
19+
vitest: '*'
20+
```
21+
22+
`@objectstack/plugin-auth` still fails the same way on npm 10, because its `better-auth` dependency declares its own optional `vitest` peer. Install it with npm 11, or add `"overrides": { "vitest": "~4.0.0" }` to your project's `package.json`.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
Clause-②: no
6+
7+
A session minted by `IAuthService.mintPrincipalSession` is no longer refreshed by a read. Before,
8+
`get-session` (and every endpoint that reads the session) still renewed it on each request: #23772's
9+
guard held its expiry, but the row's `updatedAt` was rewritten, the session cookie was re-set with a
10+
7-day `Max-Age`, and `bearer()` answered `set-auth-token`. The console reads that header as a token
11+
rotation, so a browser carrying a minted cookie stayed on "Opening your console…" and the print
12+
service could not render under the requesting user. A request that carries a minted session now
13+
sets better-auth's own per-request skip-refresh switch, on the cookie lane, the bearer lane and
14+
in-process. The mint's contract, TTL, refusals and revoke are unchanged.

‎packages/cli/src/utils/driver-vocabulary-parity.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,28 @@ describe('driver vocabulary parity: `os start` and `os migrate` answer alike (#6
257257
},
258258
);
259259

260+
// The URL door's refusal is printed on the way out, and both schemes take a
261+
// `user:password@` and an `?authToken=`. Both hosts name the SCHEME, never the
262+
// URL, in the same words: the CLI host's refusal is the runtime host's minus
263+
// its `[StandaloneStack]` tag. Placeholder credentials only.
264+
it.each(['memory://', 'mingo://'])(
265+
'both hosts refuse a credentialed %s OS_DATABASE_URL in the same words, and neither prints the credentials',
266+
(scheme) => {
267+
const url = `${scheme}PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN`;
268+
const cli = refusalMessage(() => resolveDriverType(undefined, url));
269+
process.env.OS_DATABASE_URL = url;
270+
const standalone = refusalMessage(() => resolveStandaloneDatabase({ artifactPath: '/nonexistent/objectstack.json' }));
271+
for (const [host, message] of [['os start', cli], ['os migrate', standalone]] as const) {
272+
expect(message, `${host} accepted ${scheme}…`).not.toBeNull();
273+
expect(message, host).toContain(`"${scheme}"`);
274+
for (const placeholder of ['PLACEHOLDER-USER', 'PLACEHOLDER-SECRET', 'PLACEHOLDER-TOKEN']) {
275+
expect(message, `${host} prints ${placeholder}`).not.toContain(placeholder);
276+
}
277+
}
278+
expect(cli).toBe(standalone!.replace(/^\[StandaloneStack\] /, ''));
279+
},
280+
);
281+
260282
// The control on the URL door: the replacement the refusals name is accepted
261283
// by both hosts, as SQLite — so the refusal is scoped to the engine, not to
262284
// the word "memory".

‎packages/cli/src/utils/storage-driver.test.ts‎

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,15 @@ describe('inferDriverTypeFromUrl', () => {
6161

6262
// The in-memory (mingo) engine's schemes are REFUSED at inference: the engine
6363
// is no longer a boot store. Asserted on the error's TYPE and on what it names
64-
// (the URL and the two SQLite replacements), never on its prose.
65-
it.each(['memory://', 'memory://ignored-host', 'mingo://', 'MINGO://x'])(
66-
'refuses %s at inference, naming the SQLite replacements',
67-
(url) => {
64+
// (the variable, the scheme and the two SQLite replacements), never on its prose.
65+
it.each([
66+
['memory://', 'memory://'],
67+
['memory://ignored-host', 'memory://'],
68+
['mingo://', 'mingo://'],
69+
['MINGO://x', 'MINGO://'],
70+
])(
71+
'refuses %s at inference, naming its scheme and the SQLite replacements',
72+
(url, scheme) => {
6873
let err: unknown;
6974
try {
7075
inferDriverTypeFromUrl(url);
@@ -75,12 +80,62 @@ describe('inferDriverTypeFromUrl', () => {
7580
const refusal = err as UnsupportedDriverError;
7681
// Not a kind this resolver produces — the allowlist pin's oracle reads this.
7782
expect(refusal.recognized).toBe(false);
78-
expect(refusal.message).toContain(`"${url}"`);
83+
expect(refusal.driverType).toBe(scheme);
84+
expect(refusal.message).toContain('OS_DATABASE_URL');
85+
expect(refusal.message).toContain(`"${scheme}"`);
7986
expect(refusal.message).toContain('--fresh');
8087
expect(refusal.message).toContain(':memory:');
8188
},
8289
);
8390

91+
// The refusal is printed to the console on the way out, and both schemes take
92+
// anything after the `//`: a `user:password@` and an `?authToken=` ride along.
93+
// It names the scheme, never the URL — in the message AND in the field a
94+
// caller may log. Placeholder credentials only.
95+
const PLACEHOLDERS = ['PLACEHOLDER-USER', 'PLACEHOLDER-SECRET', 'PLACEHOLDER-TOKEN'] as const;
96+
const CREDENTIALED_TAIL = 'PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN';
97+
it.each(['memory://', 'mingo://'])(
98+
'refuses a %s URL carrying a user, password and token, naming the scheme and none of the three',
99+
(scheme) => {
100+
const url = `${scheme}${CREDENTIALED_TAIL}`;
101+
let err: unknown;
102+
try {
103+
inferDriverTypeFromUrl(url);
104+
} catch (e) {
105+
err = e;
106+
}
107+
expect(err, `inferDriverTypeFromUrl accepted ${scheme}…`).toBeInstanceOf(UnsupportedDriverError);
108+
const refusal = err as UnsupportedDriverError;
109+
expect(refusal.recognized).toBe(false);
110+
expect(refusal.driverType).toBe(scheme);
111+
expect(refusal.message).toContain(`"${scheme}"`);
112+
expect(refusal.message).toContain('--fresh');
113+
expect(refusal.message).toContain(':memory:');
114+
for (const placeholder of PLACEHOLDERS) {
115+
expect(refusal.message, `the refusal prints ${placeholder}`).not.toContain(placeholder);
116+
expect(refusal.driverType, `driverType carries ${placeholder}`).not.toContain(placeholder);
117+
}
118+
expect(refusal.message).not.toContain('127.0.0.1');
119+
},
120+
);
121+
122+
// CONTROL: the same credentialed tail under every other scheme classifies as
123+
// before — the redaction is scoped to the retired engine's arm, not to URLs
124+
// that carry credentials.
125+
it.each([
126+
['postgres://', 'postgres'],
127+
['postgresql://', 'postgres'],
128+
['pg://', 'postgres'],
129+
['mysql://', 'mysql'],
130+
['mysql2://', 'mysql'],
131+
['mongodb://', 'mongodb'],
132+
['mongodb+srv://', 'mongodb'],
133+
['libsql://', 'turso'],
134+
['wasm-sqlite://', 'sqlite-wasm'],
135+
])('classifies a %s URL carrying the same user, password and token as %s', (scheme, kind) => {
136+
expect(inferDriverTypeFromUrl(`${scheme}${CREDENTIALED_TAIL}`)).toBe(kind);
137+
});
138+
84139
// The sqlite `:memory:` PSEUDO-FILE is SQLite's own in-memory mode — NOT the
85140
// mingo engine. It stays `sqlite`, and it is the replacement the refusal names.
86141
it('keeps sqlite `:memory:` mapped to sqlite (the replacement, distinct from memory://)', () => {

‎packages/cli/src/utils/storage-driver.ts‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,10 +328,20 @@ export function inferDriverTypeFromUrl(url: string | undefined): string {
328328
// the same URLs. Kept ABOVE the sqlite test so it is not shadowed, and
329329
// deliberately distinct from sqlite's `:memory:` pseudo-file below, which is
330330
// the replacement this refusal names.
331-
if (/^(memory|mingo):\/\//i.test(u)) {
331+
//
332+
// The refusal names the SCHEME, never the URL: both schemes accept anything
333+
// after the `//`, a `user:password@` and a `?authToken=` included, and this
334+
// sentence is printed to the console on the way out. The subject is the
335+
// runtime host's own (`refuseRetiredMemoryEngine` in
336+
// `packages/runtime/src/standalone-stack.ts`, module-local there, so written
337+
// out here), and `driver-vocabulary-parity.test.ts` holds the two equal.
338+
// The scheme is also what the error carries as `driverType`, as the
339+
// unrecognised-scheme refusal below does.
340+
const retiredScheme = /^(memory|mingo):\/\//i.exec(u)?.[0];
341+
if (retiredScheme) {
332342
throw new UnsupportedDriverError(
333-
u,
334-
retiredMemoryEngineMessage(`OS_DATABASE_URL "${u}"`),
343+
retiredScheme,
344+
retiredMemoryEngineMessage(`An OS_DATABASE_URL with the "${retiredScheme}" scheme`),
335345
// Not a kind this resolver can produce — `database-driver-allowlist.pin`
336346
// reads `recognized` to tell the two apart.
337347
{ recognized: false },

‎packages/metadata-core/package.json‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,6 @@
4545
"@objectstack/spec": "workspace:*",
4646
"zod": "^4.6.5"
4747
},
48-
"peerDependencies": {
49-
"vitest": "^4.0.0"
50-
},
51-
"peerDependenciesMeta": {
52-
"vitest": {
53-
"optional": true
54-
}
55-
},
5648
"devDependencies": {
5749
"@types/node": "^26.6.3",
5850
"fast-check": "^4.10.2",

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21757,7 +21757,7 @@ export class ObjectStackProtocolImplementation implements
2175721757
const others = named.length === 1 ? `${named[0]} already is` : `${named.join(' and ')} already are`;
2175821758
const remedy = unique.some((h) => h.shipped)
2175921759
? `Save '${saveName}' with \`isDefault: false\`: a page a package ships stays the default unless the `
21760-
+ `deployment is composed without that package.`
21760+
+ `deployment is composed without that package's page.`
2176121761
: `Save '${saveName}' with \`isDefault: false\`, or first save ${unique.map((h) => `'${h.page}'`).join(' and ')} `
2176221762
+ `with \`isDefault: false\` to make '${saveName}' the default instead.`;
2176321763
const err = new Error(

0 commit comments

Comments
 (0)