Skip to content

AUTH_ROUTE_LEDGER's client names are never resolved against a real client — the #3528 guard has an auth-shaped hole #11359

Closed

Description

@os-zhuang

Found while implementing #10974 (binding auth.setInitialPassword). Not a live wire defect today — every sdk row in AUTH_ROUTE_LEDGER happens to name a method that does exist. What is missing is the check that keeps it that way.

The claim that does not hold

packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts:152 is the auth ledger's "every sdk entry names its client method" test. Read literally, it checks only that the field is non-empty:

const sdkWithout = AUTH_ROUTE_LEDGER.filter((e) => e.disposition === 'sdk' && !e.client).map((e) => e.route);

!e.client is falsy-on-absent. A row spelled client: 'auth.setInitialPasword' (typo), or naming a method that was later renamed or deleted, passes this test unchanged.

The resolution check exists for every other ledger, and not this one

The sibling ledgers each have a client-side half that instantiates an ObjectStackClient and asserts typeof resolve(e.client) === 'function':

ledger resolution guard
ROUTE_LEDGER (dispatcher) packages/client/src/route-ledger-coverage.test.ts
REST_ROUTE_LEDGER packages/client/src/rest-route-ledger-coverage.test.ts
STORAGE_ROUTE_LEDGER packages/client/src/service-route-ledger-coverage.test.ts
I18N_ROUTE_LEDGER same file
DATASOURCE_ROUTE_LEDGER same file
AUTH_ROUTE_LEDGER none

Measured — every consumer of the symbol repo-wide:

packages/qa/dogfood/test/route-ledger-live-mount-parity.dogfood.test.ts
packages/client/src/client-url-conformance.test.ts      → uses r.route only (compiled into PATTERNS)
packages/client/src/route-ledger-response-schema.test.ts → uses rows for response-shape, not client resolution
packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts
scripts/check-ratchet-remedy-authority.mjs · scripts/check-auth-mount-ledger.mjs

Nothing in that list resolves an auth row's client string against a client instance. client-url-conformance.test.ts imports AUTH_ROUTE_LEDGER but reads r.route exclusively — it matches URLs the SDK builds against routes, which is the opposite direction and does not read the client field at all.

Why it matters

AUTH_ROUTE_LEDGER is the largest of the ledgers (56 rows, 54 of them sdk) and it is the one whose rows are written by hand alongside a vendor surface that moves under it. The failure this guard family was built for is stated in route-ledger-coverage.test.ts's own header:

This is the direction #3528 shipped through: the ledger equivalent of the day would have said "resume → automation.resume" while no such method existed.

That exact shape is currently unguarded for auth.

Note for whoever picks this up

#10974's body asserts that a sdk row naming a nonexistent method is "the exact failure packages/client/src/route-ledger-coverage.test.ts was written for". That test reads ROUTE_LEDGER (the dispatcher's), not AUTH_ROUTE_LEDGER, so it would not have caught an auth row. The sequencing argument in #10974 partly rests on that — worth reading together with it.

Likely shape of the fix

Add an AUTH_ROUTE_LEDGER leg to packages/client/src/service-route-ledger-coverage.test.ts (or a sibling file), reusing the brokenIn helper already there. The ledger is pure data with no imports, so the existing relative-source-import convention applies unchanged — no new package edge.

Refs

Found during #10974 · #3528 (the originating failure) · #3563 / #3636 (the guard family) · #3656 (the auth ledger's charter)

Activity

  1. added theissue type on Aug 23, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Aug 23, 2026
  3. changed the issue type fromtoon Aug 23, 2026
  4. claude commented on Aug 23, 2026

    @claude
    Contributor

    Triage: lands in packages/plugins/plugin-auth (conformance test tightening); rationale: the ledger test claims "every sdk entry names its client method" but only checks non-emptiness — restoring a declared invariant, no product fork. pm:queue, domain:services, Bug. Serial note: #10974 (in-flight, same auth surface) is where this was found — answer fold-or-serial against its file surface at dispatch.


    Generated by Claude Code

  5. claude commented on Aug 23, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 11359,
      "status": "done",
      "branch": "claude/issue-11359-auth-ledger-client-resolution",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11403",
      "premise_still_valid": true,
      "summary": "FOLD-OR-SERIAL: SERIAL. #10974's PR #11360 changes exactly two files (packages/client/src/index.ts + .changeset/client-auth-set-initial-password.md) — disjoint from this diff, so this proceeded independently. PLACEMENT: followed the CARD (packages/client), not the triage note (plugin-auth), on measurement: all 3 repo-wide files carrying a `typeof resolve(...) === 'function'` ledger guard live in packages/client/src and 0 live elsewhere; @objectstack/plugin-auth has no dependency on @objectstack/client in any of dependencies/devDependencies/peerDependencies, so the guard there would have created a backwards plugin-auth->client package edge; and the auth ledger's own header already says 'the client-side guard imports it as a relative SOURCE file'. Chose a sibling file, packages/client/src/auth-route-ledger-coverage.test.ts, over a fourth leg in service-route-ledger-coverage.test.ts (the card allowed either): that file's title and docblock are a statement about the #3636 SERVICE tranche ('all THREE server surfaces') and auth is a plugin, so folding it in would have made the file's own title false. Two legs: every client-naming row resolves to a function, plus a guard-the-guard leg (population floor + positive and negative controls derived from the ledger) that makes this card's ablation permanent. Also added a comment at the server-side test (auth-route-ledger.conformance.test.ts) recording that it checks PRESENCE only and naming where resolution now happens — the misleading title is what let this sit unnoticed. ZONE-2 ASSUMPTIONS: (1) resolved above in the card's favour; (2) confirmed and STRONGER than assumed — no new package edge AND no new cross-package radius, because packages/plugins/plugin-auth/src/auth-route-ledger.ts was already declared in CROSS_PACKAGE_TEST_INPUTS['@objectstack/client'] and already in turbo.json's @objectstack/client#test inputs (client-url-conformance.test.ts and route-ledger-response-schema.test.ts already read it the same way); (3) confirmed — 55 rows carry a `client` name (54 sdk + one `disabled` row that deliberately keeps its name, #7735 auth.deleteUser) and ALL 55 resolve. No live defect, so clause-② `no` still holds and needed no re-grade. ⚠️ TWO THINGS FOR YOU, NOT FIXED BY ME: (a) #10974's ordering argument cites 'the exact failure packages/client/src/route-ledger-coverage.test.ts was written for' — that test reads ROUTE_LEDGER, not AUTH_ROUTE_LEDGER, so it would not have caught an auth row and nothing would have until this PR. The argument's CONCLUSION survives and is strengthened (with no guard at all a premature auth row would have sat silent), but the authority cited for it is false. PR #11360 is still open/draft, so nothing has merged on it. (b) #10975's body is stale in a way that will mislead whoever picks it up: it says the `source: 'objectstack'` pin holds 'exactly three routes' and that the card takes it 'from 3 to 4' — that list grew to ELEVEN in #10534, so the PR-body argument #10975 demands is written against a number that no longer exists. DELIBERATELY NOT ADDED: a reverse-direction leg ('every client.auth.* method is backed by a ledger row', which the service file carries for storage and i18n). It would go red the moment #11360 merges and stay red until #10975 lands, inverting the maintainer-ruled option-C ordering — a separate decision, not a rider on a guard-hole card. No changeset: `skip-changeset`, label applied via the additive POST endpoint and READ BACK present alongside the size-labeler's own writes (size/m, tests, skip-changeset).",
      "tests": "All measurements taken at final commit 2de8c3ef71 (clean tree, `git rev-parse --short HEAD`), in worktree /home/user/objectstack-11359. Heavy steps serialized through scripts/pm/os-verify-lock.sh; verdicts read from each gate's own printed line with the exit code captured before any pipe (never a bare $? after a tail). BUILD FIRST: os-verify-lock -c \"pnpm --workspace-concurrency=2 --filter '@objectstack/client^...' build\" -> 'BUILD_EXIT=0' and 'os-verify-lock: VERDICT command-exit 0 · held the lock 375s · waited 240s'. SUITES: `pnpm --filter @objectstack/client test -- --maxWorkers=2` -> 'Test Files 24 passed (24) / Tests 316 passed (316)' (pnpm echoed '> @objectstack/client@17.2.0 test', so this was not a zero-match silent green). `pnpm --filter @objectstack/client typecheck` -> EXIT=0, and its check:test-typecheck leg printed 'OK — @objectstack/client's test layer compiles under packages/client/tsconfig.test.json; 0 file(s) / 0 error(s) held in test-typecheck-debt.json', so the new test file adds no debt. `pnpm --filter @objectstack/plugin-auth typecheck` -> EXIT=0. ABLATION (signature predicted in writing BEFORE running: typo auth.changePassword -> auth.changePasword; new leg red naming that row, old server-side check green). REBUILD QUESTION, ANSWERED: the mutation subject packages/plugins/plugin-auth/src/auth-route-ledger.ts is imported by the suite as a RELATIVE SOURCE path — no `exports` resolution and no vitest alias, so dist is not on its resolution path and no rebuild is needed for the mutation to reach the test; corroborated by check-test-source-alias's KNOWN_UNALIASED_TEST_IMPORTS, where @objectstack/client's dist-resolved imports are six package specifiers and @objectstack/plugin-auth is not among them. The dependency closure was built first anyway (above), so nothing ran against a stale dist. MUTATION CONFIRMED ON DISK by anchored grep in BOTH directions plus a control, not by the editor's exit code: removed text \"client: 'auth.changePassword'\" count = 0 (expect 0); injected text \"client: 'auth.changePasword'\" count = 1 (expect 1); untouched sibling row \"client: 'auth.changeEmail'\" count = 1 (expect 1); blob 123e1a87dced -> 2390fa5efc84. RESULTS: LEG0 baseline LEG0_EXIT=0 'Tests 2 passed (2)'. LEG1 new guard under mutation LEG1_EXIT=1, 'AssertionError: auth ledger entries claiming a client method that does not exist: POST /api/v1/auth/change-password -> client.auth.changePasword: expected [ Array(1) ] to deeply equal []', 'Tests 1 failed | 1 passed (2)' — exactly the predicted signature. LEG2, the point of the card: the EXISTING server-side check (plugin-auth auth-route-ledger.conformance.test.ts -t 'names its client method') under the SAME mutation LEG2_EXIT=0, 'Tests 1 passed | 9 skipped (10)' — green, waving the typo through. RESTORE LEG: restored blob 123e1a87dced equals the original, 'RESTORE=BYTE-IDENTICAL' via git hash-object, removed text back (count 1) and injected text gone (count 0); LEG3 re-run after restore LEG3_EXIT=0 'Tests 2 passed (2)'. Whole script ran under trap restore EXIT INT TERM. SECOND REVERSE CONTROL (on the one declaration this PR adds; predicted red before running): removing the new glob from CROSS_PACKAGE_TEST_INPUTS and its $TURBO_ROOT$ twin from turbo.json made check-cross-package-test-inputs exit 1 with 'FAIL: cross-package test inputs are not declared consistently. - @objectstack/client names path(s) no declared glob covers ... packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts (named in packages/client/src/auth-route-ledger-coverage.test.ts)'; both files restored and verified IDENTICAL against their HEAD blobs. So the declaration is load-bearing, not decorative. GATE UNION derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, NO hand-supplied paths, and RE-DERIVED after the final commit (same 4-path change set, same 12 families; stderr confirmed the answer came from objectstack-ai/objectstack at 9337cd8104 and that --repo held against the checkout's origin). All green: check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, node scripts/check-ci-filter-parity.mjs ('OK: all 89 declared cross-package glob(s) (77 unique) are covered by `core` or `crosspkg` ...'), node scripts/check-plugin-teardown-shape.mjs ('✓ ... 63 Plugin implementation(s) across 4536 source(s)'), node scripts/docs-audit/check-affected-docs.mjs; plus the convention-triggered families for a new test file: check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage — all EXIT=0. NOTE ON TWO OF THEM: `pnpm check:ci-filter-parity` and `pnpm check:plugin-teardown-shape` exited 254 (ERR_PNPM_NO_SCRIPT — those two have no package.json script alias); re-run as `node scripts/...mjs` they are the greens quoted above. DECLARED NARROWINGS (2, both stated rather than skipped): (1) `pnpm check:type-check-debt --re-measure` needs the whole workspace built and was NOT run locally; it cannot move here — plugin-auth's tsconfig.json excludes **/*.test.ts, its ledger entry is the hidden test layer (errors: 109), and this PR's only change to that package is comment lines inside an existing `it` body (no identifiers, no imports). The structural half, check:type-check-coverage, is green. (2) `pnpm lint` (eslint . --no-inline-config, repo-wide) narrowed to the changed files, with all three pieces of evidence: population read from eslint's OWN config (it reports turbo.json as 'File ignored because no matching configuration was supplied', so 3 of the 4 paths are in scope); counts read from --format json (4 entries, 0 errors, 1 warning and that warning is the turbo.json ignore notice, IDENTICAL on origin/main's turbo.json via a base-version control run); and configuration invariance for untouched files — eslint.config.mjs states in its own words that this repo 'runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, test or not', so no untouched file's verdict can move because of this diff. CI runs both in full regardless. CI STATUS AT REPORT TIME: in_progress — reported at draft-PR time per the standing clause, not after CI convergence.",
      "open_questions": [
        {
          "question": "#10974's ordering argument cites `packages/client/src/route-ledger-coverage.test.ts` as the guard that would catch a `sdk` row naming a nonexistent method. That is false — it reads ROUTE_LEDGER, not AUTH_ROUTE_LEDGER. Its PR #11360 is open and draft (needs:contract-review, pm:blocking), so nothing has merged on the false authority. What, if anything, should be corrected?",
          "options": [
            "A — leave both as-is: the conclusion (SDK method first, then the ledger row) is unchanged and in fact strengthened by this card, since before PR #11403 NOTHING guarded an auth row; the cited authority is decoration on a sound argument",
            "B — edit #10974's body to cite the real guard (now `packages/client/src/auth-route-ledger-coverage.test.ts`, which this PR adds) so a contract reviewer does not verify the ordering against a test that cannot see auth rows",
            "C — file a separate finding card recording the misattribution"
          ],
          "recommendation": "B, because #10974 carries needs:contract-review and pm:blocking: a reviewer asked to sign off on published-surface widening will read that sentence as a verified mechanical claim and check the named file, find ROUTE_LEDGER, and either stall or wave it through on a misread. One sentence in the body removes that, and after PR #11403 the corrected citation is a REAL guard rather than a second aspiration. A leaves a live trap in the one card that is explicitly under review; C spends a card on a sentence edit. This is your call, not mine — I did not touch another card's reasoning."
        },
        {
          "question": "#10975 (the follow-up that adds the `set-initial-password` ledger row, pm:blocked on #10974) requires its PR body to argue the `source: 'objectstack'` pin 'from 3 to 4'. That pin now holds ELEVEN routes — it grew in #10534 — so the acceptance criterion as written is against a number that no longer exists. Fix now or at dispatch?",
          "options": [
            "A — edit #10975's body now, while it is blocked and nobody is on it",
            "B — leave it and correct it in the dispatch words when #10975 is dispatched",
            "C — leave it entirely; the dev will read auth-route-ledger.conformance.test.ts and self-correct"
          ],
          "recommendation": "A, because the stale number is inside the card's ARGUE-IT requirement rather than in background prose, and a dev who takes '3 to 4' literally will write a PR body asserting something the file contradicts — cheap to fix while the card is parked, and it costs nothing to also note there that PR #11403 now makes that row's `client` name mechanically resolved, which is new information for whoever writes it."
        }
      ],
      "out_of_scope_findings": []
    }
    

    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions