Skip to content

Commit 9bdb092

Browse files
committed
Merge origin/main into claude/issue-21310-cli-readme-flags
2 parents ebe3797 + f9bcd08 commit 9bdb092

82 files changed

Lines changed: 3623 additions & 396 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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
'@objectstack/plugin-approvals': patch
4+
---
5+
6+
Datasource and approval refusals, warnings, field help and generated-draft comments no longer cite tracker numbers; each one states the decision behind it in words
7+
8+
Clause-②: no
9+
10+
Some strings these two packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.
11+
12+
- `@objectstack/service-datasource`: the credential-migration refusal says an unbindable key is either an alias spelling from before inline credentials were refused at publish, which no connection builder reads, or turso's `encryptionKey`, which has no secret slot of its own because the one slot carries the `authToken`; the remote-primary-key comment in a generated object draft says a driver's introspection can report only the first column of a composite key, so the list is a lower bound.
13+
- `@objectstack/plugin-approvals`: the `queue` approver warning says the platform has no ownership queue to expand the type from, that the type is no longer offered for authoring, and to route the step to a team, department or position instead; the live-record warnings say approvers are being resolved against the trigger snapshot instead of the live record they are normally resolved from; the recall refusal's log line names the admin override; the `sys_approval_action` `via_override` help (in every shipped locale) says a platform or organization admin may act on any pending request, so that one nobody in its slate can decide never stays stuck; the cross-organization team, team-member and manager warnings, the expanded-to-nobody warning, the revise-window refusal, the `attachments` help and the `sys_approval_delegation` description drop their citations.
14+
15+
Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
'@objectstack/plugin-audit': minor
3+
'@objectstack/platform-objects': minor
4+
'@objectstack/plugin-auth': minor
5+
'@objectstack/plugin-sharing': minor
6+
'@objectstack/plugin-approvals': minor
7+
'@objectstack/objectql': minor
8+
'@objectstack/runtime': patch
9+
---
10+
11+
fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal`
12+
13+
Clause-②: no (narrowing)
14+
15+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key, export or config field is removed or renamed: the change narrows what the generic data path and the audit ledger return for platform-owned columns, and nothing an author wrote needs rewriting. The objectql half adds exports only. -->
16+
17+
**BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.**
18+
19+
**What changed.**
20+
21+
- The audit plugin's CRUD mirror now omits every field declared `internal: true` from the
22+
rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an
23+
update, delete `old_value`, and the activity row. It already masked `secret` and `password`
24+
fields; `internal` is the same contract the generic data path already enforces ("never
25+
returned on the generic data path"). An update that changes only an `internal` field still
26+
writes its row, with neither value.
27+
- These platform fields are now declared `internal: true`, so neither the generic data path
28+
nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential
29+
columns of the one-time verification object (`sys_verification`), the two-factor secret and
30+
backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh
31+
token columns, the OAuth client secret digest, the SCIM credential digest, the share link's
32+
token and password hash, and the approval action-token digest. API key digests and email
33+
headers were already `internal`; the ledger now honours that too.
34+
- Every built-in consumer that needs one of these values reads it back through the engine's
35+
privileged accessor rather than the generic path: JWT signing, password reset and the other
36+
one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret
37+
migration, OAuth client authentication, share-link redemption (the password gate is held)
38+
and the creator's share-link list, which keeps returning each link's token. The runtime's
39+
share-link resolve route (the dispatcher twin of the plugin's) still answers "password
40+
required" for a protected link rather than the unknown-link shape.
41+
- The one-time verification object's record title is now the fixed label `Verification`; it no
42+
longer shows the identifier column.
43+
- `@objectstack/objectql` exports two helpers from its main and `/core` entries:
44+
`collectInternalReadFields` (the names of an object's `internal` fields) and
45+
`readInternalColumn` (recovers one `internal` column for rows already read, through the
46+
engine's privileged accessor, and fails closed when the value cannot be recovered).
47+
48+
**What to do after upgrading.**
49+
50+
- **Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten
51+
(the ledger is append-only), so a signing key that existed before the upgrade may have a copy
52+
in the ledger. Rotate the keys so that copy signs nothing.
53+
- **Revoke and re-mint share links that must stay private.** A share link's token is a
54+
capability that stays valid until the link expires or is revoked, and links minted before this
55+
release may have a copy in the ledger.
56+
- A copy of a one-time verification credential is usable only while that credential is still
57+
outstanding: once it is consumed or expires, its copy names nothing that will be accepted.
58+
- An integration that read any of these columns through `GET /api/v1/data/...` no longer
59+
receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO
60+
providers through their auth routes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
fix(plugin-security,platform-objects): an org member reading a colleague's `sys_user` row is no longer served the identity object's `Admin` field group, directly or through the activity stream (#21237)
7+
8+
Clause-②: no
9+
10+
- **What a member was served.** The platform baseline `member_default` opens every org peer's `sys_user` row (the `sys_user_org_members` policy) and declared no field-level security on it. An org member reading a colleague's row was therefore served the whole `Admin` field group: the sign-in trail, the lockout state, the ban reason and expiry, the password and MFA stamps, the legacy platform role scalar and the AI-seat flag. With object-level read on `sys_activity`, the colleague's activity metadata carried the same fields, because the activity field redaction serves exactly what the data plane serves.
11+
- **What changes.** `member_default` and `viewer_readonly` now declare the `Admin` group `readable: false` through the permission set's existing `fields` entries. The withheld set is built from the identity object's declaration, so a field the declaration adds to the group is withheld from the day it is declared. `admin_full_access` and `organization_admin` (and so `organization_admin_no_bypass`) declare the group readable and editable, the same state as a field no set names, so an administrator's reads and writes are unchanged. `member_default` is the additive baseline every authenticated user resolves, and field grants merge most-permissively, which is why the admin sets carry that keeping entry.
12+
- **What a member sees now.** On the direct read, the list read and the activity metadata, a member is served no `Admin`-group field of a colleague's row. The directory fields (name, email, image) are still served. Field-level security does not distinguish rows, so the member's own row read through the generic data API is withheld the group too; every platform reader of those fields on a member's own row (the auth gates, the sign-in stamps, the session, the AI-seat resolution) reads under system or auth context and is unaffected. A member's query that filters or sorts on a withheld field is refused (`403 PERMISSION_DENIED`, the filter-oracle rule). A member's user-context write that names a withheld field is refused by the field-level write gate (`403 PERMISSION_DENIED`), and a payload mixing such a field with profile fields no longer lands partially.
13+
- **The deactivation flag is directory data.** `sys_user.banned` moves from the `Admin` field group to the `Account` group in `@objectstack/platform-objects`, so members are still served it. Every user picker filters its candidates on it, and a filter on a withheld field would be refused. Its reason and expiry stay in the `Admin` group. In a record form the field now renders in the `Account` section.
14+
15+
**Migration.** None for shipped apps. A custom permission set that grants an org member read on `sys_user` and is meant to show them the `Admin` group must name those fields `readable: true` in its `fields` entries. A client that filtered members' `sys_user` queries on an `Admin`-group field must drop that predicate or run it with an administrator's grant.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/sdui-parser': minor
4+
'@objectstack/lint': minor
5+
---
6+
7+
The metric sub-caption is retired at both ends. A dashboard widget keeps one authored description, `widget.description`, which renders as the card-header subtitle and is translated by the widget's `description` translation key. The widget translation key `subCaption` is refused, and the server no longer writes a widget's `options.description`.
8+
9+
Clause-②: no (narrowing)
10+
11+
<!-- adr-0087: registered translation-widget-sub-caption-removed, translation-widget-sub-caption-retired -->
12+
13+
**What is retired.** `dashboards.DASHBOARD.widgets.WIDGET.subCaption` in a translation bundle (`defineTranslationBundle`, `stack.translations`, the platform bundle) and in a registered `translation` item. It overlaid a caption under a metric's value onto the widget's `options.description`. The dashboard schema never declared `options.description`, and no authored widget wrote it, so `translateDashboard`'s overlay was the key's only writer. That overlay is removed: `translateDashboard` now translates a widget's `title` and `description` and carries `options` through untouched.
14+
15+
**BREAKING** — an accept-set narrowing, shipped as `minor` under the launch-window convention.
16+
17+
### FROM → TO
18+
19+
| wrote | write instead |
20+
| --- | --- |
21+
| `dashboards.DASHBOARD.widgets.WIDGET.subCaption: 'TEXT'` | delete the entry. If the copy belongs on the card, put it in the widget's `description` and translate it under `dashboards.DASHBOARD.widgets.WIDGET.description`. |
22+
| `dashboards.DASHBOARD.widgets.WIDGET.subtitle: 'TEXT'` | `subtitle` was only ever a rename suggestion for `subCaption`. Card-header copy goes under `description`; a caption under the value has nowhere to render, so delete it. |
23+
24+
**The one-line fix: delete every `subCaption:` entry under `dashboards.*.widgets.*` in your translation bundles.** `os migrate meta --from 17` lists the mechanical edits for existing sources; stored `translation` items are converted when they are read.
25+
26+
**What an author now sees.** Writing `subCaption` fails `tsc` (its input type is the retired-key mark) and fails the parse with a prescription naming the widget's `description`. Writing `subtitle` on a widget translation fails the parse with both readings named, instead of a rename suggestion onto a key that is refused next. `os validate`, `os build` and `os lint` now raise the `unconsumed-widget-option` warning on an authored widget `options.description`, like any other options key the dataset-bound render path does not read. It is a warning, so none of the three fails on it.
27+
28+
**Measured producers: none.** Zero `subCaption` entries and zero authored widget `options.description` in the four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and in the bundles `@objectstack/platform-objects` ships, so no shipped exit code changes.
29+
30+
### The retirement kit
31+
32+
- **Tombstone.** `subCaption` is a `retiredKey()` tombstone on the widget translation node, so the refusal carries the prescription on all three faces the node is spread into (per-app bundle entry, platform bundle entry, `translation` item). The node sits under two records (`dashboards`, `widgets`), below the authorable-surface walk, so it has no `RETIRED_KEYS_BY_MAJOR` row, the same as the `submitLabel` component-copy key before it.
33+
- **The former alias.** The `subtitle` → `subCaption` rename suggestion moves to the node's `guidance` table. An alias whose target is a tombstone is the shape the alias-integrity audit refuses, and repointing it at `description` would silently change what the word is taken to mean.
34+
- **Conversion.** `translation-widget-sub-caption-removed` (protocol 18) strips the key from bundle entries and bare translation items as a lossless delete. It is retired from the load path, so authors are refused at parse while stored rows and `os migrate meta` replay it. Its D3 record is the semantic entry `translation-widget-sub-caption-retired`.
35+
- **`@objectstack/sdui-parser`.** `CONSUMED_WIDGET_OPTION_KEYS` drops `description`, its one undeclared member, which existed only because the overlay wrote it. `check:widget-option-census`'s `NON_DECLARED_MEMBERS` ledger is now empty, so the census asserts that nothing writes an undeclared key into `options`.

‎.github/workflows/ci.yml‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,48 @@ jobs:
178178
- 'pnpm-lock.yaml'
179179
- 'tsconfig.json'
180180
- '.github/workflows/ci.yml'
181+
# BUILD INPUTS outside every package (#21202): turbo.json itself,
182+
# its `globalDependencies`, and each `$TURBO_ROOT$/…` input of a
183+
# build task Build Core's `pnpm build` runs. A diff confined to
184+
# one of them moves the build hashes it reaches, and before these
185+
# entries it matched nothing here, so Build Core skipped and the
186+
# merge queue ran that build first (PR #21199, turbo.json only).
187+
# Every core-gated job starts on such a diff, not Build Core
188+
# alone: each one builds, and turbo re-runs every task downstream
189+
# of a moved build hash.
190+
#
191+
# scripts/check-ci-filter-parity.mjs DERIVES the required set from
192+
# turbo.json and the root `build` script, and reds while a declared
193+
# build input is uncovered here, or while a literal entry beside
194+
# them covers none (a leftover). `tsconfig.json` and the
195+
# `packages/cli/...` input are covered by the entries above;
196+
# `@objectstack/docs#build` (its input is `content/**`) is the
197+
# docs filter's, since `pnpm build` excludes that package.
198+
#
199+
# WIDTH: literal files, not `scripts/**`. Over the 3288
200+
# first-parent commits of `main` in the 30 days to `1371dc980c`,
201+
# through picomatch 2.3.1 and 4.0.5 (they agree on every row),
202+
# `core` matched 2247; these entries add 28, `scripts/**` with
203+
# turbo.json and tsup.config.ts would add 593, 516 of them diffs
204+
# touching a `scripts/pm/` tool or a `scripts/check-*` gate.
205+
- 'turbo.json'
206+
- 'tsup.config.ts'
207+
- 'scripts/build-input-hash.mjs'
208+
- 'scripts/check-dev-prereqs.mjs'
209+
- 'scripts/check-dts-emitted.mjs'
210+
- 'scripts/check-dts-references.mjs'
211+
- 'scripts/check-regen-pending.mjs'
212+
- 'scripts/cli-build-prerequisite.mjs'
213+
- 'scripts/git-env.mjs'
214+
- 'scripts/import-prerequisite.mjs'
215+
- 'scripts/invoked-as.mjs'
216+
- 'scripts/js-comment-mask.mjs'
217+
- 'scripts/regen-artifacts.mjs'
218+
- 'scripts/sync-scaffold-emission-policy.mjs'
219+
- 'scripts/sync-template-versions.mjs'
220+
- 'scripts/ts-parse.mjs'
221+
- 'scripts/tsup-drop-sources-content.mjs'
222+
- 'scripts/workspace-enumerator.mjs'
181223
# Build inputs of the vendored Console SPA — see the Console Pin Gate
182224
# job at the bottom of this file. `.objectui-sha` is a ROOT DOTFILE, so
183225
# it matches neither filter above: a pin-only diff skipped `core` and
@@ -2047,6 +2089,16 @@ jobs:
20472089
- name: Install dependencies
20482090
run: pnpm install --frozen-lockfile
20492091

