Skip to content

Commit 94f328e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14685-claude-md-excerpts
2 parents e1f5d18 + 5ff5f95 commit 94f328e

10 files changed

Lines changed: 703 additions & 282 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(plugin-auth): the mail transport, brand and locale bindings no longer depend on `registerRoutes`
6+
7+
`AuthPlugin` bound five things to the live kernel on `kernel:ready` — the
8+
outbound mail transport (`setEmailService`), the SMS transport
9+
(`setSmsService`), the deployment email locale (`setDefaultEmailLocale`), the
10+
brand name (`setAppName`) and the SMS locale (`setDefaultSmsLocale`) — from
11+
inside the same hook that mounts `/api/v1/auth/*`, and that hook was gated on
12+
`registerRoutes`.
13+
14+
`registerRoutes` answers a transport-mounting question: should this plugin put
15+
its own routes on the kernel's `http-server`. The bindings are service
16+
composition, and they are true of an embedding regardless of who serves the
17+
routes. So an embedding that serves auth routes itself — the whole point of
18+
`registerRoutes: false` — came up with no mail transport, no locale on either
19+
channel and no brand binding. Silently: the `logger.info` lines that report the
20+
wiring were inside the same skipped block, and the `localization` settings
21+
namespace was not even read. One visible consequence was that the workspace
22+
language could not reach auth mail on such a host at all, and
23+
`/api/v1/auth/config` answered `requireEmailVerification: false` because
24+
`resolveRequireEmailVerification()` saw no transport.
25+
26+
The composition block now registers as its own unconditional
27+
`ctx.hook('kernel:ready', …)` — the shape the sibling diagnosis and dev-seed
28+
hooks in this plugin already use — placed before the route hook so a routing
29+
host keeps the ordering the single combined hook gave it.
30+
31+
Route registration itself stays gated: a `registerRoutes: false` kernel still
32+
mounts no auth routes.
33+
34+
**Behaviour change for `registerRoutes: false` embeddings.** They now resolve
35+
the `email`, `sms`, `i18n` and `settings` services at `kernel:ready`, apply
36+
`branding.workspace_name` and `localization.locale` (subscribing to both), seed
37+
the built-in auth SMS templates when phone sign-in is enabled, and emit the
38+
four wiring `info` lines. Hosts that had compensated by wiring these by hand
39+
should expect the plugin's own binding to run as well; both paths are
40+
idempotent setters, and an explicit workspace setting keeps outranking a
41+
manifest default exactly as it does on a routing host. Nothing changes for a
42+
host that leaves `registerRoutes` at its default.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(automation): a `try_catch` whose `catch` region itself fails now keeps the step record of both regions
6+
7+
`try_catch` returns a failure from three sites. #13803 taught the engine to fold
8+
a dying container's carried steps off the THROW channel, #14184 taught its
9+
returned-failure branch (`if (!result.success)`) to do the same, and #14184 also
10+
taught the first producer — a `try_catch` with no `catch` region — to supply
11+
them. The second producer was left unfolded: when a `catch` region is present
12+
and the handler itself fails, the return dropped `childSteps` entirely.
13+
14+
That is the same defect one path over, and the worst of the three for an
15+
operator, because TWO regions ran. The try region may have written rows before
16+
it failed; the handler may have written more before IT failed; the run log kept
17+
a step for neither, so the run summary folded over that log reported `acted: 0`
18+
over writes that had genuinely landed. `acted: 0` on a failed run reads as
19+
"nothing happened, safe to re-run", which for a non-idempotent region invites
20+
double-execution.
21+
22+
Closing it needed the half that was genuinely missing rather than the available
23+
one: the failed try attempts were already in scope, but the catch region ran
24+
without a `partialSteps` sink, so when the handler threw, the handler's own
25+
completed steps unwound with the stack. The catch region now receives the same
26+
sink the try region already had (`runRegion`'s fifth argument), and the failing
27+
return carries `[...failedTryAttempts, ...catchAttempts]` — failed try attempts
28+
first, because they happened first, which is the ordering the successful-catch
29+
return has always used. `runRegion`'s existing tagger supplies `regionKind:
30+
'try'` / `'catch'` and `parentNodeId` on its failure path as well as its
31+
success path, so the two halves stay distinguishable in the log.
32+
33+
Additive to the RECORD only. This return already reported failure with the same
34+
error text, already produced a `NODE_FAILURE` step, already set `$error` and was
35+
already routable by a `fault` edge; none of that moves, and neither does the
36+
successful-catch path or the retry/throw semantics. No engine change was needed
37+
— the fold that reads these steps has been in place since #14184.

content/docs/permissions/system-context.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ that silently does not happen.
9797
| 8 | `explain()` may target a principal other than the caller | plugin-security | Get: no `manage_users` / delegated-admin check | `security-plugin.ts:3857` |
9898
| 9 | Anonymous-deny treats the caller as authenticated | core | Get: passes the 401 seam with no `userId` | `anonymous-deny.ts:154` |
9999
| 10 | Permission-set projection middleware skipped | plugin-security | Lose: projection of permission-set-derived columns | `permission-set-projection.ts:1015` |
100-
| 11 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `auth-plugin.ts:1353` |
100+
| 11 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `auth-plugin.ts:1380` |
101101
| 12 | Per-request performance timings disclosed | observability | Get: timing headers a normal caller cannot pull | `perf-timing.ts:474` |
102102
| 13 | Permission-set **overlay discard** skips the tenant-admin assertion | plugin-security | Get: an overlay can be discarded with no authenticated tenant administrator | `permission-set-overlay-discard.ts:142` |
103103
| 14 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `stdio-data-bridge.ts:246` |
@@ -198,7 +198,7 @@ assuming `isSystem` covers it is a documented source of bugs.
198198
| "It skips validation rules" | **No.** Field shape, `format`, `script` and the rest still run. The `readonly` strip runs *before* validation precisely so a discarded value is not judged | `objectql/src/engine.ts:10007``10024` |
199199
| "It preserves a supplied `updated_at` / `updated_by`" | **No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only | `field.zod.ts:1516` (#3493 / #6640) |
200200
| "It stamps `created_by`" | **No.** Audit stamping reads `userId` from the context. A user-less system write stamps nothing — that is today's behaviour, not an error | `runtime-identity.ts:280``281` |
201-
| "It bypasses every guard" | **No.** The last-admin guard applies to **every** context, `isSystem` included — the deprovision path that actually locks an org out is the system one | `last-admin-guard.ts:286` |
201+
| "It bypasses every guard" | **No.** The last-admin guard applies to **every** context, `isSystem` included — the deprovision path that actually locks an org out is the system one | `last-admin-guard.ts:299` |
202202
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1522`, `:1551`; `domains/actions.ts:404` |
203203

204204
---

packages/plugins/plugin-auth/src/auth-manager.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5135,7 +5135,7 @@ export class AuthManager {
51355135
* generated docs and the #13816 refusal all asserted the ban.
51365136
*
51375137
* This method restores declared = enforced by routing the state to the
5138-
* platform's OWN ban write (`admin-ban-endpoints.ts`):
5138+
* platform's OWN ban write (`user-ban-write.ts`):
51395139
*
51405140
* - `active: false` on a row that is not banned ⇒ `applyUserBan` with
51415141
* `SCIM_DEACTIVATION_BAN_REASON` and no expiry. The vendor's
@@ -5164,8 +5164,14 @@ export class AuthManager {
51645164
* A consequence worth stating: on 1.7.2 a SCIM `DELETE /Users/{id}` no
51655165
* longer deletes the better-auth user (the vendor tombstones the source);
51665166
* it leaves the user with no active source, so this callback disables the
5167-
* account. Re-provisioning through the tombstone re-links the same user,
5168-
* the state turns active, and the SCIM ban is lifted by the second bullet.
5167+
* account — by the SAME branch as `active: false`, including over an
5168+
* administrator's timed ban, whose `banExpires` a DELETE therefore clears
5169+
* too: a deprovision cannot be outlived by an expiry the administrator set.
5170+
* For the same reason a DELETE is judged by the `beforeUpdate` guard below
5171+
* and never by any `beforeDelete` — deleting the last administrator through
5172+
* SCIM is refused exactly as deactivating them is. Re-provisioning through
5173+
* the tombstone re-links the same user, the state turns active, and the
5174+
* SCIM ban is lifted by the second bullet.
51695175
*
51705176
* The break-glass last-administrator guard (ADR-0024 D5.2, #5892) is an
51715177
* ENGINE `beforeUpdate` hook on `sys_user`, so it judges this write exactly

0 commit comments

Comments
 (0)