Skip to content

Commit 8f481c2

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15556-subflow-bubble-swallow
2 parents bbeeb31 + da1cffb commit 8f481c2

74 files changed

Lines changed: 2697 additions & 503 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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
The Expression Protocol dialect table no longer names `cron-parser` as the `cron` engine. That package is not a dependency of any ObjectStack package; the row shipped to authors through the generated reference page (`content/docs/references/shared/expression.mdx`) and pointed them at the wrong library for field counts, alias vocabulary and second-field semantics.
6+
7+
The row now says what the code does: no cron syntax is judged at parse time; `croner` evaluates a cron expression only when `CronSchedule.expression` is scheduled (`toBoundaryJobSchedule``CronJobAdapter`, where an invalid pattern is refused); every other cron-typed slot is parsed and reaches no engine; and `@objectstack/formula`'s registered `cron` engine has no caller outside that package. Documentation only — no schema, accept set or behaviour changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os diff` with no path arguments no longer prints its usage error on stdout — in either face.
6+
7+
The refusal sat **above** the command's first `if (!flags.json)`, so the face was still undecided when it ran and it fired in **both**. `printError` plus three `console.log` calls — all four writing to stdout — then `process.exit(1)`. Measured on the published entry `bin/run.js` with `NO_COLOR=1` and the streams captured separately, `os diff --json` and bare `os diff` answered byte-identically: exit 1, **141 bytes of prose on stdout, an empty stderr**, and `JSON.parse(stdout)` throwing on the one stream `--json` reserves for the machine.
8+
9+
The diagnostic now goes to stderr, where the rest of this CLI's diagnostics already go. The 141 bytes moved intact — stdout 141 → 0, stderr 0 → 141. Nothing else moves:
10+
11+
- **the exit code is still 1**, so a consumer branching on exit status sees no change at all;
12+
- **the wording is unchanged**, both usage hints included, so a human reading a terminal sees the same four lines;
13+
- **nothing is accepted or rejected differently** — no invocation that worked before fails now.
14+
15+
⚠️ **No error payload is invented on this path.** What a `--json` consumer should *receive* on a refusal is an open envelope question, entangled with `os lint --eval --json`'s bare `{ error }` (no `code`, no `httpStatus`), and it is deliberately left open here — this change settles only that the machine's channel no longer carries prose. `--json` on this path emits nothing on stdout; a consumer must still read the exit status, exactly as it must today.
16+
17+
This is the sibling of the `resolveConfigPath` repair, and a genuinely different site: that one is reached through `loadConfig()`, this one is `diff.ts`'s own usage error, raised before any config work happens. The existing pin drives `os diff` with two paths precisely so the run gets *past* this check, so it could not see this path. A new pin (`diff-usage-error-stream.e2e.test.ts`) drives the bare form in both faces, and carries a structural tripwire: across 62 command modules, 27 of which offer `--json`, `diff` was the only one with a stdout write above its guard, and the tripwire goes red if another arrives.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
An environment-scoped URL now reaches a dispatcher domain instead of answering 404.
6+
7+
`HttpDispatcher.dispatch()` reads the scoped-URL prefix in three places — the environment-id hint parser, the OAuth-on-MCP gate, and the scope strip that lets `DomainHandlerRegistry` match the remainder. Only the first had been moved to the ADR-0006 `/environments/` spelling; the other two still matched the retired `/projects/` one. The strip therefore never fired on a real scoped URL, and since the registry matches from the head of the path, every environment-scoped request arriving through the `@objectstack/hono` catch-all — the entry cloud hosts mount, and the only one that hands `dispatch()` a still-scoped path — matched no domain at all:
8+
9+
```
10+
GET /api/v1/environments/<id>/data/task -> 404 ROUTE_NOT_FOUND (now: reaches /data)
11+
GET /api/v1/environments/<id>/health -> 404 ROUTE_NOT_FOUND (now: 200)
12+
GET /api/v1/data/task (control) -> reaches /data, unchanged
13+
```
14+
15+
The dispatcher-plugin's own scoped mounts were never affected: they pass a pre-stripped subpath (`${prefix}/environments/:environmentId/automation` dispatches the literal `/automation`), which is why the standalone server showed nothing.
16+
17+
The OAuth 2.1 gate moved with it. An access token is honoured only on the MCP surface, and that test runs against the still-scoped path — so `/api/v1/environments/<id>/mcp` would have reached the MCP domain with its token refused had the strip been repaired alone.
18+
19+
**If you still emit the old spelling**: replace `/api/v1/projects/:projectId/...` with `/api/v1/environments/:environmentId/...`, as `content/docs/api/environment-routing.mdx` has instructed since ADR-0006 D2. That prefix is no longer stripped, and it was never a working alias in the first place: nothing parses `/projects/<id>`, so stripping it discarded the only place the request named an environment and served it from the host default instead. ADR-0006 D2 retired `project` on the API surface with no aliases, so the repair is one spelling in all three readings rather than a two-prefix alternation.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
'@objectstack/plugin-auth': minor
4+
---
5+
6+
`sys_session.revoke_reason` accepts `organization_membership_ended` — "Remove member" now actually signs the person out
7+
8+
Removing a member deleted the `sys_member` row and left the session alive, for up to seven
9+
days. #15409 closed the security half per request (a session whose `activeOrganizationId`
10+
is not backed by a membership resolves with no active organization). This is the courtesy
11+
half an admin was promised, and it is **never the enforcement**: a trigger can be missed,
12+
an evaluation cannot.
13+
14+
- **New `revoke_reason` value, `organization_membership_ended`** — an accept-set widening
15+
on a published system object, hence `minor` on `@objectstack/platform-objects`. Every
16+
reason before it is a timer (`idle_timeout`, `absolute_max`, `concurrent_cap`) or an
17+
interactive revoke (`user_revoked`, `admin`); this is the first authorization-event
18+
cause. There is no Zod enum behind the column — it is free `text` — so the field's own
19+
description is the published vocabulary, and that is where the value is declared. The
20+
string deliberately matches the one the API-key arm of the same ruling family already
21+
mints for this event (`authRefusal.reason` in `resolve-authz-context.ts`), so one grep
22+
finds every place the platform acts on a membership ending.
23+
- **The trigger acts on the ORGANIZATION'S CLAIM, never on the user** (maintainer ruling,
24+
decision batch #49 item 4, option B). A user who still holds another membership is
25+
**re-pointed** to it — never signed out of organizations they legitimately belong to. A
26+
user with no remaining membership has their session revoked through the existing
27+
`revoked_at` / `revoke_reason` mechanism, which expires it in place: better-auth returns
28+
nothing on the next request and the Console's existing 401 → login redirect handles it,
29+
with **no client change**.
30+
- **The seam is an engine hook on `sys_member`**, not a hook on better-auth's
31+
`/organization/remove-member`. A census measured that the endpoint, a direct delete, a
32+
bulk delete, the cascade from a `sys_user` delete and an organization re-point all reach
33+
the hook, while an endpoint hook would have reached one of them. Same precedent as
34+
`last-admin-guard.ts`.
35+
- **New public surface on `@objectstack/plugin-auth`**`MEMBERSHIP_ENDED_REVOKE_REASON`,
36+
`endSessionClaimsForEndedMembership` and `registerMembershipEndedSessionTrigger`, hence
37+
`minor` rather than `patch`.
38+
39+
Known open by measurement, not by omission: a raw driver delete bypasses the trigger
40+
entirely, and cloud's package-uninstall sample-data purge is one (filed as cloud#2003). The
41+
per-request check covers it; the courtesy does not.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
'@objectstack/plugin-auth': minor
4+
---
5+
6+
feat(platform-objects,plugin-auth): `sys_business_unit.timezone` and `sys_organization.timezone` — the organization hierarchy carries the IANA zone a date boundary is computed in (#14238)
7+
8+
<!-- adr-0087: not-required (no-migration-prescription) A NON-BREAKING ADDITION, registered here in writing because ADR-0087's registries have no additive entry kind (their three tables are semantic TODOs, retired keys and retired defs, and `spec-changes.json`'s `added[]` is the release-time export diff of `@objectstack/spec`, which platform-object columns are not on). Two nullable `text` columns are added to two `isSystem` platform objects; no metadata key, export, config field or stored shape is renamed, retired, re-typed or tombstoned, so `objectstack migrate meta` has nothing to rewrite and no consumer has to change anything. The physical columns are provisioned by boot schema-sync, which is additive-only (`initObjects` creates missing columns and never alters existing ones). MIGRATION NOTE, as the ruling requires it stated: existing deployments resolve to UTC until the root default is set — every pre-existing row reads null in both columns, null on `sys_organization.timezone` means UTC, and null on `sys_business_unit.timezone` means inherit (parent chain, then the organization, then UTC), so a deployment computes every date boundary in UTC after upgrading exactly as it did before, until an administrator sets `sys_organization.timezone`. -->
9+
10+
Maintainer ruling 2026-09-02 (director summon #8), quoted verbatim and untranslated: 「同意」 — adopting option A on #14238.
11+
12+
**The gap.** No platform object carried a timezone, so every application that has to answer "when does this day / week / period end?" invented a column of its own — on its tenant object, its team object or its user — and two apps in one deployment would disagree about when Tuesday ended, with nothing to report. A date boundary decides *which record exists*, not how one is shown: a monthly duty "due on the 5th" expires at midnight, and in UTC+8 that midnight is 08:00 UTC.
13+
14+
**What lands.**
15+
16+
- `sys_business_unit.timezone``text`, optional, `maxLength: 64`, `valueDomain: 'iana_time_zone'`, no default, in the Hierarchy group. Null means **inherit**: the nearest ancestor up the `parent_business_unit_id` chain that carries a value, then `sys_organization.timezone`, then `UTC`.
17+
- `sys_organization.timezone` — the same shape, in the Configuration group: the **root default** of that chain. Null means `UTC`.
18+
- plugin-auth registers `sys_organization.timezone` as an ADR-0105 D7 extension field (the collision guard proves better-auth's organization schema owns no `timezone` at the pinned version) and as generically editable under the ADR-0092 D2 identity write guard — the same tier as `require_mfa` and the group-structure fields. A root default the guard stripped on every administrator write would be a column nobody can set. `sys_business_unit` is `managedBy: 'platform'` and needs no entry.
19+
20+
**The inheritance is a documented contract, not a mechanism.** Measured on the tree: nothing on the platform walks `parent_business_unit_id` *upward* to resolve an attribute. The three existing walkers (plugin-sharing's business-unit graph, plugin-approvals' recursive department approver, plugin-security's delegated-admin frontier) all descend to a unit's *descendants* and read no column beyond the parent link, `active` and `organization_id`. **No resolver API ships with this change** — the ruling holds option B ("the effective zone for this record") for a second consumer — so an application resolving a boundary reads the columns and walks the chain itself, in the order above. Nothing on the platform reads either column yet; both docblocks say so, so the next author does not read inheritance onto a field that stores what was written.
21+
22+
**Validated on write.** Both columns declare `valueDomain: 'iana_time_zone'` — the ruling's own precondition (「rather than shipping an unvalidated text column」), met now that the record validator reads the key (#14168 / #15161). A non-member written to either column (`Mars/Olympus`, `Europe/Munich`, `UTC+8`) is refused with the ADR-0114 field code `value_domain` and `constraint.valueDomain`; membership is the shared `Intl.DateTimeFormat` probe, never the `Intl.supportedValuesOf('timeZone')` enumeration, which omits `UTC` — the very fallback this contract names. `UTC` is admitted, and pinned.
23+
24+
**One shape, on purpose.** The platform's own two earlier IANA columns disagree with each other — `sys_job.timezone` (`maxLength: 100`, no default) and `sys_report_schedule.timezone` (`maxLength: 64`, default `UTC`), neither validated. The ruled pair takes 64 (the smaller precedent, and twice the domain's real ceiling: the enumeration's longest name on the repo's Node baseline is 30 characters, the longest tzdb link 32) and no schema default on either column (a default on the unit would mean "stop inheriting"; one on the organization would give UTC two spellings). Those two precedent columns are not retrofitted here — outside the ruling's scope, carded separately.
25+
26+
**Not the home.** `sys_user` (option C): two people in different zones owning work in the same period would compute different boundaries for what the business considers one period. A per-user zone is a display preference on top of an org-resolved boundary, not a substitute for it. This change is distinct from the settings door's `localization.timezone` (the deployment-wide default analytics buckets dates in today); how the two relate is the future resolver's question.

0 commit comments

Comments
 (0)