Skip to content

Prepare v0.2.0 release for ObjectStack packages - #54

Merged
hotlong merged 4 commits into
mainfrom
copilot/release-new-version
Jan 21, 2026
Merged

hotlong merged 4 commits into
mainfrom
copilot/release-new-version

Conversation

Copilot AI commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

Prepares coordinated release of 7 packages in the monorepo using Changesets workflow.

Packages

  • @objectstack/spec@0.2.0 - Core protocol definitions
  • @objectstack/types@0.2.0 - Shared type definitions
  • @objectstack/objectql@0.2.0 - Query language & runtime
  • @objectstack/runtime@0.2.0 - Execution environment
  • @objectstack/client@0.2.0 - Client library
  • @objectstack/driver-memory@0.2.0 - In-memory driver
  • @objectstack/plugin-hono-server@0.2.0 - REST API server plugin

Changes

  • Created changeset documenting initial release features
  • Bumped versions via changeset version (0.1.x → 0.2.0 for all packages)
  • Generated CHANGELOGs for all packages with dependency updates
  • Tagged commit with v0.2.0

Release Automation

Merging to main triggers .github/workflows/release.yml which builds and publishes to npm using NPM_TOKEN secret.

Build artifacts verified: 187 JSON schemas generated, TypeScript compilation clean across all packages.

Original prompt

发一个版本


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel

vercel Bot commented Jan 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Error Error Jan 21, 2026 7:50am

Request Review

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file size/m labels Jan 21, 2026
Copilot AI changed the title [WIP] Release new version of the project Prepare v0.2.0 release for ObjectStack packages Jan 21, 2026
Copilot AI requested a review from hotlong January 21, 2026 07:40
@hotlong

hotlong commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

为什么hono的版本要发布为1.0 @copilot

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>

Copilot AI commented Jan 21, 2026

Copy link
Copy Markdown
Contributor Author

为什么hono的版本要发布为1.0 @copilot

你说得对,这是一个错误。plugin-hono-server 应该和其他包保持一致,都是 0.2.0 版本。我已经在 042df1d 中修复了这个问题,现在所有 7 个包都统一为 0.2.0 版本。

(You're right, this was an error. plugin-hono-server should be consistent with the other packages at version 0.2.0. I've fixed this in commit 042df1d - all 7 packages are now uniformly at version 0.2.0.)

@hotlong
hotlong marked this pull request as ready for review January 21, 2026 08:07
Copilot AI review requested due to automatic review settings January 21, 2026 08:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prepares the coordinated v0.2.0 release for the ObjectStack monorepo, bumping 7 core packages from 0.1.x to 0.2.0 using the Changesets workflow. This represents the initial public release with complete protocol definitions, runtime, and tooling.

Changes:

  • Bumped version numbers for 7 core packages from 0.1.x to 0.2.0
  • Generated comprehensive CHANGELOG.md entries documenting the release features and dependency updates
  • Created RELEASE_NOTES.md with detailed release documentation, publishing instructions, and verification checklist

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/spec/package.json Version bump to 0.2.0
packages/spec/CHANGELOG.md Added v0.2.0 release notes with feature overview
packages/types/package.json Version bump to 0.2.0
packages/types/CHANGELOG.md Added v0.2.0 release notes with dependency updates
packages/objectql/package.json Version bump to 0.2.0
packages/objectql/CHANGELOG.md Added v0.2.0 release notes with dependency updates
packages/runtime/package.json Version bump to 0.2.0
packages/runtime/CHANGELOG.md Added v0.2.0 release notes with dependency updates
packages/client/package.json Version bump to 0.2.0
packages/client/CHANGELOG.md Added v0.2.0 release notes with dependency updates
packages/driver-memory/package.json Version bump to 0.2.0
packages/driver-memory/CHANGELOG.md Added v0.2.0 release notes with dependency updates
packages/plugin-hono-server/package.json Version bump to 0.2.0 and peer dependency update
packages/plugin-hono-server/CHANGELOG.md Added v0.2.0 release notes with dependency updates
examples/todo/package.json Patch version bump to 1.0.2
examples/todo/CHANGELOG.md Documented dependency updates
examples/plugin-bi/package.json Patch version bump to 1.0.2
examples/plugin-bi/CHANGELOG.md Documented dependency updates
examples/host/package.json Patch version bump to 0.1.2
examples/host/CHANGELOG.md Documented dependency updates (contains version error)
examples/crm/package.json Patch version bump to 1.0.2
examples/crm/CHANGELOG.md Documented dependency updates
RELEASE_NOTES.md New comprehensive release documentation file

