Skip to content

fix(plugin-auth): the auth email locale, mail transport, brand and SMS bindings are gated on registerRoutes, so a routes-less embedding gets none of them #14724

Description

@hotlong

Symptom

AuthPlugin binds four things to the live kernel on kernel:ready:

line (packages/plugins/plugin-auth/src/auth-plugin.ts) binding
739 authManager.setEmailService(ctx.getService('email')) — the outbound mail transport
834 authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung
862 authManager.setAppName(...)branding.workspace_name
889 authManager.setDefaultSmsLocale(...) — the #2815 SMS locale

All four sit inside one hook, and that hook is gated on a flag about HTTP routing:

// auth-plugin.ts:727
if (this.options.registerRoutes) {
  ctx.hook('kernel:ready', async () => {   // 728
    ...                                    // 734..978 — all four bindings
  });                                      // 979
}                                          // 980

registerRoutes answers "should this plugin mount its own /api/v1/auth/* routes on the kernel's http-server". It is a transport-mounting question. Wiring the mail service, the deployment email locale, the brand name and the SMS locale are service-composition questions, and they are true of an embedding regardless of who serves the routes.

So an embedding that serves auth routes itself — the whole point of registerRoutes: false — silently gets none of them. The auth manager comes up with no mail transport, no locale on either channel, and no brand binding, and nothing logs a word about it: the four ctx.logger.info lines that would say so are inside the same skipped block.

Who this is

objectstack-ai/cloud is the live one. Every tenant environment kernel constructs AuthPlugin with registerRoutes: false (packages/objectos-runtime/src/artifact-kernel-factory.ts:728) because the per-env kernel has no http-server — the host worker's AuthProxyPlugin forwards /api/v1/auth/* to the env's better-auth handler. That is the intended, documented arrangement, and cloud has already had to re-mount one other casualty of the same gate by hand (the OAuth/OIDC discovery documents, cloud#1339).

The result on the hosted product: the #14591 ruling — localization.locale outranks build-time i18n.defaultLocale for auth mail — cannot reach a tenant environment at all, because the binding that carries it never runs there. That reconciliation is objectstack-ai/cloud#1857, and it cannot be satisfied on the cloud side without either duplicating this resolution logic (a second authority, which #14591 exists to prevent) or reaching into plugin internals.

Measured, not read

Against this repo at 655b106c (the SHA cloud currently pins), same harness shape as auth-plugin.test.ts's existing #14319 block, with an EXPLICIT localization.locale = zh-CN settings double and an email service present, varying only registerRoutes:

registerRoutes=TRUE  -> {"settingsRead":[["localization","locale",{}]],
                         "emailLocaleCalls":[["en"],["zh-CN"]],
                         "smsLocaleCalls":[["zh-CN"]],
                         "emailServiceCalls":[[{}]],"appNameCalls":[[null]]}
registerRoutes=FALSE -> {"settingsRead":[],"emailLocaleCalls":[],"smsLocaleCalls":[],
                         "emailServiceCalls":[],"appNameCalls":[]}

With registerRoutes: false the settings namespace is not even read. The same structure holds in the shipped artifact, not only in source: brace-matching dist/index.js puts all four call sites inside the registerRoutes block (offsets 473307 / 475311 / 476100 / 476982 within block 472971..479750).

Confirmed again on a real boot of the cloud rig at that pin — one process pair, two kernels:

cloud.log     (control plane, registerRoutes defaults true)
  INFO Auth: email service wired (transactional mail enabled)
  INFO Auth: bound auth email locale to i18n default=en
  INFO Auth: bound appName to settings namespace=branding

objectos.log  (tenant env kernel, registerRoutes:false) — zero matches
  ...though the same log shows the kernel DID build with auth on:
  INFO Initializing Auth Plugin... / Auth Plugin initialized successfully
  INFO [ArtifactKernelFactory] kernel ready {... "authEnabled":true}

Corroborating third reading: that env answers requireEmailVerification: false at GET /api/v1/auth/config, which is what resolveRequireEmailVerification() returns when it sees no transport — the absent setEmailService showing through a second surface.

Suggested shape

Split the hook: keep route registration under if (this.options.registerRoutes), and move the four service bindings (plus ensureAuthSettingsBound) into their own ctx.hook('kernel:ready', ...) registered unconditionally — the shape the sibling hooks at auth-plugin.ts:993 / :1032 already use, and which their comments already name ("Registered independently of registerRoutes so an embedding that serves no auth routes still gets the diagnosis"). The diagnosis hook got that treatment; the wiring it diagnoses did not.

That is one edit in one file, and it is where the authority already lives — the alternative is every routes-less host re-deriving localization.locale precedence for itself, which is the second authority #14591 removed.

Worth deciding as part of the same change: whether the "no email service registered" branch (auth-plugin.ts:745-758) should keep logging at info, given it is now reachable on hosts that never see it today.

Acceptance

  • A kernel with registerRoutes: false, a settings service answering an explicit localization.locale, and an email service, ends kernel:ready with the transport wired and both locales bound — asserted by extending the #14319 describe block in auth-plugin.test.ts to cover both values of registerRoutes rather than only the default.
  • Route registration itself stays gated: a registerRoutes: false kernel still mounts no auth routes.

Cloud-side reconciliation this unblocks: objectstack-ai/cloud#1857.

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions