@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.
- package/CHANGELOG.md +42 -0
- package/CODE_OF_CONDUCT.md +39 -0
- package/CONTRIBUTING.md +78 -0
- package/MAINTAINERS.md +43 -0
- package/README.md +111 -55
- package/ROADMAP.md +58 -0
- package/SECURITY.md +37 -0
- package/dist/access/evaluator.d.ts +3 -0
- package/dist/access/evaluator.d.ts.map +1 -0
- package/dist/access/index.d.ts +3 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/index.js +4 -0
- package/dist/access/types.d.ts +38 -0
- package/dist/access/types.d.ts.map +1 -0
- package/dist/access-CGrsDMwu.js +61 -0
- package/dist/access-CGrsDMwu.js.map +1 -0
- package/dist/api-client/api-config.server.d.ts +27 -0
- package/dist/api-client/api-config.server.d.ts.map +1 -1
- package/dist/api-client/error-codes.d.ts +6 -31
- package/dist/api-client/error-codes.d.ts.map +1 -1
- package/dist/api-client/index.js +3 -3
- package/dist/api-client/server.d.ts +1 -1
- package/dist/api-client/server.d.ts.map +1 -1
- package/dist/api-client/server.js +4 -4
- package/dist/{api-client-DsrxK05Z.js → api-client-CgZUMukO.js} +9 -26
- package/dist/api-client-CgZUMukO.js.map +1 -0
- package/dist/auth/auth.server.d.ts +21 -16
- package/dist/auth/auth.server.d.ts.map +1 -1
- package/dist/auth/authorization-parameters.d.ts +5 -0
- package/dist/auth/authorization-parameters.d.ts.map +1 -0
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/redis-session-storage.server.d.ts +18 -1
- package/dist/auth/redis-session-storage.server.d.ts.map +1 -1
- package/dist/auth/route-protection.server.d.ts +20 -4
- package/dist/auth/route-protection.server.d.ts.map +1 -1
- package/dist/auth/server.d.ts +1 -1
- package/dist/auth/server.d.ts.map +1 -1
- package/dist/auth/server.js +5 -5
- package/dist/auth/types.d.ts +88 -21
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth.server-D5oiz9aF.js +1066 -0
- package/dist/auth.server-D5oiz9aF.js.map +1 -0
- package/dist/auth.server-lC94lmlx.js +5 -0
- package/dist/identity/identity-context.server.d.ts +2 -0
- package/dist/identity/identity-context.server.d.ts.map +1 -1
- package/dist/identity/identity-store.d.ts +19 -2
- package/dist/identity/identity-store.d.ts.map +1 -1
- package/dist/identity/index.js +2 -2
- package/dist/identity/server.js +3 -3
- package/dist/identity/types.d.ts +3 -1
- package/dist/identity/types.d.ts.map +1 -1
- package/dist/{identity-CFa1txoj.js → identity-C5un-xAs.js} +104 -60
- package/dist/identity-C5un-xAs.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -10
- package/dist/logging/index.js +1 -1
- package/dist/{logging-_h2mi3L9.js → logging-DviiAkLx.js} +2 -2
- package/dist/{logging-_h2mi3L9.js.map → logging-DviiAkLx.js.map} +1 -1
- package/dist/permission-service-CvGpwI0d.js +95 -0
- package/dist/permission-service-CvGpwI0d.js.map +1 -0
- package/dist/permissions/constants.d.ts +3 -0
- package/dist/permissions/constants.d.ts.map +1 -1
- package/dist/permissions/index.js +2 -1
- package/dist/permissions/permission-store.d.ts +1 -1
- package/dist/permissions/types.d.ts +1 -1
- package/dist/permissions/types.d.ts.map +1 -1
- package/dist/{permissions-D5ScGGnd.js → permissions-_n1hIG7Q.js} +8 -106
- package/dist/permissions-_n1hIG7Q.js.map +1 -0
- package/dist/query-client/index.js +1 -1
- package/dist/query-client/types.d.ts +9 -9
- package/dist/query-client/types.d.ts.map +1 -1
- package/dist/{query-client-Bpe08pk9.js → query-client-Bh3TyB8W.js} +2 -2
- package/dist/query-client-Bh3TyB8W.js.map +1 -0
- package/dist/react-router/index.js +12 -10
- package/dist/react-router/server.js +19 -17
- package/dist/redis-session-storage.server-v2VcpFsQ.js +500 -0
- package/dist/redis-session-storage.server-v2VcpFsQ.js.map +1 -0
- package/dist/{server-BHfKHKF_.js → server-B6PCxc-T.js} +39 -87
- package/dist/server-B6PCxc-T.js.map +1 -0
- package/dist/{server-DZmcd2Lr.js → server-C96NMFBy.js} +18 -10
- package/dist/server-C96NMFBy.js.map +1 -0
- package/dist/{server-BxvRxjCK.js → server-Ca-3DJf2.js} +44 -11
- package/dist/server-Ca-3DJf2.js.map +1 -0
- package/dist/{server-C3L8J7iE.js → server-Dxsww9v9.js} +6 -5
- package/dist/server-Dxsww9v9.js.map +1 -0
- package/dist/{server-CouYoZiw.js → server-hGrElJMG.js} +2 -2
- package/dist/{server-CouYoZiw.js.map → server-hGrElJMG.js.map} +1 -1
- package/dist/server.js +19 -17
- package/dist/signalr/index.js +1 -1
- package/dist/signalr/types.d.ts +1 -1
- package/dist/{signalr-SJ-1sIxw.js → signalr-k-70VHKo.js} +3 -2
- package/dist/signalr-k-70VHKo.js.map +1 -0
- package/dist/tenant/index.js +2 -2
- package/dist/tenant/server.js +4 -4
- package/dist/tenant/tenant-context.d.ts +17 -1
- package/dist/tenant/tenant-context.d.ts.map +1 -1
- package/dist/tenant/tenant-cookie.server.d.ts.map +1 -1
- package/dist/tenant/tenant-store.d.ts +54 -9
- package/dist/tenant/tenant-store.d.ts.map +1 -1
- package/dist/tenant/tenant.server.d.ts +20 -1
- package/dist/tenant/tenant.server.d.ts.map +1 -1
- package/dist/tenant/types.d.ts +29 -9
- package/dist/tenant/types.d.ts.map +1 -1
- package/dist/tenant-DfLemb6n.js +384 -0
- package/dist/tenant-DfLemb6n.js.map +1 -0
- package/dist/{types-BnKaWdfe.js → types-BaaE6wjY.js} +1 -1
- package/dist/{types-BnKaWdfe.js.map → types-BaaE6wjY.js.map} +1 -1
- package/docs/adapters.md +87 -0
- package/docs/architecture.md +193 -0
- package/docs/backend-adaptation.md +249 -0
- package/docs/codex-for-oss-application.md +31 -0
- package/docs/codex-maintenance.md +47 -0
- package/docs/installation.md +213 -0
- package/docs/module-reference.md +207 -0
- package/docs/releasing.md +92 -0
- package/package.json +27 -4
- package/dist/api-client-DsrxK05Z.js.map +0 -1
- package/dist/auth.server-BPiCtlJE.js +0 -5
- package/dist/auth.server-Cx2h2qu3.js +0 -724
- package/dist/auth.server-Cx2h2qu3.js.map +0 -1
- package/dist/identity-CFa1txoj.js.map +0 -1
- package/dist/permissions-D5ScGGnd.js.map +0 -1
- package/dist/query-client-Bpe08pk9.js.map +0 -1
- package/dist/redis-session-storage.server-BR_0y-QO.js +0 -250
- package/dist/redis-session-storage.server-BR_0y-QO.js.map +0 -1
- package/dist/server-BHfKHKF_.js.map +0 -1
- package/dist/server-BxvRxjCK.js.map +0 -1
- package/dist/server-C3L8J7iE.js.map +0 -1
- package/dist/server-DZmcd2Lr.js.map +0 -1
- package/dist/signalr-SJ-1sIxw.js.map +0 -1
- package/dist/tenant-C6KPaSCX.js +0 -256
- package/dist/tenant-C6KPaSCX.js.map +0 -1
- /package/dist/{src-CJ2RLi94.js → src-B9rDUgSb.js} +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This document describes how Spine is structured and where each kind of logic should live.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
Spine tries to solve a common problem in internal platform code:
|
|
8
|
+
|
|
9
|
+
- product A builds infrastructure
|
|
10
|
+
- product B copies it
|
|
11
|
+
- product C forks the copy
|
|
12
|
+
- shared behavior starts drifting
|
|
13
|
+
|
|
14
|
+
Spine centralizes the reusable layer while keeping enough extension points for app-specific behavior.
|
|
15
|
+
|
|
16
|
+
## Layer Model
|
|
17
|
+
|
|
18
|
+
Spine is organized into three layers:
|
|
19
|
+
|
|
20
|
+
1. Core primitives
|
|
21
|
+
2. Framework adapters
|
|
22
|
+
3. Application adapters
|
|
23
|
+
|
|
24
|
+
### 1. Core Primitives
|
|
25
|
+
|
|
26
|
+
Core primitives should:
|
|
27
|
+
|
|
28
|
+
- avoid framework-specific response helpers
|
|
29
|
+
- use standard Web `Request` and `Response` APIs
|
|
30
|
+
- expose configuration points for backend conventions
|
|
31
|
+
- stay reusable across multiple apps
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
|
|
35
|
+
- token refresh
|
|
36
|
+
- session handling
|
|
37
|
+
- tenant cookie helpers
|
|
38
|
+
- identity cache
|
|
39
|
+
- permission checking
|
|
40
|
+
- API client configuration
|
|
41
|
+
|
|
42
|
+
### 2. Framework Adapters
|
|
43
|
+
|
|
44
|
+
Framework adapters make core primitives ergonomic in a specific framework.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
|
|
48
|
+
- `@eminuckan/spine/react-router`
|
|
49
|
+
- `@eminuckan/spine/react-router/server`
|
|
50
|
+
|
|
51
|
+
Today the React Router adapter is intentionally thin. That is a feature, not a gap: it keeps the core honest and leaves room for future adapters such as Next.js.
|
|
52
|
+
|
|
53
|
+
### 3. Application Adapters
|
|
54
|
+
|
|
55
|
+
Application adapters belong in the consuming app and translate Spine into product policy.
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
|
|
59
|
+
- setup route redirects
|
|
60
|
+
- entitlement gating
|
|
61
|
+
- domain-specific permission constants
|
|
62
|
+
- generated backend API clients
|
|
63
|
+
- UI wrappers around generic hooks
|
|
64
|
+
|
|
65
|
+
## Module Responsibilities
|
|
66
|
+
|
|
67
|
+
### Auth
|
|
68
|
+
|
|
69
|
+
Owns:
|
|
70
|
+
|
|
71
|
+
- login
|
|
72
|
+
- callback handling
|
|
73
|
+
- logout
|
|
74
|
+
- token refresh
|
|
75
|
+
- Redis-backed session and OAuth state
|
|
76
|
+
- generic route protection primitives
|
|
77
|
+
|
|
78
|
+
Does not own:
|
|
79
|
+
|
|
80
|
+
- product-specific post-login routing
|
|
81
|
+
- setup flow decisions
|
|
82
|
+
|
|
83
|
+
### Tenant
|
|
84
|
+
|
|
85
|
+
Owns:
|
|
86
|
+
|
|
87
|
+
- tenant cookie primitives
|
|
88
|
+
- current tenant resolution
|
|
89
|
+
- available tenant resolution
|
|
90
|
+
- tenant client store and provider
|
|
91
|
+
- membership-aware tenant state
|
|
92
|
+
|
|
93
|
+
Does not own:
|
|
94
|
+
|
|
95
|
+
- product-specific no-tenant redirects
|
|
96
|
+
- tenant provisioning flows
|
|
97
|
+
|
|
98
|
+
### Identity
|
|
99
|
+
|
|
100
|
+
Owns:
|
|
101
|
+
|
|
102
|
+
- identity context caching
|
|
103
|
+
- permission fetch orchestration
|
|
104
|
+
- transformation from identity context to tenant-aware user info
|
|
105
|
+
- client identity store and provider
|
|
106
|
+
|
|
107
|
+
Does not own:
|
|
108
|
+
|
|
109
|
+
- backend DTO generation
|
|
110
|
+
- product-specific setup data semantics beyond generic fields
|
|
111
|
+
|
|
112
|
+
### Permissions
|
|
113
|
+
|
|
114
|
+
Owns:
|
|
115
|
+
|
|
116
|
+
- client permission store
|
|
117
|
+
- permission hooks
|
|
118
|
+
- generic permission components
|
|
119
|
+
- generic server-side permission route protection
|
|
120
|
+
|
|
121
|
+
Does not own:
|
|
122
|
+
|
|
123
|
+
- your product's permission vocabulary
|
|
124
|
+
- domain-specific permission constants
|
|
125
|
+
|
|
126
|
+
### API Client
|
|
127
|
+
|
|
128
|
+
Owns:
|
|
129
|
+
|
|
130
|
+
- base URL and header composition
|
|
131
|
+
- auth header injection
|
|
132
|
+
- tenant header injection
|
|
133
|
+
- fetch middleware
|
|
134
|
+
- retry and error handling
|
|
135
|
+
|
|
136
|
+
Does not own:
|
|
137
|
+
|
|
138
|
+
- generated SDK clients
|
|
139
|
+
- product-specific endpoint wrappers
|
|
140
|
+
|
|
141
|
+
## Composition Pattern
|
|
142
|
+
|
|
143
|
+
Spine prefers configuration and composition over inheritance or deep factories.
|
|
144
|
+
|
|
145
|
+
The pattern is:
|
|
146
|
+
|
|
147
|
+
1. Core exposes a generic primitive
|
|
148
|
+
2. App provides fetchers, endpoint conventions, or redirect policies
|
|
149
|
+
3. App re-exports a thin local adapter if it wants ergonomic imports
|
|
150
|
+
|
|
151
|
+
This keeps the public API reusable without forcing all apps into the same workflow.
|
|
152
|
+
|
|
153
|
+
## Where New Code Should Go
|
|
154
|
+
|
|
155
|
+
Use this checklist before adding a new primitive.
|
|
156
|
+
|
|
157
|
+
### Put it in Core if
|
|
158
|
+
|
|
159
|
+
- it solves a repeated platform problem
|
|
160
|
+
- it can be configured instead of hardcoded
|
|
161
|
+
- it does not depend on a single app's domain
|
|
162
|
+
- it can be explained without referencing a product name
|
|
163
|
+
|
|
164
|
+
### Put it in a Framework Adapter if
|
|
165
|
+
|
|
166
|
+
- it depends on framework routing or request conventions
|
|
167
|
+
- it is still reusable across many apps using that framework
|
|
168
|
+
|
|
169
|
+
### Keep it in the App if
|
|
170
|
+
|
|
171
|
+
- it encodes product policy
|
|
172
|
+
- it references app-only routes
|
|
173
|
+
- it assumes app-only claims, plans, or workflows
|
|
174
|
+
- it is mostly a domain-specific convenience layer
|
|
175
|
+
|
|
176
|
+
## Current Tradeoffs
|
|
177
|
+
|
|
178
|
+
Spine is already framework-agnostic at the request/response level, but not every module is maximally abstract yet. Some modules still use environment-driven configuration where factory-based configuration may eventually be better.
|
|
179
|
+
|
|
180
|
+
That is acceptable as long as:
|
|
181
|
+
|
|
182
|
+
- the surface remains reusable
|
|
183
|
+
- product logic stays outside core
|
|
184
|
+
- adapters remain thin
|
|
185
|
+
|
|
186
|
+
## Future Adapter Direction
|
|
187
|
+
|
|
188
|
+
The intended path is:
|
|
189
|
+
|
|
190
|
+
- keep root package framework-agnostic
|
|
191
|
+
- keep adapter packages shallow
|
|
192
|
+
- let apps build local adapters for product policy
|
|
193
|
+
- eventually add first-class Next.js support without changing the core mental model
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Backend Adaptation
|
|
2
|
+
|
|
3
|
+
Spine should adapt to your backend, not force your backend to look like the original app it came from.
|
|
4
|
+
|
|
5
|
+
This guide covers the main extension points.
|
|
6
|
+
|
|
7
|
+
## Adaptation Categories
|
|
8
|
+
|
|
9
|
+
There are four major kinds of backend adaptation:
|
|
10
|
+
|
|
11
|
+
1. Auth claims
|
|
12
|
+
2. Identity endpoints
|
|
13
|
+
3. Permission resolution
|
|
14
|
+
4. Tenant conventions
|
|
15
|
+
|
|
16
|
+
## Auth Claim Mapping
|
|
17
|
+
|
|
18
|
+
Use `configureAuthClaimMapping` when your identity provider uses different claim names.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { configureAuthClaimMapping } from '@eminuckan/spine/server';
|
|
22
|
+
|
|
23
|
+
configureAuthClaimMapping({
|
|
24
|
+
subject: ['sub', 'user_id'],
|
|
25
|
+
email: ['email', 'preferred_username'],
|
|
26
|
+
givenName: ['given_name', 'first_name'],
|
|
27
|
+
familyName: ['family_name', 'last_name'],
|
|
28
|
+
permissions: ['permissions', 'scope'],
|
|
29
|
+
tenantIds: ['tenant_ids'],
|
|
30
|
+
tenantRoles: ['tenant_roles'],
|
|
31
|
+
isOnboarded: ['is_onboarded', 'profile_complete'],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Identity API Fetching
|
|
36
|
+
|
|
37
|
+
Use `configureIdentityAPIFetcher` to tell Spine how to fetch the current identity context.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { configureIdentityAPIFetcher } from '@eminuckan/spine/identity/server';
|
|
41
|
+
|
|
42
|
+
configureIdentityAPIFetcher(async (request) => {
|
|
43
|
+
const response = await fetch('https://api.example.com/me/context', {
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: request.headers.get('Authorization') ?? '',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const payload = await response.json();
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
userId: payload.user.id,
|
|
53
|
+
email: payload.user.email,
|
|
54
|
+
memberships: payload.memberships,
|
|
55
|
+
hasAnyMembership: payload.memberships.length > 0,
|
|
56
|
+
isOnboarded: payload.flags.isOnboarded,
|
|
57
|
+
hasEntitlement: payload.flags.hasEntitlement,
|
|
58
|
+
contextVersion: payload.version,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Permission Fetching
|
|
64
|
+
|
|
65
|
+
Use `configurePermissionFetcher` when permissions are loaded separately from identity context.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { configurePermissionFetcher } from '@eminuckan/spine/identity/server';
|
|
69
|
+
|
|
70
|
+
configurePermissionFetcher(async (request, tenantId) => {
|
|
71
|
+
const response = await fetch(`https://api.example.com/tenants/${tenantId}/permissions`, {
|
|
72
|
+
headers: {
|
|
73
|
+
Authorization: request.headers.get('Authorization') ?? '',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const payload = await response.json();
|
|
78
|
+
return payload.permissions;
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Client Identity Store
|
|
83
|
+
|
|
84
|
+
Simple apps can configure endpoint paths for browser-side identity refresh.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { configureIdentityStore } from '@eminuckan/spine/identity';
|
|
88
|
+
|
|
89
|
+
configureIdentityStore({
|
|
90
|
+
contextEndpoint: '/api/me/context',
|
|
91
|
+
permissionsEndpoint: '/api/me/permissions',
|
|
92
|
+
logoutPath: '/session/logout',
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Apps with different response contracts can own the fetch and mapping logic.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
configureIdentityStore({
|
|
100
|
+
fetchContext: async () => {
|
|
101
|
+
const response = await fetch('/session/me');
|
|
102
|
+
const payload = await response.json();
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
userId: payload.account.id,
|
|
106
|
+
email: payload.account.email,
|
|
107
|
+
memberships: payload.workspaces.map((workspace) => ({
|
|
108
|
+
tenantId: workspace.id,
|
|
109
|
+
tenantName: workspace.name,
|
|
110
|
+
})),
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
fetchPermissions: async () => {
|
|
114
|
+
const response = await fetch('/session/capabilities');
|
|
115
|
+
const payload = await response.json();
|
|
116
|
+
return payload.capabilities;
|
|
117
|
+
},
|
|
118
|
+
onUnauthorized: ({ logoutPath }) => {
|
|
119
|
+
window.location.assign(`${logoutPath}?reason=expired`);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Tenant Cookie Policy
|
|
125
|
+
|
|
126
|
+
Use `configureTenantCookie` to match your app's cookie requirements.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { configureTenantCookie } from '@eminuckan/spine/tenant/server';
|
|
130
|
+
|
|
131
|
+
configureTenantCookie({
|
|
132
|
+
name: '__app_tenant',
|
|
133
|
+
httpOnly: false,
|
|
134
|
+
sameSite: 'Lax',
|
|
135
|
+
path: '/',
|
|
136
|
+
secure: process.env.NODE_ENV === 'production',
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This is especially useful when:
|
|
141
|
+
|
|
142
|
+
- the tenant cookie must be client-readable
|
|
143
|
+
- the cookie name differs across products
|
|
144
|
+
- secure/samesite behavior varies by deployment model
|
|
145
|
+
|
|
146
|
+
## Tenant Resolution
|
|
147
|
+
|
|
148
|
+
Use `configureTenantResolution` so tenant helpers can derive tenant state from any identity context shape.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { configureTenantResolution } from '@eminuckan/spine/tenant/server';
|
|
152
|
+
import { fetchIdentityContext } from './identity.server';
|
|
153
|
+
|
|
154
|
+
configureTenantResolution({
|
|
155
|
+
identityContextFetcher: fetchIdentityContext,
|
|
156
|
+
resolveInitialTenant: ({ identityContext }) => {
|
|
157
|
+
return identityContext?.workspaces?.[0]?.workspaceId ?? null;
|
|
158
|
+
},
|
|
159
|
+
resolveAvailableTenants: ({ identityContext }) => {
|
|
160
|
+
return identityContext?.workspaces?.map((workspace) => workspace.workspaceId) ?? [];
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The older `configureIdentityContextFetcher` helper remains available for simple
|
|
166
|
+
identity contexts that already expose `memberships[].tenantId`.
|
|
167
|
+
|
|
168
|
+
## Route Protection Policy
|
|
169
|
+
|
|
170
|
+
Use `configureRouteProtection` for product-specific route policy that still belongs close to auth.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { configureRouteProtection } from '@eminuckan/spine/server';
|
|
174
|
+
|
|
175
|
+
configureRouteProtection({
|
|
176
|
+
getLoginReturnUrl: ({ request }) => new URL(request.url).pathname,
|
|
177
|
+
resolveRoute: async ({ protection, getIdentityContext }) => {
|
|
178
|
+
const identityContext = await getIdentityContext();
|
|
179
|
+
if (protection === 'auth' && identityContext?.isOnboarded === false) {
|
|
180
|
+
return { location: '/setup' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Use this for framework/app coordination, not domain constants.
|
|
189
|
+
|
|
190
|
+
## Permission Route Protection
|
|
191
|
+
|
|
192
|
+
Use `configurePermissionRouteProtection` to tell Spine how to get the current session and permission context.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { configurePermissionRouteProtection } from '@eminuckan/spine/server';
|
|
196
|
+
|
|
197
|
+
configurePermissionRouteProtection({
|
|
198
|
+
getSession: async (request) => {
|
|
199
|
+
return {
|
|
200
|
+
user: { sub: '123' },
|
|
201
|
+
accessToken: 'token',
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
resolveContext: async () => {
|
|
205
|
+
return {
|
|
206
|
+
permissions: ['Identity.Users.View'],
|
|
207
|
+
currentTenant: 'tenant-1',
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## API Client Adaptation
|
|
214
|
+
|
|
215
|
+
Use `createAPIConfigFactory` to keep auth and tenant resolution centralized.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { createAPIConfigFactory } from '@eminuckan/spine/api-client/server';
|
|
219
|
+
|
|
220
|
+
const { createAPIConfig, getAPIBaseURL } = createAPIConfigFactory(
|
|
221
|
+
getAccessToken,
|
|
222
|
+
getCurrentTenant,
|
|
223
|
+
logger,
|
|
224
|
+
{
|
|
225
|
+
baseURL: process.env.API_BASE_URL,
|
|
226
|
+
userAgent: 'MyApp/1.0',
|
|
227
|
+
tenantHeaderName: 'X-Workspace-Id',
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
If your backend does not use bearer auth or tenant headers, set `authHeaderName`
|
|
233
|
+
or `tenantHeaderName` to `null` and provide `buildHeaders`.
|
|
234
|
+
|
|
235
|
+
## Practical Rule
|
|
236
|
+
|
|
237
|
+
If the value could change from one backend to another, it should usually be configured.
|
|
238
|
+
|
|
239
|
+
Examples:
|
|
240
|
+
|
|
241
|
+
- claim names
|
|
242
|
+
- endpoint paths and response shapes
|
|
243
|
+
- logout URLs
|
|
244
|
+
- cookie policies
|
|
245
|
+
- redirect decisions
|
|
246
|
+
- auth and tenant header names
|
|
247
|
+
- permission loading
|
|
248
|
+
|
|
249
|
+
If the logic is universal infrastructure, it should usually stay in core.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Codex For Open Source Application Notes
|
|
2
|
+
|
|
3
|
+
This is the maintainer-facing application packet for the Codex for Open Source program. Do not add secrets, private customer details, or non-public product information here.
|
|
4
|
+
|
|
5
|
+
## Repository
|
|
6
|
+
|
|
7
|
+
- GitHub: `https://github.com/eminuckan/spine`
|
|
8
|
+
- npm: `https://www.npmjs.com/package/@eminuckan/spine`
|
|
9
|
+
- Package: `@eminuckan/spine`
|
|
10
|
+
- License: MIT
|
|
11
|
+
- Role: primary maintainer
|
|
12
|
+
|
|
13
|
+
## Qualification Draft
|
|
14
|
+
|
|
15
|
+
Spine is an MIT SaaS frontend infrastructure library for auth, identity, tenancy, permissions, API access, realtime clients, and framework adapters. It is extracted from repeated production-facing SaaS frontend needs, has a published npm package, docs, tests, CI, and an example app, and is being maintained as a reusable OSS foundation.
|
|
16
|
+
|
|
17
|
+
## API Credit Use Draft
|
|
18
|
+
|
|
19
|
+
Use Codex for core OSS maintenance: PR review, issue triage, release checklist automation, security review of auth/session code, docs drift checks, adapter scaffolding for Next.js/Clerk/Supabase, and regression repair. All changes remain maintainer-reviewed and must pass `pnpm check`.
|
|
20
|
+
|
|
21
|
+
## Anything Else Draft
|
|
22
|
+
|
|
23
|
+
Spine is early-stage but solves a common repeated SaaS problem: teams copy auth, tenant, permission, API, query, and realtime infrastructure between apps. The project has been rebooted as independent OSS with generic contracts, public docs, tests, issue templates, roadmap, and a maintenance plan for Codex-assisted review and release work.
|
|
24
|
+
|
|
25
|
+
## Evidence To Refresh Before Submission
|
|
26
|
+
|
|
27
|
+
- GitHub stars, forks, issues, pull requests, and latest push date.
|
|
28
|
+
- npm version and last-month download count.
|
|
29
|
+
- CI status on the default branch.
|
|
30
|
+
- Latest published changelog entry.
|
|
31
|
+
- Public production-facing usage links, if the maintainer wants to include them in the private form.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Codex Maintenance Plan
|
|
2
|
+
|
|
3
|
+
This document explains how Spine intends to use Codex for open source maintenance work.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Spine is a compact infrastructure library, but the maintenance surface is broad: auth, identity, tenancy, permissions, API access, realtime clients, framework adapters, examples, tests, docs, and release workflows. Codex should reduce maintainer load while keeping human review and ownership explicit.
|
|
8
|
+
|
|
9
|
+
## Planned Workflows
|
|
10
|
+
|
|
11
|
+
- Pull request review: summarize changes, identify risk areas, suggest missing tests, and check public API impact.
|
|
12
|
+
- Issue triage: classify bug reports, adapter requests, backend-contract questions, and documentation gaps.
|
|
13
|
+
- Release workflow: verify changelog entries, package contents, CI status, and npm dry runs before publishing.
|
|
14
|
+
- Documentation maintenance: keep installation, backend adaptation, adapter, and module reference docs aligned with code.
|
|
15
|
+
- Adapter scaffolding: draft Next.js, Clerk, Supabase, Vue, and Svelte adapter experiments behind issues and review gates.
|
|
16
|
+
- Security hardening: review session, OIDC, redirect, cookie, and token-handling changes with focused checklists.
|
|
17
|
+
- Regression repair: investigate failing tests or consuming-app compatibility issues without leaking app-specific policy into core.
|
|
18
|
+
|
|
19
|
+
## Guardrails
|
|
20
|
+
|
|
21
|
+
- The maintainer makes final merge and release decisions.
|
|
22
|
+
- All code changes must pass `pnpm check`.
|
|
23
|
+
- Package contents should be checked with `npm pack --dry-run` before publishing.
|
|
24
|
+
- Private application code, credentials, tokens, customer data, and non-public business context should not be included in public issues or documentation.
|
|
25
|
+
- Codex should not scan or review repositories unless the maintainer owns them or has authorization.
|
|
26
|
+
- Product-specific behavior should stay in consuming apps unless it has been generalized and documented.
|
|
27
|
+
|
|
28
|
+
## API Credit Use
|
|
29
|
+
|
|
30
|
+
API credits would be used for:
|
|
31
|
+
|
|
32
|
+
- automated pull request review experiments
|
|
33
|
+
- issue triage and duplicate detection
|
|
34
|
+
- release checklist automation
|
|
35
|
+
- security-focused reviews for auth and session code
|
|
36
|
+
- adapter proof-of-concept generation
|
|
37
|
+
- documentation drift checks between source and public docs
|
|
38
|
+
|
|
39
|
+
These are core OSS maintenance tasks, not product-specific development work.
|
|
40
|
+
|
|
41
|
+
## Success Signals
|
|
42
|
+
|
|
43
|
+
- Faster response time on issues and pull requests.
|
|
44
|
+
- More complete adapter examples without expanding core policy.
|
|
45
|
+
- Fewer release mistakes.
|
|
46
|
+
- Better test coverage for security-sensitive code.
|
|
47
|
+
- Clearer docs for teams integrating Spine into different SaaS backends.
|