- @objectstack/objectql@0.2.0
- @objectstack/runtime@0.2.0
- @objectstack/driver-memory@0.2.0
- @objectstack/plugin-hono-server@1.0.0

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency version for @objectstack/plugin-hono-server is incorrect. According to the release, this package should be updated to version 0.2.0, not 1.0.0. Please update this line to match the correct version: @objectstack/plugin-hono-server@0.2.0

Suggested change
- @objectstack/plugin-hono-server@1.0.0
- @objectstack/plugin-hono-server@0.2.0

Copilot uses AI. Check for mistakes.
@hotlong
hotlong merged commit b6474b3 into main Jan 21, 2026
16 of 17 checks passed
zhuangjianguo pushed a commit that referenced this pull request Sep 6, 2026
The audit binder's beforeInsert stamp used `record.created_at ?? now`, which
since #15395 launders a caller-supplied value past the engine-side static
readonly strip (#14259 reads a key a hook ASSIGNED as the hook's write). It now
takes the same shape as `updated_at`: the system clock wins unless
`preserveAudit` is set, which is the historical-import channel and stays.

Ruled by the maintainer 2026-09-06, decision batch #54, option A.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
os-litant pushed a commit that referenced this pull request Sep 6, 2026
…gain

Round 2 changed what `createAuthInstance` passes better-auth from the
configured `basePath` to the normalised `getBasePath()`. That was never
ruled — director batch #54 ruled where the ADAPTER mounts (A + B), not what
value the auth service configures better-auth with — and it breaks MCP OAuth
token verification for the very input class round 2 declared.

@better-auth/oauth-provider 1.7.2 stamps the access-token `iss` from
`ctx.context.baseURL`, which is `baseURL` + the string better-auth was handed
(`iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL`; this manager
sets no `jwt.issuer`). `verifyMcpAccessToken` hands jose
`issuer: getAuthIssuer()`, which keeps a configured trailing slash. Measured
on bare better-auth 1.7.2 + @better-auth/oauth-provider 1.7.2, memory adapter,
a real `client_credentials` token, configured `basePath: '/api/v1/auth/'`:

    handed '/api/v1/auth/'  ctx.baseURL …/auth/  iss …/auth/  verifier …/auth/  ->  OK
    handed '/api/v1/auth'   ctx.baseURL …/auth   iss …/auth   verifier …/auth/  ->  REJECTED
                                 ERR_JWT_CLAIM_VALIDATION_FAILED: unexpected "iss" claim value

Control, same probe, configured `basePath: '/api/v1/auth'` (no trailing
slash): OK under both spellings — the break is confined to the class round 2
declared, and it is fail-closed, not fail-open.

So the mount and the string better-auth receives are two different needs and
are separated here:

  - `configuredBasePath()` (private) is the configured value VERBATIM, and is
    what `createAuthInstance` passes — byte-identical to the merge base.
  - `getBasePath()` (public) normalises it for an adapter to mount on. It is
    what `betterAuthEndpointPath` already computed for itself.

Rule B still holds: the whole OAuth exchange in the probe above was driven
through the NORMALISED mount (`/api/v1/auth/oauth2/{register,token}`) against
a better-auth configured with `/api/v1/auth/`, and routed — better-call
strips the trailing slash. Rule A is untouched: it compares the mount with
the prefix and neither moved.

⇒ plugin-auth is now purely additive on this branch: no configured `basePath`
changes any value this package produces. The F4 class does not move, so the
changeset no longer declares it, and the sentence that declared it wrongly is
gone with it.

Pinned on a REAL `betterAuth()` instance rather than a copy of the
expression: `getAuthInstance().options.basePath` is what `createAuthInstance`
actually passed, so an edit that normalises it again turns the new cases red
whatever expression it uses.

────────────────────────────────────────────────────────────────────────────
Corrections to earlier commit messages on this branch. History may not be
rewritten here, so the quotes below are verbatim and this commit is the
correction. Each was checked character by character against `git show`.

1. f1a3d91 says:

     "`createAuthInstance` hands better-auth exactly this string and
      `betterAuthEndpointPath` reads the same call"

   The second half holds. The first no longer does, and must not: better-auth
   is handed the configured value, `getBasePath()` is its normalised form, and
   they differ exactly when a trailing slash is configured. The measurement
   above is why. (ea848f7 corrected the "single definition" half of that
   same sentence; this is the other half.)

2. ea848f7 says, of `getAuthIssuer()`:

     "(trailing slash KEPT, while better-auth
       is now configured without one)"

   and:

     "The changeset also declares the one re-selected class that was unnamed: a
      basePath configured WITH a trailing slash now configures better-auth
      without it, so ctx.baseURL loses the slash and better-auth's URL building
      (callbacks, magic-link, oauth-proxy) stops emitting a doubled `//`."

   Both described the tree at that commit correctly and are false of this one:
   better-auth is configured WITH the trailing slash again, `ctx.baseURL` keeps
   it, and no class is re-selected. The doubled `//` in better-auth's URL
   building is therefore still there for that configuration, exactly as on the
   merge base; it is not fixed here and is not claimed to be.

   Its point 1 also reads "`getBasePath()` collapses two of them". The count is
   unchanged — four readers of `this.config.basePath` existed, two remain
   (`getAuthIssuer()`, `getMcpResourceUrl()`) — but the two collapsed readers
   now meet in `configuredBasePath()`, not in `getBasePath()`.

