Skip to content

AuthPlugin reads the auth settings namespace in the pre-bind window — stored sys_setting rows are ignored at boot under the shipped os serve order #11579

Description

@os-steve

Found by the step-1 measurement of #11045 (the settings bind-window gate). Filed per-plugin rather than fixed there, per that card's ruling: #11045 is the gate, not a fourth declaration.

What happens

AuthPlugin reaches getService('settings') from kernel:ready handlers it registers in start(), and it uses the handle in the same tick:

  • packages/plugins/plugin-auth/src/auth-plugin.ts:1207bindAuthSettings() resolves the handle and then await applySettings(), which calls settings.getNamespace('auth').
  • packages/plugins/plugin-auth/src/auth-plugin.ts:728 — the second acquisition in the same plugin.

It is reached transitively, at depth 3 from one of the registrations:

ctx.hook('kernel:ready', () => runBackfill('kernel:ready')) // auth-plugin.ts:1029
→ runBackfill (a const inside start())
→ this.ensureAuthSettingsBound(ctx) // :1188
→ this.bindAuthSettings(ctx) // :1202
→ ctx.getService<SettingsReadSurface>('settings') // :1207
→ await settings.getNamespace('auth')

and at depth 2 from auth-plugin.ts:674.

SettingsServicePlugin binds its data engine from its ownstart()-registered kernel:ready hook (settings-service-plugin.ts:153). Handlers run in registration order, and registration order is start() order. AuthPlugin declares dependencies = ['com.objectstack.engine.objectql'] and nothing about settings, so nothing orders it after the settings plugin.

Why it fires on the shipped composition

packages/cli/src/commands/serve.ts does kernel.use(new AuthPlugin(...)) at line 2693; SettingsServicePlugin arrives later, from the capability loop at line ~3417. resolvePluginOrder preserves insertion order for plugins with no edge between them, so auth starts first.

Measured with the real resolvePluginOrder from packages/core/src/plugin-order.ts, fed the verbatim declarations of the five plugins involved in the serve insertion order:

start() order: ["com.objectstack.engine.objectql","com.objectstack.auth","com.objectstack.audit","com.objectstack.service.settings","com.objectstack.mcp"]
auth before settings? true

So auth's kernel:ready handlers are registered — and therefore fire — before the engine bind.

Consequence

In that window SettingsService.loadRows takes its this.memory branch, so getNamespace('auth') answers the manifest defaults with source: 'default' while the deployment's persisted sys_setting rows go unread (this is the exact class #10250 documented and made audible via reportPreBindRead). Everything applySettings() derives is therefore computed from defaults at boot: the ADR-0093 membership policy the D6 backfill runs under, and the google_* social-provider config. settings.subscribe('auth', …) only re-applies on a later change, so a workspace that configured auth in Setup and never touched it again keeps booting with the wrong values.

Note the D6 backfill's own comment at auth-plugin.ts:1175 already reasons about this ordering hazard between auth's two hooks — the hazard against the settings plugin is the one still open.

Repair

The #10250 shape — one declaration on AuthPlugin:

optionalDependencies=['com.objectstack.service.settings'];

requiresServices would not do it: that asserts the service is registered before init() (it always is), and carries no start() ordering.

Verification: after the fix, pnpm check:settings-bind-window must go green with the com.objectstack.auth entry deleted from KNOWN_PRE_BIND_READS in scripts/check-settings-bind-window.mjs — that ledger is shrink-only and errors on a stale entry, so the deletion is part of the fix, not follow-up.

Suggested lane: whoever owns packages/plugins/plugin-auth.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions