Skip to content

Commit ae5d8af

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20654-credential-literal-advisory
2 parents f7fe981 + 05cb2bc commit ae5d8af

113 files changed

Lines changed: 2503 additions & 756 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
fix(cli)!: `objectstack validate`, `objectstack build` and `objectstack lint` read the project's `sdui.manifest.json` beside the config they were given, not in the directory they were run from (#20166)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata changes shape and nothing an author wrote is renamed or removed, so `objectstack migrate meta` has nothing to rewrite. What moves is which manifest file judges a run whose config path names another directory. -->
10+
11+
**BREAKING for runs given a config path in another directory.**
12+
13+
**What changed.** These commands check the `source` of each `kind: 'html'` page
14+
against an SDUI component manifest: the project's own `sdui.manifest.json` first,
15+
then the copy `@objectstack/console` ships. They located everything else about a
16+
project from the directory of its config, but looked for the project's own
17+
manifest in the directory the command was run from. So
18+
`objectstack validate path/to/app/objectstack.config.ts`, run from anywhere else,
19+
never read `path/to/app/sdui.manifest.json`, and a manifest that happened to sit in
20+
the directory it was run from judged a project it does not belong to. They now read
21+
the manifest beside the config.
22+
23+
## Which manifest each run reads
24+
25+
| the run | the project manifest it read | the project manifest it reads now |
26+
|:--|:--|:--|
27+
| `objectstack validate` / `build` / `lint` with no config path, in the project's directory | `./sdui.manifest.json` | `./sdui.manifest.json` (unchanged) |
28+
| the same commands given `path/to/app/objectstack.config.ts`, run from another directory | that other directory's `sdui.manifest.json` | `path/to/app/sdui.manifest.json` |
29+
30+
When the project carries no manifest of its own, both rows then fall back to the
31+
copy `@objectstack/console` ships, as before.
32+
33+
**Which runs change, and which way.** Only runs whose config path names a directory
34+
other than the one they run in. For those, the verdict can move in both directions:
35+
36+
- A page the project's own manifest does not declare is now refused
37+
(`jsx-forbidden-tag`, `jsx-unknown-component`, `jsx-unknown-prop`, exit 1), where
38+
the other directory's manifest, or the console's copy, used to admit it.
39+
- A project manifest that is present but not usable is now refused (exit 1), naming
40+
that file, where the run used to read some other file.
41+
- A project with no manifest of its own is now checked against the console's copy,
42+
where the other directory's manifest used to decide.
43+
- In the other direction, a page the other directory's manifest refused, and that
44+
the project's own manifest (or the console's copy) declares, is now admitted.
45+
46+
If such a run now fails, the manifest that belongs to the project is the one to
47+
keep beside its config.
48+
49+
**What is not affected.** A run in the project's own directory, with or without a
50+
config path, reads the same file as before. `objectstack init`'s check of a freshly
51+
generated scaffold keeps reading the directory it was run from.
52+
53+
**A correction to this release's console-fallback entry.** That entry says these
54+
commands "look first for the `sdui.manifest.json` in the directory the command runs
55+
in". From this release they look first beside the config the command was given,
56+
which is the same directory whenever the command runs in the project.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): the stored-filter conversion rewrites a filter on a block whose rows are inline, as it does on any other block
6+
7+
The ADR-0087 D2 conversion `page-component-filter-record-to-rule-array` no longer leaves every filter of a page component whose rows are inline (`data: { provider: 'value', … }`, a `data` array, or `staticData`) as stored. Such a filter, the binding's `dataSource.filter` included, is now rewritten to the `[{ field, operator, value }, ...]` rule array exactly as it is on a block that queries an object. What still stays as stored, and is still reported as a TODO, is only a filter with a part that has no lossless rule spelling: a combinator, a null value, or an operator the rule vocabulary does not spell. That holds on any block.
8+
9+
Why the conversion declined, and why it no longer needs to: the `object-map`, `object-tree`, `object-calendar` and `object-gantt` blocks match that filter against their own rows in objectui's in-memory data source (`ValueDataSource.find`). The conversion was written against an objectui version whose `find` excluded every row for a rule array, so it left those filters alone and said so in the TODO. The objectui version this repository pins (`.objectui-sha`, the same pin the previous release shipped) lowers a rule array before it matches, and it selects the rows the stored form selected. That was measured over every operator the conversion maps: 114 filters on eight rows, null and missing values included. The same filters select no row on the objectui build just before that fix. So the decline was already protecting nothing: it only left convertible filters unconverted and reported TODOs that no longer needed to exist.
10+
11+
What an operator sees:
12+
13+
- `os migrate meta --stored` now lists such a page as a pending rewrite. It used to list it as a `skipped` row with a TODO. A preview over a database whose only legacy filters sat on inline-row blocks therefore exits 1 until `os migrate meta --stored --apply` rewrites them.
14+
- Until then, every stored-row read replays the same rewrite, so the block reads the rule array and shows the same rows.
15+
- Nothing an author writes is accepted or refused differently. The conversion stays retired from the authoring path, and no schema changes.
16+
17+
The migration entries `element-data-source-and-object-block-filter-rule-array` and `object-grid-default-filters-rule-array`, and the protocol-18 step rationale, no longer say that inline-row filters are left as stored.
18+
19+
ADR-0087 disposition: already registered. This changes the behaviour of the registered D2 conversion `page-component-filter-record-to-rule-array` and edits its two D3 entries. There is nothing new to register.
20+
21+
Clause-②: no
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/core": minor
3+
"@objectstack/objectql": minor
4+
---
5+
6+
fix(core,objectql)!: a temporal filter comparand is refused with `INVALID_FILTER` / 400 exactly when the same value is refused as a written value — a day that does not exist (`"2026-02-30"`) is no longer rolled over or compared as text, and a non-ISO `datetime` spelling (`"07/15/2026 10:00"`) is no longer read in the server's zone (#20549); and a `time` comparand whose instant has no four-digit UTC year (`"+010000-01-01T10:00:00Z"`) is refused rather than compared as text (#20480)
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a filter comparand VALUE at the engine's temporal-comparand door (where, a per-aggregation filter, having) and, through the same predicate, the analytics raw-SQL decline: no authorable key, spelling or stored shape of metadata moves, `packages/spec` is untouched, and no stored row is read or rewritten. What is refused is a temporal string naming a day that does not exist, a datetime string outside the ISO spellings, and a bare integer string; which instant such a string meant (a host zone, a locale's day order, a year or epoch milliseconds) is not something a ledger entry can decide. The other categories are closed on facts: both packages publish (not `unpublished`); no ADR-0087 id covers a comparand value check (not `registered` / `already-registered`); and the change is runtime behaviour, not a TypeScript declaration (not `runtime-interface-only` / `type-surface-only`). -->
11+
12+
**BREAKING**: this narrows what a `date`, a `datetime` and a `time` field accept as a filter comparand. It ships as `minor` under the launch-window convention for accept-set narrowings (`check-changeset-no-major` refuses `major` until GA; the breaking-ness is carried by this banner and the ADR-0087 disposition above).
13+
14+
The record validator already refused the `date` / `datetime` classes as written values (`VALIDATION_FAILED` / `invalid_date`). The comparand door was wider, so a filter admitted what a write refused and answered the wrong rows. The two predicates moved into `@objectstack/core`'s `isUninterpretableTemporalComparand`, and both doors now ask that one rule. A comparand is refused with `INVALID_FILTER` / 400, naming the field, before any driver read, at `where`, a per-aggregation `filter` and `having`:
15+
16+
- **A day that does not exist**, on a `date` or as the day part of a `datetime`: `"2026-02-30"`, `"2026-02-29"` (2026 is not a leap year), `"2026-04-31"`, `"2026-02-30T10:00:00Z"`. `"2028-02-29"` is a real day and is read.
17+
- **A `datetime` string in any spelling but the ISO 8601 ones the platform writes**, after trimming: `YYYY-MM-DD` (midnight UTC); `YYYY-MM-DDTHH:MM[:SS[.fraction]]` followed by `Z`, a `±HH:MM` or `±HHMM` offset, or nothing (a zone-naive wall clock is UTC, ADR-0074); and `YYYY-MM-DD HH:MM[:SS[.fraction]]` with no zone. Refused now, for example: `"07/15/2026 10:00"`, `"2026/07/15 10:00"`, `"15 July 2026 10:00"`, `"07/08/2026"`, `"Wed, 15 Jul 2026 10:00:00 GMT"`, `"2026-07-15 10:00:00+08:00"` (write it with a `T`), and a bare integer string such as `"2026"` or `"1784109600000"`.
18+
- **An instant on a `time` column in either class above.** A `time` column reads a comparand that is not a bare wall clock as an instant, by the `datetime` rule, and keeps its UTC time of day — so `"07/15/2026 10:00"` was the host zone's time of day, and `"1784109600000"` a string of epoch milliseconds. A wall clock (`"10:00"`, `"10:00:00.5"`), an ISO instant, a `Date` and an epoch-millisecond number are read as before, in a four-digit year (next).
19+
- **An instant on a `time` column whose UTC year has no four-digit spelling**, in every spelling (#20480): `"+010000-01-01T10:00:00Z"`, `"-000001-01-01T10:00:00Z"`, `"9999-12-31T23:00:00-02:00"` (year 10000 in UTC), and the epoch-millisecond number or `Date` of any of them. A `time` column keeps the UTC time of day of an instant only when that instant spells a four-digit year; any other one reached the driver as written and was compared with the stored `HH:MM:SS` text. No time of day is read from an extended year. Year 0 (`"0000-06-15T10:00:00Z"`) spells four digits, and its time of day is read as before.
20+
21+
Epoch milliseconds stay a `datetime` comparand as a JSON number: `{ "$gt": 1784109600000 }` is read exactly as before. As a string, a bare integer was read as epoch milliseconds, so `"2026"` meant two seconds after 1970 and matched every later row; send the number, or an ISO instant.
22+
23+
What a caller sees through `POST /api/v1/data/:object/query`, the process in America/New_York, PostgreSQL 16 at `Asia/Shanghai`:
24+
25+
| `where` | memory | SQLite | PostgreSQL | now, on all three |
26+
|:--|:--|:--|:--|:--|
27+
| `datetime` `$eq "2026-02-30T10:00:00Z"` | 200, the row stored at `2026-03-02T10:00:00.000Z` | the same | the same | 400 `INVALID_FILTER` |
28+
| `datetime` `$eq "07/15/2026 10:00"`, `"2026/07/15 10:00"` | 200, the row at `2026-07-15T14:00:00.000Z`, the server process's zone | the same | the same | 400 `INVALID_FILTER` |
29+
| `date` `$eq "2026-02-30"` | 200 `[]`, compared as text | the same | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` |
30+
| `datetime` `$gt "2026"` | 200, every row (read as 2026 epoch milliseconds) | the same | the same | 400 `INVALID_FILTER` |
31+
| `time` `$gt "+010000-01-01T10:00:00Z"`, rows `09:00` / `10:30` / `12:00` | 200, 3 of 3 (compared as text) | the same | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` |
32+
| `time` `$gt` the number of that instant | 200 `[]` | 200, 3 of 3 | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` |
33+
34+
The refusal names the field and the value, says the filter was not applied, and names the spellings that are read. The rows a non-ISO comparand matched were a property of the deployment host: the same request answered differently on two servers.
35+
36+
**Who is affected.** A caller that filters a `date` or `datetime` field with a string: a REST or SDK client, a saved report or view filter, a dashboard's analytics query (the raw-SQL strategy declines such a comparand, and the engine refuses it), an MCP `query_records` call written by a model. A `{placeholder}` such as `{30_days_ago}`, the empty string, a JS `Date` and an epoch-millisecond number are unchanged.
37+
38+
**Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL:
39+
40+
- a real leap day: `date` `"2028-02-29"`, `datetime` `"2028-02-29T10:00:00Z"`;
41+
- each ISO spelling above, compared as the same instant whatever the host's zone: `"2026-07-15T14:00:00Z"`, `"2026-07-15T22:00:00+08:00"`, `"2026-07-15 14:00"` (UTC, not the host zone);
42+
- a `date` comparand with a leading real `YYYY-MM-DD`, still compared as that day (`"2026-07-15T10:00:00Z"` on a `date` is July 15);
43+
- the same wall clock as a 2026 instant on a `time` column: `$gt "2026-07-15T10:00:00Z"` answers the `10:30` and `12:00` rows, as does its epoch-millisecond number or `Date`;
44+
- the year range 0001..9999, and every written value (the record validator now asks the same rule it copied, and answers exactly as before).
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/driver-turso': minor
4+
---
5+
6+
fix(spec,driver-turso)!: a turso config that forces `mode: 'local'` beside a `syncUrl` is refused where it is written and when the driver is built, instead of running as an embedded replica under a `local` label
7+
8+
Clause-②: yes (narrowing) — the accept set of the `turso` `datasource.config` contract narrows by one combination. No key is added, removed or renamed, and no exported symbol moves.
9+
10+
A `syncUrl` names the remote an embedded replica syncs with. A config that forced `mode: 'local'` on a `file:` url (or `:memory:`) beside a non-empty `syncUrl` was accepted by `@objectstack/spec`'s `TursoConfigSchema`, by the published mirror in `@objectstack/driver-turso`, and by `new TursoDriver()`. Measured on the driver source before this change, with a client that counts syncs: it constructed with `transportMode` `'local'`, then synced on connect, started the sync interval, and `isSyncEnabled()` answered `true` — exactly what the same config with no `mode` (a replica) did. A datasource declared local was kept in sync with a remote, and only a label said otherwise.
11+
12+
**BREAKING** accept-set narrowing on a published schema and a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now, at both doors together, with one message whose prescription names both ways out:
13+
14+
- **at authoring**, as one `custom` issue on `mode` (`config.mode` on a datasource): `DatasourceSchema`, `validateDriverConfig`, `defineStack` / `os validate`, and a save or test connection through the datasource admin service;
15+
- **at construction**, `VALIDATION_ERROR` / 400 from `new TursoDriver()` (and `createTursoDriver()`), before any client or database is opened.
16+
17+
The message is the same text at both doors, and a test holds the constructor's copy equal to the schema's issue byte for byte. It is the twin of the forced `mode: 'replica'`-without-`syncUrl` refusal, the other way round: honouring `mode: 'local'` by skipping the sync would ignore a declared `syncUrl` instead, which is the same defect with the keys swapped. The sibling refusals keep their order: a forced local mode on a remote url or a bare path still meets its `url` refusal first. An empty `syncUrl` is unset and is still accepted. The driver mirror declares no `mode` key and strips an authored one, so it cannot see a forced mode: this refusal reaches it only as byte-identical text, and the spec contract and the constructor are the two doors that judge it.
18+
19+
### Migration: FROM → TO
20+
21+
| You wrote | Write instead |
22+
| --- | --- |
23+
| `url: 'file:./data/replica.db', mode: 'local', syncUrl: 'libsql://my-db.turso.io'` | an embedded replica: drop `mode` (`url` and `syncUrl` select the replica) |
24+
| the same | a plain local database: drop `syncUrl` (and `sync`), keeping `url: 'file:./data/app.db'` with or without `mode: 'local'` |
25+
26+
A datasource row stored in this shape is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted and synced with the remote under a `local` label. The way out is the table above.
27+
28+
Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors the shape, and no host default or environment variable sets `mode` or `syncUrl` (a turso `mode` reaches the driver only from an authored `datasource.config`). Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero.
29+
30+
<!-- adr-0087: registered turso-config-forced-local-with-sync-url-refused -->
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
Provenance comments in `plugin-security` were re-anchored
6+
7+
Comment and docblock lines under `src/` that cited tracker numbers which no
8+
longer resolve on GitHub now cite the record in this repository that decided
9+
the matter (an ADR where one exists, otherwise the commit in this repository's
10+
history), and say in their own words what was decided. Comments only: no type,
11+
schema, export, log or refusal text, or runtime behaviour changes.

0 commit comments

Comments
 (0)