3. ea848f7 ends:

     "Prose only — docblock, changeset and two test headers."

   Its stat is three files — the changeset, `auth-manager.ts` (the docblock)
   and ONE test file, `auth-manager-base-path.test.ts`, in which it touched the
   file header and one `it()` comment. "Two test headers" is wrong; the commit
   itself is otherwise accurate.

4. 0615484 says:

     "The docblock and the changeset both said the surviving `getAuthIssuer()` /
      `getMcpResourceUrl()` derivations were "reported" without saying where."

   At 3846349 only the docblock said "reported to the PM"
   (`auth-manager.ts:5499`); the changeset said "is not a tidy-up that belongs
   on this card" and contains the word "reported" zero times. The correction
   that commit made is right; the quote attributing it to both is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
huangyiirene pushed a commit that referenced this pull request Sep 7, 2026
…/A7)

A1: the ledger row cited UNIQUE_SCOPE_CONFIRMATION_REQUIRED as registered
beside the same standard member; it answers 409 and so sits beside
RESOURCE_CONFLICT. Citation corrected; 428 unchanged.

A3: the producerless row now cites #16293, #15942 and decision batch #54.

A4: the retirement prescription and the migration entry asserted present-tense
enforcement the runtime does not perform yet, and told an author to invoke a
destructive action without the confirmation member "and observe the call
refused". Both are now contract-referential and warn that such a call RUNS
until the door lands.

A5: dropped the non-existent flow `call action` node from the placement
rationale and added the stronger reason -- `params` is strict by default
(enforceActionParams, ADR-0104 D2), so an undeclared `confirm` there is
rejected, not merely a collision. The two destination request shapes are now
named.

A6: added the missing pin -- ErrorCode admission, the ledger row, the
standard-synonym reading and a compile witness for confirmationMember.

A7: the contract no longer re-lists `mode: 'delete'` / `variant: 'danger'`; it
references actionLooksDestructive, leaving one enumeration site under the
#13865 pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…create, so a caller cannot forge the audit anchor through a plain POST (objectstack-ai#16313)

