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
739authManager.setEmailService(ctx.getService('email')) — the outbound mail transport
834authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung
862authManager.setAppName(...)branding.workspace_name
889authManager.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:727if(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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    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
    739authManager.setEmailService(ctx.getService('email')) — the outbound mail transport
    834authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung
    862authManager.setAppName(...)branding.workspace_name
    889authManager.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:727if(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

    Metadata

    Metadata

    Assignees

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      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
      739authManager.setEmailService(ctx.getService('email')) — the outbound mail transport
      834authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung
      862authManager.setAppName(...)branding.workspace_name
      889authManager.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:727if(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

      Metadata

      Metadata

      Assignees

      Labels

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        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
        739authManager.setEmailService(ctx.getService('email')) — the outbound mail transport
        834authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung
        862authManager.setAppName(...)branding.workspace_name
        889authManager.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:727if(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

        Metadata

        Metadata

        Assignees

        Labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          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
          739authManager.setEmailService(ctx.getService('email')) — the outbound mail transport
          834authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung
          862authManager.setAppName(...)branding.workspace_name
          889authManager.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:727if(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

          Metadata

          Metadata

          Assignees

          Labels

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            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
            739authManager.setEmailService(ctx.getService('email')) — the outbound mail transport
            834authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung
            862authManager.setAppName(...)branding.workspace_name
            889authManager.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:727if(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

            Metadata

            Metadata

            Assignees

            Labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              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
              739authManager.setEmailService(ctx.getService('email')) — the outbound mail transport
              834authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung
              862authManager.setAppName(...)branding.workspace_name
              889authManager.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:727if(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

              Metadata

              Metadata

              Assignees

              Labels

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                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
                739authManager.setEmailService(ctx.getService('email')) — the outbound mail transport
                834authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung
                862authManager.setAppName(...)branding.workspace_name
                889authManager.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:727if(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

                Metadata

                Metadata

                Assignees

                Labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions