Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/15204-s6b2a-meta-resync-example-binders.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/cli': minor
'@objectstack/plugin-security': patch
'@objectstack/driver-sql': patch
'@objectstack/runtime': patch
---

feat(cli)!: `os meta resync` is deleted

Clause-②: yes (narrowing)

<!-- adr-0087: not-required (no-migration-prescription) A CLI command is removed; it reconciled platform rows no reader uses for authorization, and no key of any spec schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. The package publishes (not unpublished); no ADR-0087 id covers this rule and this diff adds none (not registered / already-registered). -->

**BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes.

`os meta resync` reconciled the platform-default `sys_permission_set` rows (`admin_full_access`, `member_default`, …) to the shipped declaration. Since the authorization resolver reads permission sets from the security catalog (ADR-0131 D3), an edited default set is served from its declaration at the next boot, and the rows the command reconciled decide nothing. The command, its `--yes` / `--json` modes and its skip-count explanation are gone; a script that calls it gets oclif's unknown-command error.

In `@objectstack/plugin-security`, `@objectstack/driver-sql` and `@objectstack/runtime`, shipped comments and one warning sentence (the permission-set backfill's re-provision list) no longer name the command. Their behaviour is unchanged.
2 changes: 1 addition & 1 deletion .changeset/22581-migrate-reads-env-files.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/cli': patch
---
Expand All @@ -6,7 +6,7 @@

`os serve`, `os start` and `os dev` load the project's `.env*` files before they read any variable. `os migrate` read only the process environment. So a project keeping `OS_DATABASE_URL` in `.env` served one database and migrated another: `os migrate plan` reported `.objectstack/data/objectstack.db` while `os serve` opened the `.env` database. An `OS_AUTH_SECRET` kept in `.env` left the auth family (`sys_user`, `sys_account`, …) out of the plan.

Every command that boots the one-shot schema stack now loads the same files first, through the same `dotenv-flow` call, with `os serve`'s mode rule: `NODE_ENV`, else `production`, and `development` for `os migrate security-catalog-overlays --dev`, which composes as `os serve --dev`. That covers every `os migrate` subcommand, and also `os meta resync`, `os secret rewrap`, `os secret orphans` and `os storage orphans`. A variable exported in the shell still wins over `.env`, as it does for `os serve`. The SQLite occupancy check that `os migrate apply` and the other write commands run before booting reads the same files, so it checks the database the command is about to migrate.
Every command that boots the one-shot schema stack now loads the same files first, through the same `dotenv-flow` call, with `os serve`'s mode rule: `NODE_ENV`, else `production`, and `development` for `os migrate security-catalog-overlays --dev`, which composes as `os serve --dev`. That covers every `os migrate` subcommand, and also `os secret rewrap`, `os secret orphans` and `os storage orphans`. A variable exported in the shell still wins over `.env`, as it does for `os serve`. The SQLite occupancy check that `os migrate apply` and the other write commands run before booting reads the same files, so it checks the database the command is about to migrate.

This supersedes two phrasings in this release's `os migrate plan` / `apply` auth-family entry: `os migrate` now does read the project's `.env` files, and the deployment's `OS_AUTH_SECRET` composes the auth family whether it is exported or kept in `.env`.

Expand Down
2 changes: 1 addition & 1 deletion content/docs/data-modeling/drivers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ A file-backed SQLite database is switched to **WAL** (write-ahead logging) the
first time ObjectStack connects to it. SQLite's own default is a rollback
journal (`journal_mode = delete`), which is the wrong trade for the way this
platform is actually used — **several processes on one file**: a dev server,
`os migrate`, `os meta resync`, a test run.
`os migrate`, a test run.

| | rollback journal (SQLite default) | WAL (ObjectStack default) |
|:---|:---|:---|
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/platform-checklist/areas/access-security.json
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@
"app": "showcase",
"requires": [
"showcase_inquiry is OWD private with create granted to the baseline — depth only matters where the baseline stops (permission-sets.ts docblock)",
"personas staffed by granting the sets (sys_user_permission_set) or assigning the bound positions manager/exec/ops (bind-position-sets.ts wires position → set at boot)"
"personas staffed by granting the sets (sys_user_permission_set) or assigning the positions manager/exec/ops (each declares its set in permissionSets, examples/app-showcase/src/security/positions.ts)"
]
},
"steps": [
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/platform-checklist/areas/search.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"provenance": "run #7629 (framework, 2026-08-11) drove search.rls-both-personas to 4/4 PASS on exactly this persona — 3 invoices visible to the member vs 15 to admin. Promoted from prose on that one item to a replayable area recipe by the 17.1.0 sweep (#10236 B1); the Mei/Ada measurements above are from #10257. Step 1 re-pointed invite-first by #21060 after QA run #21056 met the invite_only default (403 SELF_REGISTRATION_CLOSED on an uninvited sign-up).",
"app": "showcase",
"requires": [
"a stock dev boot — the position→permission-set bindings are created automatically at startup on `kernel:bootstrapped`, including ['contributor','showcase_contributor'] (examples/app-showcase/src/security/bind-position-sets.ts, POSITION_PERMISSION_SET_BINDINGS). ⛔ Do NOT hand-create a sys_position_permission_set row: it already exists, and a duplicate proves nothing about the merge",
"a stock dev boot — the `contributor` position declares `showcase_contributor` in its `permissionSets` (examples/app-showcase/src/security/positions.ts), which the resolver reads by name (ADR-0131 D3/D4). ⛔ Do NOT hand-create a sys_position_permission_set row: no resolver reads it, so it proves nothing about the merge",
"system-context write access for the position binding (the sys_user_position insert) — the same pattern access-security.json's permission-set items already replay",
"an isolated boot (own port + file DB, dogfood §0); export OS_PORT=<port> as well as passing -p (RUNNER.md's connector self-URL fact) so unrelated flow noise does not pollute the run",
"an admin session to INVITE the step-1 address before step 1: POST /api/v1/auth/organization/invite-member {\"email\": \"<the step-1 address>\", \"role\": \"member\"}. The stock showcase declares no auth.audience, so its posture is the invite_only default, which answers an uninvited sign-up 403 SELF_REGISTRATION_CLOSED; a pending, unexpired sys_invitation row for the address is what admits it (an admin POST /api/v1/auth/admin/create-user is the other door, and answers mustChangePassword:true)"
Expand Down
10 changes: 0 additions & 10 deletions examples/app-crm/objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
SalesUserPermissionSet,
GuestPortalProfile,
} from './src/security/index.js';
import { registerCrmPositionBindings } from './src/security/bind-position-sets.js';
import { CrmSeedData } from './src/data/index.js';
import { CrmDatasource, CrmAnalyticsDatasource } from './src/datasources/crm.datasource.js';
import { CrmTranslationBundle } from './src/translations/crm.translation.js';
Expand Down Expand Up @@ -118,12 +117,3 @@ export default defineStack({
// Seed data
data: CrmSeedData,
});

/**
* [#8060] Ensure the persona position↔permission-set bindings exist after the
* security bootstraps (cannot be a seed — see bind-position-sets.ts). Mirrors
* app-showcase's own `onEnable` wiring.
*/
export const onEnable = async (ctx: unknown): Promise<void> => {
registerCrmPositionBindings(ctx as Parameters<typeof registerCrmPositionBindings>[0]);
};
121 changes: 0 additions & 121 deletions examples/app-crm/src/security/bind-position-sets.ts

This file was deleted.

18 changes: 9 additions & 9 deletions examples/app-crm/test/position-bindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@

/**
* [ADR-0131 D3/D4] The CRM personas' permission sets are declared on their
* positions (`permissionSets`), which the authorization resolver reads. The
* junction binder (`src/security/bind-position-sets.ts`) still writes the same
* pairs for the readers that have not moved off `sys_position_permission_set`;
* until it is deleted, the two must name the same pairs, or those readers
* judge a position by bindings the resolver does not grant.
* positions (`permissionSets`), which the authorization resolver reads; no
* junction row is written for them. Pinned literally so a persona losing its
* set is a red here, not a silently narrower boot.
*/

import { describe, expect, it } from 'vitest';
import { BINDINGS } from '../src/security/bind-position-sets.js';
import { FinanceApproverPosition, SalesManagerPosition, SalesRepPosition } from '../src/security/sales-positions.js';

describe('CRM position bindings', () => {
it('the junction binder writes exactly the bindings the position definitions declare', () => {
it('each persona position declares crm_sales_user', () => {
const declared = [SalesRepPosition, SalesManagerPosition, FinanceApproverPosition]
.flatMap((p) => (p.permissionSets ?? []).map((s) => `${p.name} -> ${s}`))
.sort();
expect(declared).toHaveLength(3);
expect(BINDINGS.map(([p, s]) => `${p} -> ${s}`).sort()).toEqual(declared);
expect(declared).toEqual([
'finance_approver -> crm_sales_user',
'sales_manager -> crm_sales_user',
'sales_rep -> crm_sales_user',
]);
});
});
4 changes: 0 additions & 4 deletions examples/app-showcase/objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { ShowcaseExternalDatasource } from './src/system/datasources/showcase-ex
import { ExternalCustomer, ExternalOrder } from './src/data/objects/external/index.js';
import { setupShowcaseExternalDatasource } from './src/system/datasources/external-fixture.js';
import { registerRecalcEndpoint } from './src/system/server/recalc-endpoint.js';
import { registerShowcasePositionBindings } from './src/security/bind-position-sets.js';
import { registerShowcaseApprovalDemo } from './src/security/seed-approval-demo.js';
import { TaskViews, ProjectViews, InquiryViews, BusinessUnitViews, ContactViews, FieldZooViews } from './src/ui/views/index.js';
import { ShowcaseApp } from './src/ui/apps/index.js';
Expand Down Expand Up @@ -293,9 +292,6 @@ export const onEnable = async (ctx: unknown): Promise<void> => {
await setupShowcaseExternalDatasource(ctx as Parameters<typeof setupShowcaseExternalDatasource>[0]);
// Mount the custom REST endpoint behind the `showcase_recalc_estimate` api action.
registerRecalcEndpoint(ctx as Parameters<typeof registerRecalcEndpoint>[0]);
// [#2926 ②] Ensure the persona position↔permission-set bindings exist after
// the security bootstraps (cannot be a seed — see bind-position-sets.ts).
registerShowcasePositionBindings(ctx as Parameters<typeof registerShowcasePositionBindings>[0]);
// Make the v16 approval features (会签 / quorum) demonstrable on a fresh boot:
// assign the dev admin the approver positions and launch the signoff flows so
// real pending requests land in the inbox (cannot be a seed — see
Expand Down
10 changes: 4 additions & 6 deletions examples/app-showcase/src/data/seed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,10 @@ const orgUnits = SeedSchema.parse({
],
});

// [#2926 ②] Position ↔ permission-set bindings are NOT seeded here: the seed
// loader runs before the security bootstrap creates the sys_position /
// sys_permission_set rows, so the required name references cannot resolve.
// They are ensured imperatively on kernel:bootstrapped instead (after every
// kernel:ready handler, incl. the security bootstrap, has settled) — see
// `src/security/bind-position-sets.ts` (wired via `onEnable`).
// [#2926 ②, ADR-0131 D3/D4] Position ↔ permission-set bindings are NOT seeded
// here: each position declares the sets it distributes in `permissionSets`
// (`src/security/positions.ts`), and the authorization resolver reads them
// from the definition — no binding row exists to seed.

const teams = defineSeed(Team, {
mode: 'upsert',
Expand Down
Loading
Loading