Skip to content

Commit bc2d3af

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24817-later-step-original-submitter
2 parents bbbbbe4 + b3fc122 commit bc2d3af

69 files changed

Lines changed: 1676 additions & 502 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.

‎.changeset/24529-declared-webhooks-from-registry.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ What changes:
2222

2323
On a database an 18.0 install created (expand first — nothing is moved or deleted):
2424

25-
- The rows the 18.0 seeder wrote stay where they are. A pristine one (`managed_by: 'package'`, not `customized`) is ignored in favour of its declaration, so a declared webhook is never delivered twice; it follows the declaration's URL and switch, as the seeder's rewrite on every boot made it do.
25+
- The rows the 18.0 seeder wrote stay where they are until the ownership ceremony settles them, and the v18 boot guard refuses the database until the ceremony has certified it. A pristine copy (`managed_by: 'package'`, not `customized`) is deleted by the ceremony, its deliveries re-pointed at the declaration, so a declared webhook is never delivered twice.
2626
- A seeded row an administrator edited (`customized: true`), or a row an administrator authored under a declared name, answers as itself, and the declaration is not dispatched beside it — an administrator's `active: false` on a noisy webhook keeps holding. This is logged at `warn` once per name per process, with the remedy.
2727
- A seeded row whose declaration was removed keeps delivering, as it did on 18.0.
28-
- Redelivering an old delivery of a pristine seeded row is judged by the declaration that now answers for it, so a webhook switched off through the ledger is not re-sent.
28+
- Redelivering an old delivery of a pristine seeded copy is judged by the declaration, because the ceremony re-points that delivery at `declared:<name>`; a webhook switched off through the ledger is not re-sent.
2929

3030
Deleting the seeded rows, and re-pointing their `sys_http_delivery` history at the declaration, is the ownership ceremony's `declared-webhooks` step (`os migrate organization-ownership`).

‎.changeset/24563-list-view-out-of-closure.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Clause-②: yes (narrowing)
2222