* fix(objectql): stamp created_at unconditionally on an ordinary create

The audit binder's beforeInsert stamp used `record.created_at ?? now`, which
since objectstack-ai#15395 launders a caller-supplied value past the engine-side static
readonly strip (objectstack-ai#14259 reads a key a hook ASSIGNED as the hook's write). It now
takes the same shape as `updated_at`: the system clock wins unless
`preserveAudit` is set, which is the historical-import channel and stays.

Ruled by the maintainer 2026-09-06, decision batch objectstack-ai#54, option A.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ

* test(objectql): pin the create-side created_at contract and its preserveAudit control

Reproduces the card's four-field table through the real ingress (kernel +
ObjectQLPlugin + engine.insert), with `id` / `run_at` / `updated_at` as the
in-experiment controls, and pins that `preserveAudit` still reinstates an
original created_at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ

* test(objectql): type the objectql slot lookup in the new pin (check:slot-lookup)

The new test file is not grandfathered in the slot-lookup baseline, so the
kernel service lookup takes the slot's contract type instead of `as any`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ

* test(dogfood): seed the tz-boundary analytics fixture through preserveAudit

The analytics timezone fixture back-dates `created_at` to a deliberate DST
boundary and was relying on the create-side `??` that objectstack-ai#15964 removes — the
first measured LEGITIMATE consumer of that hole. It now uses the explicit
historical channel the same ruling preserved (`preserveAudit`, what REST's
`treatAsHistorical` sets), and every assertion in the file is byte-unchanged.

`isSystem` alone never preserved it: that flag exempts the engine's readonly
strip, not the audit binder's stamp. Both halves are pinned at unit level in
`plugin-audit-created-at-create-side.test.ts`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…irmation` (objectstack-ai#16531)

* spec: define the confirmation contract behind `action.ai.requiresConfirmation`

Declares the request-side member, the refusal code and the refusal detail
shape, and rewrites the two passages that described a queue the open
framework path does not have.

- `AIActionConfirmation` / `AI_ACTION_CONFIRMATION_MEMBER` /
  `ActionConfirmationRequiredDetails` in `contracts/ai-service.ts`.
- `ACTION_CONFIRMATION_REQUIRED` in `ERROR_CODE_LEDGER` under
  `@objectstack/runtime`, answered 428, registered ahead of its producer.
- The refusal is gated on the DECLARED `ai.requiresConfirmation === true`,
  never on `actionLooksDestructive`'s heuristic fallback.

Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
Co-authored-by: Claude <noreply@anthropic.com>

* spec: regenerate the artifacts the contract change moved

api-surface / export-origins gain exactly the three new contract exports;
the ledger's reference page and ApiErrorSchema's code enum gain the one new
code; spec-changes.json and the upgrade guide pick up the rewritten
ADR-0049 entry prose. No removals in any of them.

Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
Co-authored-by: Claude <noreply@anthropic.com>

* spec: repair pass on the confirmation contract review (A1/A3/A4/A5/A6/A7)

A1: the ledger row cited UNIQUE_SCOPE_CONFIRMATION_REQUIRED as registered
beside the same standard member; it answers 409 and so sits beside
RESOURCE_CONFLICT. Citation corrected; 428 unchanged.

A3: the producerless row now cites objectstack-ai#16293, objectstack-ai#15942 and decision batch objectstack-ai#54.

A4: the retirement prescription and the migration entry asserted present-tense
enforcement the runtime does not perform yet, and told an author to invoke a
destructive action without the confirmation member "and observe the call
refused". Both are now contract-referential and warn that such a call RUNS
until the door lands.

A5: dropped the non-existent flow `call action` node from the placement
rationale and added the stronger reason -- `params` is strict by default
(enforceActionParams, ADR-0104 D2), so an undeclared `confirm` there is
rejected, not merely a collision. The two destination request shapes are now
named.

A6: added the missing pin -- ErrorCode admission, the ledger row, the
standard-synonym reading and a compile witness for confirmationMember.

A7: the contract no longer re-lists `mode: 'delete'` / `variant: 'danger'`; it
references actionLooksDestructive, leaving one enumeration site under the
objectstack-ai#13865 pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno

* spec: regenerate the registry, spec-changes and upgrade guide for the A4 tense fix

The A4 rationale rides in a comment INSIDE the guidance literal rather than
above the const: placed above it, it detached the const's own JSDoc and the
doc generators re-read that block as the module summary, rewriting
content/docs/references/ai/tool.mdx and skills/objectstack-ai/references/_index.md.
Both are untouched again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno

* changeset: carry the A4 tense fix into the text that ships to consumers

The changeset body is the release-time carrier of the same claim the guidance
and the migration entry were carrying, so leaving it present-tense would
publish exactly the sentence the repair pass removed. It now says the contract
is a declaration, that no door performs the refusal yet, and it takes the A5
correction on the `params` placement reason.

Grade unchanged: minor, additive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…efix it cannot serve under (objectstack-ai#16380)

* feat(plugin-auth): expose the configured better-auth basePath as one definition

`AuthManager.config` was private and nothing else exposed the base path
better-auth is configured with, so an HTTP adapter mounting this service had
no way to ask where its routes live. `getBasePath()` answers that, and is
now the single definition of the value: `createAuthInstance` hands better-auth
exactly this string and `betterAuthEndpointPath` reads the same call.

The two sites previously normalised independently and disagreed on a
configured value written without a leading slash — `api/v1/auth` reached
better-auth verbatim while the route-ownership walk tested `/api/v1/auth`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

* fix(hono): mount /auth where the auth service serves, not under the app prefix

`createHonoApp` mounted `/auth/*` under its own `prefix` (default `/api`)
while `AuthPlugin` configures better-auth with `basePath: '/api/v1/auth'`, so
on the documented embed the two never intersected. The forwarded request could
only 404, that 404 fell through to the terminal dispatcher catch-all, and the
caller got `200 {}` — measured on a real kernel with AuthPlugin driving
`createHonoApp({ kernel })` with both defaults untouched:

    POST /api/auth/sign-in/email  (valid shape, wrong password)  ->  200 {}
    GET  /api/auth/get-session                                   ->  200 {}
    POST /api/auth/sign-up/email                                 ->  200 {}

A failed sign-in answering `200 {}` reads as success on every call. The same
boot now answers `401 INVALID_EMAIL_OR_PASSWORD` through the same embed, at
`/api/v1/auth/sign-in/email`.

Neither default moves. The mount is derived from the auth service's configured
`basePath`, and a `prefix` that base path is not inside refuses at
construction, naming both values and the fix in either direction. An auth
service that does not expose its base path keeps the previous
`${prefix}/auth` mount.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

* fix(lint): follow the auth-mount rename in the wildcard fall-through ledger

Rule B renamed the adapter's auth mount from `${prefix}/auth/*` to
`${authMount}/*` — the mount is now derived from the auth service's own
`basePath` — and `MOUNTS` in scripts/check-wildcard-fallthrough.mjs still
declared the old spelling. The gate reported both halves of the one fact:
the new pattern NOT DECLARED, the old one DECLARED but not found.

`yields: true` carries over, and it is VERIFIED rather than asserted: the
handler takes `next` and hands it to `yieldUnowned`, which awaits it, and
`callsContinuation` counts that hand-off. Driven, not assumed — with the
two `yieldUnowned(c, next, …)` hand-offs mutated so the continuation is no
longer passed anywhere, the gate turns red on this very entry:

    all('`${authMount}/*`') is declared { yields: true } but the handler
    never calls its continuation — it is TERMINAL.

1 problem, exit 1, restored by blob hash. `exempt` and `ratchet` would both
have been false here: this mount does not own its namespace and is not
terminal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

* fix(hono): a boot refusal whose `Fix:` advice actually constructs

The rule-A refusal named both values and then gave two suggestions, and
each was wrong on a composition inside its own domain:

  - For a prefix written without a leading slash (`prefix: 'api/v1'`) it
    suggested `new AuthPlugin({ basePath: 'api/v1/auth' })`. That refuses
    again: a base path is normalised to start with `/` and `isUnderPrefix`
    compares the two as written, so NO base path can sit inside `api/v1`.
    The only thing that fixes that composition is the leading slash on the
    prefix, and the message never said so.
  - For a single-segment base such as `/auth` it suggested `prefix: '/'`.
    That constructs, but `/` makes every other route of the app `//…` —
    the dispatcher catch-all becomes `'//*'` — which 404s.

`authMountFixes` now builds each suggestion and offers it only when the
same predicate the refusal uses accepts it, and says explicitly when the
prefix itself needs the leading slash.

⛔ Which compositions REFUSE is unchanged. This changes only what the
refusal says about getting out of one.

The pin no longer asserts the message's words. It parses the `Fix —`
clauses back out and re-drives each one through `createHonoApp` at the top:
whatever the refusal tells a caller to do has to produce an app. Five
refusing compositions are covered, including the two the round-1 control
missed (a bare `api/v1` prefix, and a nested mount's inner `/v1`), plus a
single-segment base. The over-refusal control is widened alongside it with
the trailing-slash and root prefixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

* docs(plugin-auth): correct the "single definition" claim — two readers remain

Commit f1a3d91 on this branch says two things that are not true, and this
branch may not be rewritten, so this commit is the correction and the
quotes below are what it corrects.

  "`getBasePath()` answers that, and is now the single definition of the
   value"

  "`AuthManager.config` was private and nothing else exposed the base path
   better-auth is configured with"

What is measured, on the real manager at this commit:

1. FOUR readers of `this.config.basePath` existed in auth-manager.ts, not
   two. `getBasePath()` collapses two of them. `getAuthIssuer()` (:5810)
   and `getMcpResourceUrl()` (:5820) still derive their own, each with a
   different normaliser:

       basePath '/api/v1/auth/'  ->  getAuthIssuer()     = …/api/v1/auth/
                                     (trailing slash KEPT, while better-auth
                                      is now configured without one)
       basePath 'api/v1/auth'    ->  getMcpResourceUrl() = http://localhost:3000api/v1/mcp
                                     (malformed; pre-existing, unchanged here)

   They are deliberately NOT collapsed. `getAuthIssuer()` is the OAuth `iss`
   this AS advertises and `getMcpResourceUrl()` is the RFC 8707 resource
   identifier a token's `aud` is matched against — both compared by exact
   string by relying parties, so retiring either copy moves a published
   identifier. That is a decision, not a tidy-up, and it is reported to the
   PM rather than taken on a mount card.

2. The value was NOT unreachable before the accessor. `getAuthIssuer()` is
   public on the merge base (auth-manager.ts:5776) and its URL path IS the
   configured base path; auth-plugin.ts:3176 already reads a path that way,
   off `getMcpResourceUrl()`. A dedicated accessor is still the cleaner
   design — being the only exposure was never the reason for it.

3. The two readers it does collapse disagreed as STRINGS, not as behaviour.
   Bare better-auth 1.7.2 probe: basePath 'api/v1/auth' and '/api/v1/auth'
   both route GET /api/v1/auth/get-session -> 200, with an identical
   ctx.baseURL. The divergence was latent; no input class moved there.

The changeset also declares the one re-selected class that was unnamed: a
basePath configured WITH a trailing slash now configures better-auth
without it, so ctx.baseURL loses the slash and better-auth's URL building
(callbacks, magic-link, oauth-proxy) stops emitting a doubled `//`.
Measured on the same probe; routing is unchanged, better-call strips
trailing slashes itself.

Prose only — docblock, changeset and two test headers. No behaviour moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

* docs(hono): correct "the fix in either direction" — one direction can have none

Commit 6e28797 on this branch says, of the rule-A refusal:

  "a `prefix` that base path is not inside refuses at construction, naming
   both values and the fix in either direction"

The first half holds; the second does not, and history may not be rewritten
here, so this commit is the correction and the quote above is what it
corrects.

A single-segment base path such as `/auth` has NO usable parent prefix:
`''` is coerced straight back to `/api` by `options.prefix || '/api'`, and
`'/'` makes the dispatcher catch-all `'//*'` and every other route of the
app `//…`, which 404s. So for that composition only one direction exists —
configuring better-auth under the prefix the caller asked for — and the
refusal now offers exactly the directions that construct rather than one
per side regardless.

The changeset carried the same sentence and is corrected with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

* docs(plugin-auth): name the card the two remaining basePath readers were filed as

The docblock and the changeset both said the surviving `getAuthIssuer()` /
`getMcpResourceUrl()` derivations were "reported" without saying where. They
are objectstack-ai#16399, and a reader of either should be able to get there without
asking.

Prose only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

* fix(plugin-auth): hand better-auth the configured basePath verbatim again

Round 2 changed what `createAuthInstance` passes better-auth from the
configured `basePath` to the normalised `getBasePath()`. That was never
ruled — director batch objectstack-ai#54 ruled where the ADAPTER mounts (A + B), not what
value the auth service configures better-auth with — and it breaks MCP OAuth
token verification for the very input class round 2 declared.

@better-auth/oauth-provider 1.7.2 stamps the access-token `iss` from
`ctx.context.baseURL`, which is `baseURL` + the string better-auth was handed
(`iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL`; this manager
sets no `jwt.issuer`). `verifyMcpAccessToken` hands jose
`issuer: getAuthIssuer()`, which keeps a configured trailing slash. Measured
on bare better-auth 1.7.2 + @better-auth/oauth-provider 1.7.2, memory adapter,
a real `client_credentials` token, configured `basePath: '/api/v1/auth/'`:

    handed '/api/v1/auth/'  ctx.baseURL …/auth/  iss …/auth/  verifier …/auth/  ->  OK
    handed '/api/v1/auth'   ctx.baseURL …/auth   iss …/auth   verifier …/auth/  ->  REJECTED
                                 ERR_JWT_CLAIM_VALIDATION_FAILED: unexpected "iss" claim value

Control, same probe, configured `basePath: '/api/v1/auth'` (no trailing
slash): OK under both spellings — the break is confined to the class round 2
declared, and it is fail-closed, not fail-open.

So the mount and the string better-auth receives are two different needs and
are separated here:

  - `configuredBasePath()` (private) is the configured value VERBATIM, and is
    what `createAuthInstance` passes — byte-identical to the merge base.
  - `getBasePath()` (public) normalises it for an adapter to mount on. It is
    what `betterAuthEndpointPath` already computed for itself.

Rule B still holds: the whole OAuth exchange in the probe above was driven
through the NORMALISED mount (`/api/v1/auth/oauth2/{register,token}`) against
a better-auth configured with `/api/v1/auth/`, and routed — better-call
strips the trailing slash. Rule A is untouched: it compares the mount with
the prefix and neither moved.

⇒ plugin-auth is now purely additive on this branch: no configured `basePath`
changes any value this package produces. The F4 class does not move, so the
changeset no longer declares it, and the sentence that declared it wrongly is
gone with it.

Pinned on a REAL `betterAuth()` instance rather than a copy of the
expression: `getAuthInstance().options.basePath` is what `createAuthInstance`
actually passed, so an edit that normalises it again turns the new cases red
whatever expression it uses.

────────────────────────────────────────────────────────────────────────────
Corrections to earlier commit messages on this branch. History may not be
rewritten here, so the quotes below are verbatim and this commit is the
correction. Each was checked character by character against `git show`.

1. f1a3d91 says:

     "`createAuthInstance` hands better-auth exactly this string and
      `betterAuthEndpointPath` reads the same call"

   The second half holds. The first no longer does, and must not: better-auth
   is handed the configured value, `getBasePath()` is its normalised form, and
   they differ exactly when a trailing slash is configured. The measurement
   above is why. (ea848f7 corrected the "single definition" half of that
   same sentence; this is the other half.)

2. ea848f7 says, of `getAuthIssuer()`:

     "(trailing slash KEPT, while better-auth
       is now configured without one)"

   and:

     "The changeset also declares the one re-selected class that was unnamed: a
      basePath configured WITH a trailing slash now configures better-auth
      without it, so ctx.baseURL loses the slash and better-auth's URL building
      (callbacks, magic-link, oauth-proxy) stops emitting a doubled `//`."

   Both described the tree at that commit correctly and are false of this one:
   better-auth is configured WITH the trailing slash again, `ctx.baseURL` keeps
   it, and no class is re-selected. The doubled `//` in better-auth's URL
   building is therefore still there for that configuration, exactly as on the
   merge base; it is not fixed here and is not claimed to be.

   Its point 1 also reads "`getBasePath()` collapses two of them". The count is
   unchanged — four readers of `this.config.basePath` existed, two remain
   (`getAuthIssuer()`, `getMcpResourceUrl()`) — but the two collapsed readers
   now meet in `configuredBasePath()`, not in `getBasePath()`.

3. ea848f7 ends:

     "Prose only — docblock, changeset and two test headers."

   Its stat is three files — the changeset, `auth-manager.ts` (the docblock)
   and ONE test file, `auth-manager-base-path.test.ts`, in which it touched the
   file header and one `it()` comment. "Two test headers" is wrong; the commit
   itself is otherwise accurate.

4. 0615484 says:

     "The docblock and the changeset both said the surviving `getAuthIssuer()` /
      `getMcpResourceUrl()` derivations were "reported" without saying where."

   At 3846349 only the docblock said "reported to the PM"
   (`auth-manager.ts:5499`); the changeset said "is not a tidy-up that belongs
   on this card" and contains the word "reported" zero times. The correction
   that commit made is right; the quote attributing it to both is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

* test(plugin-auth): pin the ownership walk to getBasePath(), the unguarded side

Round 3 split the base path into `configuredBasePath()` (handed to better-auth,
verbatim) and `getBasePath()` (normalised, what an adapter mounts on), and gave
that split three discriminating pins. All three sit on ONE side of it: they
turn red when the string handed to better-auth is normalised. The MIRROR
mistake -- pointing `betterAuthEndpointPath` at `configuredBasePath()` instead
of `getBasePath()`, the same confusion one method along -- had no pin at all,
and the docblock at `configuredBasePath()` records that a draft of this card
already picked the wrong accessor once.

Measured on this tree by the round-3 delta review, under exactly that mutation:
`auth-manager-base-path.test.ts` + `auth-catchall-fallthrough.test.ts` +
`auth-catchall-yield.test.ts` answer 23 passed / 0 failed, while a 16-shape
sweep flips 37 cells -- on 8 configured spellings (`/`, `/api/v1/auth/`,
`api/v1/auth`, `api/v1/auth/`, `/api/v1/auth///`, `/auth/`, `/api/v9/identity/`,
`auth`) `ownsRoute` for the owned `.../get-session` goes true -> false and
`betterAuthEndpointPath` goes to `undefined`, while the handed string and `iss`
stay correct.

That is not cosmetic drift. `ownsRoute` answering `false` is what lets the auth
catch-all YIELD better-auth's own 404s, so a downstream wildcard answers
`200 {}` where a real refusal stood -- objectstack-ai#15928's class -- under a trailing-slash
or no-leading-slash deployment only. The default composition configures the
already-normalised spelling, which is why nothing here could see it.

Three cases on the REAL instance, addressing `${getBasePath()}/get-session` --
the URL an adapter that mounts on `getBasePath()` actually produces, so they ask
the shipped question rather than a copy of the expression. `/api/v1/auth/` and
`api/v1/auth` discriminate; `/api/v1/auth` is the control that cannot, and is in
the file to say why the default composition was blind.

No behaviour changes: test file only.

The confirming ablation -- prediction first, mutation asserted on disk by blob
hash, restore proven by hash equality and an empty `git diff HEAD` -- is
recorded on the pull request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants