@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +425 -0
  4. package/dist/api-client/index.d.ts +3 -0
  5. package/dist/api-client/index.js +4 -0
  6. package/dist/api-client/server.d.ts +4 -0
  7. package/dist/api-client/server.js +5 -0
  8. package/dist/api-client-Ftxqm3Ep.js +212 -0
  9. package/dist/api-client-Ftxqm3Ep.js.map +1 -0
  10. package/dist/auth/index.d.ts +2 -0
  11. package/dist/auth/index.js +0 -0
  12. package/dist/auth/server.d.ts +3 -0
  13. package/dist/auth/server.js +6 -0
  14. package/dist/auth.server-C979TR6W.js +724 -0
  15. package/dist/auth.server-C979TR6W.js.map +1 -0
  16. package/dist/auth.server-DU-x2LPA.js +5 -0
  17. package/dist/identity/index.d.ts +4 -0
  18. package/dist/identity/index.js +3 -0
  19. package/dist/identity/server.d.ts +4 -0
  20. package/dist/identity/server.js +4 -0
  21. package/dist/identity-CFa1txoj.js +235 -0
  22. package/dist/identity-CFa1txoj.js.map +1 -0
  23. package/dist/index-BIJOC8Fb.d.ts +272 -0
  24. package/dist/index-BIJOC8Fb.d.ts.map +1 -0
  25. package/dist/index-Bt9XE6gj.d.ts +104 -0
  26. package/dist/index-Bt9XE6gj.d.ts.map +1 -0
  27. package/dist/index-C2hGpzGr.d.ts +74 -0
  28. package/dist/index-C2hGpzGr.d.ts.map +1 -0
  29. package/dist/index-CfQsb3l_.d.ts +170 -0
  30. package/dist/index-CfQsb3l_.d.ts.map +1 -0
  31. package/dist/index-D6RK_5vm.d.ts +199 -0
  32. package/dist/index-D6RK_5vm.d.ts.map +1 -0
  33. package/dist/index-DrttYPAj.d.ts +30 -0
  34. package/dist/index-DrttYPAj.d.ts.map +1 -0
  35. package/dist/index-geY2ynnN.d.ts +141 -0
  36. package/dist/index-geY2ynnN.d.ts.map +1 -0
  37. package/dist/index-kr8Wk7v5.d.ts +1 -0
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.js +10 -0
  40. package/dist/logging/index.d.ts +3 -0
  41. package/dist/logging/index.js +3 -0
  42. package/dist/logging-_h2mi3L9.js +118 -0
  43. package/dist/logging-_h2mi3L9.js.map +1 -0
  44. package/dist/permissions/index.d.ts +2 -0
  45. package/dist/permissions/index.js +3 -0
  46. package/dist/permissions-D5ScGGnd.js +281 -0
  47. package/dist/permissions-D5ScGGnd.js.map +1 -0
  48. package/dist/query-client/index.d.ts +2 -0
  49. package/dist/query-client/index.js +3 -0
  50. package/dist/query-client-DRPQIKHX.js +174 -0
  51. package/dist/query-client-DRPQIKHX.js.map +1 -0
  52. package/dist/react-router/index.d.ts +12 -0
  53. package/dist/react-router/index.js +10 -0
  54. package/dist/react-router/server.d.ts +18 -0
  55. package/dist/react-router/server.js +17 -0
  56. package/dist/redis-session-storage.server-DhufyPjd.js +250 -0
  57. package/dist/redis-session-storage.server-DhufyPjd.js.map +1 -0
  58. package/dist/server-BoeUiXSh.d.ts +40 -0
  59. package/dist/server-BoeUiXSh.d.ts.map +1 -0
  60. package/dist/server-C-DiVP9e.js +185 -0
  61. package/dist/server-C-DiVP9e.js.map +1 -0
  62. package/dist/server-CLbksnvg.d.ts +144 -0
  63. package/dist/server-CLbksnvg.d.ts.map +1 -0
  64. package/dist/server-CNp6gjFE.js +78 -0
  65. package/dist/server-CNp6gjFE.js.map +1 -0
  66. package/dist/server-CXGaRaiS.d.ts +256 -0
  67. package/dist/server-CXGaRaiS.d.ts.map +1 -0
  68. package/dist/server-CiSbg-9C.js +352 -0
  69. package/dist/server-CiSbg-9C.js.map +1 -0
  70. package/dist/server-DLgH3FRR.js +312 -0
  71. package/dist/server-DLgH3FRR.js.map +1 -0
  72. package/dist/server-DazqdM63.d.ts +93 -0
  73. package/dist/server-DazqdM63.d.ts.map +1 -0
  74. package/dist/server-iWm3oeCM.js +145 -0
  75. package/dist/server-iWm3oeCM.js.map +1 -0
  76. package/dist/server-zEKMDpP7.d.ts +80 -0
  77. package/dist/server-zEKMDpP7.d.ts.map +1 -0
  78. package/dist/server.d.ts +18 -0
  79. package/dist/server.js +17 -0
  80. package/dist/signalr/index.d.ts +4 -0
  81. package/dist/signalr/index.js +3 -0
  82. package/dist/signalr-BJGcXc0m.js +274 -0
  83. package/dist/signalr-BJGcXc0m.js.map +1 -0
  84. package/dist/src-D7mh9MKA.js +0 -0
  85. package/dist/tenant/index.d.ts +3 -0
  86. package/dist/tenant/index.js +3 -0
  87. package/dist/tenant/server.d.ts +3 -0
  88. package/dist/tenant/server.js +5 -0
  89. package/dist/tenant-C6KPaSCX.js +256 -0
  90. package/dist/tenant-C6KPaSCX.js.map +1 -0
  91. package/dist/types-BTjFzIhv.d.ts +31 -0
  92. package/dist/types-BTjFzIhv.d.ts.map +1 -0
  93. package/dist/types-DoIfDhl6.d.ts +55 -0
  94. package/dist/types-DoIfDhl6.d.ts.map +1 -0
  95. package/dist/types-DufLRY7A.d.ts +164 -0
  96. package/dist/types-DufLRY7A.d.ts.map +1 -0
  97. package/dist/types-uDrDMkCq.d.ts +93 -0
  98. package/dist/types-uDrDMkCq.d.ts.map +1 -0
  99. 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 };