2323
- A list view bound to an object inside the closure (the package's own, a declared dependency's, a platform/system object, an overlay row): same field verdicts, same refusals.
2424
- A write that names no package (an overlay write) is judged against the whole collection, exactly as before. An object that exists nowhere gets no closure verdict there; #24668 refuses it as `list-view-object-unknown` (see that changeset).
25-
- The flow door: `flow-trigger-unknown-object` stays an advisory.
25+
- The flow door: under the same closure, `flow-trigger-unknown-object` refuses an out-of-closure trigger object too (#24669, see that changeset).
2626
- `os validate`, `os build` and `os lint`: no closure is stated, so the rule skips an unresolvable binding as before.
2727
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
2828
- Measured before crossing: the 11 views shipped in this repository carry 39 list-view bindings, in `examples/app-crm`, `app-todo`, `app-showcase` and `app-multi-package`, the `create-objectstack` blank template, and `qa/downstream-contract`. None is outside its package's closure, and the door's `view` rule set refuses none of them, with the package's closure or without it. None of the 97 platform built-in list views retargets `data.object`. Deployed rows were not measured.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/plugin-webhooks': minor
3+
'@objectstack/platform-objects': minor
4+
---
5+
6+
refactor(plugin-webhooks,platform-objects)!: 18.x step 7c retires the dual reads steps 1–2 added for 18.0 databases. Any `sys_webhook` row that holds a declared webhook's name now answers as itself. `sys_setting_audit.scope` no longer declares `global`.
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any metadata schema is removed, renamed or re-shaped, and no public export, type or error code is removed or renamed. Two things narrow at run time. The dispatcher no longer skips a pristine seeded webhook row in favour of its declaration (an internal reader, never exported). A stored select value is refused on one platform object. Both apply only to rows the v18 ownership ceremony settles or moves before a database is certified, and the v18 boot guard refuses a database it has not certified. -->
11+
12+
**BREAKING** narrowing, shipped as `minor` under the launch-window convention for breaking changes (ADR-0087, the ratified pre-launch launch-window section; the decisions retired here are ADR-0131 D2, D6 and D7). The 18.x upgrade ceremony settles every row these readers existed for before a database is certified, so a certified database and a fresh install read the same either way.
13+
14+
- **Declared webhooks (plugin-webhooks).** Before: a `sys_webhook` row seeded by the 18.0 seeder under a declared name, and never edited (`managed_by` `package`/`platform`, not `customized`), was skipped as a "mirror". The declaration answered in its place, for dispatch and for redelivery. Now: any row holding a declared name answers as itself and the declaration is withheld, reported once per name (regime C). The ceremony (step 5a) deletes or settles every such copy, so on a certified database this changes one case only. A seeded row the ceremony kept, because no package declared its name then, now withholds a declaration of that name added later, where before it was skipped. **Remedy:** rename or delete that row, and the declaration answers. The provenance stamp (`customized`) stays: the ceremony still reads it when a superseding reviewed plan re-runs the ceremony from the start.
15+
- **Settings audit (platform-objects).** Before: `sys_setting_audit.scope` declared `global`, `tenant` and `user`, and a reader of the global rung's history read both `sys_setting_audit` and `sys_platform_setting_audit`. Now: `sys_setting_audit.scope` declares `tenant` and `user` only, and an insert with `scope: 'global'` is refused (`VALIDATION_FAILED` on `scope`). The global rung's history is `sys_platform_setting_audit` alone. The settings service already wrote global-rung rows there, and the ceremony (step 5b) moves every 18.0 row. **Remedy:** none for a certified database. A script that wrote audit rows by hand writes global-scope rows to `sys_platform_setting_audit`.
16+
- **Unchanged.** Identity mail: only docblocks change. The tenant mail log keeps its body read gate, and a mail job with no object still reads `sys_email`, because a tenant mail row an 18.0 process queued stays there.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
fix(lint)!: the save door refuses a flow written into a package whose trigger object is outside that package's closure (#24669)
6+
7+
Clause-②: yes (narrowing)
8+
9+
**Why.** The runtime save door judges a write into a package against that package's closure (ruling #9612): the package's own objects, the objects of the packages its manifest declares in `dependencies`, platform/system objects, and provenance-less overlay rows. Every other object is dropped from the snapshot before any rule runs. The dataset door already refused a reference outside that closure (`object-reference-unknown`), and the view door refuses one too (`list-view-object-outside-closure`). The flow door only advised: `flow-trigger-unknown-object` stayed a `warning` there, with the hint "ignore this if another installed package declares it". Under a closure that hedge is false, because the snapshot is the closure: an object outside it is one the package never declared it may reference. So the same mistake was refused at two doors and stored with an advisory at the third.
10+
11+
**What is refused.** A flow written into a package whose record trigger (`config.objectName` on a `record-*` start node) or time-relative sweep (`config.timeRelative.object`) names an object outside the package's closure is refused with one `flow-trigger-unknown-object` issue at `error`, located at that key. The hint names the remedy and the package. An object that exists nowhere is refused the same way, because the closure is all the snapshot sees. A `sys_`-prefixed name is still not judged, exactly as without a closure.
12+
13+
**BREAKING — what moves for consumers.** This is an accept-set narrowing on the metadata save door, shipped as `minor` under the launch-window convention for accept-set narrowings (`.changeset/pre.json` is in `exit` mode, so the next version pass cuts 18.0.0 and this change ships in it).
14+
15+
- FROM: a publish-mode write of a flow into a package, triggered on or sweeping an object outside the package's closure, answered `200` and was stored, with a `flow-trigger-unknown-object` save advisory. This covers `PUT /api/v1/meta/flow/:name?package=…` (and `saveMetaItem` in publish mode), the promotion of a draft (`POST /api/v1/meta/flow/:name/publish`, `publishMetaItem`), a package draft publish (`publishPackageDrafts`), and a package copy (`duplicatePackage`), which writes its copies through the same door.
16+
- TO: `saveMetaItem` in publish mode, the draft promotion and `publishPackageDrafts` answer `422 INVALID_METADATA`, with the `flow-trigger-unknown-object` issue at `error` at the trigger key, and nothing is stored. A package draft publish fails the item, and the batch with it. In `duplicatePackage`, a copied flow whose trigger object stays outside the copy package's closure is refused with the same rule and listed in its `failed[]`. One way that happens is when that object's own copy was refused.
17+
- Fix: declare a dependency on the package that defines the trigger object in the `dependencies` of the writing package's manifest, or trigger the flow on an object the package may reach (its own, a declared dependency's, or a platform object). Then save again. Saving the flow as a draft (`mode: 'draft'`) is still allowed, because drafts are never gated; publishing that draft is judged.
18+
19+
**The next save of a stored flow whose trigger object is outside its package's closure is refused with the fix; stored flows keep loading and firing until then.** The door judges writes only. A stored row is not re-judged at boot.
20+
21+
**Unchanged.**
22+
23+
- A flow triggered on an object inside the closure (the package's own, a declared dependency's, a platform/system object, an overlay row): no finding, as before.
24+
- A write that names no package (an overlay write), a write naming a package the registry cannot produce, and the `sys_metadata` sentinel state no closure: an object the whole collection does not carry is still a `flow-trigger-unknown-object` save advisory, byte-identical to before.
25+
- `os validate`, `os build` and `os lint`: no closure is stated, so the rule's verdict, severity and wording are unchanged.
26+
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
27+
- Measured before the change: every record-triggered or time-relative start node shipped in this repository (21, all in `examples/app-showcase` and `examples/app-todo`; no flow in `packages/apps`, `packages/platform-objects` or the `create-objectstack` template) triggers on an object its own stack defines, and objectui ships no such flow. Deployed rows were not measured.
28+
29+
**API.**
30+
31+
- `validateFlowTriggerReadiness(stack, options?)` accepts an optional `{ packageScope }` (`{ packageId, dependencies }`). Stated, an unknown trigger object is an `error`; omitted, the rule answers as before.
32+
- The runtime publish gate hands `AuthoringRuleContext.packageScope` to this rule, which now reads it beside `validateListViewFieldRefs`.
33+
34+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at the metadata save door of a flow written into a package whose trigger object is outside that package's dependency closure: no authorable key, spelling, export or stored shape moves, and no stored row is read, rewritten or converted. A stored flow with such a trigger keeps loading and firing until it is next saved, and the repair is the author's choice of which package to declare as a dependency (or which object to trigger on), which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this door (not already-registered); and the change is a door verdict, not a declaration or an export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-hono-server': minor
3+
---
4+
5+
fix(plugin-hono-server)!: the CORS default admits no cross-origin caller, and `'*'` with credentials is refused at boot
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a narrowing of the transport's CORS admission default plus a boot refusal of one configuration: no authorable metadata key, spelling, export, type or stored shape moves, so `objectstack migrate meta` has nothing to rewrite. The remedy is a deployment variable, which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this door (not already-registered); and the change is an admission verdict, not an interface, a type or an export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
10+
11+
**BREAKING** — an admission narrowing of the transport's CORS answer, shipped as `minor` under the launch-window convention for accept-set narrowings. The governing text is `docs/HARDENING.md` § CORS: never `'*'` with credentials, and an explicit origin list.
12+
13+
- **Before.** With no origin list configured (no `cors.origins` option and no `OS_CORS_ORIGIN`), the CORS middleware admitted every origin for credentialed cross-origin requests. A configured `'*'` with credentials on was accepted and answered the same way.
14+
- **Now.** With no list, no cross-origin caller is admitted. A loopback origin (`localhost`, `127.0.0.1`, `[::1]`, any port) is admitted on a development boot only: `NODE_ENV` exactly `development`, which `os dev` and `os serve --dev` set. A list that contains `'*'` while credentials are on is refused in `init()`, so the boot fails, and the message names both remedies.
15+
- **Unchanged.** Same-origin requests need no CORS answer, so the Console under `/_console` and every page the server itself serves work as before. A configured list, exact origins or wildcard patterns, is admitted exactly as before, credentials included. `'*'` with credentials off still boots. `cors: false` and `OS_CORS_ENABLED=false` still disable the middleware. Allowed and exposed headers, methods and max-age are unchanged.
16+
17+
**What an operator does.** A front-end served from another origin that calls this server is now named: `OS_CORS_ORIGIN=https://app.example.com` (comma-separated for several), or the plugin's `cors.origins` option. A boot that now refuses `'*'` either names its origins or sets `OS_CORS_CREDENTIALS=false` to keep serving `'*'` without credentials. `OS_TRUSTED_ORIGINS` is auth's own list, the origins it accepts for callbacks and CSRF checks, and it admits nothing to CORS. Auth also trusts every origin listed in `OS_CORS_ORIGIN`, so that one variable covers a cross-origin front-end that signs in. A list given through the plugin's `cors.origins` option instead is not seen by auth, so such a front-end is then also named in `OS_TRUSTED_ORIGINS`.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/service-settings': patch
3+
---
4+
5+
fix(service-settings): Setup → Authentication says what the sign-up and Google toggles do beside the audience and the credentials (#24776)
6+
7+
Clause-②: no
8+
9+
Two controls on the Authentication settings page read as contradicting the system on a fresh install. Their wording changes; no default, `visible` rule, `required` flag, option or stored key moves, and no deployment admits or signs in anyone it did not before.
10+
11+
- **`auth.signup_enabled`** is now labelled *Allow email sign-up* (it was *Allow self-service registration*) and carries help text. It is the email-and-password sign-up form's own switch, and the *Self-registration audience* decides who the form admits: under `invite_only`, only an address holding a pending invitation; under `email_domain`, the allowlisted domains and invited addresses; under `open`, anyone. Off closes the form under every audience, invitations included. So ON beside *Invitation only* means the form is open to invited addresses.
12+
- **`auth.google_enabled`** help now says that Google sign-in is offered only once a client ID and a client secret both resolve, saved on the page or set as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` on the server (which win over saved values). Until then the login page shows no Google button, even while the toggle is on. Off withdraws Google sign-in even when the server sets both.
13+
14+
The English, Chinese, Japanese and Spanish bundles carry the new label and help.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
fix(plugin-auth): the account lockout resolves the account the sign-in credential check resolves
6+
7+
Clause-②: no
8+
9+
- **One account per sign-in.** The ADR-0069 D2 lock gate, its failed sign-in counter and the `login_failed` / `account_locked` account events now share one resolver. It looks the account up exactly as better-auth's `/sign-in/email` credential check does, so all four name the same account for every identifier the credential check resolves to it.
10+
- **What that guarantees.** Every refused sign-in that resolves to an account counts toward that account's one lock. An existing lock refuses every sign-in that resolves to the account, including one with the correct password. A successful sign-in clears the counter, as before. An identifier that resolves to no account changes no account and writes no account event.
11+
- **Nothing to migrate.** The lockout state stays on the account's own `sys_user` row (`failed_login_count`, `locked_until`), written by its id, and `lockoutThreshold` / `lockoutDurationMinutes` keep their meaning.

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ read at startup unless noted otherwise. Boolean variables accept `true` / `false
3636
> +100) and the banner prints the actual bound port. In **production**
3737
> (`os start`) a busy port is a hard error (exit 1) — it never silently
3838
> drifts, because a shifted port breaks reverse-proxy upstreams, `OS_AUTH_URL`
39-
> callbacks, and `OS_TRUSTED_ORIGINS` (CORS). Pin the port explicitly in
39+
> callbacks, and `OS_TRUSTED_ORIGINS` (auth CSRF). Pin the port explicitly in
4040
> production (`OS_PORT=8080 os start`) and keep `OS_AUTH_URL` / `OS_TRUSTED_ORIGINS`
4141
> in sync with it.
4242
@@ -123,8 +123,8 @@ Auth settings precedence:
123123
| Variable | Type | Default | Description |
124124
|:---|:---|:---|:---|
125125
| `OS_CORS_ENABLED` | boolean | `true` | Enable CORS middleware. |
126-
| `OS_CORS_ORIGIN` | csv | `*` | Allowed origins (comma-separated, or `*`). |
127-
| `OS_CORS_CREDENTIALS` | boolean | `true` | Allow credentialed cross-origin requests. |
126+
| `OS_CORS_ORIGIN` | csv | — | Origins admitted to cross-origin requests: exact origins or patterns such as `https://*.example.com`, comma-separated. Unset, no cross-origin caller is admitted (loopback origins excepted on a development boot); same-origin requests, the Console included, need no entry. `*` is refused at boot unless `OS_CORS_CREDENTIALS=false`. |
127+
| `OS_CORS_CREDENTIALS` | boolean | `true` | Allow credentialed cross-origin requests from the admitted origins. Must be `false` to serve `OS_CORS_ORIGIN=*`. |
128128
| `OS_CORS_MAX_AGE` | number | `86400` | Preflight cache TTL in seconds. |
129129

130130
---

‎content/docs/deployment/production-readiness.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ the [HARDENING.md recipes](https://github.com/objectstack-ai/objectstack/blob/ma
118118
response `X-Request-Id` header.
119119
- [ ] better-auth session TTL / refresh / revoke verified (curl
120120
checklist in HARDENING.md).
121+
- [ ] CORS admits exactly the cross-origin front-ends you serve. With no
122+
`OS_CORS_ORIGIN` none is admitted (same-origin pages, the Console
123+
included, need no entry); otherwise it lists each origin explicitly,
124+
never `*` — the server refuses `*` at boot while credentials are on.
125+
See [HARDENING.md § CORS](https://github.com/objectstack-ai/objectstack/blob/main/docs/HARDENING.md#cors).
121126
- [ ] Cross-tenant negative tests in CI.
122127
- [ ] Multi-org deployments: tenant isolation is **active**, not degraded —
123128
confirm `features.degradedTenancy` is `false` in `/auth/config`. A

0 commit comments

Comments
 (0)