Version Packages - #969
Merged
Merged
Conversation
|
Deployment failed for project stack-docs with the following error: Learn More: https://vercel.com/open-saas?upgradeToPro=build-rate-limit |
github-actions
Bot
force-pushed
the
changeset-release/main
branch
5 times, most recently
from
August 19, 2026 19:57
ffbbc6e to
64d74d2
Compare
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 19, 2026 19:58
64d74d2 to
0837842
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 22, 2026 01:37
0837842 to
cb844a0
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 22, 2026 02:01
cb844a0 to
2017c57
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 22, 2026 02:27
2017c57 to
24a8d28
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 22, 2026 02:51
24a8d28 to
6a3ce02
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 22, 2026 03:09
6a3ce02 to
b79ae9e
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
6 times, most recently
from
August 22, 2026 05:56
4cffe7a to
f52ba93
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
4 times, most recently
from
August 22, 2026 06:54
7c624f6 to
ebfc6a7
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
11 times, most recently
from
August 22, 2026 10:34
ac6e315 to
5895043
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 22, 2026 11:08
5895043 to
c7a5689
Compare
borisno2
enabled auto-merge (squash)
August 22, 2026 11:13
Contributor
Author
Coverage Report for Core Package Coverage (./packages/core)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for UI Package Coverage (./packages/ui)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for CLI Package Coverage (./packages/cli)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for Auth Package Coverage (./packages/auth)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for Storage Package Coverage (./packages/storage)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for RAG Package Coverage (./packages/rag)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)
File CoverageNo changed files found. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@opensaas/stack-auth@0.40.0
Minor Changes
#1015
72c4ba3Thanks @borisno2! - Bump thebetter-authdev dependency to1.7.1and move the peer range off the stale^1.3.29floor to^1.4.0(the release line where better-auth'sindex: trueflags — the basis for #937's index emission — first shipped; below it the generated schema silently omitted indexes better-auth itself declares).New required
account.issuercolumn. better-auth 1.7 adds a requiredissuercolumn to itsaccountmodel. BecausederiveAuthListsderives the Auth lists from better-auth's owngetAuthTables()(#987/#997), this column now appears in the generated schema automatically — no code change was needed, only verification against real generated output. Existing projects upgrading tobetter-auth@^1.7will see a new NOT NULL column on theiraccounttable and need a backfill for existing rows. Following better-auth's owncreateLocalAccountIssuer/createOAuthAccountIssuerhelpers (@better-auth/core/db):"Account"/`Account`above is the stack's own greenfield default table name — substitute your project's actual (and, on Postgres, schema-qualified) table name if you renamed it viaauthPlugin({ account: { tableName } })or adopted an existing install withadoptBetterAuthTables({ useBetterAuthTableNames: true })(physical tableaccount, commonly under a non-publicschema).URL-encode
providerIdif it can contain characters outside[A-Za-z0-9_-]. If you configured a custom OIDC provider with its own issuer URL, use that provider's real issuer instead of the syntheticlocal:oauth:value.The new
@@unique([issuer, accountId])constraint is not yet emitted. better-auth 1.7 also declares this composite unique index at the table level, but the stack only derives field-levelunique/indexflags today — table-level index derivation is #985, which hasn't landed. This is deliberately out of scope here (per #986's own triage note: build on #985 once it lands, don't duplicate it). When it does land, make sure your backfilledissuervalues don't collide on(issuer, accountId)for any account, or the constraint will fail to apply.Breaking (MCP plugin users only):
@better-auth/mcpis now a separate package. better-auth 1.7 split themcpplugin out ofbetter-auth/pluginsinto its own package, rebuilt on the OAuth Provider RFC 8707/9728 resource model.@opensaas/stack-auth/pluginsnow re-exportsmcpfrom@better-auth/mcp(added as an optional peer — install it if you use MCP). The plugin also now requires aresourceoption:better-auth 1.7's MCP plugin also declares a substantially different OAuth table set — the old
oauthApplication/oauthAccessToken/oauthConsentthree became seven tables (oauthClient/oauthAccessToken/oauthConsent/oauthRefreshToken/oauthResource/oauthClientResource/oauthClientAssertion). Since the derivation is schema-driven this needed no code changes, but if you have the MCP plugin enabled, runningpnpm generatewill produce a significantly different Prisma schema for these tables (new/renamed models, andSessiongains reverse relations to the two token tables that now reference it). Review the diff and migrate your database accordingly.Workspace-wide: pins
@better-auth/utilsto0.5.0via a rootpnpm.overrides. better-auth 1.7.1's own published packages disagree on this transitive dependency —better-authpins it at exactly0.4.2whilebetter-call(used by@better-auth/core,@better-auth/oauth-provider, and@better-auth/mcp) requires^0.5.0— so pnpm resolves two separate physical instances of@better-auth/coredepending on which peer chain a given package sits in. That split is invisible at runtime but breaks TypeScript:jwt()(frombetter-auth/plugins) andmcp()(from@better-auth/mcp) end up typed against different@better-auth/coreinstances, sobetterAuthPlugins: [jwt(), mcp(...)]fails to type-check with aBetterAuthPluginstructural-mismatch error even though both plugins are otherwise correctly configured. The override forces one instance workspace-wide. If you hit the same error in your own app, add the equivalent override to your ownpackage.json.Also fixes two
deriveAuthListsgaps surfaced by the MCP plugin's expanded schema:defaultValuethrough fornumber-typed fields (integer()/bigInt()), matching the existingstring/booleanbehavior (e.g.oauthResource.policyVersion, which defaults to1)createdAt/updatedAtupstream (several of the new OAuth tables declarecreatedAtalone) is no longer silently dropped — it derives as an ordinary required column instead. Previously any model with an asymmetric timestamp pair had the field skipped entirely with no replacement, which crashed the first real write that supplied it (better-auth's own OAuth Provider does this atbetterAuth()init time, seeding anoauthResourcerow)#1019
77b7314Thanks @borisno2! - Extend the ADR-0036 credential field read-deny from the four base Auth models to better-auth plugin tables, and add acredentialFieldsconfig option for plugins the stack doesn't seed a set for.The following fields now ship field-level
read-denied, on top of the existingSession.token/Verification.value/Account.password/accessToken/refreshToken/idToken:oauthClient.clientSecret,oauthAccessToken.token,oauthRefreshToken.token(themcp/oauth-provider plugin)twoFactor.secret,twoFactor.backupCodes(twoFactor())An application opening one of these lists (e.g. declaring
OauthClientunder its ownliststo grant access) no longer also exposes the credential column — same behavior as the existing base-model deny,sudo()still reads it.For a plugin the stack has no seeded credential set for, mark a field yourself:
credentialFieldsis additive only — it can add fields to any model (including a seeded one) but can never unmark a seeded field. An entry naming a field missing from a model your app actually derives throws at config time; an entry for a model your app doesn't derive is a no-op.#1017
b30fa61Thanks @{! - Let an application declare model-level indexes (db.indexes) on the derived auth lists (User/Session/Account/Verification/RateLimit).Each per-model block in
authPlugin()now acceptsindexes, in the same shape as a list's owndb.indexes:An entry covering a column the stack already derives an index for (e.g.
User.email) suppresses that derived index for that column and emits only the app's entry, rather than erroring — the application's declaration wins (ADR-0035). Suppression is per-column: every other derived index on the model is unaffected.This also fixes a related generator gap: a list's
db.indexescan now referencecreatedAt/updatedAteven when the list has no explicit field for them and relies ondb.timestampsfor the auto-injected columns (previously only a list with an explicitly declaredcreatedAt/updatedAtfield could be indexed on it).#1005
b67fdd1Thanks @borisno2! - Better-auth plugin tables (e.g. the MCP plugin'soauthApplication/oauthAccessToken/oauthConsent) are now derived through the same registry as the four base Auth models, instead of a separate converter that dropped every reference to a bare column. Reference fields now become realrelationship()foreign keys with the correctonDeletecascade, index, uniqueness and nullability, closing a data-integrity defect where deleting a user left their OAuth rows orphaned (neither the database nor better-auth's owndeleteUsercleaned them up). Plugin-table scalar fields now also honourfieldNamecolumn maps andindex: true, and list keys are PascalCased withdb.maprestoring the original physical table name. A reference whose target field isn't the target'sid(e.g.oauthAccessToken.clientId→oauthApplication.clientId) is left as a plain scalar column, sincerelationship()only supportsid-based foreign keys.No config changes are required —
authPlugin()/getAuthLists()are unchanged. If your app already had an MCP-enabled config generated with an older version, regenerate and diff your schema: the OAuth tables'userIdcolumns gain a foreign key, cascade and index they didn't have before.#1013
49687eaThanks @borisno2! - BREAKING (pre-1.0): The derived auth lists' credential-bearing fields now ship with a field-levelreaddeny, so opening operation-level access to a list no longer exposes them:Session.tokenVerification.valueAccount.passwordAccount.accessTokenAccount.refreshTokenAccount.idTokenA denied field is stripped from a returned row, not an error — a
context.dbread on an opened list still succeeds and returns every other field, including afindUniquelookup that selects the row by the denied field itself (e.g.context.db.session.findUnique({ where: { token } })still finds the session; the returnedtokencomes back stripped). Naming a denied field infindMany's (orcount's)where/orderByis different: the existing predicate-time read-access check (validateQueryFieldReadAccess) throws aValidationErrorthere instead, the same as it already does for any other field-levelreaddeny.sudo()bypasses both — it's the supported path for an application with a genuine need. Sign-in, session refresh, email verification, and password reset are unaffected — better-auth's own flows write through the raw Prisma adapter, bypassing access control entirely.If your application opens one of these lists today and deliberately reads one of these fields through
context.db— a returned row, afindMany/countpredicate, or afindUniqueselector — switch that access tocontext.sudo().db.... See ADR-0036.Patch Changes
#1020
8e6707aThanks @borisno2! - Read-denied credential fields (ADR-0036) now also declareui.listView.defaultColumn: false, so they're curated out of the admin's default table columns instead of rendering as permanently empty columns.#997
ed6ffcdThanks @borisno2! -deriveAuthListsnow derives the Auth lists (User/Session/Account/Verification/RateLimit) from better-auth's owngetAuthTables()output instead of a hand-written transcription, closing the drift class behind #935/#937/#921/#986. Generated schema output is unchanged for existing projects — no migration needed.#972
08c3787Thanks @borisno2! - Clean up comments inpackages/auth/srcper the CLAUDE.md Comments rule — removed restating/duplicated comments, kept public-API TSDoc and footgun/external-constraint warnings. No behavior changes.#996
cfd366cThanks @borisno2! - Add a test that compares the derived Auth lists against better-auth's owngetAuthTables()definitions, failing the build on future upstream schema drift instead of relying on a human to notice.#990
37d7905Thanks @borisno2! - The derivedSession.user/Account.userforeign keys are now indexed (isIndexed: true, wasfalse), andVerification.identifieris now indexed too — matching the three indexes better-auth itself declares (session_userId_idx,account_userId_idx,verification_identifier_idx).prisma migrate diffagainst a real better-auth install no longer reads these as three dropped indexes, andSession/Accountlookups byuserIdare no longer unindexed.Migration note: existing projects will see a migration on their next
prisma migrate dev/db pushadding the three indexes.#989
9cc6f8dThanks @borisno2! - FixSession.user/Account.userforeign key generating auserphysical column instead ofuserId, mismatching better-auth's own schema and breaking clean-diff adoption (ADR-0007). An explicitfields: { userId: ... }override is unaffected.Migration note: existing greenfield projects need to rename the column on
SessionandAccount(e.g.ALTER TABLE "Session" RENAME COLUMN "user" TO "userId";and the same forAccount) to match the new generated schema.#1002
48d2762Thanks @borisno2! - Fix admin UI URL round-trip for a list keyed with anything other than strict PascalCase (issue #991).getListKeyFromUrlreconstructs a list key by string transformation, which is lossy for a non-PascalCase key — a real example is a better-auth plugin's derived list (e.g.oauthApplication, from themcpplugin's OAuth tables). Such a list appeared in navigation but its own link resolved to a key that did not exist inconfig.lists, rendering "List not found".@opensaas/stack-coreaddsresolveListKeyFromUrl(urlSegment, listKeys)alongside the existinggetListKeyFromUrl, which is unchanged and still exported. The new resolver matches a URL segment against the config's actual list keys viagetUrlKey— the same helper that builds the URL — instead of reconstructing one, so route lookup and URL generation cannot drift apart. It returnsundefinedfor a segment matching no list (so callers keep rendering their existing "not found" state), and throws if two distinct list keys would produce the same URL segment.@opensaas/stack-ui'sAdminUInow usesresolveListKeyFromUrlfor its route resolution, fixing the broken link for any such list.@opensaas/stack-auth'sconvertBetterAuthSchemanow PascalCases a better-auth plugin's camelCasemodelNamewhen deriving a list key (oauthApplication→OauthApplication,rateLimit→RateLimit), matching the repo's PascalCase list-key convention and fixing the same round-trip bug at the source for these lists.Schema-affecting for
@opensaas/stack-authusers with a better-auth plugin that declares extra tables (e.g.mcp's OAuth tables, orrateLimit.storage: 'database'with nomodelNameremap configured): the generated Prisma model name changes to match the new PascalCase list key. The physical table name does not change — the previous camelCase name is preserved viadb.map(@@map) — soprisma db push/prisma migrate devsees a model rename, not a table rename, andcontext.db.oauthApplication(the camelCase db accessor) keeps working unchanged. Regenerate (pnpm generate) and re-run your migration/push step after upgrading.@opensaas/stack-cli@0.40.0
Minor Changes
#1017
b30fa61Thanks @{! - Let an application declare model-level indexes (db.indexes) on the derived auth lists (User/Session/Account/Verification/RateLimit).Each per-model block in
authPlugin()now acceptsindexes, in the same shape as a list's owndb.indexes:An entry covering a column the stack already derives an index for (e.g.
User.email) suppresses that derived index for that column and emits only the app's entry, rather than erroring — the application's declaration wins (ADR-0035). Suppression is per-column: every other derived index on the model is unaffected.This also fixes a related generator gap: a list's
db.indexescan now referencecreatedAt/updatedAteven when the list has no explicit field for them and relies ondb.timestampsfor the auto-injected columns (previously only a list with an explicitly declaredcreatedAt/updatedAtfield could be indexed on it).#1003
9de43c8Thanks @borisno2! - Addcontext.withSession(session)— a sibling tosudo()for the other axis. It derives aStackContextthat reuses the receiver's already-resolved config, client (including a transaction client — a call insidecontext.transaction()stays in that transaction), and storage, but carries a substituted session, so access control and hooks run against the new session as normal.This closes a gap for callers that are legitimately authorised but arrive without the session a list
validatehook expects — an unattended dispatcher, a service principal, or a job runner:withSessiongrants no authority of its own — the derived context can do exactly what any context built with that session directly could do. It's orthogonal tosudo():context.withSession(s).sudo()andcontext.sudo().withSession(s)are equivalent, sincewithSessionpreserves the receiver's sudo state instead of resetting it.The generated
Context<TSession>type (.opensaas/types.ts) now includeswithSession: (session: TSession | null) => Context<TSession>alongsidesudo, so the method is typed in application code — runopensaas generate(orpnpm generate) to pick it up.Patch Changes
#967
ca20d45Thanks @borisno2! - Clean up stale/restating comments in migration, MCP, and commands source per CLAUDE.md's Comments rule. No behavior changes.#968
026489cThanks @borisno2! - Clean up restating/duplicated comments inpackages/cli/src/generator/per the CLAUDE.md Comments rule. No behavior change.Updated dependencies [
8e6707a,afd1a60,b30fa61,16da817,51ae299,f85c7d1,0f2e12a,05c747a,0b5b51e,4ce64b4,48d2762,9de43c8,52dfdd2]:@opensaas/stack-core@0.40.0
Minor Changes
#1020
8e6707aThanks @borisno2! - Addui.listView.defaultColumnto field config — a declared, presentation-only flag (defaulttrue) controlling whether a field belongs in a list/related-list table's default column set. Naming a field explicitly inui.listView.initialColumnsor a relationship'sui.itemView.columnsalways shows it regardless of this flag.password()now sets this flag tofalseby default instead of the admin UI matching on field type — a password field can opt back into default columns withui: { listView: { defaultColumn: true } } }.#1011
afd1a60Thanks @borisno2! -OperationAccess.createnow throwsInvalidCreateAccessResultErrorwhen the rule returns anything other thantrue/false— most notably a Prisma filter, which previously fell through thecreateaccess check unrecognised and was silently treated as a full allow (both the top-level write pipeline and nested-create paths were affected).Create has no existing row to scope a filter against, so a filter can no longer be honoured here:
create: () => falsestill denies via Silent failure as before; only a non-boolean result now throws.#984
51ae299Thanks @borisno2! - ExtendisIndexedtointeger,timestamp, andselect, matchingtext,decimal,bigInt,calendarDay, andrelationship.isIndexed: truegenerates a block-level@@index([field]);isIndexed: 'unique'generates an inline@unique.selectsupports both under the default string column and a native-enum column (db: { type: 'enum' }). No field type's default indexing behavior changes — an existing config generates the same schema as before.#1007
4ce64b4Thanks @borisno2! - The derived MCPquerytool now accepts an optionalfieldsprojection — the wire form of the runtime's existing fragment field selection — so an assistant can select scalars and nested relation fields (withwhere/orderBy/take/skip, and a to-many's row count) in a single call instead of following a foreign key with a second one. Omittingfieldsis unchanged, a bare read exactly as before.{ "name": "list_post_query", "arguments": { "fields": { "title": true, "author": { "fields": { "name": true } }, "comments": { "fields": { "text": true }, "where": { "approved": { "equals": true } }, "take": 5, "count": true } } } }The generated tool schema enumerates two levels of each list's own fields and relations, per session, and refuses (as an
isErrortool result, never a protocol error) anything it doesn't advertise — an unknown field, or a relation named a third level deep. See the ADR (docs/adr/0033-mcp-tools-advertise-a-bounded-projection.md) for the full design.Behaviour change:
tools/listis now evaluated per session. A list whose operation-levelqueryaccess denies the session outright no longer appears in the tool listing at all — none of its four CRUD tools, and no relation entry elsewhere pointing at it. Previously every list's tools were listed regardless of session.#1002
48d2762Thanks @borisno2! - Fix admin UI URL round-trip for a list keyed with anything other than strict PascalCase (issue #991).getListKeyFromUrlreconstructs a list key by string transformation, which is lossy for a non-PascalCase key — a real example is a better-auth plugin's derived list (e.g.oauthApplication, from themcpplugin's OAuth tables). Such a list appeared in navigation but its own link resolved to a key that did not exist inconfig.lists, rendering "List not found".@opensaas/stack-coreaddsresolveListKeyFromUrl(urlSegment, listKeys)alongside the existinggetListKeyFromUrl, which is unchanged and still exported. The new resolver matches a URL segment against the config's actual list keys viagetUrlKey— the same helper that builds the URL — instead of reconstructing one, so route lookup and URL generation cannot drift apart. It returnsundefinedfor a segment matching no list (so callers keep rendering their existing "not found" state), and throws if two distinct list keys would produce the same URL segment.@opensaas/stack-ui'sAdminUInow usesresolveListKeyFromUrlfor its route resolution, fixing the broken link for any such list.@opensaas/stack-auth'sconvertBetterAuthSchemanow PascalCases a better-auth plugin's camelCasemodelNamewhen deriving a list key (oauthApplication→OauthApplication,rateLimit→RateLimit), matching the repo's PascalCase list-key convention and fixing the same round-trip bug at the source for these lists.Schema-affecting for
@opensaas/stack-authusers with a better-auth plugin that declares extra tables (e.g.mcp's OAuth tables, orrateLimit.storage: 'database'with nomodelNameremap configured): the generated Prisma model name changes to match the new PascalCase list key. The physical table name does not change — the previous camelCase name is preserved viadb.map(@@map) — soprisma db push/prisma migrate devsees a model rename, not a table rename, andcontext.db.oauthApplication(the camelCase db accessor) keeps working unchanged. Regenerate (pnpm generate) and re-run your migration/push step after upgrading.#1003
9de43c8Thanks @borisno2! - Addcontext.withSession(session)— a sibling tosudo()for the other axis. It derives aStackContextthat reuses the receiver's already-resolved config, client (including a transaction client — a call insidecontext.transaction()stays in that transaction), and storage, but carries a substituted session, so access control and hooks run against the new session as normal.This closes a gap for callers that are legitimately authorised but arrive without the session a list
validatehook expects — an unattended dispatcher, a service principal, or a job runner:withSessiongrants no authority of its own — the derived context can do exactly what any context built with that session directly could do. It's orthogonal tosudo():context.withSession(s).sudo()andcontext.sudo().withSession(s)are equivalent, sincewithSessionpreserves the receiver's sudo state instead of resetting it.The generated
Context<TSession>type (.opensaas/types.ts) now includeswithSession: (session: TSession | null) => Context<TSession>alongsidesudo, so the method is typed in application code — runopensaas generate(orpnpm generate) to pick it up.Patch Changes
#1017
b30fa61Thanks @{! - Let an application declare model-level indexes (db.indexes) on the derived auth lists (User/Session/Account/Verification/RateLimit).Each per-model block in
authPlugin()now acceptsindexes, in the same shape as a list's owndb.indexes:An entry covering a column the stack already derives an index for (e.g.
User.email) suppresses that derived index for that column and emits only the app's entry, rather than erroring — the application's declaration wins (ADR-0035). Suppression is per-column: every other derived index on the model is unaffected.This also fixes a related generator gap: a list's
db.indexescan now referencecreatedAt/updatedAteven when the list has no explicit field for them and relies ondb.timestampsfor the auto-injected columns (previously only a list with an explicitly declaredcreatedAt/updatedAtfield could be indexed on it).#983
16da817Thanks @borisno2! - FixHashedPassword.toJSON()returning the raw bcrypt hash, soJSON.stringifyof a row (e.g. a server→client prop,Response.json(), an MCP tool response) no longer leaks the stored hash for apassword()field.toJSON()now returns{ isSet: boolean }, matching the redaction the admin UI already applies viavalueForClientSerialization.toString(),valueOf(),[Symbol.toPrimitive], and==comparison against the hash are unchanged. If you parseJSON.stringify'd rows and read the password field as a string, update that code to read.isSetinstead — this is a visible output/type change onHashedPassword.toJSON(), though the field's read access remains the application's to configure (unchanged).#999
f85c7d1Thanks @borisno2! - MCP derived CRUD tool and custom tool failures (access denial, thrown engine/database errors, input schema validation) now return a successful JSON-RPC response withresult.isError: trueinstead of a JSON-RPCerrorobject, so the calling model can see and recover from them. Genuine protocol failures (unknown method, malformed request, unknown tool name) are unchanged. Note: the wire shape of tool failures changes — a consumer asserting on the olderrorshape will need to update.#1006
0f2e12aThanks @borisno2! -relationship({ ref: 'ListName' })list-only refs now acceptdb.foreignKey: { map: '...' }to rename the foreign key column. The boolean form (true/false) is still rejected there since ownership is implicit on a list-only ref.#1004
05c747aThanks @borisno2! - Fix a nested create/update/delete through a list-only ref's synthetic reverse relation (from_<List>_<field>) silently bypassing the target list's hooks and validation. It now runs the same pipeline a declared relationship field's nested write gets. Undersudo(), an undeclared key that isn't a synthetic reverse relation is now refused rather than passed through unchecked.#1000
0b5b51eThanks @borisno2! - FixP2002unique-constraint errors losing per-field detail under Prisma 7 driver adapters (@prisma/adapter-pg, PGlite), wheremeta.targetis left empty. The error handler now recovers the violated columns and constraint name from the adapter's error shape, and a newuniqueConstraintOf(error)helper exposes this to callers ofcontext.db.*directly. Unique-violation messages under driver adapters change from the generic fallback back to field-specific text.#1001
52dfdd2Thanks @borisno2! - Fixincludeon a to-one relationship throwingPrismaClientValidationErrorwhen the related list'squeryaccess resolves to a filter (Prisma only accepts a nestedwhereon a to-many include). The relation is now fetched and access-scoped via a batched existence check instead, returningnullfor an excluded related row rather than throwing — a caller relying on the previous exception, or whose types assumed a non-null relation, should re-check nullability.@opensaas/stack-ui@0.40.0
Minor Changes
#1020
8e6707aThanks @borisno2! - Replace the admin UI's hardcodedpassword/createdAt/updatedAtdefault-column exclusion with curation driven by each field's declaredui.listView.defaultColumn(issue #1018). The list view, related-list tables, and theListTablestandalone component now share one implementation (computeDefaultColumns) instead of three independent name/type-matching copies, and a list's structuralcreatedAt/updatedAttimestamp columns are identified from its own timestamp configuration rather than by name.Behavior change: an application field literally named (or typed)
password,createdAt, orupdatedAtthat does NOT declareui.listView.defaultColumn: false— and isn't your list's actual auto-timestamp column — is no longer hidden from default admin columns purely by name/type match. Real password fields (built withpassword()) and real system timestamps are unaffected; they're excluded via the declared flag instead.ListTablegains an optionalfields?: Record<string, SerializableFieldConfig>prop to supply this curation metadata; without it (as before), everyfieldTypescolumn shows absent an explicitcolumnslist.#1016
98465a5Thanks @borisno2! - Password columns are now identified by field type, not field name, across the list view, standaloneListTable, and item-view Relationship tables. A field declaredsecret: password()is now excluded from default columns even though it isn't namedpassword; a field merely namedpassword(e.g.password: text()) is no longer excluded unless it is actually apassword()field.A
passwordCell is now registered in the cell registry, so a password-typed column shown via an explicitcolumnsprop renders a fixed••••••••mask instead of the raw value.BREAKING (shipped as minor — pre-1.0 packages ship breaking changes as minor): the unused
getFieldDisplayValueexport has been removed from@opensaas/stack-ui. It had no callers in the rendering path — Cells render each field type directly — so nothing in this package depended on it; a consumer importing it directly should port to a project-local formatter.Patch Changes
#971
dfdca11Thanks @borisno2! - Remove comments that restated the line below them or duplicated rationale already stated elsewhere inpackages/ui/src. No behavior changes.#1002
48d2762Thanks @borisno2! - Fix admin UI URL round-trip for a list keyed with anything other than strict PascalCase (issue #991).getListKeyFromUrlreconstructs a list key by string transformation, which is lossy for a non-PascalCase key — a real example is a better-auth plugin's derived list (e.g.oauthApplication, from themcpplugin's OAuth tables). Such a list appeared in navigation but its own link resolved to a key that did not exist inconfig.lists, rendering "List not found".@opensaas/stack-coreaddsresolveListKeyFromUrl(urlSegment, listKeys)alongside the existinggetListKeyFromUrl, which is unchanged and still exported. The new resolver matches a URL segment against the config's actual list keys viagetUrlKey— the same helper that builds the URL — instead of reconstructing one, so route lookup and URL generation cannot drift apart. It returnsundefinedfor a segment matching no list (so callers keep rendering their existing "not found" state), and throws if two distinct list keys would produce the same URL segment.@opensaas/stack-ui'sAdminUInow usesresolveListKeyFromUrlfor its route resolution, fixing the broken link for any such list.@opensaas/stack-auth'sconvertBetterAuthSchemanow PascalCases a better-auth plugin's camelCasemodelNamewhen deriving a list key (oauthApplication→OauthApplication,rateLimit→RateLimit), matching the repo's PascalCase list-key convention and fixing the same round-trip bug at the source for these lists.Schema-affecting for
@opensaas/stack-authusers with a better-auth plugin that declares extra tables (e.g.mcp's OAuth tables, orrateLimit.storage: 'database'with nomodelNameremap configured): the generated Prisma model name changes to match the new PascalCase list key. The physical table name does not change — the previous camelCase name is preserved viadb.map(@@map) — soprisma db push/prisma migrate devsees a model rename, not a table rename, andcontext.db.oauthApplication(the camelCase db accessor) keeps working unchanged. Regenerate (pnpm generate) and re-run your migration/push step after upgrading.create-opensaas-app@0.3.4
Patch Changes
8f76533Thanks @borisno2! - Comment cleanup only, no behavior change: removed restating/narration comments, kept TSDoc on public config options and field builders, and kept external API/behavior constraint notes (Prisma, S3, Vercel Blob, Keystone parity, Next.js SSR, Zod).@opensaas/stack-rag@0.40.0
Patch Changes
fa1819bThanks @borisno2! - Clean up comments inpackages/rag/srcper the CLAUDE.md Comments rule — removed restatement and stale narration, kept public-API TSDoc, external-constraint notes, and genuine footgun warnings. No behavior changes.@opensaas/stack-storage@0.40.0
Patch Changes
8f76533Thanks @borisno2! - Comment cleanup only, no behavior change: removed restating/narration comments, kept TSDoc on public config options and field builders, and kept external API/behavior constraint notes (Prisma, S3, Vercel Blob, Keystone parity, Next.js SSR, Zod).@opensaas/stack-storage-s3@0.40.0
Patch Changes
8f76533Thanks @borisno2! - Comment cleanup only, no behavior change: removed restating/narration comments, kept TSDoc on public config options and field builders, and kept external API/behavior constraint notes (Prisma, S3, Vercel Blob, Keystone parity, Next.js SSR, Zod).@opensaas/stack-storage-vercel@0.40.0
Patch Changes
8f76533Thanks @borisno2! - Comment cleanup only, no behavior change: removed restating/narration comments, kept TSDoc on public config options and field builders, and kept external API/behavior constraint notes (Prisma, S3, Vercel Blob, Keystone parity, Next.js SSR, Zod).@opensaas/stack-tiptap@0.40.0
Patch Changes
8f76533Thanks @borisno2! - Comment cleanup only, no behavior change: removed restating/narration comments, kept TSDoc on public config options and field builders, and kept external API/behavior constraint notes (Prisma, S3, Vercel Blob, Keystone parity, Next.js SSR, Zod).