@eminuckan/spine 0.2.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.
- package/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +425 -0
- package/dist/api-client/index.d.ts +3 -0
- package/dist/api-client/index.js +4 -0
- package/dist/api-client/server.d.ts +4 -0
- package/dist/api-client/server.js +5 -0
- package/dist/api-client-Ftxqm3Ep.js +212 -0
- package/dist/api-client-Ftxqm3Ep.js.map +1 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +0 -0
- package/dist/auth/server.d.ts +3 -0
- package/dist/auth/server.js +6 -0
- package/dist/auth.server-C979TR6W.js +724 -0
- package/dist/auth.server-C979TR6W.js.map +1 -0
- package/dist/auth.server-DU-x2LPA.js +5 -0
- package/dist/identity/index.d.ts +4 -0
- package/dist/identity/index.js +3 -0
- package/dist/identity/server.d.ts +4 -0
- package/dist/identity/server.js +4 -0
- package/dist/identity-CFa1txoj.js +235 -0
- package/dist/identity-CFa1txoj.js.map +1 -0
- package/dist/index-BIJOC8Fb.d.ts +272 -0
- package/dist/index-BIJOC8Fb.d.ts.map +1 -0
- package/dist/index-Bt9XE6gj.d.ts +104 -0
- package/dist/index-Bt9XE6gj.d.ts.map +1 -0
- package/dist/index-C2hGpzGr.d.ts +74 -0
- package/dist/index-C2hGpzGr.d.ts.map +1 -0
- package/dist/index-CfQsb3l_.d.ts +170 -0
- package/dist/index-CfQsb3l_.d.ts.map +1 -0
- package/dist/index-D6RK_5vm.d.ts +199 -0
- package/dist/index-D6RK_5vm.d.ts.map +1 -0
- package/dist/index-DrttYPAj.d.ts +30 -0
- package/dist/index-DrttYPAj.d.ts.map +1 -0
- package/dist/index-geY2ynnN.d.ts +141 -0
- package/dist/index-geY2ynnN.d.ts.map +1 -0
- package/dist/index-kr8Wk7v5.d.ts +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/logging/index.d.ts +3 -0
- package/dist/logging/index.js +3 -0
- package/dist/logging-_h2mi3L9.js +118 -0
- package/dist/logging-_h2mi3L9.js.map +1 -0
- package/dist/permissions/index.d.ts +2 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions-D5ScGGnd.js +281 -0
- package/dist/permissions-D5ScGGnd.js.map +1 -0
- package/dist/query-client/index.d.ts +2 -0
- package/dist/query-client/index.js +3 -0
- package/dist/query-client-DRPQIKHX.js +174 -0
- package/dist/query-client-DRPQIKHX.js.map +1 -0
- package/dist/react-router/index.d.ts +12 -0
- package/dist/react-router/index.js +10 -0
- package/dist/react-router/server.d.ts +18 -0
- package/dist/react-router/server.js +17 -0
- package/dist/redis-session-storage.server-DhufyPjd.js +250 -0
- package/dist/redis-session-storage.server-DhufyPjd.js.map +1 -0
- package/dist/server-BoeUiXSh.d.ts +40 -0
- package/dist/server-BoeUiXSh.d.ts.map +1 -0
- package/dist/server-C-DiVP9e.js +185 -0
- package/dist/server-C-DiVP9e.js.map +1 -0
- package/dist/server-CLbksnvg.d.ts +144 -0
- package/dist/server-CLbksnvg.d.ts.map +1 -0
- package/dist/server-CNp6gjFE.js +78 -0
- package/dist/server-CNp6gjFE.js.map +1 -0
- package/dist/server-CXGaRaiS.d.ts +256 -0
- package/dist/server-CXGaRaiS.d.ts.map +1 -0
- package/dist/server-CiSbg-9C.js +352 -0
- package/dist/server-CiSbg-9C.js.map +1 -0
- package/dist/server-DLgH3FRR.js +312 -0
- package/dist/server-DLgH3FRR.js.map +1 -0
- package/dist/server-DazqdM63.d.ts +93 -0
- package/dist/server-DazqdM63.d.ts.map +1 -0
- package/dist/server-iWm3oeCM.js +145 -0
- package/dist/server-iWm3oeCM.js.map +1 -0
- package/dist/server-zEKMDpP7.d.ts +80 -0
- package/dist/server-zEKMDpP7.d.ts.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +17 -0
- package/dist/signalr/index.d.ts +4 -0
- package/dist/signalr/index.js +3 -0
- package/dist/signalr-BJGcXc0m.js +274 -0
- package/dist/signalr-BJGcXc0m.js.map +1 -0
- package/dist/src-D7mh9MKA.js +0 -0
- package/dist/tenant/index.d.ts +3 -0
- package/dist/tenant/index.js +3 -0
- package/dist/tenant/server.d.ts +3 -0
- package/dist/tenant/server.js +5 -0
- package/dist/tenant-C6KPaSCX.js +256 -0
- package/dist/tenant-C6KPaSCX.js.map +1 -0
- package/dist/types-BTjFzIhv.d.ts +31 -0
- package/dist/types-BTjFzIhv.d.ts.map +1 -0
- package/dist/types-DoIfDhl6.d.ts +55 -0
- package/dist/types-DoIfDhl6.d.ts.map +1 -0
- package/dist/types-DufLRY7A.d.ts +164 -0
- package/dist/types-DufLRY7A.d.ts.map +1 -0
- package/dist/types-uDrDMkCq.d.ts +93 -0
- package/dist/types-uDrDMkCq.d.ts.map +1 -0
- package/package.json +147 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.2.0] - 2026-03-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added framework-agnostic permission route protection primitives.
|
|
10
|
+
- Added configurable tenant cookie policy in core.
|
|
11
|
+
- Added generic server-side identity context resolution helpers.
|
|
12
|
+
- Added React Router adapter entry points.
|
|
13
|
+
- Added open source contributor documentation, security policy, and repository scaffolding.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Moved tenant, identity, and permission server orchestration out of the dashboard app and into Spine.
|
|
18
|
+
- Expanded tenant primitives to support membership-aware state.
|
|
19
|
+
- Updated package metadata for a fresh public open source release.
|
|
20
|
+
- Rewrote README around the current adapter-based architecture.
|
|
21
|
+
- Renamed remaining package-level branding defaults from legacy product names to Spine-oriented names where appropriate.
|
|
22
|
+
|
|
23
|
+
## [0.1.16] - 2026-02-28
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Switched publishing target to npmjs.org with public access.
|
|
28
|
+
- Updated README package references and publishing instructions for npm public releases.
|
|
29
|
+
- Included `LICENSE` in published package files.
|
|
30
|
+
|
|
31
|
+
## [0.1.2] - 2025-12-05
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Added `REDIS_KEY_PREFIX` support for Redis session storage.
|
|
36
|
+
|
|
37
|
+
## [0.1.1] - 2025-12-05
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Fixed author field encoding issue in `package.json`.
|
|
42
|
+
|
|
43
|
+
## [0.1.0] - 2025-12-04
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- Initial release of Spine.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Muhammet Emin Üçkan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Spine
|
|
2
|
+
|
|
3
|
+
Framework-agnostic SaaS primitives for authentication, identity, permissions, multi-tenancy, API access, realtime events, query configuration, and logging.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
- Core package: `@eminuckan/spine`
|
|
10
|
+
- Current adapter: `@eminuckan/spine/react-router`
|
|
11
|
+
- Server primitives use Web `Request`/`Response` objects, not framework-specific response helpers
|
|
12
|
+
- React Router adapter is intentionally thin today so other adapters can be added without changing the core contract
|
|
13
|
+
|
|
14
|
+
## Why Spine
|
|
15
|
+
|
|
16
|
+
Most internal app infrastructure starts the same way: one product grows an auth layer, a tenant switcher, permission checks, a query client, an API wrapper, and a realtime client. Then a second product appears and copies all of it. Spine exists to stop that drift.
|
|
17
|
+
|
|
18
|
+
Spine aims to be:
|
|
19
|
+
|
|
20
|
+
- Framework-agnostic at its core
|
|
21
|
+
- Adapter-friendly for framework integration
|
|
22
|
+
- Config-driven for backend conventions
|
|
23
|
+
- Honest about boundaries between reusable primitives and app-specific policy
|
|
24
|
+
- Small enough to understand, flexible enough to extend
|
|
25
|
+
|
|
26
|
+
## Design Principles
|
|
27
|
+
|
|
28
|
+
- Core before adapter: framework-specific behavior belongs in dedicated adapter entry points
|
|
29
|
+
- Configure, do not fork: backend claim names, cookie behavior, endpoints, and redirects should be configured first
|
|
30
|
+
- 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
|
|
32
|
+
- Open for composition: apps should be able to wrap Spine primitives instead of rewriting them
|
|
33
|
+
|
|
34
|
+
## Package Surfaces
|
|
35
|
+
|
|
36
|
+
| Entry point | Purpose |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `@eminuckan/spine` | Client-side primitives and shared types |
|
|
39
|
+
| `@eminuckan/spine/server` | Framework-agnostic server primitives |
|
|
40
|
+
| `@eminuckan/spine/react-router` | React Router client adapter |
|
|
41
|
+
| `@eminuckan/spine/react-router/server` | React Router server adapter |
|
|
42
|
+
| `@eminuckan/spine/auth` | Auth types |
|
|
43
|
+
| `@eminuckan/spine/auth/server` | Auth/session/route protection primitives |
|
|
44
|
+
| `@eminuckan/spine/tenant` | Tenant store, provider, and types |
|
|
45
|
+
| `@eminuckan/spine/tenant/server` | Tenant cookie and server helpers |
|
|
46
|
+
| `@eminuckan/spine/identity` | Identity store/provider/types |
|
|
47
|
+
| `@eminuckan/spine/identity/server` | Identity context cache and server orchestration |
|
|
48
|
+
| `@eminuckan/spine/api-client` | Client-side API types and errors |
|
|
49
|
+
| `@eminuckan/spine/api-client/server` | Server-side API config and Axios helpers |
|
|
50
|
+
| `@eminuckan/spine/permissions` | Client-side permission primitives |
|
|
51
|
+
| `@eminuckan/spine/logging` | Logging primitives |
|
|
52
|
+
| `@eminuckan/spine/query-client` | TanStack Query helpers |
|
|
53
|
+
| `@eminuckan/spine/signalr` | Realtime client helpers |
|
|
54
|
+
|
|
55
|
+
## What Spine Includes
|
|
56
|
+
|
|
57
|
+
- OAuth2/OIDC login, callback handling, logout, and token refresh
|
|
58
|
+
- Redis-backed session and OAuth state storage
|
|
59
|
+
- Server-side route protection primitives
|
|
60
|
+
- Server-side permission route protection configuration
|
|
61
|
+
- Multi-tenant client state and server helpers
|
|
62
|
+
- Identity context cache, fetch orchestration, and client store/provider
|
|
63
|
+
- API client factory and Axios interceptor setup
|
|
64
|
+
- TanStack Query client defaults and cache presets
|
|
65
|
+
- SignalR client helpers
|
|
66
|
+
- Logging primitives
|
|
67
|
+
|
|
68
|
+
## What Spine Does Not Include
|
|
69
|
+
|
|
70
|
+
- Your product's onboarding rules
|
|
71
|
+
- Your product's subscription rules
|
|
72
|
+
- Your product's permission vocabulary
|
|
73
|
+
- Your backend DTOs or generated API clients
|
|
74
|
+
- Your page structure, layouts, or UI system
|
|
75
|
+
|
|
76
|
+
Those belong in the consuming app or in a product-specific adapter package.
|
|
77
|
+
|
|
78
|
+
## Architecture
|
|
79
|
+
|
|
80
|
+
```mermaid
|
|
81
|
+
flowchart LR
|
|
82
|
+
A["Spine"] --> B["Server Primitives"]
|
|
83
|
+
A --> C["Client Primitives"]
|
|
84
|
+
B --> D["Framework Adapter"]
|
|
85
|
+
C --> D
|
|
86
|
+
D --> E["Application Adapter Layer"]
|
|
87
|
+
E --> F["Product UI + Backend APIs"]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The important boundary is between reusable infrastructure and product policy:
|
|
91
|
+
|
|
92
|
+
- Spine owns generic primitives
|
|
93
|
+
- Framework adapters own framework glue
|
|
94
|
+
- Application adapters own product-specific routing, claims, endpoints, and workflow rules
|
|
95
|
+
|
|
96
|
+
Detailed architecture notes live in [docs/architecture.md](docs/architecture.md).
|
|
97
|
+
|
|
98
|
+
## Installation
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pnpm add @eminuckan/spine
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If you use client-side React features, install peer dependencies too:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pnpm add react @tanstack/react-query
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Environment Variables
|
|
111
|
+
|
|
112
|
+
The built-in auth/session layer currently reads these environment variables:
|
|
113
|
+
|
|
114
|
+
| Variable | Required | Purpose |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `OIDC_AUTHORITY` | Yes | OAuth/OIDC issuer base URL |
|
|
117
|
+
| `OIDC_CLIENT_ID` | Yes | Client identifier |
|
|
118
|
+
| `OIDC_REDIRECT_URI` | Yes | OAuth callback URL |
|
|
119
|
+
| `OIDC_CLIENT_SECRET` | No | Client secret for confidential clients |
|
|
120
|
+
| `OIDC_SCOPE` | No | Requested scope string |
|
|
121
|
+
| `OIDC_POST_LOGOUT_REDIRECT_URI` | No | Logout return URL |
|
|
122
|
+
| `OIDC_APPLICATION_TYPE` | No | `no-landing-page`, `landing-page`, or legacy aliases |
|
|
123
|
+
| `OIDC_SSO_LOGOUT` | No | Explicit full-logout override |
|
|
124
|
+
| `OIDC_HAS_LANDING_PAGE` | No | Explicit landing-page behavior override |
|
|
125
|
+
| `REDIS_URL` | No | Redis connection string for sessions and OAuth state |
|
|
126
|
+
| `REDIS_KEY_PREFIX` | No | Prefix for Redis keys |
|
|
127
|
+
| `API_BASE_URL` | No | Default API base URL for `createAPIConfigFactory` |
|
|
128
|
+
|
|
129
|
+
## Quick Start
|
|
130
|
+
|
|
131
|
+
### 1. Configure Identity and Tenant Adapters
|
|
132
|
+
|
|
133
|
+
Spine's server modules are generic, so your app should provide backend-specific fetchers once.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// app/lib/mimir/identity.server.ts
|
|
137
|
+
import {
|
|
138
|
+
configureIdentityAPIFetcher,
|
|
139
|
+
configurePermissionFetcher,
|
|
140
|
+
contextToUserInfo,
|
|
141
|
+
getIdentityContext,
|
|
142
|
+
} from '@eminuckan/spine/identity/server';
|
|
143
|
+
import { createAPIConfigFactory } from '@eminuckan/spine/api-client/server';
|
|
144
|
+
import { getAccessToken } from '@eminuckan/spine/react-router/server';
|
|
145
|
+
import { getCurrentTenant } from '@eminuckan/spine/tenant/server';
|
|
146
|
+
import { Configuration, IdentityApi } from '~/lib/api-clients/api';
|
|
147
|
+
|
|
148
|
+
const { createAPIConfig } = createAPIConfigFactory(getAccessToken, getCurrentTenant);
|
|
149
|
+
|
|
150
|
+
configureIdentityAPIFetcher(async (request) => {
|
|
151
|
+
const config = await createAPIConfig(request, {
|
|
152
|
+
requireTenant: false,
|
|
153
|
+
includeAuth: true,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const api = new IdentityApi(
|
|
157
|
+
new Configuration({
|
|
158
|
+
basePath: config.basePath,
|
|
159
|
+
accessToken: config.accessToken,
|
|
160
|
+
baseOptions: { headers: config.headers },
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const response = await api.identityGetMyContext();
|
|
165
|
+
|
|
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
|
+
};
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
configurePermissionFetcher(async (request, tenantId) => {
|
|
177
|
+
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
|
+
);
|
|
185
|
+
|
|
186
|
+
const response = await api.identityGetMyPermissions(tenantId);
|
|
187
|
+
return response.data.permissions || [];
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export { contextToUserInfo, getIdentityContext };
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// app/lib/mimir/tenant.server.ts
|
|
195
|
+
import {
|
|
196
|
+
configureIdentityContextFetcher,
|
|
197
|
+
configureTenantCookie,
|
|
198
|
+
getAvailableTenants,
|
|
199
|
+
getCurrentTenant,
|
|
200
|
+
initializeTenant,
|
|
201
|
+
} from '@eminuckan/spine/tenant/server';
|
|
202
|
+
import { fetchIdentityContext } from './identity.server';
|
|
203
|
+
|
|
204
|
+
configureTenantCookie({
|
|
205
|
+
httpOnly: false,
|
|
206
|
+
sameSite: 'Lax',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
configureIdentityContextFetcher(fetchIdentityContext);
|
|
210
|
+
|
|
211
|
+
export {
|
|
212
|
+
getAvailableTenants,
|
|
213
|
+
getCurrentTenant,
|
|
214
|
+
initializeTenant,
|
|
215
|
+
};
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 2. Configure Server-Side Permission Protection
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// app/lib/mimir/permissions.server.ts
|
|
222
|
+
import {
|
|
223
|
+
configurePermissionRouteProtection,
|
|
224
|
+
requirePermission,
|
|
225
|
+
} from '@eminuckan/spine/server';
|
|
226
|
+
import { getAuthSession } from '@eminuckan/spine/react-router/server';
|
|
227
|
+
import { contextToUserInfo, getIdentityContext } from './identity.server';
|
|
228
|
+
import { getCurrentTenant } from './tenant.server';
|
|
229
|
+
|
|
230
|
+
configurePermissionRouteProtection({
|
|
231
|
+
getSession: getAuthSession,
|
|
232
|
+
resolveContext: async (request, session) => {
|
|
233
|
+
if (!session.user?.sub) {
|
|
234
|
+
return { permissions: [], currentTenant: null };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const identityContext = await getIdentityContext(request, session.user.sub);
|
|
238
|
+
const currentTenant = await getCurrentTenant(request);
|
|
239
|
+
const userInfo = await contextToUserInfo(identityContext, {
|
|
240
|
+
currentTenant,
|
|
241
|
+
request,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
permissions: userInfo.permissions,
|
|
246
|
+
currentTenant: userInfo.currentTenant,
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
export { requirePermission };
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### 3. Protect Routes
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// app/routes/_protected.tsx
|
|
258
|
+
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';
|
|
261
|
+
|
|
262
|
+
export async function loader({ request }: { request: Request }) {
|
|
263
|
+
return authRoute(request, async (user) => {
|
|
264
|
+
const [accessToken, identityContext, currentTenant] = await Promise.all([
|
|
265
|
+
getAccessToken(request),
|
|
266
|
+
getIdentityContext(request, user.sub),
|
|
267
|
+
getCurrentTenant(request),
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
const initResult =
|
|
271
|
+
!currentTenant && identityContext.hasAnyMembership
|
|
272
|
+
? await initializeTenant(request)
|
|
273
|
+
: null;
|
|
274
|
+
|
|
275
|
+
const userInfo = await contextToUserInfo(identityContext, {
|
|
276
|
+
currentTenant: initResult?.tenantId ?? currentTenant,
|
|
277
|
+
request,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
user,
|
|
282
|
+
accessToken,
|
|
283
|
+
identity: {
|
|
284
|
+
...identityContext,
|
|
285
|
+
permissions: userInfo.permissions,
|
|
286
|
+
currentTenant: userInfo.currentTenant,
|
|
287
|
+
},
|
|
288
|
+
tenantHeaders: initResult?.headers ?? null,
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 4. Wire Client Providers
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
298
|
+
import {
|
|
299
|
+
TenantProvider,
|
|
300
|
+
IdentityContextProvider,
|
|
301
|
+
PermissionInitializer,
|
|
302
|
+
createQueryClient,
|
|
303
|
+
} from '@eminuckan/spine';
|
|
304
|
+
|
|
305
|
+
const queryClient = createQueryClient();
|
|
306
|
+
|
|
307
|
+
export function AppProviders({
|
|
308
|
+
children,
|
|
309
|
+
tenant,
|
|
310
|
+
identity,
|
|
311
|
+
accessToken,
|
|
312
|
+
}: {
|
|
313
|
+
children: React.ReactNode;
|
|
314
|
+
tenant: { currentTenant: string | null; availableTenants: string[]; memberships: any[] };
|
|
315
|
+
identity: { permissions: string[]; isLoading?: boolean } & Record<string, unknown>;
|
|
316
|
+
accessToken?: string | null;
|
|
317
|
+
}) {
|
|
318
|
+
return (
|
|
319
|
+
<QueryClientProvider client={queryClient}>
|
|
320
|
+
<TenantProvider
|
|
321
|
+
initialTenant={tenant.currentTenant}
|
|
322
|
+
initialTenants={tenant.availableTenants}
|
|
323
|
+
initialMemberships={tenant.memberships}
|
|
324
|
+
>
|
|
325
|
+
<IdentityContextProvider
|
|
326
|
+
initialContext={identity}
|
|
327
|
+
accessToken={accessToken}
|
|
328
|
+
>
|
|
329
|
+
<PermissionInitializer
|
|
330
|
+
permissions={identity.permissions as string[]}
|
|
331
|
+
isLoading={Boolean(identity.isLoading)}
|
|
332
|
+
>
|
|
333
|
+
{children}
|
|
334
|
+
</PermissionInitializer>
|
|
335
|
+
</IdentityContextProvider>
|
|
336
|
+
</TenantProvider>
|
|
337
|
+
</QueryClientProvider>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### 5. Configure Backend Conventions Instead of Forking
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import {
|
|
346
|
+
configureAuthClaimMapping,
|
|
347
|
+
configureIdentityStore,
|
|
348
|
+
configureRouteProtection,
|
|
349
|
+
} from '@eminuckan/spine/server';
|
|
350
|
+
|
|
351
|
+
configureAuthClaimMapping({
|
|
352
|
+
tenantIds: ['organizations', 'tenant_ids'],
|
|
353
|
+
tenantRoles: ['organization_roles', 'tenant_roles'],
|
|
354
|
+
permissions: ['permissions', 'scope'],
|
|
355
|
+
isOnboarded: ['profile_complete', 'is_onboarded'],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
configureIdentityStore({
|
|
359
|
+
contextEndpoint: '/api/me/context',
|
|
360
|
+
permissionsEndpoint: '/api/me/permissions',
|
|
361
|
+
logoutPath: '/session/logout',
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
configureRouteProtection({
|
|
365
|
+
getLoginReturnUrl: ({ request }) => new URL(request.url).pathname,
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
More adaptation examples live in [docs/backend-adaptation.md](docs/backend-adaptation.md).
|
|
370
|
+
|
|
371
|
+
## Module Guides
|
|
372
|
+
|
|
373
|
+
- [Architecture](docs/architecture.md)
|
|
374
|
+
- [Adapters](docs/adapters.md)
|
|
375
|
+
- [Backend Adaptation](docs/backend-adaptation.md)
|
|
376
|
+
- [Module Reference](docs/module-reference.md)
|
|
377
|
+
- [Releasing](docs/releasing.md)
|
|
378
|
+
- [Contributing](CONTRIBUTING.md)
|
|
379
|
+
- [Security](SECURITY.md)
|
|
380
|
+
|
|
381
|
+
## Current Boundaries
|
|
382
|
+
|
|
383
|
+
Spine already owns the reusable infrastructure for:
|
|
384
|
+
|
|
385
|
+
- Session lifecycle
|
|
386
|
+
- OAuth state and token refresh
|
|
387
|
+
- Tenant state and tenant cookie handling
|
|
388
|
+
- Identity cache and permission resolution
|
|
389
|
+
- Permission route protection
|
|
390
|
+
- API client setup
|
|
391
|
+
|
|
392
|
+
Consuming apps should still own:
|
|
393
|
+
|
|
394
|
+
- Product-specific onboarding pages and redirects
|
|
395
|
+
- Product-specific subscription policies
|
|
396
|
+
- Permission constants and domain vocabulary
|
|
397
|
+
- Generated API clients
|
|
398
|
+
- UI-specific wrappers and design system components
|
|
399
|
+
|
|
400
|
+
## Roadmap
|
|
401
|
+
|
|
402
|
+
- 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
|
|
407
|
+
|
|
408
|
+
## Development
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
pnpm install
|
|
412
|
+
pnpm typecheck
|
|
413
|
+
pnpm build
|
|
414
|
+
pnpm lint
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Detailed contributor guidance lives in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
418
|
+
|
|
419
|
+
## Versioning
|
|
420
|
+
|
|
421
|
+
Spine currently uses semver with a `0.x` release line. Breaking changes can still happen more frequently than a mature `1.x` package, but they should be documented in [CHANGELOG.md](CHANGELOG.md).
|
|
422
|
+
|
|
423
|
+
## License
|
|
424
|
+
|
|
425
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import "../types-BTjFzIhv.js";
|
|
2
|
+
import { APIConfig, APILogger, ApiError$1 as ApiError, CreateAPIConfigOptions, ErrorCodes$1 as ErrorCodes, ErrorHandler$1 as ErrorHandler, GetAccessTokenFn, GetCurrentTenantFn, ProblemDetails, RetryConfig, TokenRefreshFn, TokenRefreshResult, ValidationError, calculateBackoffDelay$1 as calculateBackoffDelay, createRetryFetch$1 as createRetryFetch, fetchWithTimeout$1 as fetchWithTimeout, handleApiError$1 as handleApiError, isRetryableError$1 as isRetryableError, withRetry$1 as withRetry } from "../index-D6RK_5vm.js";
|
|
3
|
+
export { APIConfig, APILogger, ApiError, CreateAPIConfigOptions, ErrorCodes, ErrorHandler, GetAccessTokenFn, GetCurrentTenantFn, ProblemDetails, RetryConfig, TokenRefreshFn, TokenRefreshResult, ValidationError, calculateBackoffDelay, createRetryFetch, fetchWithTimeout, handleApiError, isRetryableError, withRetry };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "../logging-_h2mi3L9.js";
|
|
2
|
+
import { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createRetryFetch, fetchWithTimeout, handleApiError, isRetryableError, withRetry } from "../api-client-Ftxqm3Ep.js";
|
|
3
|
+
|
|
4
|
+
export { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createRetryFetch, fetchWithTimeout, handleApiError, isRetryableError, withRetry };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "../types-BTjFzIhv.js";
|
|
2
|
+
import { APIConfig, APILogger, ApiError$1 as ApiError, CreateAPIConfigOptions, ErrorCodes$1 as ErrorCodes, ErrorHandler$1 as ErrorHandler, GetAccessTokenFn, GetCurrentTenantFn, ProblemDetails, RetryConfig, TokenRefreshFn, TokenRefreshResult, ValidationError, calculateBackoffDelay$1 as calculateBackoffDelay, createRetryFetch$1 as createRetryFetch, fetchWithTimeout$1 as fetchWithTimeout, handleApiError$1 as handleApiError, isRetryableError$1 as isRetryableError, withRetry$1 as withRetry } from "../index-D6RK_5vm.js";
|
|
3
|
+
import { APIConfigFactory, APIConfigFactoryOptions, AxiosSetupOptions, CreateEnhancedClientOptions, createAPIConfigFactory$1 as createAPIConfigFactory, createEnhancedClient$1 as createEnhancedClient, setupAxiosInterceptors$1 as setupAxiosInterceptors } from "../server-CLbksnvg.js";
|
|
4
|
+
export { APIConfig, APIConfigFactory, APIConfigFactoryOptions, APILogger, ApiError, AxiosSetupOptions, CreateAPIConfigOptions, CreateEnhancedClientOptions, ErrorCodes, ErrorHandler, GetAccessTokenFn, GetCurrentTenantFn, ProblemDetails, RetryConfig, TokenRefreshFn, TokenRefreshResult, ValidationError, calculateBackoffDelay, createAPIConfigFactory, createEnhancedClient, createRetryFetch, fetchWithTimeout, handleApiError, isRetryableError, setupAxiosInterceptors, withRetry };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import "../logging-_h2mi3L9.js";
|
|
2
|
+
import { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createRetryFetch, fetchWithTimeout, handleApiError, isRetryableError, withRetry } from "../api-client-Ftxqm3Ep.js";
|
|
3
|
+
import { createAPIConfigFactory, createEnhancedClient, setupAxiosInterceptors } from "../server-DLgH3FRR.js";
|
|
4
|
+
|
|
5
|
+
export { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createAPIConfigFactory, createEnhancedClient, createRetryFetch, fetchWithTimeout, handleApiError, isRetryableError, setupAxiosInterceptors, withRetry };
|