2092+
# 67 package build commands end with `node …/scripts/check-dts-emitted.mjs`,
2093+
# so the `pnpm build` below runs that checker on every package it builds,
2094+
# and until this step no workflow ran its `--self-test`. It surfaced when
2095+
# the `core:` filter began naming the file as a build input (#21202) and
2096+
# `check:self-test-wired` saw a workflow name a self-test it never runs.
2097+
# Milliseconds, no prerequisites; placed before the build so a broken
2098+
# checker is reported before the 67 verdicts that rely on it.
2099+
- name: check-dts-emitted self-test
2100+
run: node scripts/check-dts-emitted.mjs --self-test
2101+
20502102
# ── Turbo remote cache (#21186) ─────────────────────────────────────
20512103
# Vercel's managed Turborepo remote cache. objectstack-ai/cloud's
20522104
# test.yml builds this repo at its pinned SHA with the same command and

‎.github/workflows/lint.yml‎

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2660,19 +2660,17 @@ jobs:
26602660
# GAP 1 — SCHEDULING. CLOSED by #9829 / #10014. The original claim:
26612661
# `create-objectstack#test` is reachable, at PR time, only from ci.yml's
26622662
# `test` job; that job was gated on the `core` paths-filter ALONE; and
2663-
# `core` — `packages/**`, `examples/**`, `apps/!(docs)/**`,
2664-
# `package.json`, `pnpm-lock.yaml`, `tsconfig.json`,
2665-
# `.github/workflows/ci.yml` — matches no path under `scripts/`, so a diff
2666-
# confined to scripts/sync-template-versions.mjs skipped Test Core
2667-
# ENTIRELY, and the vitest with it. `core` is still false on that diff — it
2668-
# was never widened — but the `test` job now ORs in a SECOND filter output,
2669-
# `scripts: ['scripts/**']`, which matches, so
2670-
# `if: ... (core != 'false' || scripts != 'false')` resolves to RUN.
2671-
# Re-measured against the merged filter with picomatch 2.3.1 — the version
2672-
# dorny/paths-filter@v4's own package-lock.json resolves, NOT this repo's
2673-
# 4.0.5, which is what the old text cited; the two agree on these globs, so
2674-
# the wrong figure never produced a wrong verdict — `core=false`,
2675-
# `scripts=true`. With the job running the rest of the chain follows:
2663+
# `core` then matched no path under `scripts/`, so a diff confined to
2664+
# scripts/sync-template-versions.mjs skipped Test Core ENTIRELY, and the
2665+
# vitest with it. Two changes closed it, either one enough: the `test` job
2666+
# ORs in a SECOND filter output, `crosspkg:` (named `scripts:` at #9829),
2667+
# whose `scripts/**` matches; and `core` itself now names that file,
2668+
# because it is a declared build input of `create-objectstack#build`
2669+
# (#21202). So `if: ... (core != 'false' || crosspkg != 'false')` resolves
2670+
# to RUN. Re-measured against the filter at #21202 with picomatch 2.3.1 —
2671+
# the version dorny/paths-filter@v4's own package-lock.json resolves, NOT
2672+
# this repo's 4.0.5; the two agree on these globs — `core=true`,
2673+
# `crosspkg=true`. With the job running the rest of the chain follows:
26762674
# `--union-into` is a step inside it, so it runs and unions
26772675
# create-objectstack back in off its declared globs; `create-objectstack#test`
26782676
# declares

0 commit comments

Comments
 (0)