fix(plugin-auth): register the auth service-composition bindings independently of registerRoutes - #14810

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes
Sep 3, 2026
Merged

fix(plugin-auth): register the auth service-composition bindings independently of registerRoutes#14810
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14724

AuthPlugin registered its service-composition bindings inside the same kernel:ready hook that mounts /api/v1/auth/*, and that hook was gated on registerRoutes. registerRoutes answers a transport-mounting question; the bindings are service composition and are true of an embedding regardless of who serves the routes. Every routes-less embedding — every cloud tenant environment kernel is one — therefore came up with no mail transport, no locale on either channel and no brand binding, silently, without even reading the localization settings namespace.

The composition block is now its own unconditional ctx.hook('kernel:ready', ...), registered before the route hook. Route registration itself stays gated.

Comments read: 2. Ruling of record: issuecomment-5518030363 (triage, 2026-09-02 23:39:05Z). Its three binding points are all satisfied below: route registration stays gated (pinned by a test, both directions), the #14319 describe block now runs both values of the flag, and the log-level question is not decided here.

Reading of the 421-line churn

The gross diff on auth-plugin.ts is +224/−197. Ignoring whitespace it is +33/−6:

git diff --stat origin/main...HEAD -- .../auth-plugin.ts -> 421 changed (224 ins, 197 del)
git diff -w --stat origin/main...HEAD -- .../auth-plugin.ts -> 39 changed ( 33 ins, 6 del)

So ~93% of the churn is re-indentation forced by de-nesting a ~205-line block out of one if. No statement inside the moved block changed — the -w diff contains only the new explanatory comment, the moved if (this.options.registerRoutes) {, one added });, and one added ctx.hook( line. This is the minimum shape the fix can take in a brace-and-indent language; it is not gratuitous reformatting, and it was not widened further here.

Two structural points verified rather than assumed:

  • Ordering is preserved for routing hosts.dispatchHookPropagating (packages/core/src/hook-dispatch.ts:146) awaits handlers in registration order and does not catch. Composition is registered first, so it still completes before routes, and a throw in composition still aborts before any route is mounted — exactly what the single combined hook gave for free.
  • The sibling diagnosis hook is unaffected. It reads both the kernel email service and the manager's own view specifically so its answer is "independent of hook registration order" (its own comment), so inserting composition ahead of it changes nothing it reports.

One thing worth naming explicitly: the issue lists four bindings; the moved block actually carries fivesetSmsService (#2780) sits between setEmailService and the locale rungs. It moves because it is inside the block, not as added scope; the changeset says five.

Ablation

Subject resolves through a relative source import (import { AuthPlugin } from './auth-plugin'), so the test exercises src/ directly — no dist/ leg, and no build-and-preflight pair applies. Mutation: line 747 re-gated to the pre-fix semantics, addressed by line number because the anchor text occurs 8 times in the file.

Mutation confirmed on disk before measuring — blob hash moved and both unique-text counts flipped:

HEAD blob = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe
pre hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe (equal -> tree was at HEAD)
post hash = deb7da1cd90046c29bda989ae51b9e98d888ee05 (moved -> mutation reached disk)
pre counts : anchor-form=8 gated-form=0
post counts : anchor-form=7 gated-form=1
line 747 now : if (this.options.registerRoutes) ctx.hook('kernel:ready', async () => {

Results:

ABLATED Tests 9 failed | 79 passed (88) vitest exit=1
RESTORED Tests 88 passed (88) vitest exit=0

All 9 failures are in the registerRoutes: false branch and nowhere else — the exact direction predicted:

FAIL ... > registerRoutes: false (routes-less embedding) > a zh-CN workspace binds zh-CN on the EMAIL channel, not just on SMS
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a ja-JP workspace
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a es-ES workspace
FAIL ... > registerRoutes: false (routes-less embedding) > a workspace that never chose a language keeps the app build-time default
FAIL ... > registerRoutes: false (routes-less embedding) > names NO locale when neither producer speaks — the documented en-US fallback
FAIL ... > registerRoutes: false (routes-less embedding) > binds the build-time default when there is no settings service at all
FAIL ... > registerRoutes: false (routes-less embedding) > re-binds live when the workspace switches language
FAIL ... > registerRoutes: false (routes-less embedding) > leaves the build-time default standing when the settings read fails
FAIL ... > registerRoutes: false (routes-less embedding) > ends kernel:ready with the transport wired and BOTH locales bound

The registerRoutes: true branch stayed fully green under the mutation, and the route-gating test stayed green in both branches — so the ablation isolates composition from routing rather than knocking the suite over.

Restore proved by blob equality plus an empty diff, not by an exit code:

restored hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe == HEAD blob
restore counts: anchor-form=8 gated-form=0
git diff HEAD -- .../auth-plugin.ts : EMPTY
git diff HEAD (whole tree) : EMPTY

The mutation script carried a trap ... EXIT INT TERM restore on an absolute path derived from git rev-parse --show-toplevel.

The census row

content/docs/permissions/system-context.mdx row 11 moved auth-plugin.ts:1353 to :1380 (the file grew 27 lines above that site). Confirmed tool-produced, not hand-edited, by reconstruction: restoring origin/main's copy of the page and running the gate's own repair arm reproduces the branch file byte-for-byte.

node scripts/check-system-context-census.mjs --fix
re-anchored content/docs/permissions/system-context.mdx:100 `auth-plugin.ts:1353` -> `auth-plugin.ts:1380`
check-system-context-census --fix: 1 anchor(s) rewritten
after --fix = 26928a4a0a19364698a75552b085e2745d09b42e
branch blob = 26928a4a0a19364698a75552b085e2745d09b42e (identical)

Gate green as-is on the branch:

check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files,
all anchored; 145 anchors resolve, 27 declared non-read.

Tests and gates

Run at 358319608 (the pushed head), after merging origin/maine6ac0c6fd.

pnpm --filter '@objectstack/plugin-auth^...' build -> exit 0
pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-plugin.test.ts
Test Files 1 passed (1)
Tests 88 passed (88)
pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0
check:test-typecheck: OK — test layer compiles under tsconfig.test.json

Typecheck coverage of the edited files was measured, not assumed — tsc --listFiles returns 1 hit for auth-plugin.ts in the main project and 1 hit for auth-plugin.test.ts in tsconfig.test.json, so neither edit is outside its program.

Gate family re-derived on the merged tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (62 commands, derived from the merge-base change set, not a hand-written path list). 57 green. Five are NOT MEASURED locally — each by its own printed verdict, none a violation:

gatecodeverdict text
check-test-completeness.mjs3PREREQUISITE NOT MET — grades a saved turbo run test log; the script itself says to "record this gate as NOT MEASURED"
pm/check-half-states.mjs124timed out reaching the GitHub API through the proxy; not a tree gate
spec check:skill-examples1self-test passed, then packages/client-react/dist holds no .d.ts declarations — the package is not built
check:dual-build-cjs-loads3Run pnpm build first. This is NOT a pass: nothing was measured.
check:type-check-debt3prerequisite; says nothing about whether any debt number is still correct

Topically closest gate, pnpm check:auth-mount-ledger, is green. Exit codes were captured before any pipe (redirect, then read).

Log-level fence — named, not decided

The issue asks whether the "no email service registered" branch should stay at info now that it becomes reachable on hosts that never saw it. Per the ruling, that is not decided here. Verified untouched: the branch is byte-identical to origin/main modulo the two-space de-indent, and still reads ctx.logger.info('Auth: no email service registered — transactional mail disabled'), with the requireEmailVerification sibling still at error.

Clause 2 — no

No new exported symbol, no new payload key, no option added (registerRoutes already existed), and nothing changed about what the contract accepts or rejects — the -w diff shows zero statement changes, only the hook's registration site. The delta is runtime behaviour for registerRoutes: false embeddings, which is the bug being fixed rather than a contract surface move; the changeset states it explicitly for release notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…pendently of registerRoutes
`AuthPlugin` bound the outbound mail transport, the SMS transport, the
deployment email locale, the brand name and the SMS locale inside the same
`kernel:ready` hook that mounts `/api/v1/auth/*`, and that hook was gated on
`registerRoutes`.
`registerRoutes` answers a transport-mounting question. The bindings are
service composition and are true of an embedding regardless of who serves the
routes, so every routes-less embedding came up with no mail transport, no
locale on either channel and no brand binding — silently, because the
`logger.info` lines that would have reported the wiring sat inside the same
skipped block.
Split the hook: the composition block moves verbatim into its own
unconditional `ctx.hook('kernel:ready', …)`, registered before the route hook
so the ordering a routing host had is preserved. Route registration itself
stays under `if (this.options.registerRoutes)`.
This is the shape the sibling hooks in this file already use and already name
("Registered independently of `registerRoutes` so an embedding that serves no
auth routes still gets the diagnosis") — the file applied the distinction to
the diagnosis hook and not to the wiring the diagnosis exists to report on.
The `#14319` describe block now runs against both values of `registerRoutes`
rather than only the default, and pins both acceptance criteria: the
composition completes either way, and a `registerRoutes: false` kernel still
mounts no auth routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…s row
The census row for `plugin-auth`'s session-resolution middleware anchors a
LINE NUMBER in `auth-plugin.ts`; the hook split moved that read site from
:1353 to :1380 without changing a character of it. Re-anchored with the gate's
own `--fix`, which is the repair it prescribes for pure line rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/deployment/self-hosting.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/permissions/authentication.mdx(via /api/v1/auth/* (route, a path literal in start))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197ddapackageMentionDocs.

Which tree this was computed on

This run read content/docs from dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 — the merge of head 3583196089c29cdad2433f26fd662c5ed70b91ed into base 5a5336b399db2ef18dd4700f97d579a328197dda, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 && git checkout dc0bd0eedb73aa3a16a76d29a8e590e919df35c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5336b399db2ef18dd4700f97d579a328197dda 3583196089c29cdad2433f26fd662c5ed70b91ed && git checkout -B drift-repro 5a5336b399db2ef18dd4700f97d579a328197dda && git merge --no-ff 3583196089c29cdad2433f26fd662c5ed70b91ed
node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197dda

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5a5336b399db2ef18dd4700f97d579a328197dda → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716400023 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 63 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716742053 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 65 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, '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): register the auth service-composition bindings independently of registerRoutes - #14810

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes
Sep 3, 2026
Merged

fix(plugin-auth): register the auth service-composition bindings independently of registerRoutes#14810
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14724

AuthPlugin registered its service-composition bindings inside the same kernel:ready hook that mounts /api/v1/auth/*, and that hook was gated on registerRoutes. registerRoutes answers a transport-mounting question; the bindings are service composition and are true of an embedding regardless of who serves the routes. Every routes-less embedding — every cloud tenant environment kernel is one — therefore came up with no mail transport, no locale on either channel and no brand binding, silently, without even reading the localization settings namespace.

The composition block is now its own unconditional ctx.hook('kernel:ready', ...), registered before the route hook. Route registration itself stays gated.

Comments read: 2. Ruling of record: issuecomment-5518030363 (triage, 2026-09-02 23:39:05Z). Its three binding points are all satisfied below: route registration stays gated (pinned by a test, both directions), the #14319 describe block now runs both values of the flag, and the log-level question is not decided here.

Reading of the 421-line churn

The gross diff on auth-plugin.ts is +224/−197. Ignoring whitespace it is +33/−6:

git diff --stat origin/main...HEAD -- .../auth-plugin.ts -> 421 changed (224 ins, 197 del)
git diff -w --stat origin/main...HEAD -- .../auth-plugin.ts -> 39 changed ( 33 ins, 6 del)

So ~93% of the churn is re-indentation forced by de-nesting a ~205-line block out of one if. No statement inside the moved block changed — the -w diff contains only the new explanatory comment, the moved if (this.options.registerRoutes) {, one added });, and one added ctx.hook( line. This is the minimum shape the fix can take in a brace-and-indent language; it is not gratuitous reformatting, and it was not widened further here.

Two structural points verified rather than assumed:

  • Ordering is preserved for routing hosts.dispatchHookPropagating (packages/core/src/hook-dispatch.ts:146) awaits handlers in registration order and does not catch. Composition is registered first, so it still completes before routes, and a throw in composition still aborts before any route is mounted — exactly what the single combined hook gave for free.
  • The sibling diagnosis hook is unaffected. It reads both the kernel email service and the manager's own view specifically so its answer is "independent of hook registration order" (its own comment), so inserting composition ahead of it changes nothing it reports.

One thing worth naming explicitly: the issue lists four bindings; the moved block actually carries fivesetSmsService (#2780) sits between setEmailService and the locale rungs. It moves because it is inside the block, not as added scope; the changeset says five.

Ablation

Subject resolves through a relative source import (import { AuthPlugin } from './auth-plugin'), so the test exercises src/ directly — no dist/ leg, and no build-and-preflight pair applies. Mutation: line 747 re-gated to the pre-fix semantics, addressed by line number because the anchor text occurs 8 times in the file.

Mutation confirmed on disk before measuring — blob hash moved and both unique-text counts flipped:

HEAD blob = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe
pre hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe (equal -> tree was at HEAD)
post hash = deb7da1cd90046c29bda989ae51b9e98d888ee05 (moved -> mutation reached disk)
pre counts : anchor-form=8 gated-form=0
post counts : anchor-form=7 gated-form=1
line 747 now : if (this.options.registerRoutes) ctx.hook('kernel:ready', async () => {

Results:

ABLATED Tests 9 failed | 79 passed (88) vitest exit=1
RESTORED Tests 88 passed (88) vitest exit=0

All 9 failures are in the registerRoutes: false branch and nowhere else — the exact direction predicted:

FAIL ... > registerRoutes: false (routes-less embedding) > a zh-CN workspace binds zh-CN on the EMAIL channel, not just on SMS
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a ja-JP workspace
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a es-ES workspace
FAIL ... > registerRoutes: false (routes-less embedding) > a workspace that never chose a language keeps the app build-time default
FAIL ... > registerRoutes: false (routes-less embedding) > names NO locale when neither producer speaks — the documented en-US fallback
FAIL ... > registerRoutes: false (routes-less embedding) > binds the build-time default when there is no settings service at all
FAIL ... > registerRoutes: false (routes-less embedding) > re-binds live when the workspace switches language
FAIL ... > registerRoutes: false (routes-less embedding) > leaves the build-time default standing when the settings read fails
FAIL ... > registerRoutes: false (routes-less embedding) > ends kernel:ready with the transport wired and BOTH locales bound

The registerRoutes: true branch stayed fully green under the mutation, and the route-gating test stayed green in both branches — so the ablation isolates composition from routing rather than knocking the suite over.

Restore proved by blob equality plus an empty diff, not by an exit code:

restored hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe == HEAD blob
restore counts: anchor-form=8 gated-form=0
git diff HEAD -- .../auth-plugin.ts : EMPTY
git diff HEAD (whole tree) : EMPTY

The mutation script carried a trap ... EXIT INT TERM restore on an absolute path derived from git rev-parse --show-toplevel.

The census row

content/docs/permissions/system-context.mdx row 11 moved auth-plugin.ts:1353 to :1380 (the file grew 27 lines above that site). Confirmed tool-produced, not hand-edited, by reconstruction: restoring origin/main's copy of the page and running the gate's own repair arm reproduces the branch file byte-for-byte.

node scripts/check-system-context-census.mjs --fix
re-anchored content/docs/permissions/system-context.mdx:100 `auth-plugin.ts:1353` -> `auth-plugin.ts:1380`
check-system-context-census --fix: 1 anchor(s) rewritten
after --fix = 26928a4a0a19364698a75552b085e2745d09b42e
branch blob = 26928a4a0a19364698a75552b085e2745d09b42e (identical)

Gate green as-is on the branch:

check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files,
all anchored; 145 anchors resolve, 27 declared non-read.

Tests and gates

Run at 358319608 (the pushed head), after merging origin/maine6ac0c6fd.

pnpm --filter '@objectstack/plugin-auth^...' build -> exit 0
pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-plugin.test.ts
Test Files 1 passed (1)
Tests 88 passed (88)
pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0
check:test-typecheck: OK — test layer compiles under tsconfig.test.json

Typecheck coverage of the edited files was measured, not assumed — tsc --listFiles returns 1 hit for auth-plugin.ts in the main project and 1 hit for auth-plugin.test.ts in tsconfig.test.json, so neither edit is outside its program.

Gate family re-derived on the merged tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (62 commands, derived from the merge-base change set, not a hand-written path list). 57 green. Five are NOT MEASURED locally — each by its own printed verdict, none a violation:

gatecodeverdict text
check-test-completeness.mjs3PREREQUISITE NOT MET — grades a saved turbo run test log; the script itself says to "record this gate as NOT MEASURED"
pm/check-half-states.mjs124timed out reaching the GitHub API through the proxy; not a tree gate
spec check:skill-examples1self-test passed, then packages/client-react/dist holds no .d.ts declarations — the package is not built
check:dual-build-cjs-loads3Run pnpm build first. This is NOT a pass: nothing was measured.
check:type-check-debt3prerequisite; says nothing about whether any debt number is still correct

Topically closest gate, pnpm check:auth-mount-ledger, is green. Exit codes were captured before any pipe (redirect, then read).

Log-level fence — named, not decided

The issue asks whether the "no email service registered" branch should stay at info now that it becomes reachable on hosts that never saw it. Per the ruling, that is not decided here. Verified untouched: the branch is byte-identical to origin/main modulo the two-space de-indent, and still reads ctx.logger.info('Auth: no email service registered — transactional mail disabled'), with the requireEmailVerification sibling still at error.

Clause 2 — no

No new exported symbol, no new payload key, no option added (registerRoutes already existed), and nothing changed about what the contract accepts or rejects — the -w diff shows zero statement changes, only the hook's registration site. The delta is runtime behaviour for registerRoutes: false embeddings, which is the bug being fixed rather than a contract surface move; the changeset states it explicitly for release notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…pendently of registerRoutes
`AuthPlugin` bound the outbound mail transport, the SMS transport, the
deployment email locale, the brand name and the SMS locale inside the same
`kernel:ready` hook that mounts `/api/v1/auth/*`, and that hook was gated on
`registerRoutes`.
`registerRoutes` answers a transport-mounting question. The bindings are
service composition and are true of an embedding regardless of who serves the
routes, so every routes-less embedding came up with no mail transport, no
locale on either channel and no brand binding — silently, because the
`logger.info` lines that would have reported the wiring sat inside the same
skipped block.
Split the hook: the composition block moves verbatim into its own
unconditional `ctx.hook('kernel:ready', …)`, registered before the route hook
so the ordering a routing host had is preserved. Route registration itself
stays under `if (this.options.registerRoutes)`.
This is the shape the sibling hooks in this file already use and already name
("Registered independently of `registerRoutes` so an embedding that serves no
auth routes still gets the diagnosis") — the file applied the distinction to
the diagnosis hook and not to the wiring the diagnosis exists to report on.
The `#14319` describe block now runs against both values of `registerRoutes`
rather than only the default, and pins both acceptance criteria: the
composition completes either way, and a `registerRoutes: false` kernel still
mounts no auth routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…s row
The census row for `plugin-auth`'s session-resolution middleware anchors a
LINE NUMBER in `auth-plugin.ts`; the hook split moved that read site from
:1353 to :1380 without changing a character of it. Re-anchored with the gate's
own `--fix`, which is the repair it prescribes for pure line rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/deployment/self-hosting.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/permissions/authentication.mdx(via /api/v1/auth/* (route, a path literal in start))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197ddapackageMentionDocs.

Which tree this was computed on

This run read content/docs from dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 — the merge of head 3583196089c29cdad2433f26fd662c5ed70b91ed into base 5a5336b399db2ef18dd4700f97d579a328197dda, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 && git checkout dc0bd0eedb73aa3a16a76d29a8e590e919df35c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5336b399db2ef18dd4700f97d579a328197dda 3583196089c29cdad2433f26fd662c5ed70b91ed && git checkout -B drift-repro 5a5336b399db2ef18dd4700f97d579a328197dda && git merge --no-ff 3583196089c29cdad2433f26fd662c5ed70b91ed
node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197dda

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5a5336b399db2ef18dd4700f97d579a328197dda → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716400023 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 63 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716742053 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 65 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, '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): register the auth service-composition bindings independently of registerRoutes - #14810

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes
Sep 3, 2026
Merged

fix(plugin-auth): register the auth service-composition bindings independently of registerRoutes#14810
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14724

AuthPlugin registered its service-composition bindings inside the same kernel:ready hook that mounts /api/v1/auth/*, and that hook was gated on registerRoutes. registerRoutes answers a transport-mounting question; the bindings are service composition and are true of an embedding regardless of who serves the routes. Every routes-less embedding — every cloud tenant environment kernel is one — therefore came up with no mail transport, no locale on either channel and no brand binding, silently, without even reading the localization settings namespace.

The composition block is now its own unconditional ctx.hook('kernel:ready', ...), registered before the route hook. Route registration itself stays gated.

Comments read: 2. Ruling of record: issuecomment-5518030363 (triage, 2026-09-02 23:39:05Z). Its three binding points are all satisfied below: route registration stays gated (pinned by a test, both directions), the #14319 describe block now runs both values of the flag, and the log-level question is not decided here.

Reading of the 421-line churn

The gross diff on auth-plugin.ts is +224/−197. Ignoring whitespace it is +33/−6:

git diff --stat origin/main...HEAD -- .../auth-plugin.ts -> 421 changed (224 ins, 197 del)
git diff -w --stat origin/main...HEAD -- .../auth-plugin.ts -> 39 changed ( 33 ins, 6 del)

So ~93% of the churn is re-indentation forced by de-nesting a ~205-line block out of one if. No statement inside the moved block changed — the -w diff contains only the new explanatory comment, the moved if (this.options.registerRoutes) {, one added });, and one added ctx.hook( line. This is the minimum shape the fix can take in a brace-and-indent language; it is not gratuitous reformatting, and it was not widened further here.

Two structural points verified rather than assumed:

  • Ordering is preserved for routing hosts.dispatchHookPropagating (packages/core/src/hook-dispatch.ts:146) awaits handlers in registration order and does not catch. Composition is registered first, so it still completes before routes, and a throw in composition still aborts before any route is mounted — exactly what the single combined hook gave for free.
  • The sibling diagnosis hook is unaffected. It reads both the kernel email service and the manager's own view specifically so its answer is "independent of hook registration order" (its own comment), so inserting composition ahead of it changes nothing it reports.

One thing worth naming explicitly: the issue lists four bindings; the moved block actually carries fivesetSmsService (#2780) sits between setEmailService and the locale rungs. It moves because it is inside the block, not as added scope; the changeset says five.

Ablation

Subject resolves through a relative source import (import { AuthPlugin } from './auth-plugin'), so the test exercises src/ directly — no dist/ leg, and no build-and-preflight pair applies. Mutation: line 747 re-gated to the pre-fix semantics, addressed by line number because the anchor text occurs 8 times in the file.

Mutation confirmed on disk before measuring — blob hash moved and both unique-text counts flipped:

HEAD blob = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe
pre hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe (equal -> tree was at HEAD)
post hash = deb7da1cd90046c29bda989ae51b9e98d888ee05 (moved -> mutation reached disk)
pre counts : anchor-form=8 gated-form=0
post counts : anchor-form=7 gated-form=1
line 747 now : if (this.options.registerRoutes) ctx.hook('kernel:ready', async () => {

Results:

ABLATED Tests 9 failed | 79 passed (88) vitest exit=1
RESTORED Tests 88 passed (88) vitest exit=0

All 9 failures are in the registerRoutes: false branch and nowhere else — the exact direction predicted:

FAIL ... > registerRoutes: false (routes-less embedding) > a zh-CN workspace binds zh-CN on the EMAIL channel, not just on SMS
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a ja-JP workspace
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a es-ES workspace
FAIL ... > registerRoutes: false (routes-less embedding) > a workspace that never chose a language keeps the app build-time default
FAIL ... > registerRoutes: false (routes-less embedding) > names NO locale when neither producer speaks — the documented en-US fallback
FAIL ... > registerRoutes: false (routes-less embedding) > binds the build-time default when there is no settings service at all
FAIL ... > registerRoutes: false (routes-less embedding) > re-binds live when the workspace switches language
FAIL ... > registerRoutes: false (routes-less embedding) > leaves the build-time default standing when the settings read fails
FAIL ... > registerRoutes: false (routes-less embedding) > ends kernel:ready with the transport wired and BOTH locales bound

The registerRoutes: true branch stayed fully green under the mutation, and the route-gating test stayed green in both branches — so the ablation isolates composition from routing rather than knocking the suite over.

Restore proved by blob equality plus an empty diff, not by an exit code:

restored hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe == HEAD blob
restore counts: anchor-form=8 gated-form=0
git diff HEAD -- .../auth-plugin.ts : EMPTY
git diff HEAD (whole tree) : EMPTY

The mutation script carried a trap ... EXIT INT TERM restore on an absolute path derived from git rev-parse --show-toplevel.

The census row

content/docs/permissions/system-context.mdx row 11 moved auth-plugin.ts:1353 to :1380 (the file grew 27 lines above that site). Confirmed tool-produced, not hand-edited, by reconstruction: restoring origin/main's copy of the page and running the gate's own repair arm reproduces the branch file byte-for-byte.

node scripts/check-system-context-census.mjs --fix
re-anchored content/docs/permissions/system-context.mdx:100 `auth-plugin.ts:1353` -> `auth-plugin.ts:1380`
check-system-context-census --fix: 1 anchor(s) rewritten
after --fix = 26928a4a0a19364698a75552b085e2745d09b42e
branch blob = 26928a4a0a19364698a75552b085e2745d09b42e (identical)

Gate green as-is on the branch:

check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files,
all anchored; 145 anchors resolve, 27 declared non-read.

Tests and gates

Run at 358319608 (the pushed head), after merging origin/maine6ac0c6fd.

pnpm --filter '@objectstack/plugin-auth^...' build -> exit 0
pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-plugin.test.ts
Test Files 1 passed (1)
Tests 88 passed (88)
pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0
check:test-typecheck: OK — test layer compiles under tsconfig.test.json

Typecheck coverage of the edited files was measured, not assumed — tsc --listFiles returns 1 hit for auth-plugin.ts in the main project and 1 hit for auth-plugin.test.ts in tsconfig.test.json, so neither edit is outside its program.

Gate family re-derived on the merged tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (62 commands, derived from the merge-base change set, not a hand-written path list). 57 green. Five are NOT MEASURED locally — each by its own printed verdict, none a violation:

gatecodeverdict text
check-test-completeness.mjs3PREREQUISITE NOT MET — grades a saved turbo run test log; the script itself says to "record this gate as NOT MEASURED"
pm/check-half-states.mjs124timed out reaching the GitHub API through the proxy; not a tree gate
spec check:skill-examples1self-test passed, then packages/client-react/dist holds no .d.ts declarations — the package is not built
check:dual-build-cjs-loads3Run pnpm build first. This is NOT a pass: nothing was measured.
check:type-check-debt3prerequisite; says nothing about whether any debt number is still correct

Topically closest gate, pnpm check:auth-mount-ledger, is green. Exit codes were captured before any pipe (redirect, then read).

Log-level fence — named, not decided

The issue asks whether the "no email service registered" branch should stay at info now that it becomes reachable on hosts that never saw it. Per the ruling, that is not decided here. Verified untouched: the branch is byte-identical to origin/main modulo the two-space de-indent, and still reads ctx.logger.info('Auth: no email service registered — transactional mail disabled'), with the requireEmailVerification sibling still at error.

Clause 2 — no

No new exported symbol, no new payload key, no option added (registerRoutes already existed), and nothing changed about what the contract accepts or rejects — the -w diff shows zero statement changes, only the hook's registration site. The delta is runtime behaviour for registerRoutes: false embeddings, which is the bug being fixed rather than a contract surface move; the changeset states it explicitly for release notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…pendently of registerRoutes
`AuthPlugin` bound the outbound mail transport, the SMS transport, the
deployment email locale, the brand name and the SMS locale inside the same
`kernel:ready` hook that mounts `/api/v1/auth/*`, and that hook was gated on
`registerRoutes`.
`registerRoutes` answers a transport-mounting question. The bindings are
service composition and are true of an embedding regardless of who serves the
routes, so every routes-less embedding came up with no mail transport, no
locale on either channel and no brand binding — silently, because the
`logger.info` lines that would have reported the wiring sat inside the same
skipped block.
Split the hook: the composition block moves verbatim into its own
unconditional `ctx.hook('kernel:ready', …)`, registered before the route hook
so the ordering a routing host had is preserved. Route registration itself
stays under `if (this.options.registerRoutes)`.
This is the shape the sibling hooks in this file already use and already name
("Registered independently of `registerRoutes` so an embedding that serves no
auth routes still gets the diagnosis") — the file applied the distinction to
the diagnosis hook and not to the wiring the diagnosis exists to report on.
The `#14319` describe block now runs against both values of `registerRoutes`
rather than only the default, and pins both acceptance criteria: the
composition completes either way, and a `registerRoutes: false` kernel still
mounts no auth routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…s row
The census row for `plugin-auth`'s session-resolution middleware anchors a
LINE NUMBER in `auth-plugin.ts`; the hook split moved that read site from
:1353 to :1380 without changing a character of it. Re-anchored with the gate's
own `--fix`, which is the repair it prescribes for pure line rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/deployment/self-hosting.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/permissions/authentication.mdx(via /api/v1/auth/* (route, a path literal in start))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197ddapackageMentionDocs.

Which tree this was computed on

This run read content/docs from dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 — the merge of head 3583196089c29cdad2433f26fd662c5ed70b91ed into base 5a5336b399db2ef18dd4700f97d579a328197dda, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 && git checkout dc0bd0eedb73aa3a16a76d29a8e590e919df35c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5336b399db2ef18dd4700f97d579a328197dda 3583196089c29cdad2433f26fd662c5ed70b91ed && git checkout -B drift-repro 5a5336b399db2ef18dd4700f97d579a328197dda && git merge --no-ff 3583196089c29cdad2433f26fd662c5ed70b91ed
node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197dda

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5a5336b399db2ef18dd4700f97d579a328197dda → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716400023 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 63 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716742053 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 65 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, '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): register the auth service-composition bindings independently of registerRoutes - #14810

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes
Sep 3, 2026
Merged

fix(plugin-auth): register the auth service-composition bindings independently of registerRoutes#14810
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14724

AuthPlugin registered its service-composition bindings inside the same kernel:ready hook that mounts /api/v1/auth/*, and that hook was gated on registerRoutes. registerRoutes answers a transport-mounting question; the bindings are service composition and are true of an embedding regardless of who serves the routes. Every routes-less embedding — every cloud tenant environment kernel is one — therefore came up with no mail transport, no locale on either channel and no brand binding, silently, without even reading the localization settings namespace.

The composition block is now its own unconditional ctx.hook('kernel:ready', ...), registered before the route hook. Route registration itself stays gated.

Comments read: 2. Ruling of record: issuecomment-5518030363 (triage, 2026-09-02 23:39:05Z). Its three binding points are all satisfied below: route registration stays gated (pinned by a test, both directions), the #14319 describe block now runs both values of the flag, and the log-level question is not decided here.

Reading of the 421-line churn

The gross diff on auth-plugin.ts is +224/−197. Ignoring whitespace it is +33/−6:

git diff --stat origin/main...HEAD -- .../auth-plugin.ts -> 421 changed (224 ins, 197 del)
git diff -w --stat origin/main...HEAD -- .../auth-plugin.ts -> 39 changed ( 33 ins, 6 del)

So ~93% of the churn is re-indentation forced by de-nesting a ~205-line block out of one if. No statement inside the moved block changed — the -w diff contains only the new explanatory comment, the moved if (this.options.registerRoutes) {, one added });, and one added ctx.hook( line. This is the minimum shape the fix can take in a brace-and-indent language; it is not gratuitous reformatting, and it was not widened further here.

Two structural points verified rather than assumed:

  • Ordering is preserved for routing hosts.dispatchHookPropagating (packages/core/src/hook-dispatch.ts:146) awaits handlers in registration order and does not catch. Composition is registered first, so it still completes before routes, and a throw in composition still aborts before any route is mounted — exactly what the single combined hook gave for free.
  • The sibling diagnosis hook is unaffected. It reads both the kernel email service and the manager's own view specifically so its answer is "independent of hook registration order" (its own comment), so inserting composition ahead of it changes nothing it reports.

One thing worth naming explicitly: the issue lists four bindings; the moved block actually carries fivesetSmsService (#2780) sits between setEmailService and the locale rungs. It moves because it is inside the block, not as added scope; the changeset says five.

Ablation

Subject resolves through a relative source import (import { AuthPlugin } from './auth-plugin'), so the test exercises src/ directly — no dist/ leg, and no build-and-preflight pair applies. Mutation: line 747 re-gated to the pre-fix semantics, addressed by line number because the anchor text occurs 8 times in the file.

Mutation confirmed on disk before measuring — blob hash moved and both unique-text counts flipped:

HEAD blob = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe
pre hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe (equal -> tree was at HEAD)
post hash = deb7da1cd90046c29bda989ae51b9e98d888ee05 (moved -> mutation reached disk)
pre counts : anchor-form=8 gated-form=0
post counts : anchor-form=7 gated-form=1
line 747 now : if (this.options.registerRoutes) ctx.hook('kernel:ready', async () => {

Results:

ABLATED Tests 9 failed | 79 passed (88) vitest exit=1
RESTORED Tests 88 passed (88) vitest exit=0

All 9 failures are in the registerRoutes: false branch and nowhere else — the exact direction predicted:

FAIL ... > registerRoutes: false (routes-less embedding) > a zh-CN workspace binds zh-CN on the EMAIL channel, not just on SMS
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a ja-JP workspace
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a es-ES workspace
FAIL ... > registerRoutes: false (routes-less embedding) > a workspace that never chose a language keeps the app build-time default
FAIL ... > registerRoutes: false (routes-less embedding) > names NO locale when neither producer speaks — the documented en-US fallback
FAIL ... > registerRoutes: false (routes-less embedding) > binds the build-time default when there is no settings service at all
FAIL ... > registerRoutes: false (routes-less embedding) > re-binds live when the workspace switches language
FAIL ... > registerRoutes: false (routes-less embedding) > leaves the build-time default standing when the settings read fails
FAIL ... > registerRoutes: false (routes-less embedding) > ends kernel:ready with the transport wired and BOTH locales bound

The registerRoutes: true branch stayed fully green under the mutation, and the route-gating test stayed green in both branches — so the ablation isolates composition from routing rather than knocking the suite over.

Restore proved by blob equality plus an empty diff, not by an exit code:

restored hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe == HEAD blob
restore counts: anchor-form=8 gated-form=0
git diff HEAD -- .../auth-plugin.ts : EMPTY
git diff HEAD (whole tree) : EMPTY

The mutation script carried a trap ... EXIT INT TERM restore on an absolute path derived from git rev-parse --show-toplevel.

The census row

content/docs/permissions/system-context.mdx row 11 moved auth-plugin.ts:1353 to :1380 (the file grew 27 lines above that site). Confirmed tool-produced, not hand-edited, by reconstruction: restoring origin/main's copy of the page and running the gate's own repair arm reproduces the branch file byte-for-byte.

node scripts/check-system-context-census.mjs --fix
re-anchored content/docs/permissions/system-context.mdx:100 `auth-plugin.ts:1353` -> `auth-plugin.ts:1380`
check-system-context-census --fix: 1 anchor(s) rewritten
after --fix = 26928a4a0a19364698a75552b085e2745d09b42e
branch blob = 26928a4a0a19364698a75552b085e2745d09b42e (identical)

Gate green as-is on the branch:

check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files,
all anchored; 145 anchors resolve, 27 declared non-read.

Tests and gates

Run at 358319608 (the pushed head), after merging origin/maine6ac0c6fd.

pnpm --filter '@objectstack/plugin-auth^...' build -> exit 0
pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-plugin.test.ts
Test Files 1 passed (1)
Tests 88 passed (88)
pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0
check:test-typecheck: OK — test layer compiles under tsconfig.test.json

Typecheck coverage of the edited files was measured, not assumed — tsc --listFiles returns 1 hit for auth-plugin.ts in the main project and 1 hit for auth-plugin.test.ts in tsconfig.test.json, so neither edit is outside its program.

Gate family re-derived on the merged tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (62 commands, derived from the merge-base change set, not a hand-written path list). 57 green. Five are NOT MEASURED locally — each by its own printed verdict, none a violation:

gatecodeverdict text
check-test-completeness.mjs3PREREQUISITE NOT MET — grades a saved turbo run test log; the script itself says to "record this gate as NOT MEASURED"
pm/check-half-states.mjs124timed out reaching the GitHub API through the proxy; not a tree gate
spec check:skill-examples1self-test passed, then packages/client-react/dist holds no .d.ts declarations — the package is not built
check:dual-build-cjs-loads3Run pnpm build first. This is NOT a pass: nothing was measured.
check:type-check-debt3prerequisite; says nothing about whether any debt number is still correct

Topically closest gate, pnpm check:auth-mount-ledger, is green. Exit codes were captured before any pipe (redirect, then read).

Log-level fence — named, not decided

The issue asks whether the "no email service registered" branch should stay at info now that it becomes reachable on hosts that never saw it. Per the ruling, that is not decided here. Verified untouched: the branch is byte-identical to origin/main modulo the two-space de-indent, and still reads ctx.logger.info('Auth: no email service registered — transactional mail disabled'), with the requireEmailVerification sibling still at error.

Clause 2 — no

No new exported symbol, no new payload key, no option added (registerRoutes already existed), and nothing changed about what the contract accepts or rejects — the -w diff shows zero statement changes, only the hook's registration site. The delta is runtime behaviour for registerRoutes: false embeddings, which is the bug being fixed rather than a contract surface move; the changeset states it explicitly for release notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…pendently of registerRoutes
`AuthPlugin` bound the outbound mail transport, the SMS transport, the
deployment email locale, the brand name and the SMS locale inside the same
`kernel:ready` hook that mounts `/api/v1/auth/*`, and that hook was gated on
`registerRoutes`.
`registerRoutes` answers a transport-mounting question. The bindings are
service composition and are true of an embedding regardless of who serves the
routes, so every routes-less embedding came up with no mail transport, no
locale on either channel and no brand binding — silently, because the
`logger.info` lines that would have reported the wiring sat inside the same
skipped block.
Split the hook: the composition block moves verbatim into its own
unconditional `ctx.hook('kernel:ready', …)`, registered before the route hook
so the ordering a routing host had is preserved. Route registration itself
stays under `if (this.options.registerRoutes)`.
This is the shape the sibling hooks in this file already use and already name
("Registered independently of `registerRoutes` so an embedding that serves no
auth routes still gets the diagnosis") — the file applied the distinction to
the diagnosis hook and not to the wiring the diagnosis exists to report on.
The `#14319` describe block now runs against both values of `registerRoutes`
rather than only the default, and pins both acceptance criteria: the
composition completes either way, and a `registerRoutes: false` kernel still
mounts no auth routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…s row
The census row for `plugin-auth`'s session-resolution middleware anchors a
LINE NUMBER in `auth-plugin.ts`; the hook split moved that read site from
:1353 to :1380 without changing a character of it. Re-anchored with the gate's
own `--fix`, which is the repair it prescribes for pure line rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/deployment/self-hosting.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/permissions/authentication.mdx(via /api/v1/auth/* (route, a path literal in start))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197ddapackageMentionDocs.

Which tree this was computed on

This run read content/docs from dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 — the merge of head 3583196089c29cdad2433f26fd662c5ed70b91ed into base 5a5336b399db2ef18dd4700f97d579a328197dda, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 && git checkout dc0bd0eedb73aa3a16a76d29a8e590e919df35c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5336b399db2ef18dd4700f97d579a328197dda 3583196089c29cdad2433f26fd662c5ed70b91ed && git checkout -B drift-repro 5a5336b399db2ef18dd4700f97d579a328197dda && git merge --no-ff 3583196089c29cdad2433f26fd662c5ed70b91ed
node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197dda

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5a5336b399db2ef18dd4700f97d579a328197dda → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716400023 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 63 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716742053 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 65 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, '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): register the auth service-composition bindings independently of registerRoutes - #14810

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes
Sep 3, 2026
Merged

fix(plugin-auth): register the auth service-composition bindings independently of registerRoutes#14810
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14724

AuthPlugin registered its service-composition bindings inside the same kernel:ready hook that mounts /api/v1/auth/*, and that hook was gated on registerRoutes. registerRoutes answers a transport-mounting question; the bindings are service composition and are true of an embedding regardless of who serves the routes. Every routes-less embedding — every cloud tenant environment kernel is one — therefore came up with no mail transport, no locale on either channel and no brand binding, silently, without even reading the localization settings namespace.

The composition block is now its own unconditional ctx.hook('kernel:ready', ...), registered before the route hook. Route registration itself stays gated.

Comments read: 2. Ruling of record: issuecomment-5518030363 (triage, 2026-09-02 23:39:05Z). Its three binding points are all satisfied below: route registration stays gated (pinned by a test, both directions), the #14319 describe block now runs both values of the flag, and the log-level question is not decided here.

Reading of the 421-line churn

The gross diff on auth-plugin.ts is +224/−197. Ignoring whitespace it is +33/−6:

git diff --stat origin/main...HEAD -- .../auth-plugin.ts -> 421 changed (224 ins, 197 del)
git diff -w --stat origin/main...HEAD -- .../auth-plugin.ts -> 39 changed ( 33 ins, 6 del)

So ~93% of the churn is re-indentation forced by de-nesting a ~205-line block out of one if. No statement inside the moved block changed — the -w diff contains only the new explanatory comment, the moved if (this.options.registerRoutes) {, one added });, and one added ctx.hook( line. This is the minimum shape the fix can take in a brace-and-indent language; it is not gratuitous reformatting, and it was not widened further here.

Two structural points verified rather than assumed:

  • Ordering is preserved for routing hosts.dispatchHookPropagating (packages/core/src/hook-dispatch.ts:146) awaits handlers in registration order and does not catch. Composition is registered first, so it still completes before routes, and a throw in composition still aborts before any route is mounted — exactly what the single combined hook gave for free.
  • The sibling diagnosis hook is unaffected. It reads both the kernel email service and the manager's own view specifically so its answer is "independent of hook registration order" (its own comment), so inserting composition ahead of it changes nothing it reports.

One thing worth naming explicitly: the issue lists four bindings; the moved block actually carries fivesetSmsService (#2780) sits between setEmailService and the locale rungs. It moves because it is inside the block, not as added scope; the changeset says five.

Ablation

Subject resolves through a relative source import (import { AuthPlugin } from './auth-plugin'), so the test exercises src/ directly — no dist/ leg, and no build-and-preflight pair applies. Mutation: line 747 re-gated to the pre-fix semantics, addressed by line number because the anchor text occurs 8 times in the file.

Mutation confirmed on disk before measuring — blob hash moved and both unique-text counts flipped:

HEAD blob = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe
pre hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe (equal -> tree was at HEAD)
post hash = deb7da1cd90046c29bda989ae51b9e98d888ee05 (moved -> mutation reached disk)
pre counts : anchor-form=8 gated-form=0
post counts : anchor-form=7 gated-form=1
line 747 now : if (this.options.registerRoutes) ctx.hook('kernel:ready', async () => {

Results:

ABLATED Tests 9 failed | 79 passed (88) vitest exit=1
RESTORED Tests 88 passed (88) vitest exit=0

All 9 failures are in the registerRoutes: false branch and nowhere else — the exact direction predicted:

FAIL ... > registerRoutes: false (routes-less embedding) > a zh-CN workspace binds zh-CN on the EMAIL channel, not just on SMS
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a ja-JP workspace
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a es-ES workspace
FAIL ... > registerRoutes: false (routes-less embedding) > a workspace that never chose a language keeps the app build-time default
FAIL ... > registerRoutes: false (routes-less embedding) > names NO locale when neither producer speaks — the documented en-US fallback
FAIL ... > registerRoutes: false (routes-less embedding) > binds the build-time default when there is no settings service at all
FAIL ... > registerRoutes: false (routes-less embedding) > re-binds live when the workspace switches language
FAIL ... > registerRoutes: false (routes-less embedding) > leaves the build-time default standing when the settings read fails
FAIL ... > registerRoutes: false (routes-less embedding) > ends kernel:ready with the transport wired and BOTH locales bound

The registerRoutes: true branch stayed fully green under the mutation, and the route-gating test stayed green in both branches — so the ablation isolates composition from routing rather than knocking the suite over.

Restore proved by blob equality plus an empty diff, not by an exit code:

restored hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe == HEAD blob
restore counts: anchor-form=8 gated-form=0
git diff HEAD -- .../auth-plugin.ts : EMPTY
git diff HEAD (whole tree) : EMPTY

The mutation script carried a trap ... EXIT INT TERM restore on an absolute path derived from git rev-parse --show-toplevel.

The census row

content/docs/permissions/system-context.mdx row 11 moved auth-plugin.ts:1353 to :1380 (the file grew 27 lines above that site). Confirmed tool-produced, not hand-edited, by reconstruction: restoring origin/main's copy of the page and running the gate's own repair arm reproduces the branch file byte-for-byte.

node scripts/check-system-context-census.mjs --fix
re-anchored content/docs/permissions/system-context.mdx:100 `auth-plugin.ts:1353` -> `auth-plugin.ts:1380`
check-system-context-census --fix: 1 anchor(s) rewritten
after --fix = 26928a4a0a19364698a75552b085e2745d09b42e
branch blob = 26928a4a0a19364698a75552b085e2745d09b42e (identical)

Gate green as-is on the branch:

check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files,
all anchored; 145 anchors resolve, 27 declared non-read.

Tests and gates

Run at 358319608 (the pushed head), after merging origin/maine6ac0c6fd.

pnpm --filter '@objectstack/plugin-auth^...' build -> exit 0
pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-plugin.test.ts
Test Files 1 passed (1)
Tests 88 passed (88)
pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0
check:test-typecheck: OK — test layer compiles under tsconfig.test.json

Typecheck coverage of the edited files was measured, not assumed — tsc --listFiles returns 1 hit for auth-plugin.ts in the main project and 1 hit for auth-plugin.test.ts in tsconfig.test.json, so neither edit is outside its program.

Gate family re-derived on the merged tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (62 commands, derived from the merge-base change set, not a hand-written path list). 57 green. Five are NOT MEASURED locally — each by its own printed verdict, none a violation:

gatecodeverdict text
check-test-completeness.mjs3PREREQUISITE NOT MET — grades a saved turbo run test log; the script itself says to "record this gate as NOT MEASURED"
pm/check-half-states.mjs124timed out reaching the GitHub API through the proxy; not a tree gate
spec check:skill-examples1self-test passed, then packages/client-react/dist holds no .d.ts declarations — the package is not built
check:dual-build-cjs-loads3Run pnpm build first. This is NOT a pass: nothing was measured.
check:type-check-debt3prerequisite; says nothing about whether any debt number is still correct

Topically closest gate, pnpm check:auth-mount-ledger, is green. Exit codes were captured before any pipe (redirect, then read).

Log-level fence — named, not decided

The issue asks whether the "no email service registered" branch should stay at info now that it becomes reachable on hosts that never saw it. Per the ruling, that is not decided here. Verified untouched: the branch is byte-identical to origin/main modulo the two-space de-indent, and still reads ctx.logger.info('Auth: no email service registered — transactional mail disabled'), with the requireEmailVerification sibling still at error.

Clause 2 — no

No new exported symbol, no new payload key, no option added (registerRoutes already existed), and nothing changed about what the contract accepts or rejects — the -w diff shows zero statement changes, only the hook's registration site. The delta is runtime behaviour for registerRoutes: false embeddings, which is the bug being fixed rather than a contract surface move; the changeset states it explicitly for release notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…pendently of registerRoutes
`AuthPlugin` bound the outbound mail transport, the SMS transport, the
deployment email locale, the brand name and the SMS locale inside the same
`kernel:ready` hook that mounts `/api/v1/auth/*`, and that hook was gated on
`registerRoutes`.
`registerRoutes` answers a transport-mounting question. The bindings are
service composition and are true of an embedding regardless of who serves the
routes, so every routes-less embedding came up with no mail transport, no
locale on either channel and no brand binding — silently, because the
`logger.info` lines that would have reported the wiring sat inside the same
skipped block.
Split the hook: the composition block moves verbatim into its own
unconditional `ctx.hook('kernel:ready', …)`, registered before the route hook
so the ordering a routing host had is preserved. Route registration itself
stays under `if (this.options.registerRoutes)`.
This is the shape the sibling hooks in this file already use and already name
("Registered independently of `registerRoutes` so an embedding that serves no
auth routes still gets the diagnosis") — the file applied the distinction to
the diagnosis hook and not to the wiring the diagnosis exists to report on.
The `#14319` describe block now runs against both values of `registerRoutes`
rather than only the default, and pins both acceptance criteria: the
composition completes either way, and a `registerRoutes: false` kernel still
mounts no auth routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…s row
The census row for `plugin-auth`'s session-resolution middleware anchors a
LINE NUMBER in `auth-plugin.ts`; the hook split moved that read site from
:1353 to :1380 without changing a character of it. Re-anchored with the gate's
own `--fix`, which is the repair it prescribes for pure line rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/deployment/self-hosting.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/permissions/authentication.mdx(via /api/v1/auth/* (route, a path literal in start))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197ddapackageMentionDocs.

Which tree this was computed on

This run read content/docs from dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 — the merge of head 3583196089c29cdad2433f26fd662c5ed70b91ed into base 5a5336b399db2ef18dd4700f97d579a328197dda, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 && git checkout dc0bd0eedb73aa3a16a76d29a8e590e919df35c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5336b399db2ef18dd4700f97d579a328197dda 3583196089c29cdad2433f26fd662c5ed70b91ed && git checkout -B drift-repro 5a5336b399db2ef18dd4700f97d579a328197dda && git merge --no-ff 3583196089c29cdad2433f26fd662c5ed70b91ed
node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197dda

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5a5336b399db2ef18dd4700f97d579a328197dda → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716400023 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 63 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716742053 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 65 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, '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): register the auth service-composition bindings independently of registerRoutes - #14810

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes
Sep 3, 2026
Merged

fix(plugin-auth): register the auth service-composition bindings independently of registerRoutes#14810
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14724

AuthPlugin registered its service-composition bindings inside the same kernel:ready hook that mounts /api/v1/auth/*, and that hook was gated on registerRoutes. registerRoutes answers a transport-mounting question; the bindings are service composition and are true of an embedding regardless of who serves the routes. Every routes-less embedding — every cloud tenant environment kernel is one — therefore came up with no mail transport, no locale on either channel and no brand binding, silently, without even reading the localization settings namespace.

The composition block is now its own unconditional ctx.hook('kernel:ready', ...), registered before the route hook. Route registration itself stays gated.

Comments read: 2. Ruling of record: issuecomment-5518030363 (triage, 2026-09-02 23:39:05Z). Its three binding points are all satisfied below: route registration stays gated (pinned by a test, both directions), the #14319 describe block now runs both values of the flag, and the log-level question is not decided here.

Reading of the 421-line churn

The gross diff on auth-plugin.ts is +224/−197. Ignoring whitespace it is +33/−6:

git diff --stat origin/main...HEAD -- .../auth-plugin.ts -> 421 changed (224 ins, 197 del)
git diff -w --stat origin/main...HEAD -- .../auth-plugin.ts -> 39 changed ( 33 ins, 6 del)

So ~93% of the churn is re-indentation forced by de-nesting a ~205-line block out of one if. No statement inside the moved block changed — the -w diff contains only the new explanatory comment, the moved if (this.options.registerRoutes) {, one added });, and one added ctx.hook( line. This is the minimum shape the fix can take in a brace-and-indent language; it is not gratuitous reformatting, and it was not widened further here.

Two structural points verified rather than assumed:

  • Ordering is preserved for routing hosts.dispatchHookPropagating (packages/core/src/hook-dispatch.ts:146) awaits handlers in registration order and does not catch. Composition is registered first, so it still completes before routes, and a throw in composition still aborts before any route is mounted — exactly what the single combined hook gave for free.
  • The sibling diagnosis hook is unaffected. It reads both the kernel email service and the manager's own view specifically so its answer is "independent of hook registration order" (its own comment), so inserting composition ahead of it changes nothing it reports.

One thing worth naming explicitly: the issue lists four bindings; the moved block actually carries fivesetSmsService (#2780) sits between setEmailService and the locale rungs. It moves because it is inside the block, not as added scope; the changeset says five.

Ablation

Subject resolves through a relative source import (import { AuthPlugin } from './auth-plugin'), so the test exercises src/ directly — no dist/ leg, and no build-and-preflight pair applies. Mutation: line 747 re-gated to the pre-fix semantics, addressed by line number because the anchor text occurs 8 times in the file.

Mutation confirmed on disk before measuring — blob hash moved and both unique-text counts flipped:

HEAD blob = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe
pre hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe (equal -> tree was at HEAD)
post hash = deb7da1cd90046c29bda989ae51b9e98d888ee05 (moved -> mutation reached disk)
pre counts : anchor-form=8 gated-form=0
post counts : anchor-form=7 gated-form=1
line 747 now : if (this.options.registerRoutes) ctx.hook('kernel:ready', async () => {

Results:

ABLATED Tests 9 failed | 79 passed (88) vitest exit=1
RESTORED Tests 88 passed (88) vitest exit=0

All 9 failures are in the registerRoutes: false branch and nowhere else — the exact direction predicted:

FAIL ... > registerRoutes: false (routes-less embedding) > a zh-CN workspace binds zh-CN on the EMAIL channel, not just on SMS
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a ja-JP workspace
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a es-ES workspace
FAIL ... > registerRoutes: false (routes-less embedding) > a workspace that never chose a language keeps the app build-time default
FAIL ... > registerRoutes: false (routes-less embedding) > names NO locale when neither producer speaks — the documented en-US fallback
FAIL ... > registerRoutes: false (routes-less embedding) > binds the build-time default when there is no settings service at all
FAIL ... > registerRoutes: false (routes-less embedding) > re-binds live when the workspace switches language
FAIL ... > registerRoutes: false (routes-less embedding) > leaves the build-time default standing when the settings read fails
FAIL ... > registerRoutes: false (routes-less embedding) > ends kernel:ready with the transport wired and BOTH locales bound

The registerRoutes: true branch stayed fully green under the mutation, and the route-gating test stayed green in both branches — so the ablation isolates composition from routing rather than knocking the suite over.

Restore proved by blob equality plus an empty diff, not by an exit code:

restored hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe == HEAD blob
restore counts: anchor-form=8 gated-form=0
git diff HEAD -- .../auth-plugin.ts : EMPTY
git diff HEAD (whole tree) : EMPTY

The mutation script carried a trap ... EXIT INT TERM restore on an absolute path derived from git rev-parse --show-toplevel.

The census row

content/docs/permissions/system-context.mdx row 11 moved auth-plugin.ts:1353 to :1380 (the file grew 27 lines above that site). Confirmed tool-produced, not hand-edited, by reconstruction: restoring origin/main's copy of the page and running the gate's own repair arm reproduces the branch file byte-for-byte.

node scripts/check-system-context-census.mjs --fix
re-anchored content/docs/permissions/system-context.mdx:100 `auth-plugin.ts:1353` -> `auth-plugin.ts:1380`
check-system-context-census --fix: 1 anchor(s) rewritten
after --fix = 26928a4a0a19364698a75552b085e2745d09b42e
branch blob = 26928a4a0a19364698a75552b085e2745d09b42e (identical)

Gate green as-is on the branch:

check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files,
all anchored; 145 anchors resolve, 27 declared non-read.

Tests and gates

Run at 358319608 (the pushed head), after merging origin/maine6ac0c6fd.

pnpm --filter '@objectstack/plugin-auth^...' build -> exit 0
pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-plugin.test.ts
Test Files 1 passed (1)
Tests 88 passed (88)
pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0
check:test-typecheck: OK — test layer compiles under tsconfig.test.json

Typecheck coverage of the edited files was measured, not assumed — tsc --listFiles returns 1 hit for auth-plugin.ts in the main project and 1 hit for auth-plugin.test.ts in tsconfig.test.json, so neither edit is outside its program.

Gate family re-derived on the merged tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (62 commands, derived from the merge-base change set, not a hand-written path list). 57 green. Five are NOT MEASURED locally — each by its own printed verdict, none a violation:

gatecodeverdict text
check-test-completeness.mjs3PREREQUISITE NOT MET — grades a saved turbo run test log; the script itself says to "record this gate as NOT MEASURED"
pm/check-half-states.mjs124timed out reaching the GitHub API through the proxy; not a tree gate
spec check:skill-examples1self-test passed, then packages/client-react/dist holds no .d.ts declarations — the package is not built
check:dual-build-cjs-loads3Run pnpm build first. This is NOT a pass: nothing was measured.
check:type-check-debt3prerequisite; says nothing about whether any debt number is still correct

Topically closest gate, pnpm check:auth-mount-ledger, is green. Exit codes were captured before any pipe (redirect, then read).

Log-level fence — named, not decided

The issue asks whether the "no email service registered" branch should stay at info now that it becomes reachable on hosts that never saw it. Per the ruling, that is not decided here. Verified untouched: the branch is byte-identical to origin/main modulo the two-space de-indent, and still reads ctx.logger.info('Auth: no email service registered — transactional mail disabled'), with the requireEmailVerification sibling still at error.

Clause 2 — no

No new exported symbol, no new payload key, no option added (registerRoutes already existed), and nothing changed about what the contract accepts or rejects — the -w diff shows zero statement changes, only the hook's registration site. The delta is runtime behaviour for registerRoutes: false embeddings, which is the bug being fixed rather than a contract surface move; the changeset states it explicitly for release notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…pendently of registerRoutes
`AuthPlugin` bound the outbound mail transport, the SMS transport, the
deployment email locale, the brand name and the SMS locale inside the same
`kernel:ready` hook that mounts `/api/v1/auth/*`, and that hook was gated on
`registerRoutes`.
`registerRoutes` answers a transport-mounting question. The bindings are
service composition and are true of an embedding regardless of who serves the
routes, so every routes-less embedding came up with no mail transport, no
locale on either channel and no brand binding — silently, because the
`logger.info` lines that would have reported the wiring sat inside the same
skipped block.
Split the hook: the composition block moves verbatim into its own
unconditional `ctx.hook('kernel:ready', …)`, registered before the route hook
so the ordering a routing host had is preserved. Route registration itself
stays under `if (this.options.registerRoutes)`.
This is the shape the sibling hooks in this file already use and already name
("Registered independently of `registerRoutes` so an embedding that serves no
auth routes still gets the diagnosis") — the file applied the distinction to
the diagnosis hook and not to the wiring the diagnosis exists to report on.
The `#14319` describe block now runs against both values of `registerRoutes`
rather than only the default, and pins both acceptance criteria: the
composition completes either way, and a `registerRoutes: false` kernel still
mounts no auth routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…s row
The census row for `plugin-auth`'s session-resolution middleware anchors a
LINE NUMBER in `auth-plugin.ts`; the hook split moved that read site from
:1353 to :1380 without changing a character of it. Re-anchored with the gate's
own `--fix`, which is the repair it prescribes for pure line rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/deployment/self-hosting.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/permissions/authentication.mdx(via /api/v1/auth/* (route, a path literal in start))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197ddapackageMentionDocs.

Which tree this was computed on

This run read content/docs from dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 — the merge of head 3583196089c29cdad2433f26fd662c5ed70b91ed into base 5a5336b399db2ef18dd4700f97d579a328197dda, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 && git checkout dc0bd0eedb73aa3a16a76d29a8e590e919df35c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5336b399db2ef18dd4700f97d579a328197dda 3583196089c29cdad2433f26fd662c5ed70b91ed && git checkout -B drift-repro 5a5336b399db2ef18dd4700f97d579a328197dda && git merge --no-ff 3583196089c29cdad2433f26fd662c5ed70b91ed
node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197dda

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5a5336b399db2ef18dd4700f97d579a328197dda → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716400023 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 63 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716742053 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 65 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, '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): register the auth service-composition bindings independently of registerRoutes - #14810

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes
Sep 3, 2026
Merged

fix(plugin-auth): register the auth service-composition bindings independently of registerRoutes#14810
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14724

AuthPlugin registered its service-composition bindings inside the same kernel:ready hook that mounts /api/v1/auth/*, and that hook was gated on registerRoutes. registerRoutes answers a transport-mounting question; the bindings are service composition and are true of an embedding regardless of who serves the routes. Every routes-less embedding — every cloud tenant environment kernel is one — therefore came up with no mail transport, no locale on either channel and no brand binding, silently, without even reading the localization settings namespace.

The composition block is now its own unconditional ctx.hook('kernel:ready', ...), registered before the route hook. Route registration itself stays gated.

Comments read: 2. Ruling of record: issuecomment-5518030363 (triage, 2026-09-02 23:39:05Z). Its three binding points are all satisfied below: route registration stays gated (pinned by a test, both directions), the #14319 describe block now runs both values of the flag, and the log-level question is not decided here.

Reading of the 421-line churn

The gross diff on auth-plugin.ts is +224/−197. Ignoring whitespace it is +33/−6:

git diff --stat origin/main...HEAD -- .../auth-plugin.ts -> 421 changed (224 ins, 197 del)
git diff -w --stat origin/main...HEAD -- .../auth-plugin.ts -> 39 changed ( 33 ins, 6 del)

So ~93% of the churn is re-indentation forced by de-nesting a ~205-line block out of one if. No statement inside the moved block changed — the -w diff contains only the new explanatory comment, the moved if (this.options.registerRoutes) {, one added });, and one added ctx.hook( line. This is the minimum shape the fix can take in a brace-and-indent language; it is not gratuitous reformatting, and it was not widened further here.

Two structural points verified rather than assumed:

  • Ordering is preserved for routing hosts.dispatchHookPropagating (packages/core/src/hook-dispatch.ts:146) awaits handlers in registration order and does not catch. Composition is registered first, so it still completes before routes, and a throw in composition still aborts before any route is mounted — exactly what the single combined hook gave for free.
  • The sibling diagnosis hook is unaffected. It reads both the kernel email service and the manager's own view specifically so its answer is "independent of hook registration order" (its own comment), so inserting composition ahead of it changes nothing it reports.

One thing worth naming explicitly: the issue lists four bindings; the moved block actually carries fivesetSmsService (#2780) sits between setEmailService and the locale rungs. It moves because it is inside the block, not as added scope; the changeset says five.

Ablation

Subject resolves through a relative source import (import { AuthPlugin } from './auth-plugin'), so the test exercises src/ directly — no dist/ leg, and no build-and-preflight pair applies. Mutation: line 747 re-gated to the pre-fix semantics, addressed by line number because the anchor text occurs 8 times in the file.

Mutation confirmed on disk before measuring — blob hash moved and both unique-text counts flipped:

HEAD blob = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe
pre hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe (equal -> tree was at HEAD)
post hash = deb7da1cd90046c29bda989ae51b9e98d888ee05 (moved -> mutation reached disk)
pre counts : anchor-form=8 gated-form=0
post counts : anchor-form=7 gated-form=1
line 747 now : if (this.options.registerRoutes) ctx.hook('kernel:ready', async () => {

Results:

ABLATED Tests 9 failed | 79 passed (88) vitest exit=1
RESTORED Tests 88 passed (88) vitest exit=0

All 9 failures are in the registerRoutes: false branch and nowhere else — the exact direction predicted:

FAIL ... > registerRoutes: false (routes-less embedding) > a zh-CN workspace binds zh-CN on the EMAIL channel, not just on SMS
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a ja-JP workspace
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a es-ES workspace
FAIL ... > registerRoutes: false (routes-less embedding) > a workspace that never chose a language keeps the app build-time default
FAIL ... > registerRoutes: false (routes-less embedding) > names NO locale when neither producer speaks — the documented en-US fallback
FAIL ... > registerRoutes: false (routes-less embedding) > binds the build-time default when there is no settings service at all
FAIL ... > registerRoutes: false (routes-less embedding) > re-binds live when the workspace switches language
FAIL ... > registerRoutes: false (routes-less embedding) > leaves the build-time default standing when the settings read fails
FAIL ... > registerRoutes: false (routes-less embedding) > ends kernel:ready with the transport wired and BOTH locales bound

The registerRoutes: true branch stayed fully green under the mutation, and the route-gating test stayed green in both branches — so the ablation isolates composition from routing rather than knocking the suite over.

Restore proved by blob equality plus an empty diff, not by an exit code:

restored hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe == HEAD blob
restore counts: anchor-form=8 gated-form=0
git diff HEAD -- .../auth-plugin.ts : EMPTY
git diff HEAD (whole tree) : EMPTY

The mutation script carried a trap ... EXIT INT TERM restore on an absolute path derived from git rev-parse --show-toplevel.

The census row

content/docs/permissions/system-context.mdx row 11 moved auth-plugin.ts:1353 to :1380 (the file grew 27 lines above that site). Confirmed tool-produced, not hand-edited, by reconstruction: restoring origin/main's copy of the page and running the gate's own repair arm reproduces the branch file byte-for-byte.

node scripts/check-system-context-census.mjs --fix
re-anchored content/docs/permissions/system-context.mdx:100 `auth-plugin.ts:1353` -> `auth-plugin.ts:1380`
check-system-context-census --fix: 1 anchor(s) rewritten
after --fix = 26928a4a0a19364698a75552b085e2745d09b42e
branch blob = 26928a4a0a19364698a75552b085e2745d09b42e (identical)

Gate green as-is on the branch:

check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files,
all anchored; 145 anchors resolve, 27 declared non-read.

Tests and gates

Run at 358319608 (the pushed head), after merging origin/maine6ac0c6fd.

pnpm --filter '@objectstack/plugin-auth^...' build -> exit 0
pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-plugin.test.ts
Test Files 1 passed (1)
Tests 88 passed (88)
pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0
check:test-typecheck: OK — test layer compiles under tsconfig.test.json

Typecheck coverage of the edited files was measured, not assumed — tsc --listFiles returns 1 hit for auth-plugin.ts in the main project and 1 hit for auth-plugin.test.ts in tsconfig.test.json, so neither edit is outside its program.

Gate family re-derived on the merged tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (62 commands, derived from the merge-base change set, not a hand-written path list). 57 green. Five are NOT MEASURED locally — each by its own printed verdict, none a violation:

gatecodeverdict text
check-test-completeness.mjs3PREREQUISITE NOT MET — grades a saved turbo run test log; the script itself says to "record this gate as NOT MEASURED"
pm/check-half-states.mjs124timed out reaching the GitHub API through the proxy; not a tree gate
spec check:skill-examples1self-test passed, then packages/client-react/dist holds no .d.ts declarations — the package is not built
check:dual-build-cjs-loads3Run pnpm build first. This is NOT a pass: nothing was measured.
check:type-check-debt3prerequisite; says nothing about whether any debt number is still correct

Topically closest gate, pnpm check:auth-mount-ledger, is green. Exit codes were captured before any pipe (redirect, then read).

Log-level fence — named, not decided

The issue asks whether the "no email service registered" branch should stay at info now that it becomes reachable on hosts that never saw it. Per the ruling, that is not decided here. Verified untouched: the branch is byte-identical to origin/main modulo the two-space de-indent, and still reads ctx.logger.info('Auth: no email service registered — transactional mail disabled'), with the requireEmailVerification sibling still at error.

Clause 2 — no

No new exported symbol, no new payload key, no option added (registerRoutes already existed), and nothing changed about what the contract accepts or rejects — the -w diff shows zero statement changes, only the hook's registration site. The delta is runtime behaviour for registerRoutes: false embeddings, which is the bug being fixed rather than a contract surface move; the changeset states it explicitly for release notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…pendently of registerRoutes
`AuthPlugin` bound the outbound mail transport, the SMS transport, the
deployment email locale, the brand name and the SMS locale inside the same
`kernel:ready` hook that mounts `/api/v1/auth/*`, and that hook was gated on
`registerRoutes`.
`registerRoutes` answers a transport-mounting question. The bindings are
service composition and are true of an embedding regardless of who serves the
routes, so every routes-less embedding came up with no mail transport, no
locale on either channel and no brand binding — silently, because the
`logger.info` lines that would have reported the wiring sat inside the same
skipped block.
Split the hook: the composition block moves verbatim into its own
unconditional `ctx.hook('kernel:ready', …)`, registered before the route hook
so the ordering a routing host had is preserved. Route registration itself
stays under `if (this.options.registerRoutes)`.
This is the shape the sibling hooks in this file already use and already name
("Registered independently of `registerRoutes` so an embedding that serves no
auth routes still gets the diagnosis") — the file applied the distinction to
the diagnosis hook and not to the wiring the diagnosis exists to report on.
The `#14319` describe block now runs against both values of `registerRoutes`
rather than only the default, and pins both acceptance criteria: the
composition completes either way, and a `registerRoutes: false` kernel still
mounts no auth routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…s row
The census row for `plugin-auth`'s session-resolution middleware anchors a
LINE NUMBER in `auth-plugin.ts`; the hook split moved that read site from
:1353 to :1380 without changing a character of it. Re-anchored with the gate's
own `--fix`, which is the repair it prescribes for pure line rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/deployment/self-hosting.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/permissions/authentication.mdx(via /api/v1/auth/* (route, a path literal in start))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197ddapackageMentionDocs.

Which tree this was computed on

This run read content/docs from dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 — the merge of head 3583196089c29cdad2433f26fd662c5ed70b91ed into base 5a5336b399db2ef18dd4700f97d579a328197dda, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 && git checkout dc0bd0eedb73aa3a16a76d29a8e590e919df35c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5336b399db2ef18dd4700f97d579a328197dda 3583196089c29cdad2433f26fd662c5ed70b91ed && git checkout -B drift-repro 5a5336b399db2ef18dd4700f97d579a328197dda && git merge --no-ff 3583196089c29cdad2433f26fd662c5ed70b91ed
node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197dda

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5a5336b399db2ef18dd4700f97d579a328197dda → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716400023 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 63 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716742053 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 65 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, '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): register the auth service-composition bindings independently of registerRoutes - #14810

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes
Sep 3, 2026
Merged

fix(plugin-auth): register the auth service-composition bindings independently of registerRoutes#14810
os-sales merged 5 commits into
mainfrom
claude/issue-14724-auth-bindings-off-registerroutes

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14724

AuthPlugin registered its service-composition bindings inside the same kernel:ready hook that mounts /api/v1/auth/*, and that hook was gated on registerRoutes. registerRoutes answers a transport-mounting question; the bindings are service composition and are true of an embedding regardless of who serves the routes. Every routes-less embedding — every cloud tenant environment kernel is one — therefore came up with no mail transport, no locale on either channel and no brand binding, silently, without even reading the localization settings namespace.

The composition block is now its own unconditional ctx.hook('kernel:ready', ...), registered before the route hook. Route registration itself stays gated.

Comments read: 2. Ruling of record: issuecomment-5518030363 (triage, 2026-09-02 23:39:05Z). Its three binding points are all satisfied below: route registration stays gated (pinned by a test, both directions), the #14319 describe block now runs both values of the flag, and the log-level question is not decided here.

Reading of the 421-line churn

The gross diff on auth-plugin.ts is +224/−197. Ignoring whitespace it is +33/−6:

git diff --stat origin/main...HEAD -- .../auth-plugin.ts -> 421 changed (224 ins, 197 del)
git diff -w --stat origin/main...HEAD -- .../auth-plugin.ts -> 39 changed ( 33 ins, 6 del)

So ~93% of the churn is re-indentation forced by de-nesting a ~205-line block out of one if. No statement inside the moved block changed — the -w diff contains only the new explanatory comment, the moved if (this.options.registerRoutes) {, one added });, and one added ctx.hook( line. This is the minimum shape the fix can take in a brace-and-indent language; it is not gratuitous reformatting, and it was not widened further here.

Two structural points verified rather than assumed:

  • Ordering is preserved for routing hosts.dispatchHookPropagating (packages/core/src/hook-dispatch.ts:146) awaits handlers in registration order and does not catch. Composition is registered first, so it still completes before routes, and a throw in composition still aborts before any route is mounted — exactly what the single combined hook gave for free.
  • The sibling diagnosis hook is unaffected. It reads both the kernel email service and the manager's own view specifically so its answer is "independent of hook registration order" (its own comment), so inserting composition ahead of it changes nothing it reports.

One thing worth naming explicitly: the issue lists four bindings; the moved block actually carries fivesetSmsService (#2780) sits between setEmailService and the locale rungs. It moves because it is inside the block, not as added scope; the changeset says five.

Ablation

Subject resolves through a relative source import (import { AuthPlugin } from './auth-plugin'), so the test exercises src/ directly — no dist/ leg, and no build-and-preflight pair applies. Mutation: line 747 re-gated to the pre-fix semantics, addressed by line number because the anchor text occurs 8 times in the file.

Mutation confirmed on disk before measuring — blob hash moved and both unique-text counts flipped:

HEAD blob = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe
pre hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe (equal -> tree was at HEAD)
post hash = deb7da1cd90046c29bda989ae51b9e98d888ee05 (moved -> mutation reached disk)
pre counts : anchor-form=8 gated-form=0
post counts : anchor-form=7 gated-form=1
line 747 now : if (this.options.registerRoutes) ctx.hook('kernel:ready', async () => {

Results:

ABLATED Tests 9 failed | 79 passed (88) vitest exit=1
RESTORED Tests 88 passed (88) vitest exit=0

All 9 failures are in the registerRoutes: false branch and nowhere else — the exact direction predicted:

FAIL ... > registerRoutes: false (routes-less embedding) > a zh-CN workspace binds zh-CN on the EMAIL channel, not just on SMS
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a ja-JP workspace
FAIL ... > registerRoutes: false (routes-less embedding) > and the same for a es-ES workspace
FAIL ... > registerRoutes: false (routes-less embedding) > a workspace that never chose a language keeps the app build-time default
FAIL ... > registerRoutes: false (routes-less embedding) > names NO locale when neither producer speaks — the documented en-US fallback
FAIL ... > registerRoutes: false (routes-less embedding) > binds the build-time default when there is no settings service at all
FAIL ... > registerRoutes: false (routes-less embedding) > re-binds live when the workspace switches language
FAIL ... > registerRoutes: false (routes-less embedding) > leaves the build-time default standing when the settings read fails
FAIL ... > registerRoutes: false (routes-less embedding) > ends kernel:ready with the transport wired and BOTH locales bound

The registerRoutes: true branch stayed fully green under the mutation, and the route-gating test stayed green in both branches — so the ablation isolates composition from routing rather than knocking the suite over.

Restore proved by blob equality plus an empty diff, not by an exit code:

restored hash = 6e96f028e13a1bcc58117c62b42b18a1b982e0fe == HEAD blob
restore counts: anchor-form=8 gated-form=0
git diff HEAD -- .../auth-plugin.ts : EMPTY
git diff HEAD (whole tree) : EMPTY

The mutation script carried a trap ... EXIT INT TERM restore on an absolute path derived from git rev-parse --show-toplevel.

The census row

content/docs/permissions/system-context.mdx row 11 moved auth-plugin.ts:1353 to :1380 (the file grew 27 lines above that site). Confirmed tool-produced, not hand-edited, by reconstruction: restoring origin/main's copy of the page and running the gate's own repair arm reproduces the branch file byte-for-byte.

node scripts/check-system-context-census.mjs --fix
re-anchored content/docs/permissions/system-context.mdx:100 `auth-plugin.ts:1353` -> `auth-plugin.ts:1380`
check-system-context-census --fix: 1 anchor(s) rewritten
after --fix = 26928a4a0a19364698a75552b085e2745d09b42e
branch blob = 26928a4a0a19364698a75552b085e2745d09b42e (identical)

Gate green as-is on the branch:

check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files,
all anchored; 145 anchors resolve, 27 declared non-read.

Tests and gates

Run at 358319608 (the pushed head), after merging origin/maine6ac0c6fd.

pnpm --filter '@objectstack/plugin-auth^...' build -> exit 0
pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-plugin.test.ts
Test Files 1 passed (1)
Tests 88 passed (88)
pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0
check:test-typecheck: OK — test layer compiles under tsconfig.test.json

Typecheck coverage of the edited files was measured, not assumed — tsc --listFiles returns 1 hit for auth-plugin.ts in the main project and 1 hit for auth-plugin.test.ts in tsconfig.test.json, so neither edit is outside its program.

Gate family re-derived on the merged tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (62 commands, derived from the merge-base change set, not a hand-written path list). 57 green. Five are NOT MEASURED locally — each by its own printed verdict, none a violation:

gatecodeverdict text
check-test-completeness.mjs3PREREQUISITE NOT MET — grades a saved turbo run test log; the script itself says to "record this gate as NOT MEASURED"
pm/check-half-states.mjs124timed out reaching the GitHub API through the proxy; not a tree gate
spec check:skill-examples1self-test passed, then packages/client-react/dist holds no .d.ts declarations — the package is not built
check:dual-build-cjs-loads3Run pnpm build first. This is NOT a pass: nothing was measured.
check:type-check-debt3prerequisite; says nothing about whether any debt number is still correct

Topically closest gate, pnpm check:auth-mount-ledger, is green. Exit codes were captured before any pipe (redirect, then read).

Log-level fence — named, not decided

The issue asks whether the "no email service registered" branch should stay at info now that it becomes reachable on hosts that never saw it. Per the ruling, that is not decided here. Verified untouched: the branch is byte-identical to origin/main modulo the two-space de-indent, and still reads ctx.logger.info('Auth: no email service registered — transactional mail disabled'), with the requireEmailVerification sibling still at error.

Clause 2 — no

No new exported symbol, no new payload key, no option added (registerRoutes already existed), and nothing changed about what the contract accepts or rejects — the -w diff shows zero statement changes, only the hook's registration site. The delta is runtime behaviour for registerRoutes: false embeddings, which is the bug being fixed rather than a contract surface move; the changeset states it explicitly for release notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…pendently of registerRoutes
`AuthPlugin` bound the outbound mail transport, the SMS transport, the
deployment email locale, the brand name and the SMS locale inside the same
`kernel:ready` hook that mounts `/api/v1/auth/*`, and that hook was gated on
`registerRoutes`.
`registerRoutes` answers a transport-mounting question. The bindings are
service composition and are true of an embedding regardless of who serves the
routes, so every routes-less embedding came up with no mail transport, no
locale on either channel and no brand binding — silently, because the
`logger.info` lines that would have reported the wiring sat inside the same
skipped block.
Split the hook: the composition block moves verbatim into its own
unconditional `ctx.hook('kernel:ready', …)`, registered before the route hook
so the ordering a routing host had is preserved. Route registration itself
stays under `if (this.options.registerRoutes)`.
This is the shape the sibling hooks in this file already use and already name
("Registered independently of `registerRoutes` so an embedding that serves no
auth routes still gets the diagnosis") — the file applied the distinction to
the diagnosis hook and not to the wiring the diagnosis exists to report on.
The `#14319` describe block now runs against both values of `registerRoutes`
rather than only the default, and pins both acceptance criteria: the
composition completes either way, and a `registerRoutes: false` kernel still
mounts no auth routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…s row
The census row for `plugin-auth`'s session-resolution middleware anchors a
LINE NUMBER in `auth-plugin.ts`; the hook split moved that read site from
:1353 to :1380 without changing a character of it. Re-anchored with the gate's
own `--fix`, which is the repair it prescribes for pure line rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/deployment/self-hosting.mdx(via /api/v1/auth/* (route, a path literal in start))
  • content/docs/permissions/authentication.mdx(via /api/v1/auth/* (route, a path literal in start))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197ddapackageMentionDocs.

Which tree this was computed on

This run read content/docs from dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 — the merge of head 3583196089c29cdad2433f26fd662c5ed70b91ed into base 5a5336b399db2ef18dd4700f97d579a328197dda, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc0bd0eedb73aa3a16a76d29a8e590e919df35c1 && git checkout dc0bd0eedb73aa3a16a76d29a8e590e919df35c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5336b399db2ef18dd4700f97d579a328197dda 3583196089c29cdad2433f26fd662c5ed70b91ed && git checkout -B drift-repro 5a5336b399db2ef18dd4700f97d579a328197dda && git merge --no-ff 3583196089c29cdad2433f26fd662c5ed70b91ed
node scripts/docs-audit/affected-docs.mjs --json 5a5336b399db2ef18dd4700f97d579a328197dda

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5a5336b399db2ef18dd4700f97d579a328197dda → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716400023 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 63 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33716742053 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 65 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude