-
9dbb883: Contain the blast radius of a failing optional better-auth plugin: core email/password + session auth now stays up when an optional feature plugin throws during initialization.
Previously, one throwing optional plugin (the 15.1.0 incident:
@better-auth/oauth-providerthrewCannot set properties of undefined (setting 'modelName')from a 1.6/1.7 version mix) failed the whole lazily-built better-auth instance, turning EVERY auth endpoint — sign-up, sign-in, get-session — into a 500.AuthManager.buildPluginListnow classifies plugins in two tiers. Optional feature plugins (organization, admin, phoneNumber, magicLink, genericOAuth, jwt+oauthProvider as one atomic unit, sso, scim, deviceAuthorization) are constructed through an isolation wrapper: on failure the feature is skipped with a loud actionableconsole.error, recorded ingetDegradedAuthFeatures(), and its endpoints 404 while core auth keeps working. Security-bearing plugins (bearer, twoFactor, haveIBeenPwned, customSession with its ADR-0069 authGate) still fail hard — better a hard 500 than silently weakened auth (e.g. 2FA-enrolled accounts signing in on password alone).The OIDC discovery mount (
/.well-known/{oauth-authorization-server,openid-configuration}) checks the degraded set and skips advertising an IdP whose endpoints did not come up, with a clear error log instead of sending external clients into 404s. -
01ba3b3: Fix fresh-project auth returning 500 on every endpoint (sign-up / sign-in / get-session) with
Cannot set properties of undefined (setting 'modelName').The published manifest declared
better-auth,@better-auth/core,@better-auth/oauth-provider, and@better-auth/ssoas^1.6.23, while only@better-auth/scimwas pinned to1.7.0-rc.1(GHSA-j8v8-g9cx-5qf4 is fixed only in the 1.7.0 pre-release line). The framework workspace forces the whole better-auth family to1.7.0-rc.1via pnpm overrides, but overrides do not ship with published packages — a downstreamnpx create-objectstackinstall resolved the^1.6.23ranges to 1.6.23 (still the npmlatest), and the resulting 1.7/1.6 mix crashes during better-auth initialization, so every fresh 15.1.0 project shipped with broken auth.All four packages are now pinned to the exact
1.7.0-rc.1— the only combination the workspace actually builds and tests against. The pins will be relaxed to^1.7.0once a stable better-auth 1.7.0 ships. A new CI gate (scripts/check-override-consistency.mjs) fails whenever a pnpm-workspace override target is not reachable from a publishable package's declared range, so tested-vs-published drift like this cannot recur silently.- @objectstack/spec@15.1.1
- @objectstack/core@15.1.1
- @objectstack/types@15.1.1
- @objectstack/platform-objects@15.1.1
- @objectstack/rest@15.1.1
-
f531a26: fix(plugin-auth): re-run membership backfill when app seeding settles (#2996)
The ADR-0093 D6 membership backfill — the only safety net for users created by app seeds (raw
engine.insertintosys_userbypasses better-auth'suser.create.afterreconciler) — ran only once onkernel:ready. When a seed bundle overruns its inline budget (OS_INLINE_SEED_BUDGET_MS, default 8s) it finishes in the background afterkernel:ready, so its users stayed member-less in single-orgautomode until the next restart re-ran the backfill.AppPluginnow emits a newapp:seededlifecycle event when an app's inline seed settles (success, partial, or fallback) — carrying{ appId, overBudget }, whereoverBudget: truemarks the post-kernel:readybackground case. plugin-auth subscribes and re-runs the (idempotent, self-guarding, opt-out-able)backfillMembershipson that signal, closing the window without waiting for a restart. No behavior change when a seed completes within budget, in multi-tenant mode, or underinvite-onlypolicy;OS_SKIP_MEMBERSHIP_BACKFILL=1still opts out. -
4109153: Close the
@better-auth/oauth-provider1.7 schema drift that broke platform SSO (token exchange 500:table sys_oauth_access_token has no column named authorizationCodeId).sys_oauth_access_token/sys_oauth_refresh_token: addauthorization_code_id,resources,requested_user_info_claims,confirmation(+ access-tokenrevoked; + refresh-tokenrotated_at,rotation_replay_response,rotation_replay_expires_at).sys_oauth_consent: addresources,requested_user_info_claims.sys_oauth_application: addjwks,jwks_uri,backchannel_logout_uri,backchannel_logout_session_required,dpop_bound_access_tokens.- New platform objects for the three models 1.7 introduced:
sys_oauth_resource,sys_oauth_client_resource,sys_oauth_client_assertion(RFC 8707 resource indicators + RFC 7523 client-assertion replay prevention), registered in the auth manifest and mapped inbuildOauthProviderPluginSchema(). - All camelCase→snake_case
fieldNamemappings extended accordingly, and a new parity test (oauth-provider-schema-parity.test.ts) fails the build whenever a future better-auth bump introduces model fields our objects or mappings don't cover.
-
f531a26: fix(auth): align the better-auth family on 1.7.0-rc.1, implement the new adapter methods, and add the new sys_jwks columns (#2974)
Remediating GHSA-p2fr-6hmx-4528 (
@better-auth/oauth-provider) requires the 1.7 plugin line, which importsCLIENT_ASSERTION_TYPEand other symbols that only exist in@better-auth/core1.7.x — so the whole better-auth family is pinned to1.7.0-rc.1together (mixing a 1.7 plugin with 1.6.23 core 500s on sign-in). better-auth 1.7 also extends itsCustomAdaptercontract with two new methods, which the ObjectQL adapter now implements:consumeOne— atomic single-row consume (find the guarded row, delete it, return it), used by better-auth for single-use credential consumption (e.g. verification tokens on the sign-in path).incrementOne— guarded counter mutation (field = field + deltaperincremententry plus any absolutesetvalues), returning the updated row ornullwhen the guard matches nothing.
Both are find-then-write mirrors of the existing
delete/updatemethods (ObjectQL exposes no native atomic primitive) and honour the same core/plugin field-name bridging.better-auth 1.7 also extends its
jwksmodel with two new optional columns,alg(signing algorithm, e.g.EdDSA) andcrv(curve, e.g.Ed25519), and writes them when minting signing keys. Thesys_jwksplatform object gains the matching fields — without them every JWKS write failed (table sys_jwks has no column named alg), 500ing token signing and breaking session validation (sign-in succeeded but every authenticated request 401'd). -
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [3fe9df1]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [4109153]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [627f225]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
-
Updated dependencies [f531a26]
- @objectstack/spec@15.1.0
- @objectstack/rest@15.1.0
- @objectstack/platform-objects@15.1.0
- @objectstack/core@15.1.0
- @objectstack/types@15.1.0
- Updated dependencies [02a014b]
- Updated dependencies [28b7c28]
- Updated dependencies [13749ec]
- Updated dependencies [e62c233]
- Updated dependencies [ed61c9b]
- Updated dependencies [a581a65]
- Updated dependencies [31d04d4]
- @objectstack/platform-objects@15.0.0
- @objectstack/spec@15.0.0
- @objectstack/core@15.0.0
- @objectstack/rest@15.0.0
- @objectstack/types@15.0.0
- Updated dependencies [16b4bf6]
- Updated dependencies [16b4bf6]
- Updated dependencies [10e8983]
- Updated dependencies [607aaf4]
- Updated dependencies [e46169c]
- Updated dependencies [bb71321]
- @objectstack/spec@14.8.0
- @objectstack/rest@14.8.0
- @objectstack/platform-objects@14.8.0
- @objectstack/core@14.8.0
- @objectstack/types@14.8.0
-
824a395: Tenancy mode as a first-class capability + a single owner for the user→membership lifecycle (ADR-0093, Phases 1–3).
Tenancy service (
@objectstack/types,@objectstack/plugin-auth). plugin-auth registers atenancyservice — the single source of truth for tenancy mode (mode,isolationActive,requested,degraded,defaultOrgId()). It derivesisolationActivefrom the presence of theorg-scopingservice, so the enterprise@objectstack/organizationspackage lights it up with no change. SecurityPlugin's RLS-strip gate and/auth/config(features.multiOrgEnabled, newfeatures.degradedTenancy) now consume it instead of re-deriving the fact.Fail-fast on degraded tenancy (
@objectstack/cli, ADR-0093 D5).OS_MULTI_ORG_ENABLED=truewithout a working@objectstack/organizationsnow refuses to boot — a deployment that requested tenant isolation must not serve traffic without it (tenant RLS would be silently stripped). Escape hatch:OS_ALLOW_DEGRADED_TENANCY=1boots in an explicitly branded degraded state (features.degradedTenancy). This may halt upgrades for deployments that were silently degraded — intentionally; install the enterprise package or set the escape hatch.Membership reconciler (
@objectstack/plugin-auth, ADR-0093 D1–D3, D6). A single reconciler composed into better-auth'suser.create.afterhook owns the "every new user gets a membership" invariant across all creation paths (signup, admin create-user, import, SSO JIT). It yields to any existing membership (host hooks win), honors a newmembershipPolicy: 'auto' | 'invite-only'auth option (defaultauto), and binds only to an unambiguous target org (single-org default; multi-org binds nothing). A bounded, idempotentkernel:readybackfill covers pre-existing member-less users in single-org/auto deployments (OS_SKIP_MEMBERSHIP_BACKFILL=1to opt out). The endpoint-level create-user bind from #2882 now delegates to this shared reconciler.New env vars:
OS_ALLOW_DEGRADED_TENANCY,OS_SKIP_MEMBERSHIP_BACKFILL. New docs: Deployment → Tenancy Modes & Membership.
-
da5e686: fix(auth): create-user membership bind is tenancy-mode-aware; export the ADR-0093 host API
Multi-org runtime verification (real
@objectstack/organizationslinked into a live stack) caught a gap in the #2884 endpoint bind: it resolved its target org viaresolveDefaultOrgId(slug='default' first), so in a multi-org deployment — where the bootstrap default org coexists with real tenant orgs —/admin/create-userwould have bound the new user into the default org, violating ADR-0093 D3 ("the framework never guesses in multi mode"). The bind now consults thetenancyservice (getTenancyon the endpoint deps): single mode → default org, multi mode → no bind. Verified live: multi-org create-user and sign-up both leave the new user member-less (invites / host hooks own membership there); single-org behavior unchanged.Also exports
reconcile-membershipandtenancy-servicefrom the package index as the public host API, and adds dogfood integration tests driving the REAL better-auth pipeline: sign-up membership via the reconciler hook alone, backfill bind + idempotency, invite-only refusal, and the yield-to-host-membership rule. -
Updated dependencies [d6a72eb]
-
Updated dependencies [824a395]
- @objectstack/spec@14.7.0
- @objectstack/types@14.7.0
- @objectstack/core@14.7.0
- @objectstack/platform-objects@14.7.0
- @objectstack/rest@14.7.0
-
160d565: fix(auth): guarantee an absolute https origin for every user-facing auth URL
Follow-up to the invitation-link fix. Several other user-facing links were built from the raw
config.baseUrlwith no scheme guarantee, so a bare-hostbaseUrl(e.g.cloud.objectos.ai) produced relative-looking, unclickable links. All now flow through the hardenedgetCanonicalOrigin()(prependshttps://when the scheme is missing, trims a trailing slash):- better-auth
baseURL— the reset-password, verify-email and magic-link email links are derived from it. - OAuth
loginPage/consentPageredirect targets. - Device-authorization
verificationUri. - The phone-invite SMS
{{baseUrl}}.
Deployments that already configure an absolute
baseUrlare unaffected. - better-auth
-
e4cf774: fix(auth): single-source Console page-URL construction; correct SMS + OAuth-callback landing paths
Root-cause hardening after the invitation-link fixes. Every user-facing link to a Console page is
${origin}${uiBasePath}${path}, but that composition was hand-written at each call site — which is how the scheme //_consoleprefix kept getting dropped one link at a time.plugin-auth
- New single-source
getConsolePageUrl(path)helper;loginPage,consentPage, deviceverificationUriand the invitation accept URL all compose through it, so future page links can't drift. - Phone-invite SMS now links to the actual Console sign-in page
(
${origin}${uiBasePath}/login) via a new{{loginUrl}}template variable instead of the bare origin.{{baseUrl}}is still provided for backward compatibility with tenant-overridden templates.
client
signInWithProvidernow defaultscallbackURLto the current page (window.location.href) instead of a hard-codedorigin + '/login'. The SDK cannot know the app's mount path (Console lives under/_console), so returning the user to where they started is the only base-path-correct default; it also mirrorslinkSocial. Pass an explicitcallbackURLto override.
- New single-source
-
Updated dependencies [609cb13]
-
Updated dependencies [ce6d151]
- @objectstack/spec@14.6.0
- @objectstack/platform-objects@14.6.0
- @objectstack/core@14.6.0
- @objectstack/rest@14.6.0
- @objectstack/types@14.6.0
-
a348394: feat(auth): identity write guard —
managedBy: 'better-auth'is now enforced at the engine (ADR-0092 D2/D3/D6)Every object whose schema declares
managedBy: 'better-auth'(sys_user,sys_member,sys_session,sys_api_key, …) is now protected by enginebeforeInsert/beforeUpdate/beforeDeletehooks registered by plugin-auth: user-context writes through the generic data path are rejected fail-closed with403 PERMISSION_DENIED, closing the hole where a wildcard admin permission set could raw-write any identity column (includingemailand credential stamps) via the data API. Internal writes are unaffected — the better-auth adapter,isSystemplugin/system contexts, and the identity import keep working unchanged.The only opening is a per-object update whitelist (
registerManagedUpdateWhitelist(object, fields)): non-whitelisted fields are stripped from the payload, and a payload that strips to nothing throws. The first registration ships here:sys_user → { name, image }(pure profile fields), backed by the new sharedSYS_USER_PROFILE_EDIT_FIELDS/SYS_USER_IMPORT_UPDATE_FIELDSconstants — the import upsert's field discipline is now derived from the same module (subset-by-construction, no drift).After a guarded profile edit, an
afterUpdatecompanion hook re-writes the user's cached{session, user}snapshots in better-auth's secondary storage (same TTL, mirror of better-auth's ownrefreshUserSessions) so session reads stay coherent; it rewrites rather than deletes, and no-ops when no secondary storage is wired.Migration note: server-side scripts that previously updated identity tables with a user execution context must either run with a system context (
{ isSystem: true }) if they are genuinely internal, or move to the dedicated auth endpoints (invite / create-user / set-user-password / ban / better-auth APIs). Flows and automations that wrote non-profilesys_usercolumns under a user identity are now filtered the same way. -
5bced2f: feat(auth):
passwordPolicy: 'none'is the identity import's new default — import provisions identity, not credentialsPOST /api/v1/auth/admin/import-usersnow supports (and defaults to)passwordPolicy: 'none': accounts are created without a credential record (better-auth's optional-password create), so no password material is generated, returned, or distributed at all. Users first sign in through a channel — phone OTP, magic link, or a password-reset link — and the Console's existing credential-less detection (hasLocalPassword()→ set-initial-password) nudges them to set a password afterwards.The
invitepolicy also no longer mints a throwaway password: it creates the same credential-less account and sends the set-your-password invitation (better-auth's reset flow creates the credential record on first set).temporaryis unchanged and remains the fallback for deployments without email/SMS infrastructure.Breaking-ish note:
passwordPolicywas previously required — requests that omitted it got a 400. They now succeed with thenonebehavior. -
e2c05d6: feat(auth/i18n): localised, tenant-customisable phone SMS texts (#2815)
The OTP and invitation SMS bodies were hard-coded English. They now resolve in two layers: a
sys_notification_templaterow for(auth.phone_otp | auth.phone_invite, channel 'sms', locale)— editable in Setup, seeded once with built-in en/zh rows, tenant edits never overwritten — falling back to the bundled bilingual texts. The locale follows the deployment default (localization.localesetting, live-rebound); per-user locale is deferred untilsys_usergrows a locale column. The OTP wording is purpose-neutral (one provider template covers sign-in and reset, and the SMS reveals nothing about what the code unlocks). Template lookups are best-effort — an outage never blocks an OTP send — and the no-OTP-in-logs red line is unchanged.
-
3fd87b2: fix(auth): invitation accept link is now an absolute URL under the Console base
sendInvitationEmailbuilt the accept URL straight fromconfig.baseUrlwith no scheme guarantee and no UI mount prefix —${baseUrl}/accept-invitation/<id>. Two problems surfaced in real deployments:- When
baseUrlwas a bare host (e.g.cloud.objectos.ai, no scheme), the emailed link was relative-looking; email clients would not linkify it and clicking it went nowhere. - The accept-invitation page is a Console SPA route mounted under
uiBasePath(default/_console) — the same router/basename as/login,/registerand/oauth/consent, and the exact link the Console itself generates for its "copy invitation link" action (${origin}${BASE_URL}accept-invitation/<id>). The root-path link omitted that prefix, so it 404'd at the host root instead of resolving to the page.
The link is now built as
${origin}${uiBasePath}/accept-invitation/<id>via a hardenedgetCanonicalOrigin()that guarantees an absolute origin (prependshttps://whenbaseUrlhas no scheme). The scheme hardening also applies to the OAuth issuer / consent / device-flow URLs that share the helper. Deployments that mount the Console elsewhere are honoured through the existinguiBasePathconfig. - When
-
Updated dependencies [526805e]
-
Updated dependencies [d79ca07]
-
Updated dependencies [4d9dd7b]
-
Updated dependencies [33ebd34]
-
Updated dependencies [c044f08]
-
Updated dependencies [01274eb]
-
Updated dependencies [8f23746]
-
Updated dependencies [b97af7e]
-
Updated dependencies [6da03ee]
- @objectstack/spec@14.5.0
- @objectstack/platform-objects@14.5.0
- @objectstack/rest@14.5.0
- @objectstack/core@14.5.0
- @objectstack/types@14.5.0
-
9887465: fix(auth): make the self-service "Resend Verification Email" action work
better-auth's stock
POST /send-verification-emailrequires{ email }in the body, but thesys_userresend_verification_emailaction (record-header button, "email unverified" record alert, and record-section quick action) fires with an empty body — so the request bounced with[body.email] Invalid input: expected string, received undefinedand the button was permanently broken. A thin wrapper route now defaults the address to the authenticated caller's own session email when the body omits it, then re-dispatches through the real route. An explicitly-suppliedemail(admin / verify-screen path) passes through untouched. -
Updated dependencies [7953832]
-
Updated dependencies [82e745e]
-
Updated dependencies [f3035bd]
-
Updated dependencies [82c0d94]
-
Updated dependencies [7449476]
- @objectstack/spec@14.4.0
- @objectstack/platform-objects@14.4.0
- @objectstack/core@14.4.0
- @objectstack/rest@14.4.0
- @objectstack/types@14.4.0
-
2a71f48: feat(auth): admin direct user management, phone sign-in, and identity bulk import (#2766, re-scoped #2758)
sys_useris managed by better-auth and its generic CRUD is suppressed, so until now the only way to add a teammate was the email-dependent invite flow. This ships three staged capabilities:- Admin direct user management —
POST /api/v1/auth/admin/create-userand a wrappedPOST /api/v1/auth/admin/set-user-password(ADR-0068 platform-admin gate; better-auth pipeline so credentials are real). Optional generated temporary password (returned once, never persisted or logged) and a newsys_user.must_change_passwordflag enforced through the ADR-0069 authGate (403 PASSWORD_EXPIREDuntil the user changes it). Newcreate_useraction and upgradedset_user_passwordaction on the Users list — pure schema, no frontend changes. - Phone sign-in (opt-in
auth.plugins.phoneNumber) — better-auth phoneNumber plugin, phone+password only (POST /sign-in/phone-number); OTP flows stay off until SMS infrastructure exists. Addssys_user.phone_number(unique) /phone_number_verified. Phone-only accounts get an undeliverable placeholder email (u-<random>@placeholder.invalid, never derived from the phone number); all auth mail callbacks refuse placeholder recipients. - Identity bulk import —
POST /api/v1/auth/admin/import-usersaccepts the same payloads as the generic import routes (rows/csv/xlsx, dryRun, upsert by email or phone) but writes every row through better-auth. Password policies:invite(reset-link email per created user; requires an EmailService) andtemporary(per-row one-time passwords + forced change). Sync only, ≤500 rows per request; no undo; upsert updates touch profile fields only and can never reset an existing user's password.prepareImportRequestand the CSV/xlsx parsers moved from rest-server.ts to an exportedimport-prepare.tsmodule (behavior unchanged).
- Admin direct user management —
-
c1064f1: feat(messaging/auth): SMS infrastructure + phone-number OTP first-login/reset (#2780)
#2766 shipped phone+password sign-in but no OTP — the platform had no SMS delivery capability. This adds the missing infrastructure end to end:
- New
@objectstack/plugin-sms—ISmsService/ISmsTransportcontracts (spec) with Aliyun SMS (ACS3-HMAC-SHA256, template-based) and Twilio transports plus a dev log fallback. Configured through the newsmssettings namespace (live provider rebind, encrypted secrets, send-test action;OS_SMS_*env keys win at the resolver). Deliberately NO message persistence and NO body logging — SMS bodies carry OTP codes. - Messaging
smschannel — registered at kernel:ready when ansmsservice is present;notify(channels:['sms'])resolvessys_user.phone_number, renders(topic,'sms',locale)templates, and inherits outbox retry/dead-letter. - Phone OTP flows open — the phoneNumber plugin's
sendOTP/sendPasswordResetOTPnow deliver via SMS, enabling/phone-number/send-otp+/verify(OTP sign-in/verification) and/phone-number/request-password-reset+/reset-password(self-service reset). Without a deliverable SMS service they keep failing loudly (NOT_SUPPORTED);features.phoneNumberOtpadvertises real availability. Shipped with the abuse hardening: explicitallowedAttempts: 3, always-on per-number cooldown (60s) + rolling-hour cap (5, secondaryStorage-shared across nodes),/phone-number/*in the settings-bound per-IP rate-limit rules, and OTP codes never reach logs or error messages. - Import SMS invites —
/admin/import-users'sinvitepolicy now supports phone-only rows: a credential-free invitation SMS points the employee at phone-OTP first sign-in followed by self-set password; mixed files validate the reachable channel per row.
- New
- Updated dependencies [2a71f48]
- Updated dependencies [02f6af4]
- Updated dependencies [c1064f1]
- Updated dependencies [bea4b92]
- @objectstack/platform-objects@14.3.0
- @objectstack/rest@14.3.0
- @objectstack/spec@14.3.0
- @objectstack/core@14.3.0
- @objectstack/types@14.3.0
- Updated dependencies [ac8f029]
- Updated dependencies [4ab9958]
- @objectstack/spec@14.2.0
- @objectstack/platform-objects@14.2.0
- @objectstack/core@14.2.0
- @objectstack/types@14.2.0
- Updated dependencies [5a8465f]
- Updated dependencies [7f8620b]
- Updated dependencies [82ba3a6]
- @objectstack/spec@14.1.0
- @objectstack/core@14.1.0
- @objectstack/platform-objects@14.1.0
- @objectstack/types@14.1.0
- Updated dependencies [0a8e685]
- Updated dependencies [afa8115]
- Updated dependencies [80f12ca]
- Updated dependencies [332b711]
- Updated dependencies [e2fa074]
- Updated dependencies [23c8668]
- Updated dependencies [29f017d]
- Updated dependencies [216fa9a]
- Updated dependencies [6c22b12]
- Updated dependencies [d0531c4]
- Updated dependencies [cff5aac]
- @objectstack/spec@14.0.0
- @objectstack/platform-objects@14.0.0
- @objectstack/core@14.0.0
- @objectstack/types@14.0.0
-
6d83431: ADR-0090 P1 breaking wave — permission model v2 concept convergence.
Pre-launch one-step renames and secure defaults (no compatibility aliases, per ADR-0090 D3/D4 superseding ADR-0057 D5/D7's alias discipline):
sys_role→sys_position,sys_user_role→sys_user_position(fieldrole→position),sys_role_permission_set→sys_position_permission_set(fieldrole_id→position_id);RoleSchema/defineRole→PositionSchema/definePositionwith noparent(positions are flat; hierarchy lives on the business-unit tree).ExecutionContext.roles[]→positions[]; the EvalUser/CEL contractcurrent_user.roles→current_user.positions(formula validators updated); stack propertyroles:→positions:; metadata kindsrole/profile→position(profile kind removed).isProfileremoved fromPermissionSetSchema(ADR-0090 D2);isDefaultnarrows to an install-time suggestion;appDefaultProfileName→appDefaultPermissionSetName(isDefault-only).- OWD enum drops legacy aliases
read/read_write/full; new optionalexternalSharingModel(external dial,privatedefault) lands as P1 spec shape (ADR-0090 D11). - Secure default (D1): a custom object with an owner field and NO
sharingModelnow resolvesprivate(was: fully public). System objects keep their explicit posture. Unrecognised stored values fail closed. - ExecutionContext gains the P1 principal-taxonomy shape (D10):
principalKind/audience/onBehalfOf(optional, semantics phase in later). - Sharing recipients:
role→position(expanded viasys_user_position∪ the better-auth membership transition source);role_and_subordinatesremoved —unit_and_subordinatesnow expands the business-unit subtree (finishes ADR-0057 D5's re-homing).
-
57b89b4: feat(mcp): the MCP surface is now default-on — a core platform capability (#2698)
/api/v1/mcpis served (and advertised in/discovery) out of the box; the OAuth 2.1 authorization track and Dynamic Client Registration follow it, so a fresh deployment is connectable by any MCP client with zero configuration. Operators opt OUT withOS_MCP_SERVER_ENABLED=false.- New single decision point
isMcpServerEnabled()in@objectstack/types(default on; explicitfalse/0/off/nodisables). The runtime dispatcher's/mcproute gate, the CLI's MCP plugin auto-load, the REST/discoveryadvertisement, and the auth service's OAuth/DCR follow-defaults all delegate to it — the served route, the advertised route, and the authorization track can never disagree. - The env var is now effectively tri-state: unset → HTTP surface on;
explicit
true→ additionally auto-start the long-lived stdio transport at boot (unchanged, still opt-in — a default must not claim the process's stdin/stdout); explicitfalse→ everything off, fail-closed (404, no metadata, no DCR). - The OAuth 2.1 TLS rule is unaffected: on a plain-HTTP non-loopback origin the OAuth track stays dark and the default-on surface remains API-key-only.
- New single decision point
-
5be00c3: feat(mcp): spec-compliant OAuth 2.1 authorization for
/api/v1/mcp(#2698)Any OAuth-capable MCP client (claude.ai custom connectors, Claude Desktop, Claude Code) can now connect to a deployment self-serve: no admin-minted API key, no central registry — you sign in through the browser as yourself and every tool call runs under your own permissions and row-level security.
Each deployment is its own authorization server, backed by the embedded better-auth instance (
@better-auth/oauth-provider). Rationale for the design decisions lives in #2698; the moving parts:- Discovery:
/.well-known/oauth-protected-resource(RFC 9728, incl. the path-inserted variant for/api/v1/mcp) and/.well-known/oauth-authorization-server(RFC 8414, incl. the path-inserted variant for the/api/v1/authissuer) are served from the deployment origin. 401s from/api/v1/mcpadvertise the resource metadata viaWWW-Authenticate, so clients bootstrap the flow automatically. - Dynamic Client Registration (RFC 7591) is enabled (unauthenticated, as
the MCP spec requires) whenever the MCP surface is on — every deployment is a
distinct AS, so clients cannot ship pre-registered IDs. Force it either way
with
OS_OIDC_DCR_ENABLEDor the newplugins.dynamicClientRegistrationauth-config field. The embedded AS itself auto-enables whenever the MCP surface is on — which is now the default (explicitOS_OIDC_PROVIDER_ENABLED=falsestill wins). - Authorization-code + PKCE flow with RFC 8707 resource binding: access
tokens are minted with
aud=<origin>/api/v1/mcpand verified locally (signature/issuer/audience/expiry) against the deployment's own JWKS — fail-closed parity with API keys: unknown/expired/wrong-audience tokens, sub-less M2M tokens, or a presented-but-invalid bearer never fall back to an ambient session, they 401. - Token → ExecutionContext: a valid access token resolves to the same
principal-bound
ExecutionContextas every other credential, single-sourced throughresolveAuthzContext— OAuth adds a second provenance for the principal, not a second authz model.ExecutionContextgains an optionaloauthScopesfield carrying the token's granted scopes. - Coarse scopes → tool families, enforced at tool dispatch:
data:read(list/describe/query/get),data:write(create/update/delete),actions:execute(list_actions/run_action). Constants live in@objectstack/spec/ai(MCP_OAUTH_SCOPES). Tools outside the grant are not registered — and therefore rejected — for that request. API-key and session principals are unaffected (not scope-limited). - TLS required, localhost exempt (OAuth 2.1): on a plain-HTTP non-loopback origin the OAuth track stays dark (no metadata, no bearer acceptance) and the endpoint remains API-key-only. Local clients reach intranet deployments; claude.ai web connectors additionally need public HTTPS reachability.
API keys are unchanged (dual-track):
x-api-key/Authorization: ApiKey/Authorization: Bearer osk_…keep working exactly as before for CI and headless agents — covered by new regression tests. - Discovery:
- Updated dependencies [6d83431]
- Updated dependencies [01917c2]
- Updated dependencies [b271691]
- Updated dependencies [a5a1e41]
- Updated dependencies [466adf6]
- Updated dependencies [57b89b4]
- Updated dependencies [5be00c3]
- Updated dependencies [466adf6]
- Updated dependencies [2bee609]
- Updated dependencies [9fa84f9]
- Updated dependencies [fc7e7f7]
- @objectstack/spec@13.0.0
- @objectstack/core@13.0.0
- @objectstack/platform-objects@13.0.0
- @objectstack/types@13.0.0
- Updated dependencies [6cebf22]
- Updated dependencies [21420d9]
- @objectstack/spec@12.6.0
- @objectstack/core@12.6.0
- @objectstack/platform-objects@12.6.0
- @objectstack/types@12.6.0
- Updated dependencies [8b3d363]
- @objectstack/spec@12.5.0
- @objectstack/core@12.5.0
- @objectstack/platform-objects@12.5.0
- @objectstack/types@12.5.0
- Updated dependencies [60dc3ba]
- @objectstack/spec@12.4.0
- @objectstack/core@12.4.0
- @objectstack/platform-objects@12.4.0
- @objectstack/types@12.4.0
- Updated dependencies [e7eceec]
- @objectstack/spec@12.3.0
- @objectstack/core@12.3.0
- @objectstack/platform-objects@12.3.0
- @objectstack/types@12.3.0
- Updated dependencies [fce8ff4]
- Updated dependencies [3962023]
- Updated dependencies [2bb193d]
- Updated dependencies [0426d27]
- Updated dependencies [da807f7]
- Updated dependencies [4f5b791]
- @objectstack/spec@12.2.0
- @objectstack/core@12.2.0
- @objectstack/platform-objects@12.2.0
- @objectstack/types@12.2.0
- Updated dependencies [93e6d02]
- @objectstack/spec@12.1.0
- @objectstack/core@12.1.0
- @objectstack/platform-objects@12.1.0
- @objectstack/types@12.1.0
-
07f055c: feat(auth): last-login audit fields — sys_user.last_login_at / last_login_ip (ADR-0069 D7)
Completes the ADR-0069 D7 identity-field set:
sys_user.last_login_atandsys_user.last_login_ipare stamped on every successful/sign-in/emailbyAuthManager.stampLastLogin(a best-effort after-hook, independent of the lockout-accounting path so it runs even when lockout is disabled). The IP is taken from the trusted forwarded headers (x-forwarded-for→cf-connecting-ip→x-real-ip), the same precedence as the D5 IP allow-list middleware, and capped to the 45-char column width. Both fields are system-managed, read-only, and land in the Admin group ofsys_user.The rest of ADR-0069 P1 (password complexity/history/expiry, HIBP, account lockout, enforced MFA) was already implemented; this fills the one missing D7 field pair. ADR-0069 status updated Proposed → Accepted (P1/P2 implemented) with an implementation-status matrix reflecting what is landed vs the remaining P2 gaps (per-org IP ranges, shared-store rate limiting).
-
1b1b34e: feat(auth): shared cross-node rate-limit + session store via the cache service (ADR-0069 D2)
Multi-node deployments previously rate-limited per process — better-auth's default
rateLimitstore is in-memory, so each node counted independently and an attacker could rotate nodes to bypass the limit.AuthPluginnow wires the kernelcacheservice as better-auth'ssecondaryStorageand flipsrateLimit.storageto'secondary-storage', so rate-limit counters (and the session cache) are enforced against one shared store across every node — shared iff the cache service is (Redis adapter in a cluster; memory single-node, where behavior is unchanged). When no cache service is registered the plugin logs a warning that a multi-node deployment needs a shared cache (ADR-0069 honesty — no silent per-process limiting presented as global).New
cacheSecondaryStorage(cache)adapter (ICacheService→ better-authSecondaryStorage). Note: the cache has no atomic increment, so under high concurrency the get→set counter path can slightly over-count — acceptable for a rate limiter and strictly better than independent per-node counters; a future cache adapter exposing atomic INCR can add anincrementmethod for exact counting.
- Updated dependencies [a8df396]
- Updated dependencies [e695fe0]
- Updated dependencies [07f055c]
- Updated dependencies [7c09621]
- Updated dependencies [7709db4]
- Updated dependencies [2082109]
- Updated dependencies [7c09621]
- Updated dependencies [9860de4]
- Updated dependencies [069c205]
- @objectstack/spec@12.0.0
- @objectstack/platform-objects@12.0.0
- @objectstack/core@12.0.0
- @objectstack/types@12.0.0
- Updated dependencies [6a9397e]
- Updated dependencies [c0efe5d]
- @objectstack/spec@11.10.0
- @objectstack/core@11.10.0
- @objectstack/platform-objects@11.10.0
- @objectstack/types@11.10.0
- Updated dependencies [d3595d9]
- @objectstack/spec@11.9.0
- @objectstack/core@11.9.0
- @objectstack/platform-objects@11.9.0
- @objectstack/types@11.9.0
- Updated dependencies [53d491a]
- Updated dependencies [b84726b]
- @objectstack/platform-objects@11.8.0
- @objectstack/spec@11.8.0
- @objectstack/core@11.8.0
- @objectstack/types@11.8.0
- Updated dependencies [5178906]
- @objectstack/spec@11.7.0
- @objectstack/platform-objects@11.7.0
- @objectstack/core@11.7.0
- @objectstack/types@11.7.0
- @objectstack/spec@11.6.0
- @objectstack/core@11.6.0
- @objectstack/types@11.6.0
- @objectstack/platform-objects@11.6.0
- Updated dependencies [6ee4f04]
- Updated dependencies [c1e3a65]
- @objectstack/spec@11.5.0
- @objectstack/core@11.5.0
- @objectstack/platform-objects@11.5.0
- @objectstack/types@11.5.0
- Updated dependencies [5821c51]
- Updated dependencies [a0fce3f]
- @objectstack/spec@11.4.0
- @objectstack/core@11.4.0
- @objectstack/platform-objects@11.4.0
- @objectstack/types@11.4.0
-
59576d0: fix(auth): restore the admin gate on POST /admin/oauth-application/toggle-disabled after ADR-0068
ADR-0068 stopped
customSessionfrom synthesizinguser.role = 'admin'; canonical roles now arrive inuser.roles[]withuser.isPlatformAdminas a derived alias. The OAuth-client enable/disable route was missed in that migration and still gated onsession.user.role !== 'admin', which now rejects even platform admins (the scalar is no longer synthesized). It now mirrors the sibling /admin/unlock-user gate:isPlatformAdmin/platform_admininroles[], with the legacyrolescalar as a fallback.Also corrects the now-stale
customSession()doc comment in auth-manager that still described the removeduser.role = 'admin'overwrite. -
Updated dependencies [58e8e31]
-
Updated dependencies [b4a5df0]
- @objectstack/spec@11.3.0
- @objectstack/core@11.3.0
- @objectstack/platform-objects@11.3.0
- @objectstack/types@11.3.0
- Updated dependencies [d0f4b13]
- Updated dependencies [302bdab]
- @objectstack/spec@11.2.0
- @objectstack/core@11.2.0
- @objectstack/platform-objects@11.2.0
- @objectstack/types@11.2.0
-
cbc8c02: feat(auth): opt-in SSO domain verification (ADR-0024 ②)
Add DNS-TXT domain-ownership verification for external SSO providers, gated behind a new
OS_SSO_DOMAIN_VERIFICATIONflag (off by default — today's register→login behavior is unchanged). When enabled,@better-auth/ssomounts/sso/request-domain-verification+/sso/verify-domainand enforces that a provider's email domain be DNS-verified before it may complete a login.auth-manager.ts: newssoDomainVerificationenabled-flag (readBooleanEnv) → passesdomainVerification: { enabled: true }tosso(); publicisSsoDomainVerificationEnabled()helper.register-sso-provider.ts:runRequestDomainVerification/runVerifyDomainbridges — re-dispatch through the gated better-auth endpoints and reshape the response into the{ success, data }envelope thesys_sso_provideractionresultDialogreads (request → ready-to-paste DNS TXT record; verify → clear success/error). A bare 404 from the inner endpoint is surfaced as "not enabled for this environment".auth-plugin.ts: mount the two bridges as rawApp routes (/admin/sso/{request-domain-verification,verify-domain}).sys_sso_provider:domain_verifiedfield + list column + the two actions;domainVerifieddocumented inAUTH_SSO_PROVIDER_SCHEMA.
-
ce0b4f6: Auth: password expiry — the session-validation gate (ADR-0069 D1, P1)
Builds the authentication-policy session gate ADR-0069 needs and uses it for password expiry. When
password_expiry_days(newauthsetting, 0 = off) is exceeded, an authenticated user is blocked from protected REST resources with403 PASSWORD_EXPIREDuntil they change their password — while auth + remediation paths stay reachable.- core: new pure
evaluateAuthGate/isAuthGateAllowlistedhelper (@objectstack/core/security) — single source of truth for the allow-list (auth endpoints, change-password, health, UI-bootstrap reads). - plugin-auth:
customSessioncomputes the gate posture once and attachesuser.authGate;computeAuthGatereadssys_user.password_changed_atvs the configured window;password_changed_atis stamped on sign-up / change / reset;isAuthGateActive()keeps the gate zero-overhead when off. - platform-objects: new
sys_user.password_changed_atcolumn. - rest:
resolveExecCtxcarriesauthGate;enforceAuthblocks gated sessions (independent ofrequireAuth) using the core allow-list. - service-settings: new
password_expiry_daysfield.
Default-off / additive (no upgrade behavior change); a null
password_changed_atnever expires (existing users). Per ADR-0049 the setting ships with its enforcement; timestamps written asDate(ADR-0074).This gate is the shared seam for enforced MFA (ADR-0069 D3), which lands next as a small addition (a second
authGatebranch). The dispatcher/MCP path is a follow-up (tracked in #2375); the REST surface the Console uses is fully gated here. - core: new pure
-
90bce88: Auth: enforced MFA (ADR-0069 D3, P1)
Completes the session-validation gate: when
mfa_required(newauthsetting) is on, an authenticated user without TOTP enrolled is blocked from protected resources with403 MFA_REQUIREDonce theirmfa_grace_period_days(default 7) window elapses — while the two-factor enrollment endpoints stay reachable so they can comply. Reuses theauthGateseam shipped in #2388 (a second posture branch incomputeAuthGate).- New
authsettingsmfa_required(toggle) +mfa_grace_period_days; enablingmfa_requiredalso force-enables thetwoFactorplugin so/two-factor/*enrollment exists. - New
sys_user.mfa_required_atcolumn — the grace clock, stamped lazily the first time a user is seen required-but-unenrolled. isAuthGateActive()now also trips onmfa_required(still zero-overhead when off).
Default-off / additive (no upgrade behavior change); per ADR-0049 the setting ships with its enforcement.
Needs an objectui follow-up: the Console should handle a
403 MFA_REQUIREDby showing the TOTP-enrollment prompt. Per-orgsys_organization.require_mfaand the dispatcher/MCP gate remain follow-ups (#2375). - New
-
3209ec6: Auth: session controls — idle timeout, absolute max lifetime, concurrent cap (ADR-0069 D4, P2)
Adds three
authsession-control settings (all 0 = off):session_idle_timeout_minutes— sign a user out after inactivity. Enforced incustomSession: touchessys_session.last_activity_at(throttled to once a minute) and, once the idle window is exceeded, revokes the session.session_absolute_max_hours— cap total session lifetime regardless of refresh; revoked oncecreated_atis older than the cap.max_concurrent_sessions_per_user— on sign-in, keep the newest N live sessions and revoke the rest (oldest first).
Revocation expires the session in place (
expires_atset to the past +revoked_at/revoke_reasonstamped on newsys_sessioncolumns), so better-auth returns no session on the next request → the Console's existing 401 → login redirect handles it (no client change). Note: better-auth garbage-collects expired sessions, so therevoke_reasonaudit row is best-effort; the enforcement (session killed) is not.Default-off / additive (no upgrade behavior change); per ADR-0049 each setting ships with its enforcement.
-
8c84c97: Auth: IP allow-list — network gating on the auth routes (ADR-0069 D5, P2)
Adds an
allowed_ip_rangesauth setting (CIDR ranges or exact IPs; empty = no restriction). A Hono middleware registered ahead of the better-auth handler in the auth-route registration rejects auth requests from a client IP outside the ranges with403 IP_NOT_ALLOWED, before they reach better-auth.- Client IP is read trust-proxy-aware from
x-forwarded-for(first hop) /cf-connecting-ip/x-real-ip. - The public render helpers (
/config,/bootstrap-status) are exempt so a blocked client still gets a clean login page + a clear error. - Fails OPEN when the client IP can't be determined (no proxy header), so a misconfigured proxy is a no-op rather than a lockout — an admin enabling this must ensure forwarded headers are trusted.
- IPv4 CIDR (
a.b.c.d/n) + exact IPv4/IPv6 matching.
Default-off / additive; per ADR-0049 the setting ships with its enforcement.
- Client IP is read trust-proxy-aware from
-
e011d42: Auth: per-org MFA + dispatcher/MCP gate — complete the ADR-0069 enforced-MFA story
Two follow-ups that make enforced MFA total:
- Per-org
sys_organization.require_mfa— an org may require MFA above the global floor.computeAuthGatenow treats the active org'srequire_mfaas an effective MFA requirement even when the globalmfa_requiredis off;isAuthGateActive()stays cheap via a 60s-TTL "any org requires MFA" cache (lazy background refresh), so a brand-new per-org requirement activates the gate on the next request without per-request org queries. - Dispatcher/MCP gate — the auth-policy gate now also runs in the runtime dispatcher (after
resolveExecutionContext), so MCP / GraphQL / embedded data paths enforcePASSWORD_EXPIRED/MFA_REQUIREDconsistently with the REST seam (reusing the sharedevaluateAuthGateallow-list). Previously only the REST surface (the Console) was gated.
Default-off / additive. Per ADR-0049 each setting ships with its enforcement.
- Per-org
-
6e5bdd5: feat(auth): SAML 2.0 SSO via @better-auth/sso (ADR-0069 P3)
@better-auth/sso@1.6.20ships full SAML 2.0 (samlify-backed), so SAML needs no custom plugin. Adds aregister_saml_provideraction onsys_sso_providerand arunRegisterSamlProviderFromFormbridge that reshapes the flat admin form into the nestedsamlConfigand re-dispatches through/sso/register(admin gate enforced), returning the SP ACS + metadata URLs to configure on the IdP. Updates ADR-0069 to correct the stale "SAML is out of better-auth core" premise.
-
18f9713: Auth: surface "SSO Providers" in the Setup app nav when SSO is enabled (ADR-0024 / cloud#551)
The
sys_sso_provideradmin object (register / list / delete external OIDC IdPs) had no navigation entry, so an admin could only reach it by direct URL.AuthPluginnow contributes an "SSO Providers" entry into the Setup app's Access Control group — but only when the external-IdP RP is wired (AuthManager.isSsoWired(), which captures both self-hostOS_SSO_ENABLEDand the cloud per-envplanAllowsSsoarriving viaplugins.sso). Owning-plugin-contributes pattern (ADR-0029 K2), mirroringplugin-security.isSsoWired()is made public for this gate. -
7cf81a7: Auth: org-scope registered SSO/SAML providers so any org admin can manage them (ADR-0024 / cloud#551)
@better-auth/sso's provider-management endpoints (delete / update / domain verification) gate ORG-LESS providers onprovider.userId === caller— only the original registrar could manage them, so a second org admin couldn't delete or verify an IdP someone else registered. The register bridges now resolve the caller's active organization (best-effort, via a/get-sessionre-dispatch) and scope the provider to it, so management gates onisOrgAdmininstead — any org owner/admin can manage the environment's IdPs. Falls back to org-less (no behavior change) when no active org is set.Verified E2E: an OIDC provider registered through the form lands with
organization_idset to the env's org (was null); register + delete still succeed. -
d7a88df: Auth: SSO quality polish (ADR-0024 / cloud#551)
- plugin-auth:
OS_OIDC_PROVIDER_ENABLED/OS_SSO_ENABLED/OS_SCIM_ENABLEDnow parse with the sharedreadBooleanEnvhelper (same asOS_AUTH_TWO_FACTORetc.), so the platform-standard truthy set works (true/1/yes/on, case-insensitive) instead of only the literal'true'— a repeated operator footgun whereOS_SSO_ENABLED=1silently parsed as disabled. Added unit tests. - platform-objects:
sys_sso_provider's list view gets a per-object empty state ("No SSO providers yet" + a pointer to "Register SSO Provider"), replacing the shared identity-object copy ("records are created automatically … cannot be added here") which is wrong for this object — it HAS a register action.
- plugin-auth:
-
4f8f108: Auth: make the open-source SSO-provider registration form produce a usable IdP (ADR-0024 / cloud#551)
The
sys_sso_providerregister_sso_providerUI action posted FLAT form fields to@better-auth/sso's/sso/register, which expects the OIDC fields NESTED underoidcConfig. The top-levelclientId/clientSecretwere Zod-stripped, so the form persisted anoidc_config = nullprovider that could never complete a login ("Invalid SSO provider").- plugin-auth: new shared
runRegisterSsoProviderFromFormhelper reshapes the flat form body into the nested shape and re-dispatches it through the real/sso/register(so the admin gate, the public-routabletrustedOriginsallowance, discovery hydration, and secret handling all still run). Exposed via a new/admin/sso/registerbridge route on the hostAuthPlugin. (The cloud per-env runtime mounts the same helper in itsAuthProxyPlugin— mirrorsset-initial-password.) - platform-objects:
register_sso_providerretargets to/api/v1/auth/admin/sso/registerand gainsdiscoveryEndpoint,scopes, and attribute-mapping (mapId/mapEmail/mapName) fields. Open mechanism — keeps runtime IdP registration self-service in the OSS edition.
Verified E2E: an admin registers an external OIDC IdP from the flat form → a member logs in through it (JIT-provisioned,
sys_account.provider_idset); a non-admin is rejected (403) before discovery runs. - plugin-auth: new shared
-
Updated dependencies [cbc8c02]
-
Updated dependencies [07c2773]
-
Updated dependencies [d7a88df]
-
Updated dependencies [4f8f108]
-
Updated dependencies [ce0b4f6]
-
Updated dependencies [90bce88]
-
Updated dependencies [3209ec6]
-
Updated dependencies [e011d42]
-
Updated dependencies [6e5bdd5]
-
Updated dependencies [9ccfcd6]
-
Updated dependencies [ecf193f]
-
Updated dependencies [51bec81]
-
Updated dependencies [3e593a7]
-
Updated dependencies [fdb41c0]
-
Updated dependencies [63d5403]
- @objectstack/platform-objects@11.1.0
- @objectstack/core@11.1.0
- @objectstack/spec@11.1.0
- @objectstack/types@11.1.0
-
21b3208: Auth: password complexity policy (ADR-0069 D1, P1)
Adds
password_require_complexity(toggle, default off) +password_min_classes(1–4, default 3) to theauthpassword-policy settings. A custom validator runs in the better-authbeforehook on/sign-up/email,/reset-password, and/change-password, rejecting passwords that use fewer thanpassword_min_classesof the four character classes (upper / lower / digit / symbol) withPASSWORD_POLICY_VIOLATION— better-auth natively enforces only min/max length.Default-off and additive (no upgrade behavior change); per ADR-0049 the setting ships with its enforcement. No new identity fields. Continues the ADR-0069 P1 password-policy work alongside the HIBP breached-password reject (#2361).
-
9b5bf3d: Auth: password history / no-reuse (ADR-0069 D1, P1)
Adds
password_history_count(0–24, 0 = off) to theauthpassword-policy settings. On/change-passwordand/reset-password, a new password that matches the current password or any of the last N hashes is rejected withPASSWORD_REUSE. A new boundedsys_account.previous_password_hashescolumn (JSON ring, system-managed, hidden) backs the check; it is maintained by before/after hooks (capture the old hash, append on success).Reuses better-auth's native
password.verify(no bespoke crypto) and resolves the reset-flow user via the same token lookup better-auth uses. Default-off / additive (no upgrade behavior change); per ADR-0049 the setting ships with its enforcement. -
cb5b393: Auth: account lockout + rate-limit tuning (ADR-0069 D2, P1)
Second slice of ADR-0069 — per-identity brute-force protection, reusing the setting→enforcement pattern from the HIBP PR.
- Account lockout
[custom][field]: newsys_user.failed_login_count/sys_user.locked_untilcolumns;authsettingslockout_threshold(0 = off) +lockout_duration_minutes. Enforced in the/sign-in/emailbefore/after hooks — failures increment the counter, crossing the threshold stampslocked_until, and a locked account is rejected even with the correct password (survives IP rotation, unlike rate limiting). A successful sign-in resets both. - Admin Unlock: new admin-guarded
POST /api/v1/auth/admin/unlock-userroute + anunlock_useraction onsys_user. - Rate-limit tuning
[native]:authsettingsrate_limit_max/rate_limit_window_secondswire better-auth's corerateLimitwith strictercustomRulesfor/sign-in/email,/sign-up/email,/request-password-reset,/reset-password.
All settings default off / to safe values; additive (no upgrade behavior change). Per ADR-0049 each setting ships with its enforcement. Timestamps are written as
Date(never epoch-ms) per ADR-0074. - Account lockout
-
ab5718a: Auth: reject breached passwords via Have I Been Pwned (ADR-0069 D1, P1)
First slice of ADR-0069 (enterprise authentication hardening) and the enforcement-wired pattern template the rest of the ADR follows. Adds a
password_reject_breachedauth setting (default off) bound end-to-end to better-auth's nativehaveibeenpwnedplugin — a k-anonymity range check on sign-up / change-password / reset-password (the plaintext password never leaves the process).- spec: new
passwordRejectBreachedflag onAuthPluginConfigSchema. - service-settings: new "Reject breached passwords" toggle in the
authmanifest's password-policy group (globalscope,manage_platform_settings). - plugin-auth:
bindAuthSettingsmaps the setting into the plugin config;buildPluginListgates and mounts thehaveIBeenPwnedplugin (envOS_AUTH_PASSWORD_REJECT_BREACHEDwins over config, mirroringOS_AUTH_TWO_FACTOR). - cli: surface the knob in the
serveboot config alongsidetwoFactor.
Default-off and additive — no behavior change on upgrade. Per ADR-0049 the toggle ships with its enforcement (no false surface). No new identity fields (the
[custom]D1 items — complexity / expiry / history — land in follow-up PRs). - spec: new
-
caa3ef4: Auth: trust public-routable external-IdP origins at SSO registration (ADR-0024 / cloud#551)
@better-auth/sso's discovery validation requires every IdP endpoint origin to be intrustedOrigins— even for a publicly-routable IdP. That broke ADR-0024's "register your OIDC IdP at runtime, no boot config" promise: registering any external IdP returned400 discovery_untrusted_originunless the operator had pre-listed it.When the external-SSO RP is enabled,
trustedOriginsis now exposed as a per-request function that, for aPOST /sso/register|/sso/update-provider, additionally trusts the public-routable issuer /oidcConfigendpoint origins declared in the request body (via@better-auth/core's ownisPublicRoutableHost). Private / internal / loopback hosts are never auto-trusted — they still require explicittrustedOriginsconfig (the documented SSRF escape hatch), and better-auth's own DNS-resolution checks still apply.Verified: a same-origin public IdP (GitLab.com — issuer and all discovered endpoints on one origin, like Okta / Entra / Auth0 / Keycloak) now registers at runtime with no boot config (was a hard 400). The admin gate still fires first (a non-admin is rejected before discovery runs). Note: IdPs that split endpoints across multiple domains (e.g. Google's
accounts.google.com+oauth2.googleapis.com) still need those extra origins intrustedOrigins. -
22b32c1: Auth: admin-gate self-service SSO provider registration + default-role JIT (ADR-0024 / cloud#551)
@better-auth/sso'sPOST /sso/registeronly enforces org-admin whenbody.organizationIdis supplied — a global (org-less) provider passed on nothing but a valid session, so any authenticated env member could register an env-wide external IdP (a JIT-provisioning / login-routing vector). This closed the "registerSSOProvider is admin-only" requirement of ADR-0024's first slice.- plugin-auth: a
before-hook on/sso/registernow requires the caller to be a platform admin OR an owner/admin of their active org, regardless oforganizationId. Fail-closed; unauthenticated requests still fall through tosessionMiddleware(→ 401). New helpersresolveActor()(hook-order-independent cookie/bearer resolution) andisOrgOrPlatformAdmin()(mirrorscustomSession's role derivation; reads viawithSystemReadContext). - plugin-auth:
sso()now receivesorganizationProvisioning.defaultRole:'member'so a first-time federated login lands with an explicit role (over SecurityPlugin'smember_defaultbaseline).
Additive and fail-closed — no behavior change for legitimate admins. The SSO mechanism stays framework-open (no identity-governance added).
- plugin-auth: a
-
1e8a813: feat(auth): surface
features.ssoin the public/auth/configresponsegetPublicConfig()reported every other auth capability flag (oidcProvider,twoFactor,multiOrgEnabled, …) but omitted enterprise SSO, even though the manager already computes whether the domain-routed@better-auth/ssoplugin is wired (OS_SSO_ENABLED/plugins.sso). Without it the login UI had no signal to gate on, so it rendered a "Sign in with SSO" button unconditionally — and on a self-hosted / local deployment where SSO isn't wired, clicking it only then surfaced "No SSO provider is configured for this email domain."The config now includes
features.sso.getPublicConfig()returns the coarse "is the plugin wired" flag — resolved with the EXACT logic that decides whether the plugin is mounted inbuildPlugins(), so the advertised capability can never disagree with the actual/sign-in/ssoroute. The/auth/configroute then refines it to "usable" via the newAuthManager.isSsoUsable(), which additionally requires at least onesys_sso_providerrow to exist — so a freshly-enabled but unconfigured SSO setup doesn't advertise a button that errors for everyone.isSsoUsable()only queries when wired and fails open to the wired flag on any introspection error (no data engine, query failure), so config never 500s. The console login form consumesfeatures.ssoto hide the button (objectui side). -
Updated dependencies [9b5bf3d]
-
Updated dependencies [cb5b393]
-
Updated dependencies [ab5718a]
-
Updated dependencies [4845c12]
-
Updated dependencies [c1a754a]
-
Updated dependencies [6fbe91f]
-
Updated dependencies [715d667]
-
Updated dependencies [5eef4cf]
-
Updated dependencies [72759e1]
-
Updated dependencies [6c4fbd9]
-
Updated dependencies [ef3ed67]
-
Updated dependencies [cd51229]
-
Updated dependencies [7697a0e]
-
Updated dependencies [e7e04f1]
-
Updated dependencies [cfd5ac4]
-
Updated dependencies [2be5c1f]
-
Updated dependencies [ad143ce]
-
Updated dependencies [5c4a8c8]
-
Updated dependencies [3afaeed]
-
Updated dependencies [5737261]
-
Updated dependencies [a619a3a]
-
Updated dependencies [f44c1bd]
-
Updated dependencies [795b6d1]
-
Updated dependencies [8801c02]
-
Updated dependencies [3d04e06]
-
Updated dependencies [4a84c98]
-
Updated dependencies [c715d25]
-
Updated dependencies [aa33b02]
-
Updated dependencies [d980f0d]
-
Updated dependencies [a658523]
-
Updated dependencies [82ff91c]
-
Updated dependencies [638f472]
- @objectstack/platform-objects@11.0.0
- @objectstack/spec@11.0.0
- @objectstack/types@11.0.0
- @objectstack/core@11.0.0
- @objectstack/spec@10.3.0
- @objectstack/core@10.3.0
- @objectstack/types@10.3.0
- @objectstack/platform-objects@10.3.0
- Updated dependencies [b496498]
- @objectstack/spec@10.2.0
- @objectstack/core@10.2.0
- @objectstack/platform-objects@10.2.0
- @objectstack/types@10.2.0
- Updated dependencies [49da36e]
- Updated dependencies [ac79f16]
- @objectstack/spec@10.1.0
- @objectstack/core@10.1.0
- @objectstack/platform-objects@10.1.0
- @objectstack/types@10.1.0
- Updated dependencies [d7ff626]
- Updated dependencies [2a1b16b]
- Updated dependencies [2256e93]
- Updated dependencies [7108ff3]
- Updated dependencies [30c0313]
- Updated dependencies [e16f2a8]
- Updated dependencies [e411a82]
- Updated dependencies [ae271d0]
- Updated dependencies [61ed5c7]
- Updated dependencies [a581385]
- Updated dependencies [d5f6d29]
- Updated dependencies [220ce5b]
- Updated dependencies [3efe334]
- Updated dependencies [0df063e]
- Updated dependencies [ce13bb8]
- Updated dependencies [feead7e]
- Updated dependencies [6ca20b3]
- Updated dependencies [5f875fe]
- Updated dependencies [b469950]
- Updated dependencies [47d978a]
- @objectstack/spec@10.0.0
- @objectstack/platform-objects@10.0.0
- @objectstack/core@10.0.0
- @objectstack/types@10.0.0
- Updated dependencies [e7f6539]
- Updated dependencies [2365d07]
- Updated dependencies [6595b53]
- Updated dependencies [fa8964d]
- Updated dependencies [36138c7]
- Updated dependencies [a8e4f3b]
- Updated dependencies [4c213c2]
- Updated dependencies [2afb612]
- @objectstack/spec@9.11.0
- @objectstack/core@9.11.0
- @objectstack/platform-objects@9.11.0
- @objectstack/types@9.11.0
- Updated dependencies [db02bd5]
- Updated dependencies [641675d]
- Updated dependencies [94e9040]
- Updated dependencies [4331adb]
- Updated dependencies [1f88fd9]
- Updated dependencies [1f88fd9]
- @objectstack/spec@9.10.0
- @objectstack/platform-objects@9.10.0
- @objectstack/core@9.10.0
- @objectstack/types@9.10.0
- @objectstack/spec@9.9.1
- @objectstack/core@9.9.1
- @objectstack/types@9.9.1
- @objectstack/platform-objects@9.9.1
-
0d4e3f3: feat(auth): password-policy & session settings — live, enforced (P0 security)
Extends the existing
authsettings manifest (global scope) with the security policy keys that are genuinely enforced today, rather than standing up a newsecuritynamespace full of non-functional toggles (which would be false surface):- Password policy —
password_min_length(default 8),password_max_length(default 128). Enforced by better-auth on sign-up and password reset. - Sessions —
session_expiry_days(default 7, absolute lifetime),session_refresh_days(default 1, refresh threshold).
These ride the existing
AuthPlugin.bindAuthSettings→AuthManager.applyConfigPatchpath (read onkernel:ready, re-applied live viasettings.subscribe('auth'), which invalidates the cached better-auth instance). Days are converted to seconds for better-auth'ssession.{expiresIn,updateAge}; unset (source: 'default') and malformed/non-positive values are ignored so the provider default holds. Ships en + zh-CN translations.Deliberately out of scope (no enforcement exists, so they're not declared as settings): MFA-required, IP allowlist, SSO/SAML, SCIM, API rate limits, password complexity/rotation/history. These are real features to be built, not settings toggles.
- Password policy —
- Updated dependencies [84249a4]
- Updated dependencies [11af299]
- Updated dependencies [d5774b5]
- Updated dependencies [134043a]
- Updated dependencies [90108e0]
- Updated dependencies [9afeb2d]
- Updated dependencies [6bec07e]
- Updated dependencies [601cc11]
- Updated dependencies [575448d]
- @objectstack/spec@9.9.0
- @objectstack/core@9.9.0
- @objectstack/platform-objects@9.9.0
- @objectstack/types@9.9.0
- Updated dependencies [97c55b3]
- Updated dependencies [1b1f490]
- @objectstack/spec@9.8.0
- @objectstack/core@9.8.0
- @objectstack/platform-objects@9.8.0
- @objectstack/types@9.8.0
- @objectstack/spec@9.7.0
- @objectstack/core@9.7.0
- @objectstack/types@9.7.0
- @objectstack/platform-objects@9.7.0
-
1b82b64: auth: expose
isPlatformAdminon the customSession user payloadThe session already derives a coarse
adminrole for platform admins or active-org admins, but never surfaced the underlying platform-admin signal. Console actionvisibleCEL predicates need it to gate platform-admin-only object actions (e.g.sys_environment.change_plan) without hiding org-admin actions. BothcustomSessionreturn paths now carry the boolean; org-admins who are not platform admins correctly getisPlatformAdmin: false. -
Updated dependencies [d1e930a]
-
Updated dependencies [71578f2]
-
Updated dependencies [5e3a301]
-
Updated dependencies [5db2742]
- @objectstack/spec@9.6.0
- @objectstack/core@9.6.0
- @objectstack/platform-objects@9.6.0
- @objectstack/types@9.6.0
- Updated dependencies [ee72aae]
- @objectstack/spec@9.5.1
- @objectstack/core@9.5.1
- @objectstack/platform-objects@9.5.1
- @objectstack/types@9.5.1
- Updated dependencies [d08551c]
- Updated dependencies [5be7102]
- Updated dependencies [707aeed]
- Updated dependencies [7a103d4]
- Updated dependencies [4b01250]
- @objectstack/spec@9.5.0
- @objectstack/platform-objects@9.5.0
- @objectstack/core@9.5.0
- @objectstack/types@9.5.0
- Updated dependencies [060467a]
- Updated dependencies [0856476]
- Updated dependencies [b678d8c]
- Updated dependencies [b678d8c]
- Updated dependencies [b678d8c]
- @objectstack/spec@9.4.0
- @objectstack/core@9.4.0
- @objectstack/platform-objects@9.4.0
- @objectstack/types@9.4.0
- Updated dependencies [1ada658]
- Updated dependencies [3219191]
- Updated dependencies [290f631]
- Updated dependencies [50b7b47]
- Updated dependencies [f15d6f6]
- Updated dependencies [f8684ea]
- Updated dependencies [c802327]
- Updated dependencies [b4765be]
- @objectstack/spec@9.3.0
- @objectstack/platform-objects@9.3.0
- @objectstack/core@9.3.0
- @objectstack/types@9.3.0
- Updated dependencies [2f57b75]
- Updated dependencies [2f57b75]
- @objectstack/spec@9.2.0
- @objectstack/core@9.2.0
- @objectstack/platform-objects@9.2.0
- @objectstack/types@9.2.0
- Updated dependencies [b9062c9]
- @objectstack/spec@9.1.0
- @objectstack/core@9.1.0
- @objectstack/platform-objects@9.1.0
- @objectstack/types@9.1.0
- Updated dependencies [1817845]
- @objectstack/spec@9.0.1
- @objectstack/core@9.0.1
- @objectstack/platform-objects@9.0.1
- @objectstack/types@9.0.1
-
f533f42: Settings namespace environment overrides now use the canonical ObjectStack
OS_<NAMESPACE>_<KEY>form, with no unprefixed aliases. For example,ai.openai_base_urlis nowOS_AI_OPENAI_BASE_URL, andfeature_flags.ai_enabledis nowOS_FEATURE_FLAGS_AI_ENABLED.The AI service now treats a stored or env-locked
provider=memorysetting as an explicit override, while the manifest default still leaves boot-time provider auto-detection intact.The auth plugin now binds the
authsettings namespace to better-auth runtime configuration, exposes an extension hook for provider packages, and includes a basic Google sign-in implementation configured either in Setup → Authentication or by deployment-levelGOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET.
- Updated dependencies [4c3f693]
- Updated dependencies [0bf39f1]
- Updated dependencies [f533f42]
- Updated dependencies [1c83ee8]
- @objectstack/spec@9.0.0
- @objectstack/core@9.0.0
- @objectstack/platform-objects@9.0.0
- @objectstack/types@9.0.0
- @objectstack/spec@8.0.1
- @objectstack/core@8.0.1
- @objectstack/types@8.0.1
- @objectstack/platform-objects@8.0.1
-
1e8b680: fix(security): close four P0 launch-readiness findings
- plugin-auth (P0-1):
generateSecret()now throws (fails boot) when noOS_AUTH_SECRETis set andNODE_ENV==='production', instead of silently falling back to a predictabledev-secret-<timestamp>(session forgery). The dev/test fallback is unchanged. - plugin-security (P0-2): the permission-resolution
catchnow fails closed — it logs at ERROR and throwsPermissionDeniedErrorrather thanreturn next(). A degraded metadata service can no longer let every authenticated request bypass RBAC/RLS. System operations still bypass as before. - driver-sql (P0-3): the
contains/$containsoperator now escapes LIKE metacharacters (%/_/\) in the user value and binds an explicitESCAPE '\', so a value of%matches literally instead of every row (filter bypass). Correct across SQLite/MySQL/Postgres. - driver-mongodb (P0-4): the field-operator translator now rejects unknown
$-operators instead of passing them through, blocking$where/$function/$expr(server-side JS execution / query-intent bypass). All legitimate ObjectQL operators remain allowlisted.
+12 regression tests across the four packages.
- plugin-auth (P0-1):
-
Updated dependencies [a46c017]
-
Updated dependencies [b990b89]
-
Updated dependencies [99111ec]
-
Updated dependencies [d5a8161]
-
Updated dependencies [5cf1f1b]
-
Updated dependencies [9ef89d4]
-
Updated dependencies [3306d2f]
-
Updated dependencies [c262301]
-
Updated dependencies [bc44195]
-
Updated dependencies [9e2e229]
- @objectstack/spec@8.0.0
- @objectstack/core@8.0.0
- @objectstack/platform-objects@8.0.0
- @objectstack/types@8.0.0
- @objectstack/spec@7.9.0
- @objectstack/core@7.9.0
- @objectstack/types@7.9.0
- @objectstack/platform-objects@7.9.0
- Updated dependencies [06f2bbb]
- Updated dependencies [36719db]
- Updated dependencies [424ab26]
- @objectstack/spec@7.8.0
- @objectstack/core@7.8.0
- @objectstack/platform-objects@7.8.0
- @objectstack/types@7.8.0
- Updated dependencies [b391955]
- Updated dependencies [f06b64e]
- Updated dependencies [023bf93]
- Updated dependencies [764c747]
- @objectstack/spec@7.7.0
- @objectstack/platform-objects@7.7.0
- @objectstack/core@7.7.0
- @objectstack/types@7.7.0
-
8c01eea: fix(dev): seed the dev admin in-process and fix the port-drift seed failure.
os dev(andpnpm dev:showcase) seeded the admin over HTTP against a hard-codedlocalhost:3000. In dev,serveauto-shifts off a busy port, so the seed POST hit the wrong server (or nothing) and the running instance never got an admin. A second, divergent seed inplugin-devinserted a credential-lesssys_userrow that could not log in.Consolidate to a single in-process seed:
@objectstack/plugin-auth—maybeSeedDevAdmin()runs onkernel:readyand createsadmin@objectos.ai/admin123through better-auth's realsignUpEmailpipeline (hashed credential), so the account is loginable;plugin-securitythen promotes it to platform admin. Empty-DB only (excludes the system service account), idempotent, never overwrites an existing account. Hard-gated toNODE_ENV=development; opt out withOS_SEED_ADMIN=0.@objectstack/cli— removed the HTTP seed;--seed-adminnow passesOS_SEED_ADMIN[_EMAIL|_PASSWORD]to the serve child.servepublishes its actually-bound port over IPC and to aruntime.<env>.jsonstate file underOS_HOME.@objectstack/plugin-dev— removed the credential-less raw insert;seedAdminUsermaps to the unifiedOS_SEED_ADMINtoggle.
-
b7a4f14: fix(dev): surface the seeded dev-admin credentials in the
servestartup banner.When the runtime seeds the dev admin on an empty DB, the confirmation was emitted via
ctx.loggerduringruntime.start()— inside serve's boot-quiet window — so it was swallowed and never reached the console. plugin-auth now records the seed result on theauthservice andserveprints it in the ready banner (after stdout is restored), e.g.:🔑 Dev admin: admin@objectos.ai / admin123 seeded on empty DB · dev only — do not use in productionShown only when an admin was actually seeded this boot (empty DB) — never on a DB that already had a user, so stale credentials are never displayed. Visible in both
serve --devandos dev(the child's stdout is inherited). -
Updated dependencies [955d4c8]
-
Updated dependencies [c4a4cbd]
-
Updated dependencies [b046ec2]
-
Updated dependencies [2170ad9]
-
Updated dependencies [02d6359]
-
Updated dependencies [7648242]
-
Updated dependencies [8fa1e7f]
-
Updated dependencies [7ae6abc]
-
Updated dependencies [55866f5]
-
Updated dependencies [60f9c45]
- @objectstack/spec@7.6.0
- @objectstack/platform-objects@7.6.0
- @objectstack/core@7.6.0
- @objectstack/types@7.6.0
- @objectstack/spec@7.5.0
- @objectstack/core@7.5.0
- @objectstack/types@7.5.0
- @objectstack/platform-objects@7.5.0
- @objectstack/spec@7.4.1
- @objectstack/core@7.4.1
- @objectstack/types@7.4.1
- @objectstack/platform-objects@7.4.1
-
c72daad: ADR-0029 D7 — Setup app navigation contributions.
Adds the UI-layer analog of object
own/extend: a package can contribute navigation items into an app it does not own, so a shared admin app can be a thin shell while each capability plugin ships the menu for the objects it owns.@objectstack/spec— newNavigationContributionSchema({ app, group?, priority, items }) and an optionalnavigationContributionsfield on the manifest.@objectstack/objectql—SchemaRegistry.registerAppNavContribution()plus lazy merge ingetApp/getAllApps(by target group id + priority, cloning so the stored app is never mutated); the engine wiresmanifest.navigationContributionsduring app registration.@objectstack/platform-objects— the Setup app becomes a shell of empty group anchors; its entries for platform-objects-owned objects move toSETUP_NAV_CONTRIBUTIONS.@objectstack/plugin-auth— registersSETUP_NAV_CONTRIBUTIONSalongside the Setup app it already registers.@objectstack/plugin-webhooks— contributes itsWebhooks/Webhook Deliveriesentries into the Setupgroup_integrationsslot (it ownssys_webhook/sys_webhook_deliveryper K2.a), demonstrating end-to-end cross-plugin contribution.
The rendered Setup nav is identical to the former static artifact — just assembled from its owners. A disabled/absent capability contributes nothing and its slot stays empty (in addition to the existing
requiresObjectgating). This unblocks moving each remaining K2 domain's menu out of the monolith with its objects.
- Updated dependencies [23c7107]
- Updated dependencies [c72daad]
- Updated dependencies [4404572]
- Updated dependencies [eea3f1b]
- Updated dependencies [e478e0c]
- Updated dependencies [4cc2ced]
- Updated dependencies [13632b1]
- Updated dependencies [f115182]
- Updated dependencies [2faf9f2]
- Updated dependencies [2faf9f2]
- Updated dependencies [2faf9f2]
- Updated dependencies [58b450b]
- Updated dependencies [82eb6cf]
- Updated dependencies [c381977]
- Updated dependencies [13d8653]
- Updated dependencies [ff3d006]
- Updated dependencies [5e831de]
- @objectstack/spec@7.4.0
- @objectstack/platform-objects@7.4.0
- @objectstack/core@7.4.0
- @objectstack/types@7.4.0
- Updated dependencies [5e7c554]
- @objectstack/spec@7.3.0
- @objectstack/core@7.3.0
- @objectstack/platform-objects@7.3.0
- @objectstack/types@7.3.0
-
9096dfe:
OS_env-var prefix migration (issue #1382).All ObjectStack-owned environment variables now use the
OS_prefix. Legacy names still work for one release and emit a one-shot deprecation warning via the newreadEnvWithDeprecation()helper in@objectstack/types.Renamed (with legacy fallback):
New Legacy (deprecated) OS_AUTH_SECRETAUTH_SECRET,BETTER_AUTH_SECRETOS_AUTH_URLAUTH_BASE_URL,BETTER_AUTH_URL,OS_AUTH_BASE_URLOS_PORTPORTOS_DATABASE_URLDATABASE_URLOS_ROOT_DOMAINROOT_DOMAINOS_MULTI_ORG_ENABLEDOS_MULTI_TENANTOS_CORS_ENABLEDCORS_ENABLEDOS_CORS_ORIGINCORS_ORIGINOS_CORS_CREDENTIALSCORS_CREDENTIALSOS_CORS_MAX_AGECORS_MAX_AGEOS_AI_MODELAI_MODELOS_MCP_SERVER_ENABLEDMCP_SERVER_ENABLEDOS_MCP_SERVER_NAMEMCP_SERVER_NAMEOS_MCP_SERVER_TRANSPORTMCP_SERVER_TRANSPORTOS_NODE_IDOBJECTSTACK_NODE_IDOS_METADATA_WRITABLEOBJECTSTACK_METADATA_WRITABLEOS_DEV_CRYPTO_KEYOBJECTSTACK_DEV_CRYPTO_KEYOS_HOMEOBJECTSTACK_HOMEMigration: rename in your
.env. Legacy names continue to work this release and will be removed in a future major. Industry-standard names (NODE_ENV,HOME,OPENAI_API_KEY,TURSO_*, OAuth*_CLIENT_ID/SECRET,RESEND_API_KEY,POSTMARK_TOKEN,AI_GATEWAY_*,SMTP_*) are NOT renamed. -
Updated dependencies [9096dfe]
- @objectstack/types@7.2.1
- @objectstack/spec@7.2.1
- @objectstack/core@7.2.1
- @objectstack/platform-objects@7.2.1
- @objectstack/spec@7.2.0
- @objectstack/core@7.2.0
- @objectstack/platform-objects@7.2.0
- Updated dependencies [6228609]
- Updated dependencies [47a92f4]
- @objectstack/platform-objects@7.1.0
- @objectstack/spec@7.1.0
- @objectstack/core@7.1.0
-
74470ad: New
accountApp for self-service identity management +App.hiddenshell hintAdds a dedicated Account App (
name: 'account', iconuser-circle) that exposes the three end-user identity surfaces:- Two-Factor Authentication —
sys_two_factor - Linked Accounts —
sys_account - OAuth Applications —
sys_oauth_application
The app declares no
requiredPermissions, so every authenticated user can reach it — unlike Setup, which requiressetup.accessand therefore excludes the defaultmember_defaultpermission set. Combined with the C-tierresultDialogactions already shipped on these objects (2FA QR + backup codes, OAuthclient_secretreveal,link_socialredirect), this replaces the legacy standaloneapps/accountSPA with a single console + metadata-driven surface.New
App.hidden: booleanfield (packages/spec/src/ui/app.zod.ts) hides an app from the top-level App Switcher. Hidden apps stay fully routable and permission-checked; the shell is expected to surface them through the avatar / user dropdown instead. Mirrors the GitHub Settings / Google account chip / Salesforce Personal Settings pattern. The Account app is the first user.Wiring:
plugin-authregistersACCOUNT_APPalongsideSETUP_APP/STUDIO_APP(packages/plugins/plugin-auth/src/auth-plugin.ts). The legacy duplicate entries inside Setup's Advanced group are kept unchanged — they remain admin-only for tenant-wide inspection.Follow-up for objectui: the shell's
AppSwitcherand avatarDropdownMenuneed updating to honourapp.hidden(filter hidden apps out of the switcher; render them as dropdown menu entries). Tracked separately. - Two-Factor Authentication —
-
257954d: Organization detail page — Members / Invitations / Teams tabs (slotted Page)
Adds a record-detail Page for
sys_organization(SysOrganizationDetailPage) so admins can manage the entire membership graph from a single record view instead of switching between three separate Setup list views.The page uses
kind: 'slotted'and overrides only thetabsslot — header, actions, highlights, details and discussion fall through to the synthesized default, so the existing record-header actions (Set Active,Edit,Delete,Leave Organization) are preserved unchanged.Three tabs, each a
record:related_listscoped byorganization_id:- Members —
sys_member(user, role, joined) - Invitations —
sys_invitation(email, role, status, expires, inviter) - Teams —
sys_team(name, created, updated)
Per-row actions defined on each child object (
invite_user,cancel_invitation,remove_member,transfer_ownership,create_team, …) are inherited unchanged — no admin endpoint is re-declared here.Deliberately omitted:
- OAuth Apps —
sys_oauth_applicationis owned byuser_id, notorganization_id; it surfaces on the user's Account view instead. - SSO — no
sys_sso*object exists yet; will become a fourth tab when better-auth's SSO plugin lands.
Package wiring:
@objectstack/platform-objectsexposes a new./pagessubpath export and re-exportsSysOrganizationDetailPagefrom the root.plugin-authregisters it via the existingmanifest.register({ ..., pages: [SysOrganizationDetailPage] })call alongside the platform apps and dashboards.
Verified end-to-end on the console-starter shell against
example-crm— the three tabs render and the Members/Teams tables populate with the rows better-auth creates automatically when an org is provisioned. - Members —
-
Updated dependencies [74470ad]
-
Updated dependencies [d29617e]
-
Updated dependencies [dc72172]
-
Updated dependencies [d29617e]
-
Updated dependencies [010757b]
-
Updated dependencies [257954d]
- @objectstack/spec@7.0.0
- @objectstack/platform-objects@7.0.0
- @objectstack/core@7.0.0
- @objectstack/spec@6.9.0
- @objectstack/core@6.9.0
- @objectstack/platform-objects@6.9.0
- @objectstack/spec@6.8.1
- @objectstack/core@6.8.1
- @objectstack/platform-objects@6.8.1
- Updated dependencies [6e88f77]
- Updated dependencies [c8b9f57]
- Updated dependencies [45d27c5]
- @objectstack/spec@6.8.0
- @objectstack/platform-objects@6.8.0
- @objectstack/core@6.8.0
- @objectstack/spec@6.7.1
- @objectstack/core@6.7.1
- @objectstack/platform-objects@6.7.1
- Updated dependencies [430067b]
- Updated dependencies [4f9e9d4]
- Updated dependencies [4f9e9d4]
- @objectstack/spec@6.7.0
- @objectstack/platform-objects@6.7.0
- @objectstack/core@6.7.0
- Updated dependencies [a49cfc2]
- @objectstack/spec@6.6.0
- @objectstack/core@6.6.0
- @objectstack/platform-objects@6.6.0
-
de239ef: Fix WebContainer (StackBlitz) sign-up / sign-in failing with
INTERNAL_SERVER_ERROR: No request state found. Please make sure you are calling this function within arunWithRequestStatecallback.WebContainer reports itself as Node.js but its
node:async_hooksimplementation does not propagateAsyncLocalStoragecontext acrossawaitboundaries. As a result, better-auth'srunWithRequestStatewrap installed byhandleRequestwas lost as soon as the innercustomSession→getSession()call chain awaited anything, and every endpoint that reads request state (e.g.should-session-refresh,oauth) threw "No request state found".AuthManagernow detects WebContainer and pre-populates better-auth's globalrequestStateAsyncStorageslot with a synchronous polyfill before better-auth instantiates its own. The polyfill correctly propagates the store through awaited promises within a singlerun()call, which is sufficient for WebContainer's single-flight dev server. Production environments (real Node, Bun, edge runtimes) continue to use the nativeAsyncLocalStorageand are unaffected.- @objectstack/spec@6.5.1
- @objectstack/core@6.5.1
- @objectstack/platform-objects@6.5.1
- @objectstack/spec@6.5.0
- @objectstack/core@6.5.0
- @objectstack/platform-objects@6.5.0
-
0bf6f9a: Add explicit
@better-auth/coredependency.plugin-authalready pulled@better-auth/coretransitively via@better-auth/oauth-provider, but several call sites inauth-manager.tsimport from it directly. Promote it to a first-class dependency so the resolved version is stable across the workspace andpnpm installdoesn't surface "module not found" against the transitive copy under stricter peer resolution.No behaviour change.
-
Updated dependencies [f8651cc]
-
Updated dependencies [f8651cc]
-
Updated dependencies [0bf6f9a]
- @objectstack/spec@6.4.0
- @objectstack/core@6.4.0
- @objectstack/platform-objects@6.4.0
- @objectstack/spec@6.3.0
- @objectstack/core@6.3.0
- @objectstack/platform-objects@6.3.0
-
b4c74a9: WebContainer (StackBlitz) signup compatibility:
AuthManagernow auto-detects WebContainer runtimes at construction time and swaps better-auth's defaultnode:crypto.scrypt-based password hasher for the pure-JS hasher from@better-auth/utils/password(which uses@noble/hashes/scryptunder the hood).Why: WebContainer's
node:cryptopolyfill ships an incompletescryptimplementation that throwsTypeError: y.run is not a functionon every signup, blocking template demos on StackBlitz. The pure-JS implementation is byte-compatible with the Node hasher (same scrypt params, samesalt:keyHexstorage format), so accounts created under either hasher remain mutually verifiable — no migration, no template changes.Scope: detection short-circuits to
undefinedon real Node, so production deployments are completely unaffected — the JS fallback module is only dynamically imported when one ofprocess.versions.webcontainer,SHELLcontainingjsh, orSTACKBLITZenv is present.Templates (
@template/todo,@template/contracts, …) require no changes; the fix lives entirely inside@objectstack/plugin-auth.
- Updated dependencies [b4c74a9]
- @objectstack/spec@6.2.0
- @objectstack/core@6.2.0
- @objectstack/platform-objects@6.2.0
- @objectstack/spec@6.1.1
- @objectstack/core@6.1.1
- @objectstack/platform-objects@6.1.1
- Updated dependencies [93c0589]
- @objectstack/spec@6.1.0
- @objectstack/core@6.1.0
- @objectstack/platform-objects@6.1.0
- Updated dependencies [629a716]
- Updated dependencies [dbc4f7d]
- Updated dependencies [944f187]
- @objectstack/spec@6.0.0
- @objectstack/platform-objects@6.0.0
- @objectstack/core@6.0.0
- Updated dependencies [bab2b20]
- Updated dependencies [fa011d8]
- Updated dependencies [f0f7c27]
- Updated dependencies [b806f58]
- @objectstack/platform-objects@5.2.0
- @objectstack/spec@5.2.0
- @objectstack/core@5.2.0
- Updated dependencies [75f4ee6]
- Updated dependencies [823d559]
- @objectstack/spec@5.1.0
- @objectstack/platform-objects@5.1.0
- @objectstack/core@5.1.0
- Updated dependencies [888a5c1]
- Updated dependencies [2f9073a]
- @objectstack/platform-objects@5.0.0
- @objectstack/spec@5.0.0
- @objectstack/core@5.0.0
- Updated dependencies [2869891]
- @objectstack/spec@4.2.0
- @objectstack/core@4.2.0
- @objectstack/platform-objects@4.2.0
- @objectstack/spec@4.1.1
- @objectstack/core@4.1.1
- @objectstack/platform-objects@4.1.1
- Updated dependencies [2108c30]
- Updated dependencies [23db640]
- @objectstack/spec@4.1.0
- @objectstack/core@4.1.0
- @objectstack/platform-objects@4.1.0
- 15e0df6: chore: unify all package versions to a single patch release
- Updated dependencies [15e0df6]
- @objectstack/spec@4.0.5
- @objectstack/core@4.0.5
- @objectstack/platform-objects@4.0.5
- Always register better-auth's
bearer()plugin so cross-origin browsers (where third-party cookies are blocked) and native mobile clients can authenticate viaAuthorization: Bearer <token>headers and pick up rotated tokens from theset-auth-tokenresponse header (fixes #1172).
- Updated dependencies [326b66b]
- @objectstack/spec@4.0.4
- @objectstack/core@4.0.4
- @objectstack/spec@4.0.3
- @objectstack/core@4.0.3
- Updated dependencies [5f659e9]
- @objectstack/spec@4.0.2
- @objectstack/core@4.0.2
-
e0b0a78: Deprecate DataEngineQueryOptions in favor of QueryAST-aligned EngineQueryOptions.
Engine, Protocol, and Client now use standard QueryAST parameter names:
filter→whereselect→fieldssort→orderByskip→offsetpopulate→expandtop→limit
The old DataEngine* schemas and types are preserved with
@deprecatedmarkers for backward compatibility. -
Updated dependencies [f08ffc3]
-
Updated dependencies [e0b0a78]
- @objectstack/spec@4.0.0
- @objectstack/core@4.0.0
- @objectstack/spec@3.3.1
- @objectstack/core@3.3.1
- 814a6c4: sql driver
- @objectstack/spec@3.3.0
- @objectstack/core@3.3.0
- @objectstack/spec@3.2.9
- @objectstack/core@3.2.9
- 1fe5612: fix vercel
- @objectstack/spec@3.2.8
- @objectstack/core@3.2.8
- 35a1ebb: fix auth
- @objectstack/spec@3.2.7
- @objectstack/core@3.2.7
- @objectstack/spec@3.2.6
- @objectstack/core@3.2.6
- e854538: fix beyyer-auth
- @objectstack/spec@3.2.5
- @objectstack/core@3.2.5
- f490991: fix better-auth
- @objectstack/spec@3.2.4
- @objectstack/core@3.2.4
- 0b1d7c9: fix auth
- @objectstack/spec@3.2.3
- @objectstack/core@3.2.3
-
cfaabbb: fix: AuthPlugin error handling & database adapter config
AuthManager.handleRequest()now inspectsresponse.status >= 500and logs the error body viaconsole.error, since better-auth catches internal errors and returns 500 Responses without throwing.AuthPlugin.registerAuthRoutes()also logs 500+ responses viactx.logger.errorfor structured plugin logging.createDatabaseConfig()now wraps the ObjectQL adapter as aDBAdapterInstancefactory function so better-auth'sgetBaseAdapter()correctly recognises it (viatypeof database === "function"check) instead of falling through to the Kysely adapter path.
-
Updated dependencies [46defbb]
- @objectstack/spec@3.2.2
- @objectstack/core@3.2.2
- Updated dependencies [850b546]
- @objectstack/spec@3.2.1
- @objectstack/core@3.2.1
- Updated dependencies [5901c29]
- @objectstack/spec@3.2.0
- @objectstack/core@3.2.0
- Updated dependencies [953d667]
- @objectstack/spec@3.1.1
- @objectstack/core@3.1.1
- Updated dependencies [0088830]
- @objectstack/spec@3.1.0
- @objectstack/core@3.1.0
- Updated dependencies [92d9d99]
- @objectstack/spec@3.0.11
- @objectstack/core@3.0.11
- Updated dependencies [d1e5d31]
- @objectstack/spec@3.0.10
- @objectstack/core@3.0.10
- Updated dependencies [15e0df6]
- @objectstack/spec@3.0.9
- @objectstack/core@3.0.9
- Updated dependencies [5a968a2]
- @objectstack/spec@3.0.8
- @objectstack/core@3.0.8
- Updated dependencies [0119bd7]
- Updated dependencies [5426bdf]
- @objectstack/spec@3.0.7
- @objectstack/core@3.0.7
- Updated dependencies [5df254c]
- @objectstack/spec@3.0.6
- @objectstack/core@3.0.6
- Updated dependencies [23a4a68]
- @objectstack/spec@3.0.5
- @objectstack/core@3.0.5
- Updated dependencies [d738987]
- @objectstack/spec@3.0.4
- @objectstack/core@3.0.4
- c7267f6: Patch release for maintenance updates and improvements.
- Updated dependencies [c7267f6]
- @objectstack/spec@3.0.3
- @objectstack/core@3.0.3
- Updated dependencies [28985f5]
- @objectstack/spec@3.0.2
- @objectstack/core@3.0.2
- Updated dependencies [389725a]
- @objectstack/spec@3.0.1
- @objectstack/core@3.0.1
- Release v3.0.0 — unified version bump for all ObjectStack packages.
- Updated dependencies
- @objectstack/spec@3.0.0
- @objectstack/core@3.0.0
- Updated dependencies
- @objectstack/spec@2.0.7
- @objectstack/core@2.0.7
- Patch release for maintenance and stability improvements
- Updated dependencies
- @objectstack/spec@2.0.6
- @objectstack/core@2.0.6
- Unify all package versions with a patch release
- Updated dependencies
- @objectstack/spec@2.0.5
- @objectstack/core@2.0.5
- Updated dependencies
- @objectstack/spec@2.0.4
- @objectstack/core@2.0.4
All notable changes to @objectstack/plugin-auth will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
2.0.2 - 2026-02-10
- Initial release of Auth Plugin
- Integration with better-auth library for robust authentication
- Session management and user authentication
- Support for OAuth providers (Google, GitHub, Microsoft, etc.)
- Organization/team support for multi-tenant applications
- Two-factor authentication (2FA)
- Passkey support
- Magic link authentication
- Configurable session expiry and refresh
- Automatic HTTP route registration
- Comprehensive test coverage
- Secure session token management
- Encrypted secrets support
- Rate limiting capabilities
- CSRF protection