@eminuckan/spine 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/CODE_OF_CONDUCT.md +39 -0
  3. package/CONTRIBUTING.md +78 -0
  4. package/MAINTAINERS.md +43 -0
  5. package/README.md +111 -55
  6. package/ROADMAP.md +58 -0
  7. package/SECURITY.md +37 -0
  8. package/dist/access/evaluator.d.ts +3 -0
  9. package/dist/access/evaluator.d.ts.map +1 -0
  10. package/dist/access/index.d.ts +3 -0
  11. package/dist/access/index.d.ts.map +1 -0
  12. package/dist/access/index.js +4 -0
  13. package/dist/access/types.d.ts +38 -0
  14. package/dist/access/types.d.ts.map +1 -0
  15. package/dist/access-CGrsDMwu.js +61 -0
  16. package/dist/access-CGrsDMwu.js.map +1 -0
  17. package/dist/api-client/api-config.server.d.ts +27 -0
  18. package/dist/api-client/api-config.server.d.ts.map +1 -1
  19. package/dist/api-client/error-codes.d.ts +6 -31
  20. package/dist/api-client/error-codes.d.ts.map +1 -1
  21. package/dist/api-client/index.js +3 -3
  22. package/dist/api-client/server.d.ts +1 -1
  23. package/dist/api-client/server.d.ts.map +1 -1
  24. package/dist/api-client/server.js +4 -4
  25. package/dist/{api-client-DsrxK05Z.js → api-client-CgZUMukO.js} +9 -26
  26. package/dist/api-client-CgZUMukO.js.map +1 -0
  27. package/dist/auth/auth.server.d.ts +21 -16
  28. package/dist/auth/auth.server.d.ts.map +1 -1
  29. package/dist/auth/authorization-parameters.d.ts +5 -0
  30. package/dist/auth/authorization-parameters.d.ts.map +1 -0
  31. package/dist/auth/index.d.ts +1 -1
  32. package/dist/auth/index.d.ts.map +1 -1
  33. package/dist/auth/redis-session-storage.server.d.ts +18 -1
  34. package/dist/auth/redis-session-storage.server.d.ts.map +1 -1
  35. package/dist/auth/route-protection.server.d.ts +20 -4
  36. package/dist/auth/route-protection.server.d.ts.map +1 -1
  37. package/dist/auth/server.d.ts +1 -1
  38. package/dist/auth/server.d.ts.map +1 -1
  39. package/dist/auth/server.js +5 -5
  40. package/dist/auth/types.d.ts +88 -21
  41. package/dist/auth/types.d.ts.map +1 -1
  42. package/dist/auth.server-D5oiz9aF.js +1066 -0
  43. package/dist/auth.server-D5oiz9aF.js.map +1 -0
  44. package/dist/auth.server-lC94lmlx.js +5 -0
  45. package/dist/identity/identity-context.server.d.ts +2 -0
  46. package/dist/identity/identity-context.server.d.ts.map +1 -1
  47. package/dist/identity/identity-store.d.ts +19 -2
  48. package/dist/identity/identity-store.d.ts.map +1 -1
  49. package/dist/identity/index.js +2 -2
  50. package/dist/identity/server.js +3 -3
  51. package/dist/identity/types.d.ts +3 -1
  52. package/dist/identity/types.d.ts.map +1 -1
  53. package/dist/{identity-CFa1txoj.js → identity-C5un-xAs.js} +104 -60
  54. package/dist/identity-C5un-xAs.js.map +1 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +12 -10
  58. package/dist/logging/index.js +1 -1
  59. package/dist/{logging-_h2mi3L9.js → logging-DviiAkLx.js} +2 -2
  60. package/dist/{logging-_h2mi3L9.js.map → logging-DviiAkLx.js.map} +1 -1
  61. package/dist/permission-service-CvGpwI0d.js +95 -0
  62. package/dist/permission-service-CvGpwI0d.js.map +1 -0
  63. package/dist/permissions/constants.d.ts +3 -0
  64. package/dist/permissions/constants.d.ts.map +1 -1
  65. package/dist/permissions/index.js +2 -1
  66. package/dist/permissions/permission-store.d.ts +1 -1
  67. package/dist/permissions/types.d.ts +1 -1
  68. package/dist/permissions/types.d.ts.map +1 -1
  69. package/dist/{permissions-D5ScGGnd.js → permissions-_n1hIG7Q.js} +8 -106
  70. package/dist/permissions-_n1hIG7Q.js.map +1 -0
  71. package/dist/query-client/index.js +1 -1
  72. package/dist/query-client/types.d.ts +9 -9
  73. package/dist/query-client/types.d.ts.map +1 -1
  74. package/dist/{query-client-Bpe08pk9.js → query-client-Bh3TyB8W.js} +2 -2
  75. package/dist/query-client-Bh3TyB8W.js.map +1 -0
  76. package/dist/react-router/index.js +12 -10
  77. package/dist/react-router/server.js +19 -17
  78. package/dist/redis-session-storage.server-v2VcpFsQ.js +500 -0
  79. package/dist/redis-session-storage.server-v2VcpFsQ.js.map +1 -0
  80. package/dist/{server-BHfKHKF_.js → server-B6PCxc-T.js} +39 -87
  81. package/dist/server-B6PCxc-T.js.map +1 -0
  82. package/dist/{server-DZmcd2Lr.js → server-C96NMFBy.js} +18 -10
  83. package/dist/server-C96NMFBy.js.map +1 -0
  84. package/dist/{server-BxvRxjCK.js → server-Ca-3DJf2.js} +44 -11
  85. package/dist/server-Ca-3DJf2.js.map +1 -0
  86. package/dist/{server-C3L8J7iE.js → server-Dxsww9v9.js} +6 -5
  87. package/dist/server-Dxsww9v9.js.map +1 -0
  88. package/dist/{server-CouYoZiw.js → server-hGrElJMG.js} +2 -2
  89. package/dist/{server-CouYoZiw.js.map → server-hGrElJMG.js.map} +1 -1
  90. package/dist/server.js +19 -17
  91. package/dist/signalr/index.js +1 -1
  92. package/dist/signalr/types.d.ts +1 -1
  93. package/dist/{signalr-SJ-1sIxw.js → signalr-k-70VHKo.js} +3 -2
  94. package/dist/signalr-k-70VHKo.js.map +1 -0
  95. package/dist/tenant/index.js +2 -2
  96. package/dist/tenant/server.js +4 -4
  97. package/dist/tenant/tenant-context.d.ts +17 -1
  98. package/dist/tenant/tenant-context.d.ts.map +1 -1
  99. package/dist/tenant/tenant-cookie.server.d.ts.map +1 -1
  100. package/dist/tenant/tenant-store.d.ts +54 -9
  101. package/dist/tenant/tenant-store.d.ts.map +1 -1
  102. package/dist/tenant/tenant.server.d.ts +20 -1
  103. package/dist/tenant/tenant.server.d.ts.map +1 -1
  104. package/dist/tenant/types.d.ts +29 -9
  105. package/dist/tenant/types.d.ts.map +1 -1
  106. package/dist/tenant-DfLemb6n.js +384 -0
  107. package/dist/tenant-DfLemb6n.js.map +1 -0
  108. package/dist/{types-BnKaWdfe.js → types-BaaE6wjY.js} +1 -1
  109. package/dist/{types-BnKaWdfe.js.map → types-BaaE6wjY.js.map} +1 -1
  110. package/docs/adapters.md +87 -0
  111. package/docs/architecture.md +193 -0
  112. package/docs/backend-adaptation.md +249 -0
  113. package/docs/codex-for-oss-application.md +31 -0
  114. package/docs/codex-maintenance.md +47 -0
  115. package/docs/installation.md +213 -0
  116. package/docs/module-reference.md +207 -0
  117. package/docs/releasing.md +92 -0
  118. package/package.json +27 -4
  119. package/dist/api-client-DsrxK05Z.js.map +0 -1
  120. package/dist/auth.server-BPiCtlJE.js +0 -5
  121. package/dist/auth.server-Cx2h2qu3.js +0 -724
  122. package/dist/auth.server-Cx2h2qu3.js.map +0 -1
  123. package/dist/identity-CFa1txoj.js.map +0 -1
  124. package/dist/permissions-D5ScGGnd.js.map +0 -1
  125. package/dist/query-client-Bpe08pk9.js.map +0 -1
  126. package/dist/redis-session-storage.server-BR_0y-QO.js +0 -250
  127. package/dist/redis-session-storage.server-BR_0y-QO.js.map +0 -1
  128. package/dist/server-BHfKHKF_.js.map +0 -1
  129. package/dist/server-BxvRxjCK.js.map +0 -1
  130. package/dist/server-C3L8J7iE.js.map +0 -1
  131. package/dist/server-DZmcd2Lr.js.map +0 -1
  132. package/dist/signalr-SJ-1sIxw.js.map +0 -1
  133. package/dist/tenant-C6KPaSCX.js +0 -256
  134. package/dist/tenant-C6KPaSCX.js.map +0 -1
  135. /package/dist/{src-CJ2RLi94.js → src-B9rDUgSb.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ No unreleased changes.
8
+
9
+ ## [0.3.0] - 2026-06-01
10
+
11
+ ### Added
12
+
13
+ - Added a public-surface check that blocks private project names and app-specific authorization hints from the public package.
14
+ - Added per-login allowlists for app-specific OIDC authorization parameters and public state UI context.
15
+ - Added Keycloak-compatible back-channel and front-channel logout handlers.
16
+ - Added Redis session indexes by user, OIDC `sid`, and legacy `session_state`.
17
+ - Added session listing and destroy primitives for current-user, all-user, `sid`, and `session_state` cleanup.
18
+ - Added OIDC client authentication method configuration for public and confidential clients.
19
+ - Added generic tenant, identity, permission, access, and API configuration surfaces for custom SaaS backend contracts.
20
+ - Added Vitest coverage for authorization parameters, access evaluation, permissions, tenant state, identity state, and API header configuration.
21
+ - Added React Router SaaS example app, installation docs, adapter docs, roadmap, maintainer notes, and Codex maintenance plan.
22
+ - Added an adapter request issue template for framework and provider integrations.
23
+
24
+ ### Changed
25
+
26
+ - Switched server-side auth from direct `oauth4webapi` calls to `openid-client`.
27
+ - Removed built-in product-flow redirects from route protection; consuming apps now own all setup, billing, and tenant workflow routing through `configureRouteProtection`.
28
+ - Changed built-in package defaults from legacy project branding to Spine-oriented defaults.
29
+ - Made default logout RP-Initiated Logout and kept app-local cleanup explicit with `logout=local`.
30
+ - Removed `offline_access` from the default scope; apps can opt in explicitly if they need long-lived refresh tokens.
31
+ - Updated CI and release validation to run the full `pnpm check` gate.
32
+
33
+ ## [0.2.4] - 2026-04-25
34
+
35
+ ### Fixed
36
+
37
+ - Allow HTTP OIDC requests for localhost issuers in non-production environments.
38
+ - Keep insecure OIDC requests disabled unconditionally in production.
39
+
40
+ ## [0.2.3] - 2026-04-25
41
+
42
+ ### Fixed
43
+
44
+ - Added richer OIDC discovery diagnostics, including the resolved discovery URL.
45
+ - Avoid sending stale `id_token_hint` values to a new OIDC provider during provider migrations.
46
+
5
47
  ## [0.2.0] - 2026-03-15
6
48
 
7
49
  ### Added
@@ -0,0 +1,39 @@
1
+ # Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We want Spine to be a welcoming project for contributors of all experience levels. We commit to making participation respectful, constructive, and harassment-free.
6
+
7
+ ## Our Standards
8
+
9
+ Examples of behavior that help create a positive environment:
10
+
11
+ - Being respectful in language and tone
12
+ - Giving actionable, kind feedback
13
+ - Accepting good-faith disagreement
14
+ - Prioritizing shared understanding over ego
15
+ - Helping newer contributors learn the codebase
16
+
17
+ Examples of unacceptable behavior:
18
+
19
+ - Harassment or discrimination
20
+ - Personal attacks
21
+ - Trolling or deliberately hostile behavior
22
+ - Publishing private information without permission
23
+ - Repeatedly derailing technical discussions
24
+
25
+ ## Enforcement
26
+
27
+ Project maintainers are responsible for clarifying and enforcing this code of conduct. They may remove, edit, or reject comments, commits, issues, pull requests, and other contributions that violate these standards.
28
+
29
+ ## Reporting
30
+
31
+ If you experience or witness unacceptable behavior, open a private report to the maintainer at `eminuckan@gmail.com`.
32
+
33
+ ## Scope
34
+
35
+ This code of conduct applies within project spaces and in public spaces when someone is representing the project.
36
+
37
+ ## Attribution
38
+
39
+ This document is inspired by the Contributor Covenant and adapted for this project.
@@ -0,0 +1,78 @@
1
+ # Contributing to Spine
2
+
3
+ Thanks for contributing.
4
+
5
+ Spine is intended to be a reusable foundation for SaaS infrastructure, so changes should optimize for clarity, composability, and portability across products.
6
+
7
+ ## Before You Start
8
+
9
+ - Read [README.md](README.md)
10
+ - Read [docs/architecture.md](docs/architecture.md)
11
+ - Read [docs/backend-adaptation.md](docs/backend-adaptation.md)
12
+ - Check existing issues and pull requests first
13
+
14
+ ## Local Setup
15
+
16
+ ```bash
17
+ pnpm install
18
+ pnpm check
19
+ ```
20
+
21
+ ## Project Structure
22
+
23
+ | Path | Purpose |
24
+ | --- | --- |
25
+ | `src/auth` | OAuth, sessions, token refresh, route protection |
26
+ | `src/tenant` | Tenant store, provider, cookie, and server helpers |
27
+ | `src/identity` | Identity cache, client store, and provider |
28
+ | `src/permissions` | Permission store, hooks, components, and server protection |
29
+ | `src/api-client` | API config factory, fetch middleware, and retry/error helpers |
30
+ | `src/query-client` | TanStack Query defaults and utilities |
31
+ | `src/signalr` | Realtime client primitives |
32
+ | `src/logging` | Logging utilities |
33
+ | `src/react-router` | React Router adapter entry points |
34
+ | `docs` | Project documentation |
35
+
36
+ ## Contribution Rules
37
+
38
+ - Keep core modules framework-agnostic unless the file is inside an adapter namespace
39
+ - Prefer configuration hooks over hardcoded product behavior
40
+ - Avoid shipping product-specific routes, claim names, endpoint paths, or UI assumptions into core
41
+ - Keep public APIs small and explicit
42
+ - Update documentation when behavior or public APIs change
43
+ - Add or update changelog entries for user-visible changes
44
+
45
+ ## Adapter Philosophy
46
+
47
+ When deciding where code should live:
48
+
49
+ - Put it in core if it can work across multiple apps with configuration
50
+ - Put it in an adapter if it depends on framework primitives
51
+ - Keep it in the consuming app if it expresses product policy
52
+
53
+ Examples:
54
+
55
+ - Good for core: token refresh, tenant cookie utilities, permission checks, identity caching
56
+ - Good for adapters: framework-specific redirects, loader wrappers, request helpers
57
+ - Good for apps: setup redirects, entitlement gating, domain permission constants
58
+
59
+ ## Pull Request Checklist
60
+
61
+ - `pnpm check` passes
62
+ - Public API changes are documented
63
+ - Changelog is updated when needed
64
+ - No product-specific behavior leaked into core without a configuration escape hatch
65
+
66
+ ## Commit Messages
67
+
68
+ Use concise, English commit messages.
69
+
70
+ Examples:
71
+
72
+ - `docs: rewrite README for open source release`
73
+ - `feat: add configurable permission route protection`
74
+ - `refactor: move tenant cookie policy into core config`
75
+
76
+ ## Release Notes
77
+
78
+ Maintainers should also read [docs/releasing.md](docs/releasing.md).
package/MAINTAINERS.md ADDED
@@ -0,0 +1,43 @@
1
+ # Maintainers
2
+
3
+ ## Current Maintainer
4
+
5
+ - Muhammet Emin Uckan, primary maintainer
6
+
7
+ ## Maintainer Responsibilities
8
+
9
+ The maintainer owns:
10
+
11
+ - reviewing issues and pull requests
12
+ - keeping the package installable and documented
13
+ - preserving the boundary between Spine core, framework adapters, and consuming apps
14
+ - coordinating security reports
15
+ - preparing releases and changelog entries
16
+ - validating changes with `pnpm check` and package dry runs
17
+
18
+ ## Review Policy
19
+
20
+ Changes should be reviewed against these questions:
21
+
22
+ - Does this solve a repeated SaaS infrastructure problem?
23
+ - Can it work across multiple apps without product-specific naming?
24
+ - Is the core still framework-agnostic?
25
+ - Does this belong in core, a framework adapter, provider guidance, or the app?
26
+ - Are public API changes documented and covered by tests?
27
+
28
+ ## Release Policy
29
+
30
+ Spine follows semver. While the package is on a `0.x` line, breaking changes may still happen, but they must be documented and kept migration-friendly where practical.
31
+
32
+ Before publishing:
33
+
34
+ ```bash
35
+ pnpm check
36
+ npm pack --dry-run
37
+ ```
38
+
39
+ ## Codex-Assisted Maintenance
40
+
41
+ Codex may be used to help with issue triage, pull request review, release checks, documentation updates, and security hardening. The maintainer remains responsible for all final decisions, shipped code, and release artifacts.
42
+
43
+ See [docs/codex-maintenance.md](docs/codex-maintenance.md) for the operating plan.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Spine
2
2
 
3
+ [![CI](https://github.com/eminuckan/spine/actions/workflows/ci.yml/badge.svg)](https://github.com/eminuckan/spine/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/%40eminuckan%2Fspine.svg)](https://www.npmjs.com/package/@eminuckan/spine)
5
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
3
7
  Framework-agnostic SaaS primitives for authentication, identity, permissions, multi-tenancy, API access, realtime events, query configuration, and logging.
4
8
 
5
9
  Spine is designed around one idea: the reusable parts of a SaaS platform should live in a single package, while application policy and framework quirks should stay in thin adapters. Today the package ships a framework-agnostic core plus a React Router adapter surface. Future adapters can follow the same pattern.
@@ -10,6 +14,7 @@ Spine is designed around one idea: the reusable parts of a SaaS platform should
10
14
  - Current adapter: `@eminuckan/spine/react-router`
11
15
  - Server primitives use Web `Request`/`Response` objects, not framework-specific response helpers
12
16
  - React Router adapter is intentionally thin today so other adapters can be added without changing the core contract
17
+ - Extracted from repeated production-facing SaaS frontend infrastructure needs, then rebooted as an independent OSS package
13
18
 
14
19
  ## Why Spine
15
20
 
@@ -28,7 +33,7 @@ Spine aims to be:
28
33
  - Core before adapter: framework-specific behavior belongs in dedicated adapter entry points
29
34
  - Configure, do not fork: backend claim names, cookie behavior, endpoints, and redirects should be configured first
30
35
  - Request/Response first: server modules should work anywhere a standard Web `Request` and `Response` exist
31
- - App policy stays local: onboarding flows, subscription gates, product-specific redirects, and permission taxonomies should live in the consuming app
36
+ - App policy stays local: setup flows, entitlement gates, product-specific redirects, and permission taxonomies should live in the consuming app
32
37
  - Open for composition: apps should be able to wrap Spine primitives instead of rewriting them
33
38
 
34
39
  ## Package Surfaces
@@ -54,8 +59,9 @@ Spine aims to be:
54
59
 
55
60
  ## What Spine Includes
56
61
 
57
- - OAuth2/OIDC login, callback handling, logout, and token refresh
58
- - Redis-backed session and OAuth state storage
62
+ - OpenID Connect login, callback handling, RP-Initiated Logout, and token refresh
63
+ - Redis-backed session and OAuth state storage with user/session indexes
64
+ - Keycloak-compatible front-channel and back-channel logout handlers
59
65
  - Server-side route protection primitives
60
66
  - Server-side permission route protection configuration
61
67
  - Multi-tenant client state and server helpers
@@ -67,8 +73,8 @@ Spine aims to be:
67
73
 
68
74
  ## What Spine Does Not Include
69
75
 
70
- - Your product's onboarding rules
71
- - Your product's subscription rules
76
+ - Your product's setup rules
77
+ - Your product's billing or entitlement rules
72
78
  - Your product's permission vocabulary
73
79
  - Your backend DTOs or generated API clients
74
80
  - Your page structure, layouts, or UI system
@@ -107,24 +113,75 @@ If you use client-side React features, install peer dependencies too:
107
113
  pnpm add react @tanstack/react-query
108
114
  ```
109
115
 
116
+ For a fuller setup path, see [docs/installation.md](docs/installation.md).
117
+
118
+ ## Try the Example App
119
+
120
+ ```bash
121
+ cd examples/react-router-saas
122
+ pnpm install
123
+ cp .env.example .env
124
+ pnpm dev
125
+ ```
126
+
127
+ The example includes React Router auth routes, a protected dashboard loader, client provider wiring, and local app adapters for backend identity and tenant conventions.
128
+
110
129
  ## Environment Variables
111
130
 
112
131
  The built-in auth/session layer currently reads these environment variables:
113
132
 
114
133
  | Variable | Required | Purpose |
115
134
  | --- | --- | --- |
116
- | `OIDC_AUTHORITY` | Yes | OAuth/OIDC issuer base URL |
135
+ | `OIDC_AUTHORITY` | Yes | OpenID Connect issuer base URL |
117
136
  | `OIDC_CLIENT_ID` | Yes | Client identifier |
118
- | `OIDC_REDIRECT_URI` | Yes | OAuth callback URL |
137
+ | `OIDC_REDIRECT_URI` | Yes | OIDC callback URL |
119
138
  | `OIDC_CLIENT_SECRET` | No | Client secret for confidential clients |
120
- | `OIDC_SCOPE` | No | Requested scope string |
139
+ | `OIDC_CLIENT_AUTH_METHOD` | No | `none`, `client_secret_post`, or `client_secret_basic` |
140
+ | `OIDC_SCOPE` | No | Requested scope string. Defaults to `openid profile email api` |
121
141
  | `OIDC_POST_LOGOUT_REDIRECT_URI` | No | Logout return URL |
122
142
  | `OIDC_APPLICATION_TYPE` | No | `no-landing-page`, `landing-page`, or legacy aliases |
123
- | `OIDC_SSO_LOGOUT` | No | Explicit full-logout override |
124
143
  | `OIDC_HAS_LANDING_PAGE` | No | Explicit landing-page behavior override |
144
+ | `OIDC_ALLOW_INSECURE_REQUESTS` | No | Allows non-HTTPS OIDC issuer calls only outside production |
125
145
  | `REDIS_URL` | No | Redis connection string for sessions and OAuth state |
126
146
  | `REDIS_KEY_PREFIX` | No | Prefix for Redis keys |
127
- | `API_BASE_URL` | No | Default API base URL for `createAPIConfigFactory` |
147
+ | `API_BASE_URL` | No | Optional fallback API base URL for `createAPIConfigFactory` |
148
+
149
+ ## OIDC Session Lifecycle
150
+
151
+ Spine treats the OIDC provider as the identity/session authority and the app as a relying party with its own Redis session. The default logout behavior follows RP-Initiated Logout:
152
+
153
+ - `/auth/logout` clears the current app session and redirects to the provider end-session endpoint.
154
+ - `/auth/logout?logout=local` clears only the current app session. Use this for automatic cleanup after token refresh failure or expired local state.
155
+ - `/auth/logout?logout=all` revokes all known app sessions for the current user, clears their Redis session indexes, and then redirects to the provider end-session endpoint.
156
+
157
+ For Keycloak clients, wire the provider logout callbacks to thin app routes that call Spine:
158
+
159
+ ```ts
160
+ // app/routes/auth/backchannel-logout.ts
161
+ import { handleBackChannelLogout } from '@eminuckan/spine/react-router/server';
162
+
163
+ export async function action({ request }: { request: Request }) {
164
+ return handleBackChannelLogout(request);
165
+ }
166
+ ```
167
+
168
+ ```ts
169
+ // app/routes/auth/frontchannel-logout.ts
170
+ import { handleFrontChannelLogout } from '@eminuckan/spine/react-router/server';
171
+
172
+ export async function loader({ request }: { request: Request }) {
173
+ return handleFrontChannelLogout(request);
174
+ }
175
+ ```
176
+
177
+ In Keycloak, configure the client with:
178
+
179
+ - Backchannel Logout URL: `https://your-app.example.com/auth/backchannel-logout`
180
+ - Backchannel Logout Session Required: enabled
181
+ - Frontchannel Logout URL: `https://your-app.example.com/auth/frontchannel-logout`
182
+ - Frontchannel Logout Session Required: enabled
183
+
184
+ Back-channel logout verifies the signed `logout_token` against the provider JWKS and destroys sessions by `sid` or `sub`. Front-channel logout validates `iss` and destroys sessions by `sid` when Keycloak sends it.
128
185
 
129
186
  ## Quick Start
130
187
 
@@ -133,7 +190,7 @@ The built-in auth/session layer currently reads these environment variables:
133
190
  Spine's server modules are generic, so your app should provide backend-specific fetchers once.
134
191
 
135
192
  ```ts
136
- // app/lib/mimir/identity.server.ts
193
+ // app/lib/spine/identity.server.ts
137
194
  import {
138
195
  configureIdentityAPIFetcher,
139
196
  configurePermissionFetcher,
@@ -143,7 +200,6 @@ import {
143
200
  import { createAPIConfigFactory } from '@eminuckan/spine/api-client/server';
144
201
  import { getAccessToken } from '@eminuckan/spine/react-router/server';
145
202
  import { getCurrentTenant } from '@eminuckan/spine/tenant/server';
146
- import { Configuration, IdentityApi } from '~/lib/api-clients/api';
147
203
 
148
204
  const { createAPIConfig } = createAPIConfigFactory(getAccessToken, getCurrentTenant);
149
205
 
@@ -153,47 +209,38 @@ configureIdentityAPIFetcher(async (request) => {
153
209
  includeAuth: true,
154
210
  });
155
211
 
156
- const api = new IdentityApi(
157
- new Configuration({
158
- basePath: config.basePath,
159
- accessToken: config.accessToken,
160
- baseOptions: { headers: config.headers },
161
- })
162
- );
212
+ const response = await fetch(`${config.basePath}/api/me/context`, {
213
+ headers: config.headers,
214
+ });
163
215
 
164
- const response = await api.identityGetMyContext();
216
+ if (!response.ok) {
217
+ throw new Error(`Failed to load identity context: ${response.status}`);
218
+ }
165
219
 
166
- return {
167
- ...response.data,
168
- contextVersion: Date.now(),
169
- hasSubscription: Boolean(
170
- response.data.onboarding?.subscription?.exists &&
171
- ['active', 'trialing'].includes(response.data.onboarding?.subscription?.status || '')
172
- ),
173
- };
220
+ return response.json();
174
221
  });
175
222
 
176
223
  configurePermissionFetcher(async (request, tenantId) => {
177
224
  const config = await createAPIConfig(request, { requireTenant: false });
178
- const api = new IdentityApi(
179
- new Configuration({
180
- basePath: config.basePath,
181
- accessToken: config.accessToken,
182
- baseOptions: { headers: config.headers },
183
- })
184
- );
225
+ const response = await fetch(`${config.basePath}/api/me/permissions?tenantId=${tenantId}`, {
226
+ headers: config.headers,
227
+ });
185
228
 
186
- const response = await api.identityGetMyPermissions(tenantId);
187
- return response.data.permissions || [];
229
+ if (!response.ok) {
230
+ return [];
231
+ }
232
+
233
+ const payload = await response.json();
234
+ return payload.permissions || [];
188
235
  });
189
236
 
190
237
  export { contextToUserInfo, getIdentityContext };
191
238
  ```
192
239
 
193
240
  ```ts
194
- // app/lib/mimir/tenant.server.ts
241
+ // app/lib/spine/tenant.server.ts
195
242
  import {
196
- configureIdentityContextFetcher,
243
+ configureTenantResolution,
197
244
  configureTenantCookie,
198
245
  getAvailableTenants,
199
246
  getCurrentTenant,
@@ -206,7 +253,9 @@ configureTenantCookie({
206
253
  sameSite: 'Lax',
207
254
  });
208
255
 
209
- configureIdentityContextFetcher(fetchIdentityContext);
256
+ configureTenantResolution({
257
+ identityContextFetcher: fetchIdentityContext,
258
+ });
210
259
 
211
260
  export {
212
261
  getAvailableTenants,
@@ -218,7 +267,7 @@ export {
218
267
  ### 2. Configure Server-Side Permission Protection
219
268
 
220
269
  ```ts
221
- // app/lib/mimir/permissions.server.ts
270
+ // app/lib/spine/permissions.server.ts
222
271
  import {
223
272
  configurePermissionRouteProtection,
224
273
  requirePermission,
@@ -256,8 +305,8 @@ export { requirePermission };
256
305
  ```ts
257
306
  // app/routes/_protected.tsx
258
307
  import { authRoute, getAccessToken } from '@eminuckan/spine/react-router/server';
259
- import { getCurrentTenant, initializeTenant } from '~/lib/mimir/tenant.server';
260
- import { contextToUserInfo, getIdentityContext } from '~/lib/mimir/identity.server';
308
+ import { getCurrentTenant, initializeTenant } from '~/lib/spine/tenant.server';
309
+ import { contextToUserInfo, getIdentityContext } from '~/lib/spine/identity.server';
261
310
 
262
311
  export async function loader({ request }: { request: Request }) {
263
312
  return authRoute(request, async (user) => {
@@ -349,10 +398,10 @@ import {
349
398
  } from '@eminuckan/spine/server';
350
399
 
351
400
  configureAuthClaimMapping({
352
- tenantIds: ['organizations', 'tenant_ids'],
353
- tenantRoles: ['organization_roles', 'tenant_roles'],
401
+ tenantIds: ['tenant_ids'],
402
+ tenantRoles: ['tenant_roles'],
354
403
  permissions: ['permissions', 'scope'],
355
- isOnboarded: ['profile_complete', 'is_onboarded'],
404
+ isOnboarded: ['is_onboarded'],
356
405
  });
357
406
 
358
407
  configureIdentityStore({
@@ -366,17 +415,24 @@ configureRouteProtection({
366
415
  });
367
416
  ```
368
417
 
418
+ Client contracts are configurable too. Simple apps can use endpoint configuration; apps with workspace/account/customer-specific contracts can provide `fetchTenantData`, `switchTenant`, `fetchContext`, and `fetchPermissions` functions. API header names are configurable through `createAPIConfigFactory`.
419
+
369
420
  More adaptation examples live in [docs/backend-adaptation.md](docs/backend-adaptation.md).
370
421
 
371
422
  ## Module Guides
372
423
 
373
424
  - [Architecture](docs/architecture.md)
425
+ - [Installation](docs/installation.md)
374
426
  - [Adapters](docs/adapters.md)
375
427
  - [Backend Adaptation](docs/backend-adaptation.md)
376
428
  - [Module Reference](docs/module-reference.md)
429
+ - [Roadmap](ROADMAP.md)
430
+ - [Codex Maintenance Plan](docs/codex-maintenance.md)
377
431
  - [Releasing](docs/releasing.md)
378
432
  - [Contributing](CONTRIBUTING.md)
433
+ - [Maintainers](MAINTAINERS.md)
379
434
  - [Security](SECURITY.md)
435
+ - [Code of Conduct](CODE_OF_CONDUCT.md)
380
436
 
381
437
  ## Current Boundaries
382
438
 
@@ -391,27 +447,27 @@ Spine already owns the reusable infrastructure for:
391
447
 
392
448
  Consuming apps should still own:
393
449
 
394
- - Product-specific onboarding pages and redirects
395
- - Product-specific subscription policies
450
+ - Product-specific setup pages and redirects
451
+ - Product-specific billing or entitlement policies
396
452
  - Permission constants and domain vocabulary
397
453
  - Generated API clients
398
454
  - UI-specific wrappers and design system components
399
455
 
400
456
  ## Roadmap
401
457
 
458
+ The active roadmap lives in [ROADMAP.md](ROADMAP.md). Current tracks include:
459
+
402
460
  - First-class Next.js adapter surface
403
- - More adapter authoring guidance
404
- - Cleaner permission taxonomy extension story
405
- - More example apps
406
- - Broader runtime coverage tests
461
+ - Clerk and Supabase integration guidance
462
+ - More adapter authoring tests and examples
463
+ - Cleaner migration from deprecated compatibility aliases
464
+ - Public API stability review before `1.0`
407
465
 
408
466
  ## Development
409
467
 
410
468
  ```bash
411
469
  pnpm install
412
- pnpm typecheck
413
- pnpm build
414
- pnpm lint
470
+ pnpm check
415
471
  ```
416
472
 
417
473
  Detailed contributor guidance lives in [CONTRIBUTING.md](CONTRIBUTING.md).
package/ROADMAP.md ADDED
@@ -0,0 +1,58 @@
1
+ # Spine Roadmap
2
+
3
+ Spine is a small, adapter-friendly foundation for SaaS frontend infrastructure. The roadmap favors reusable primitives, clear framework adapters, and backend-contract flexibility over product-specific features.
4
+
5
+ ## Current Focus
6
+
7
+ - Keep the core framework-agnostic and based on Web `Request` and `Response` primitives.
8
+ - Keep application policy, permission vocabularies, setup flows, and entitlement rules in consuming apps.
9
+ - Make every backend convention configurable before adding a new abstraction.
10
+ - Preserve compatibility for existing consumers while moving toward a cleaner `1.0` API.
11
+
12
+ ## Near-Term
13
+
14
+ - Publish the `0.3.x` open source reboot with generic tenant, identity, permission, API, realtime, and adapter surfaces.
15
+ - Expand the React Router example into a fuller SaaS shell with auth, tenant switching, permission gates, and API configuration.
16
+ - Add adapter authoring tests that prove new adapters can stay thin.
17
+ - Add more normalization examples for account, workspace, organization, and custom tenant contracts.
18
+ - Document migration notes from deprecated organization and subscription aliases to tenant and entitlement terminology.
19
+
20
+ ## Adapter Track
21
+
22
+ - First-class Next.js adapter entry points.
23
+ - React Router adapter refinements where framework-specific helpers remove real boilerplate.
24
+ - Research notes for Vue and Svelte adapter shapes.
25
+ - Adapter request template and contribution checklist for community proposals.
26
+
27
+ ## Provider Track
28
+
29
+ - Clerk identity/session adapter guidance.
30
+ - Supabase auth and Postgres-backed tenant examples.
31
+ - Auth0 and generic OpenID Connect recipes.
32
+ - Optional examples for API clients generated from OpenAPI without making any generated client mandatory.
33
+
34
+ ## Core Hardening
35
+
36
+ - Broader tests for route protection and OIDC/session lifecycle behavior.
37
+ - More explicit storage abstraction boundaries for sessions and OAuth state.
38
+ - Runtime compatibility notes for Node, edge-like runtimes, and serverless deployments.
39
+ - Public API stability review before `1.0`.
40
+
41
+ ## Not Planned For Core
42
+
43
+ - Product-specific route names.
44
+ - Product-specific permission constants.
45
+ - Billing plan vocabulary.
46
+ - Backend DTOs or generated SDKs.
47
+ - UI components tied to one design system.
48
+
49
+ Those belong in consuming apps or app-specific adapter packages.
50
+
51
+ ## How To Propose Work
52
+
53
+ Open an issue using the relevant template and describe:
54
+
55
+ - the repeated SaaS infrastructure problem
56
+ - whether it belongs in core, a framework adapter, provider guidance, or the consuming app
57
+ - how the API can remain generic across multiple projects
58
+ - what tests or docs would prove the behavior
package/SECURITY.md ADDED
@@ -0,0 +1,37 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Security fixes are applied to the latest published release line.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ Please do not open public GitHub issues for security problems.
10
+
11
+ Instead, report vulnerabilities privately to:
12
+
13
+ - Email: `eminuckan@gmail.com`
14
+
15
+ When possible, include:
16
+
17
+ - A clear description of the issue
18
+ - Affected versions
19
+ - Reproduction steps
20
+ - Potential impact
21
+ - Any suggested mitigation
22
+
23
+ ## Response Expectations
24
+
25
+ The goal is to acknowledge reports quickly, validate impact, and coordinate a fix before public disclosure.
26
+
27
+ ## Scope Notes
28
+
29
+ Spine provides infrastructure primitives, but consuming applications are still responsible for:
30
+
31
+ - Backend authorization enforcement
32
+ - Secure secret management
33
+ - Correct adapter configuration
34
+ - Safe redirect and cookie policies
35
+ - Protecting product-specific APIs and workflows
36
+
37
+ Frontend permission checks and route guards in Spine improve UX, but they are not a substitute for backend authorization.
@@ -0,0 +1,3 @@
1
+ import type { AccessDecision, AccessRequirement, AccessSubject } from './types';
2
+ export declare function evaluateAccessRequirement(requirement: AccessRequirement | null | undefined, subject: AccessSubject): AccessDecision;
3
+ //# sourceMappingURL=evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/access/evaluator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EAGd,iBAAiB,EACjB,aAAa,EAEd,MAAM,SAAS,CAAC;AA0CjB,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EACjD,OAAO,EAAE,aAAa,GACrB,cAAc,CAqDhB"}
@@ -0,0 +1,3 @@
1
+ export type { AccessDecision, AccessDeniedReason, AccessOperator, AccessRequirement, AccessSubject, SubscriptionAccessSnapshot, } from './types';
2
+ export { evaluateAccessRequirement } from './evaluator';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/access/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,0BAA0B,GAC3B,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ import "../permission-service-CvGpwI0d.js";
2
+ import { evaluateAccessRequirement } from "../access-CGrsDMwu.js";
3
+
4
+ export { evaluateAccessRequirement };