Skip to content

feat: extract the core from apps/interlock - #3

Merged
brammitch merged 1 commit into
mainfrom
interlock-3.5-package-core
Oct 20, 2026
Merged

brammitch merged 1 commit into
mainfrom
interlock-3.5-package-core

Conversation

@brammitch

Copy link
Copy Markdown
Collaborator

Refs DevalexLLC/interlock#26 (work item 3.5). The item stays open until apps/interlock runs on the published 0.1.0 and passes its gate rows.

What moves

The framework-agnostic half of the Interlock prototype becomes the root entry (src/core, re-exported by src/index.ts), with the file names kept:

  • createKeycloakAuthOptions(config, { database, grants, logger, cookiePrefix? }), plus createPostgresStore, cookieSettings, receiverRouteOf and KEYCLOAK_SCOPES
  • the session-grant store (KyselyGrantStore), its hooks and the refresher (createGrantRefresher(grants, { keycloak, refresh }, { logger, transport?, inFlight? })), with the refresh responses and events
  • the exposure allow-list, the back-channel receiver, RP-initiated logout on sign-out, and the startup checks
  • the 0001_session_grant migration (appMigrations, same key) and the account token invariant
  • KeycloakAuthConfig, the refresh bounds, the eight timing and path constants, and a normalizeClaims stub until 6.2

Decisions

  • The logger is required and passed explicitly. There is no singleton and no no-op default, because a silent logger would drop refused logout tokens and indeterminate refreshes. Redaction stays the host's job; the README spells out the contract.
  • better-auth is an exact peer (1.7.7) and kysely a ^0.29.0 peer. The hooks throw Better Auth's APIError, so there must be one copy. jose is a dependency.
  • The pool timing constants live in the package. The README's database host contract lists what the host's pool must do for them to hold.

Strict-mode rewrites

The prototype compiled with strict: false. The behavior-adjacent changes:

  • Refresher: the claimed holder is passed explicitly to the dispatch and end paths. msLeft replaces a null subtraction. The read race's timer may be undefined.
  • scrubProviderTokens: uses Reflect.deleteProperty and throws when a field can't be deleted. Without the check, a frozen or non-configurable token field would be serialized; there is a regression test for that.
  • Back-channel handler: refuses (subject_missing) if a validated token ever carries neither sid nor sub. The validator already refuses that case.
  • encryptToken: narrows setTokenUtil's result.
  • Grant insert: a grant written without a refresh deadline is refused.
  • Async conversions: a few functions became async for promise-function-async. InFlightWork.track now returns a promise that settles as the tracked work does, rather than the same promise.

Tests and tooling

  • Specs: the core's specs are ported from jest to vitest in a core project (Node environment, 30 s timeout), with the logger injected instead of spied. Counts match the originals file for file, 303 tests, plus 19 new: logger and cookie-prefix cases, the delete regression, and claims.
  • TLS case on Node 22.18: the trusted-TLS transport case is skipped there, because tls.setDefaultCACertificates arrives in 22.19.
  • Test helpers: copied from Interlock's own, made strict. The two sets drift unless both change together; the README notes this.
  • oxlint: env.node, and no-await-in-loop off for src/core/** only. Those loops (lease rounds, polling wait, hook composition, drain) are sequential by design. No disable comments.
  • CI: also type-checks a consumer of the root entry under NodeNext (strict, with skipLibCheck as the app has it), the way apps/interlock imports it.
  • Sonar and coverage: both exclude every __tests__ directory. Postgres-only code (KyselyGrantStore, the account-token trigger, the migration) is covered by the app's e2e suites, not here, so Sonar's figures for core/database and core/grants will read low.

Verification

  • npm run fmt:check && npm run lint && npm test && npm run build: clean. 322 tests pass on Node 24; on Node 22.18, 321 pass and 1 is skipped.
  • The CI "Verify package contents" step, run locally: the three entries import, and the NodeNext consumer type-checks. A negative control (a wrong option type) fails it.
  • App validation on the packed tarball (file: spec, not committed): unit 744/21 (1047/33 before, minus the 303 tests in 12 moved specs), e2e 178/15 against a throwaway Postgres, repo lint clean. npm ls better-auth kysely jose -w apps/interlock shows one better-auth 1.7.7 and one kysely 0.29.6.
  • Codex review of this diff: no findings.

The changeset is minor, so merging this opens the version PR for 0.1.0. The two critical Dependabot alerts on main predate this PR (tinypool through the oxfmt dev dependency).

Move the framework-agnostic half of the Interlock prototype into the root
entry: createKeycloakAuthOptions, the session-grant store, hooks and
refresher, the exposure allow-list, the back-channel receiver and
RP-initiated logout, the cookie-name policy, the session_grant migration and
the account token invariant, and a normalizeClaims stub until 6.2.

The logger is required and passed explicitly everywhere the core logs.
better-auth is an exact peer (1.7.7) and kysely a ^0.29.0 peer; jose is a
dependency. The core's specs move here as vitest in a node project, and CI
also type-checks a consumer of the root entry under NodeNext.

Refs DevalexLLC/interlock#26
@brammitch
brammitch merged commit 895c81f into main Oct 20, 2026
3 checks passed
@brammitch
brammitch deleted the interlock-3.5-package-core branch October 21, 2026 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant