@eminuckan/spine 0.3.0 → 0.3.2
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 +12 -0
- package/README.md +116 -98
- package/SECURITY.md +5 -15
- package/dist/access/index.mjs +2 -0
- package/dist/{access-CGrsDMwu.js → access-BHy1v5sr.mjs} +6 -8
- package/dist/access-BHy1v5sr.mjs.map +1 -0
- package/dist/api-client/index.mjs +3 -0
- package/dist/api-client/server.mjs +4 -0
- package/dist/{api-client-CgZUMukO.js → api-client-CB7XZaMg.mjs} +8 -10
- package/dist/api-client-CB7XZaMg.mjs.map +1 -0
- package/dist/auth/auth.server.d.ts +1 -0
- package/dist/auth/auth.server.d.ts.map +1 -1
- package/dist/auth/index.mjs +1 -0
- package/dist/auth/route-protection.server.d.ts.map +1 -1
- package/dist/auth/server.mjs +3 -0
- package/dist/{identity-C5un-xAs.js → identity/index.mjs} +24 -21
- package/dist/identity/index.mjs.map +1 -0
- package/dist/{server-Dxsww9v9.js → identity/server.mjs} +7 -8
- package/dist/identity/server.mjs.map +1 -0
- package/dist/{index.js → index.mjs} +11 -13
- package/dist/{logging-DviiAkLx.js → logging/index.mjs} +22 -24
- package/dist/logging/index.mjs.map +1 -0
- package/dist/{permission-service-CvGpwI0d.js → permission-service-DUyKomU3.mjs} +6 -10
- package/dist/permission-service-DUyKomU3.mjs.map +1 -0
- package/dist/permissions/index.mjs +3 -0
- package/dist/{permissions-_n1hIG7Q.js → permissions-C5dTP9y4.mjs} +23 -15
- package/dist/permissions-C5dTP9y4.mjs.map +1 -0
- package/dist/query-client/index.mjs +2 -0
- package/dist/{query-client-Bh3TyB8W.js → query-client-_xB300gc.mjs} +49 -15
- package/dist/query-client-_xB300gc.mjs.map +1 -0
- package/dist/react-router/{index.js → index.mjs} +11 -13
- package/dist/react-router/server.mjs +17 -0
- package/dist/{redis-session-storage.server-v2VcpFsQ.js → redis-session-storage.server-DGgQcJ9B.mjs} +30 -42
- package/dist/redis-session-storage.server-DGgQcJ9B.mjs.map +1 -0
- package/dist/{server-C96NMFBy.js → server-ByScGRXA.mjs} +39 -36
- package/dist/server-ByScGRXA.mjs.map +1 -0
- package/dist/{auth.server-D5oiz9aF.js → server-CqX2yCa4.mjs} +388 -119
- package/dist/server-CqX2yCa4.mjs.map +1 -0
- package/dist/server.mjs +90 -0
- package/dist/server.mjs.map +1 -0
- package/dist/signalr/index.mjs +2 -0
- package/dist/{signalr-k-70VHKo.js → signalr-C-TA8nEE.mjs} +11 -6
- package/dist/signalr-C-TA8nEE.mjs.map +1 -0
- package/dist/{tenant-DfLemb6n.js → tenant/index.mjs} +19 -12
- package/dist/tenant/index.mjs.map +1 -0
- package/dist/{server-Ca-3DJf2.js → tenant/server.mjs} +12 -12
- package/dist/tenant/server.mjs.map +1 -0
- package/dist/{types-BaaE6wjY.js → types-DqCXinAk.mjs} +3 -3
- package/dist/types-DqCXinAk.mjs.map +1 -0
- package/docs/codex-for-oss-application.md +14 -2
- package/package.json +14 -2
- package/dist/access/index.js +0 -4
- package/dist/access-CGrsDMwu.js.map +0 -1
- package/dist/api-client/index.js +0 -5
- package/dist/api-client/server.js +0 -6
- package/dist/api-client-CgZUMukO.js.map +0 -1
- package/dist/auth/index.js +0 -0
- package/dist/auth/server.js +0 -6
- package/dist/auth.server-D5oiz9aF.js.map +0 -1
- package/dist/auth.server-lC94lmlx.js +0 -5
- package/dist/identity/index.js +0 -3
- package/dist/identity/server.js +0 -5
- package/dist/identity-C5un-xAs.js.map +0 -1
- package/dist/logging/index.js +0 -3
- package/dist/logging-DviiAkLx.js.map +0 -1
- package/dist/permission-service-CvGpwI0d.js.map +0 -1
- package/dist/permissions/index.js +0 -4
- package/dist/permissions-_n1hIG7Q.js.map +0 -1
- package/dist/query-client/index.js +0 -3
- package/dist/query-client-Bh3TyB8W.js.map +0 -1
- package/dist/react-router/server.js +0 -20
- package/dist/redis-session-storage.server-v2VcpFsQ.js.map +0 -1
- package/dist/server-B6PCxc-T.js +0 -304
- package/dist/server-B6PCxc-T.js.map +0 -1
- package/dist/server-C96NMFBy.js.map +0 -1
- package/dist/server-Ca-3DJf2.js.map +0 -1
- package/dist/server-Dxsww9v9.js.map +0 -1
- package/dist/server-hGrElJMG.js +0 -78
- package/dist/server-hGrElJMG.js.map +0 -1
- package/dist/server.js +0 -20
- package/dist/signalr/index.js +0 -3
- package/dist/signalr-k-70VHKo.js.map +0 -1
- package/dist/src-B9rDUgSb.js +0 -0
- package/dist/tenant/index.js +0 -3
- package/dist/tenant/server.js +0 -5
- package/dist/tenant-DfLemb6n.js.map +0 -1
- package/dist/types-BaaE6wjY.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ All notable changes to this project will be documented in this file.
|
|
|
6
6
|
|
|
7
7
|
No unreleased changes.
|
|
8
8
|
|
|
9
|
+
## [0.3.2] - 2026-07-11
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Use the configured public OIDC redirect URI during authorization-code exchange so callbacks remain valid behind TLS-terminating reverse proxies.
|
|
14
|
+
|
|
15
|
+
## [0.3.1] - 2026-06-01
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Clarified README positioning, backend contract guidance, and quick-start snippets for the open source package.
|
|
20
|
+
|
|
9
21
|
## [0.3.0] - 2026-06-01
|
|
10
22
|
|
|
11
23
|
### Added
|
package/README.md
CHANGED
|
@@ -4,17 +4,20 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@eminuckan/spine)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
Framework-agnostic
|
|
7
|
+
Framework-agnostic TypeScript primitives for authentication, identity, permissions, multi-tenancy, API access, realtime events, query configuration, and logging in SaaS frontends.
|
|
8
8
|
|
|
9
|
-
Spine is designed around one idea: the reusable
|
|
9
|
+
Spine is designed around one idea: the reusable infrastructure in a SaaS frontend should live in a shared package, while product policy, backend DTOs, generated clients, and framework glue should stay in thin adapters owned by the consuming app.
|
|
10
|
+
|
|
11
|
+
The package is currently on a `0.x` release line. It is usable, tested, and published, but some compatibility aliases remain while the public API moves toward a cleaner `1.0` shape.
|
|
10
12
|
|
|
11
13
|
## Status
|
|
12
14
|
|
|
13
15
|
- Core package: `@eminuckan/spine`
|
|
14
16
|
- Current adapter: `@eminuckan/spine/react-router`
|
|
15
|
-
- Server
|
|
16
|
-
- React Router adapter is intentionally thin
|
|
17
|
-
-
|
|
17
|
+
- Server-facing APIs accept Web `Request`/`Response` objects instead of framework-specific response helpers
|
|
18
|
+
- React Router adapter is intentionally thin so future adapters can follow the same core contract
|
|
19
|
+
- Built-in auth uses OpenID Connect and Redis-backed session/OAuth state storage
|
|
20
|
+
- Extracted from repeated production-facing SaaS frontend infrastructure needs and maintained as independent OSS
|
|
18
21
|
|
|
19
22
|
## Why Spine
|
|
20
23
|
|
|
@@ -25,6 +28,7 @@ Spine aims to be:
|
|
|
25
28
|
- Framework-agnostic at its core
|
|
26
29
|
- Adapter-friendly for framework integration
|
|
27
30
|
- Config-driven for backend conventions
|
|
31
|
+
- Explicit about backend contracts instead of forcing one API shape
|
|
28
32
|
- Honest about boundaries between reusable primitives and app-specific policy
|
|
29
33
|
- Small enough to understand, flexible enough to extend
|
|
30
34
|
|
|
@@ -32,7 +36,7 @@ Spine aims to be:
|
|
|
32
36
|
|
|
33
37
|
- Core before adapter: framework-specific behavior belongs in dedicated adapter entry points
|
|
34
38
|
- Configure, do not fork: backend claim names, cookie behavior, endpoints, and redirects should be configured first
|
|
35
|
-
- Request/Response first: server
|
|
39
|
+
- Request/Response first: server-facing APIs should use standard Web `Request` and `Response` objects
|
|
36
40
|
- App policy stays local: setup flows, entitlement gates, product-specific redirects, and permission taxonomies should live in the consuming app
|
|
37
41
|
- Open for composition: apps should be able to wrap Spine primitives instead of rewriting them
|
|
38
42
|
|
|
@@ -41,7 +45,7 @@ Spine aims to be:
|
|
|
41
45
|
| Entry point | Purpose |
|
|
42
46
|
| --- | --- |
|
|
43
47
|
| `@eminuckan/spine` | Client-side primitives and shared types |
|
|
44
|
-
| `@eminuckan/spine/server` | Framework-agnostic server primitives |
|
|
48
|
+
| `@eminuckan/spine/server` | Framework-agnostic server primitives and shared exports |
|
|
45
49
|
| `@eminuckan/spine/react-router` | React Router client adapter |
|
|
46
50
|
| `@eminuckan/spine/react-router/server` | React Router server adapter |
|
|
47
51
|
| `@eminuckan/spine/auth` | Auth types |
|
|
@@ -81,6 +85,20 @@ Spine aims to be:
|
|
|
81
85
|
|
|
82
86
|
Those belong in the consuming app or in a product-specific adapter package.
|
|
83
87
|
|
|
88
|
+
## Backend Contract Model
|
|
89
|
+
|
|
90
|
+
Spine does not require your backend to expose a fixed set of endpoints. It provides defaults for simple apps, but real SaaS apps often have different vocabulary:
|
|
91
|
+
|
|
92
|
+
- tenant, workspace, account, company, project, or organization
|
|
93
|
+
- permissions, capabilities, scopes, roles, or policy grants
|
|
94
|
+
- bearer headers, custom session headers, cookie-backed APIs, or generated clients
|
|
95
|
+
|
|
96
|
+
The intended integration style is:
|
|
97
|
+
|
|
98
|
+
1. Configure claim names, cookie names, route policy, and API headers.
|
|
99
|
+
2. Provide fetchers when your identity, permission, or tenant response shape differs.
|
|
100
|
+
3. Keep product setup, billing, entitlement, and permission vocabulary in your app.
|
|
101
|
+
|
|
84
102
|
## Architecture
|
|
85
103
|
|
|
86
104
|
```mermaid
|
|
@@ -139,7 +157,7 @@ The built-in auth/session layer currently reads these environment variables:
|
|
|
139
157
|
| `OIDC_CLIENT_AUTH_METHOD` | No | `none`, `client_secret_post`, or `client_secret_basic` |
|
|
140
158
|
| `OIDC_SCOPE` | No | Requested scope string. Defaults to `openid profile email api` |
|
|
141
159
|
| `OIDC_POST_LOGOUT_REDIRECT_URI` | No | Logout return URL |
|
|
142
|
-
| `OIDC_APPLICATION_TYPE` | No | `no-landing-page`, `landing-page`, or
|
|
160
|
+
| `OIDC_APPLICATION_TYPE` | No | `no-landing-page`, `landing-page`, or deprecated aliases `dashboard` and `tenant-app` |
|
|
143
161
|
| `OIDC_HAS_LANDING_PAGE` | No | Explicit landing-page behavior override |
|
|
144
162
|
| `OIDC_ALLOW_INSECURE_REQUESTS` | No | Allows non-HTTPS OIDC issuer calls only outside production |
|
|
145
163
|
| `REDIS_URL` | No | Redis connection string for sessions and OAuth state |
|
|
@@ -185,9 +203,40 @@ Back-channel logout verifies the signed `logout_token` against the provider JWKS
|
|
|
185
203
|
|
|
186
204
|
## Quick Start
|
|
187
205
|
|
|
188
|
-
|
|
206
|
+
The snippets below show the integration shape. See [examples/react-router-saas](examples/react-router-saas) for a runnable React Router app.
|
|
207
|
+
|
|
208
|
+
### 1. Add Auth Routes
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// app/routes/auth.login.tsx
|
|
212
|
+
import { login } from '@eminuckan/spine/react-router/server';
|
|
213
|
+
|
|
214
|
+
export async function loader({ request }: { request: Request }) {
|
|
215
|
+
throw await login(request, { returnUrl: '/dashboard' });
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
// app/routes/auth.callback.tsx
|
|
221
|
+
import { handleCallback } from '@eminuckan/spine/react-router/server';
|
|
222
|
+
|
|
223
|
+
export async function loader({ request }: { request: Request }) {
|
|
224
|
+
return handleCallback(request);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// app/routes/auth.logout.tsx
|
|
230
|
+
import { logout } from '@eminuckan/spine/react-router/server';
|
|
231
|
+
|
|
232
|
+
export async function loader({ request }: { request: Request }) {
|
|
233
|
+
return logout(request);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 2. Configure Server Adapters
|
|
189
238
|
|
|
190
|
-
Spine's server modules are generic
|
|
239
|
+
Spine's server modules are generic. Your app should provide backend-specific identity and tenant fetchers once.
|
|
191
240
|
|
|
192
241
|
```ts
|
|
193
242
|
// app/lib/spine/identity.server.ts
|
|
@@ -197,20 +246,19 @@ import {
|
|
|
197
246
|
contextToUserInfo,
|
|
198
247
|
getIdentityContext,
|
|
199
248
|
} from '@eminuckan/spine/identity/server';
|
|
200
|
-
import { createAPIConfigFactory } from '@eminuckan/spine/api-client/server';
|
|
201
249
|
import { getAccessToken } from '@eminuckan/spine/react-router/server';
|
|
202
|
-
import { getCurrentTenant } from '@eminuckan/spine/tenant/server';
|
|
203
250
|
|
|
204
|
-
const
|
|
251
|
+
const apiBaseUrl = process.env.API_BASE_URL;
|
|
252
|
+
|
|
253
|
+
if (!apiBaseUrl) {
|
|
254
|
+
throw new Error('API_BASE_URL is required by this app adapter.');
|
|
255
|
+
}
|
|
205
256
|
|
|
206
257
|
configureIdentityAPIFetcher(async (request) => {
|
|
207
|
-
const
|
|
208
|
-
requireTenant: false,
|
|
209
|
-
includeAuth: true,
|
|
210
|
-
});
|
|
258
|
+
const accessToken = await getAccessToken(request);
|
|
211
259
|
|
|
212
|
-
const response = await fetch(`${
|
|
213
|
-
headers:
|
|
260
|
+
const response = await fetch(`${apiBaseUrl}/api/me/context`, {
|
|
261
|
+
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
|
214
262
|
});
|
|
215
263
|
|
|
216
264
|
if (!response.ok) {
|
|
@@ -221,9 +269,12 @@ configureIdentityAPIFetcher(async (request) => {
|
|
|
221
269
|
});
|
|
222
270
|
|
|
223
271
|
configurePermissionFetcher(async (request, tenantId) => {
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
272
|
+
const accessToken = await getAccessToken(request);
|
|
273
|
+
const url = new URL('/api/me/permissions', apiBaseUrl);
|
|
274
|
+
url.searchParams.set('tenantId', tenantId);
|
|
275
|
+
|
|
276
|
+
const response = await fetch(url, {
|
|
277
|
+
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
|
227
278
|
});
|
|
228
279
|
|
|
229
280
|
if (!response.ok) {
|
|
@@ -231,7 +282,7 @@ configurePermissionFetcher(async (request, tenantId) => {
|
|
|
231
282
|
}
|
|
232
283
|
|
|
233
284
|
const payload = await response.json();
|
|
234
|
-
return payload.permissions
|
|
285
|
+
return Array.isArray(payload.permissions) ? payload.permissions : [];
|
|
235
286
|
});
|
|
236
287
|
|
|
237
288
|
export { contextToUserInfo, getIdentityContext };
|
|
@@ -242,105 +293,58 @@ export { contextToUserInfo, getIdentityContext };
|
|
|
242
293
|
import {
|
|
243
294
|
configureTenantResolution,
|
|
244
295
|
configureTenantCookie,
|
|
245
|
-
|
|
246
|
-
getCurrentTenant,
|
|
296
|
+
getActiveTenant,
|
|
247
297
|
initializeTenant,
|
|
248
298
|
} from '@eminuckan/spine/tenant/server';
|
|
249
|
-
import {
|
|
299
|
+
import { getUser } from '@eminuckan/spine/react-router/server';
|
|
300
|
+
import { getIdentityContext } from './identity.server';
|
|
250
301
|
|
|
251
302
|
configureTenantCookie({
|
|
303
|
+
name: '__spine_tenant',
|
|
252
304
|
httpOnly: false,
|
|
253
305
|
sameSite: 'Lax',
|
|
254
306
|
});
|
|
255
307
|
|
|
256
308
|
configureTenantResolution({
|
|
257
|
-
identityContextFetcher:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
getAvailableTenants,
|
|
262
|
-
getCurrentTenant,
|
|
263
|
-
initializeTenant,
|
|
264
|
-
};
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### 2. Configure Server-Side Permission Protection
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
// app/lib/spine/permissions.server.ts
|
|
271
|
-
import {
|
|
272
|
-
configurePermissionRouteProtection,
|
|
273
|
-
requirePermission,
|
|
274
|
-
} from '@eminuckan/spine/server';
|
|
275
|
-
import { getAuthSession } from '@eminuckan/spine/react-router/server';
|
|
276
|
-
import { contextToUserInfo, getIdentityContext } from './identity.server';
|
|
277
|
-
import { getCurrentTenant } from './tenant.server';
|
|
278
|
-
|
|
279
|
-
configurePermissionRouteProtection({
|
|
280
|
-
getSession: getAuthSession,
|
|
281
|
-
resolveContext: async (request, session) => {
|
|
282
|
-
if (!session.user?.sub) {
|
|
283
|
-
return { permissions: [], currentTenant: null };
|
|
309
|
+
identityContextFetcher: async (request) => {
|
|
310
|
+
const user = await getUser(request);
|
|
311
|
+
if (!user) {
|
|
312
|
+
return null;
|
|
284
313
|
}
|
|
285
314
|
|
|
286
|
-
const
|
|
287
|
-
const currentTenant = await getCurrentTenant(request);
|
|
288
|
-
const userInfo = await contextToUserInfo(identityContext, {
|
|
289
|
-
currentTenant,
|
|
290
|
-
request,
|
|
291
|
-
});
|
|
292
|
-
|
|
315
|
+
const context = await getIdentityContext(request, user.sub);
|
|
293
316
|
return {
|
|
294
|
-
|
|
295
|
-
currentTenant: userInfo.currentTenant,
|
|
317
|
+
memberships: context.memberships ?? [],
|
|
296
318
|
};
|
|
297
319
|
},
|
|
298
320
|
});
|
|
299
321
|
|
|
300
|
-
export {
|
|
322
|
+
export { getActiveTenant, initializeTenant };
|
|
301
323
|
```
|
|
302
324
|
|
|
303
|
-
### 3. Protect
|
|
325
|
+
### 3. Protect a Route
|
|
304
326
|
|
|
305
327
|
```ts
|
|
306
|
-
// app/routes/
|
|
328
|
+
// app/routes/dashboard.tsx
|
|
307
329
|
import { authRoute, getAccessToken } from '@eminuckan/spine/react-router/server';
|
|
308
|
-
import {
|
|
309
|
-
import { contextToUserInfo, getIdentityContext } from '~/lib/spine/identity.server';
|
|
330
|
+
import { getActiveTenant, initializeTenant } from '~/lib/spine/tenant.server';
|
|
310
331
|
|
|
311
332
|
export async function loader({ request }: { request: Request }) {
|
|
312
333
|
return authRoute(request, async (user) => {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
getCurrentTenant(request),
|
|
317
|
-
]);
|
|
318
|
-
|
|
319
|
-
const initResult =
|
|
320
|
-
!currentTenant && identityContext.hasAnyMembership
|
|
321
|
-
? await initializeTenant(request)
|
|
322
|
-
: null;
|
|
323
|
-
|
|
324
|
-
const userInfo = await contextToUserInfo(identityContext, {
|
|
325
|
-
currentTenant: initResult?.tenantId ?? currentTenant,
|
|
326
|
-
request,
|
|
327
|
-
});
|
|
334
|
+
const currentTenant = await getActiveTenant(request);
|
|
335
|
+
const initializedTenant = currentTenant ? null : await initializeTenant(request);
|
|
336
|
+
const accessToken = await getAccessToken(request);
|
|
328
337
|
|
|
329
338
|
return {
|
|
330
339
|
user,
|
|
331
|
-
accessToken,
|
|
332
|
-
|
|
333
|
-
...identityContext,
|
|
334
|
-
permissions: userInfo.permissions,
|
|
335
|
-
currentTenant: userInfo.currentTenant,
|
|
336
|
-
},
|
|
337
|
-
tenantHeaders: initResult?.headers ?? null,
|
|
340
|
+
hasAccessToken: Boolean(accessToken),
|
|
341
|
+
currentTenant: initializedTenant?.tenantId ?? currentTenant,
|
|
338
342
|
};
|
|
339
343
|
});
|
|
340
344
|
}
|
|
341
345
|
```
|
|
342
346
|
|
|
343
|
-
### 4. Wire
|
|
347
|
+
### 4. Wire React Providers
|
|
344
348
|
|
|
345
349
|
```tsx
|
|
346
350
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
@@ -388,14 +392,12 @@ export function AppProviders({
|
|
|
388
392
|
}
|
|
389
393
|
```
|
|
390
394
|
|
|
391
|
-
### 5. Configure
|
|
395
|
+
### 5. Configure Conventions Instead of Forking
|
|
392
396
|
|
|
393
397
|
```ts
|
|
394
|
-
import {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
configureRouteProtection,
|
|
398
|
-
} from '@eminuckan/spine/server';
|
|
398
|
+
import { configureAuthClaimMapping, configureRouteProtection } from '@eminuckan/spine/server';
|
|
399
|
+
import { configureIdentityStore } from '@eminuckan/spine/identity';
|
|
400
|
+
import { configureTenantClient } from '@eminuckan/spine/tenant';
|
|
399
401
|
|
|
400
402
|
configureAuthClaimMapping({
|
|
401
403
|
tenantIds: ['tenant_ids'],
|
|
@@ -410,6 +412,17 @@ configureIdentityStore({
|
|
|
410
412
|
logoutPath: '/session/logout',
|
|
411
413
|
});
|
|
412
414
|
|
|
415
|
+
configureTenantClient({
|
|
416
|
+
fetchTenantData: async ({ tenantId }) => {
|
|
417
|
+
const response = await fetch(`/api/workspaces/${tenantId}`);
|
|
418
|
+
const payload = await response.json();
|
|
419
|
+
return {
|
|
420
|
+
id: payload.workspace.id,
|
|
421
|
+
name: payload.workspace.displayName,
|
|
422
|
+
};
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
413
426
|
configureRouteProtection({
|
|
414
427
|
getLoginReturnUrl: ({ request }) => new URL(request.url).pathname,
|
|
415
428
|
});
|
|
@@ -419,20 +432,25 @@ Client contracts are configurable too. Simple apps can use endpoint configuratio
|
|
|
419
432
|
|
|
420
433
|
More adaptation examples live in [docs/backend-adaptation.md](docs/backend-adaptation.md).
|
|
421
434
|
|
|
422
|
-
##
|
|
435
|
+
## Documentation
|
|
436
|
+
|
|
437
|
+
Integration docs:
|
|
423
438
|
|
|
424
439
|
- [Architecture](docs/architecture.md)
|
|
425
440
|
- [Installation](docs/installation.md)
|
|
426
441
|
- [Adapters](docs/adapters.md)
|
|
427
442
|
- [Backend Adaptation](docs/backend-adaptation.md)
|
|
428
443
|
- [Module Reference](docs/module-reference.md)
|
|
444
|
+
|
|
445
|
+
Project docs:
|
|
446
|
+
|
|
429
447
|
- [Roadmap](ROADMAP.md)
|
|
430
|
-
- [Codex Maintenance Plan](docs/codex-maintenance.md)
|
|
431
448
|
- [Releasing](docs/releasing.md)
|
|
432
449
|
- [Contributing](CONTRIBUTING.md)
|
|
433
450
|
- [Maintainers](MAINTAINERS.md)
|
|
434
451
|
- [Security](SECURITY.md)
|
|
435
452
|
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
453
|
+
- [Codex Maintenance Plan](docs/codex-maintenance.md)
|
|
436
454
|
|
|
437
455
|
## Current Boundaries
|
|
438
456
|
|
package/SECURITY.md
CHANGED
|
@@ -2,27 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Supported Versions
|
|
4
4
|
|
|
5
|
-
Security fixes are applied to the latest published release line.
|
|
5
|
+
Security fixes are applied to the latest published release line and the current `main` branch. Older releases are not supported.
|
|
6
6
|
|
|
7
7
|
## Reporting a Vulnerability
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Do not open a public issue or discussion for a suspected vulnerability.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Use [GitHub private vulnerability reporting](https://github.com/eminuckan/spine/security/advisories/new) from the repository's **Security** tab. Maintainers should coordinate investigation and remediation in a private draft security advisory.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Include the affected package entry point or adapter, impact, reproduction steps, affected version or revision, and any suggested mitigation. Redact credentials, tokens, session material, and application data from the report.
|
|
14
14
|
|
|
15
|
-
|
|
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.
|
|
15
|
+
We will validate the report, prioritize remediation by impact and exploitability, and coordinate disclosure after a fix is available.
|
|
26
16
|
|
|
27
17
|
## Scope Notes
|
|
28
18
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { PermissionChecker } from "./permission-service-
|
|
2
|
-
|
|
1
|
+
import { t as PermissionChecker } from "./permission-service-DUyKomU3.mjs";
|
|
3
2
|
//#region src/access/evaluator.ts
|
|
4
|
-
const ACTIVE_ENTITLEMENT_STATUSES = new Set(["active", "trialing"]);
|
|
3
|
+
const ACTIVE_ENTITLEMENT_STATUSES = /* @__PURE__ */ new Set(["active", "trialing"]);
|
|
5
4
|
const EMPTY_DECISION = Object.freeze({
|
|
6
5
|
allowed: true,
|
|
7
6
|
reasons: [],
|
|
@@ -34,8 +33,7 @@ function evaluateAccessRequirement(requirement, subject) {
|
|
|
34
33
|
if (requirement.requireActiveEntitlement && !hasActiveEntitlement(entitlement)) reasons.push("inactive-entitlement");
|
|
35
34
|
else if (requirement.requireActiveSubscription && !hasActiveEntitlement(entitlement)) reasons.push("inactive-subscription");
|
|
36
35
|
if (permissions.length > 0) {
|
|
37
|
-
const
|
|
38
|
-
const result = checker.hasPermissions([...permissions], {
|
|
36
|
+
const result = new PermissionChecker([...subject.permissions ?? []]).hasPermissions([...permissions], {
|
|
39
37
|
operator: requirement.permissionOperator ?? "AND",
|
|
40
38
|
wildcard: true
|
|
41
39
|
});
|
|
@@ -55,7 +53,7 @@ function evaluateAccessRequirement(requirement, subject) {
|
|
|
55
53
|
missingFeatures
|
|
56
54
|
};
|
|
57
55
|
}
|
|
58
|
-
|
|
59
56
|
//#endregion
|
|
60
|
-
export { evaluateAccessRequirement };
|
|
61
|
-
|
|
57
|
+
export { evaluateAccessRequirement as t };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=access-BHy1v5sr.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-BHy1v5sr.mjs","names":[],"sources":["../src/access/evaluator.ts"],"sourcesContent":["import { PermissionChecker } from '../permissions/permission-service';\nimport type {\n AccessDecision,\n AccessDeniedReason,\n AccessOperator,\n AccessRequirement,\n AccessSubject,\n EntitlementAccessSnapshot,\n} from './types';\n\nconst ACTIVE_ENTITLEMENT_STATUSES = new Set(['active', 'trialing']);\n\nconst EMPTY_DECISION: AccessDecision = Object.freeze({\n allowed: true,\n reasons: [],\n missingPermissions: [],\n missingFeatures: [],\n});\n\nfunction normalizeFeature(value: string): string {\n return value.trim().toLowerCase();\n}\n\nfunction hasActiveEntitlement(entitlement?: EntitlementAccessSnapshot | null): boolean {\n if (!entitlement?.exists) {\n return false;\n }\n\n return ACTIVE_ENTITLEMENT_STATUSES.has(entitlement.status?.toLowerCase() ?? '');\n}\n\nfunction checkFeatureSet(\n grantedFeatures: readonly string[],\n requiredFeatures: readonly string[],\n operator: AccessOperator,\n): string[] {\n if (requiredFeatures.length === 0) {\n return [];\n }\n\n const granted = new Set(grantedFeatures.map(normalizeFeature));\n const normalizedRequired = requiredFeatures.map(normalizeFeature).filter(Boolean);\n\n if (operator === 'OR') {\n return normalizedRequired.some((feature) => granted.has(feature)) ? [] : normalizedRequired;\n }\n\n return normalizedRequired.filter((feature) => !granted.has(feature));\n}\n\nexport function evaluateAccessRequirement(\n requirement: AccessRequirement | null | undefined,\n subject: AccessSubject,\n): AccessDecision {\n if (!requirement) {\n return EMPTY_DECISION;\n }\n\n const reasons: AccessDeniedReason[] = [];\n const permissions = requirement.permissions ?? [];\n const features = requirement.features ?? [];\n const entitlement = subject.entitlement ?? subject.subscription;\n let missingPermissions: string[] = [];\n let missingFeatures: string[] = [];\n\n if (requirement.internalOnly && !subject.isInternalUser) {\n reasons.push('internal-only');\n }\n\n if (requirement.requireActiveEntitlement && !hasActiveEntitlement(entitlement)) {\n reasons.push('inactive-entitlement');\n } else if (requirement.requireActiveSubscription && !hasActiveEntitlement(entitlement)) {\n reasons.push('inactive-subscription');\n }\n\n if (permissions.length > 0) {\n const checker = new PermissionChecker([...(subject.permissions ?? [])]);\n const result = checker.hasPermissions([...permissions], {\n operator: requirement.permissionOperator ?? 'AND',\n wildcard: true,\n });\n\n if (!result.hasPermission) {\n missingPermissions = result.missingPermissions;\n reasons.push('missing-permissions');\n }\n }\n\n if (features.length > 0) {\n missingFeatures = checkFeatureSet(\n entitlement?.featureKeys ?? [],\n features,\n requirement.featureOperator ?? 'AND',\n );\n\n if (missingFeatures.length > 0) {\n reasons.push('missing-features');\n }\n }\n\n return {\n allowed: reasons.length === 0,\n reasons,\n missingPermissions,\n missingFeatures,\n };\n}\n"],"mappings":";;AAUA,MAAM,8CAA8B,IAAI,IAAI,CAAC,UAAU,UAAU,CAAC;AAElE,MAAM,iBAAiC,OAAO,OAAO;CACnD,SAAS;CACT,SAAS,CAAC;CACV,oBAAoB,CAAC;CACrB,iBAAiB,CAAC;AACpB,CAAC;AAED,SAAS,iBAAiB,OAAuB;CAC/C,OAAO,MAAM,KAAK,CAAC,CAAC,YAAY;AAClC;AAEA,SAAS,qBAAqB,aAAyD;CACrF,IAAI,CAAC,aAAa,QAChB,OAAO;CAGT,OAAO,4BAA4B,IAAI,YAAY,QAAQ,YAAY,KAAK,EAAE;AAChF;AAEA,SAAS,gBACP,iBACA,kBACA,UACU;CACV,IAAI,iBAAiB,WAAW,GAC9B,OAAO,CAAC;CAGV,MAAM,UAAU,IAAI,IAAI,gBAAgB,IAAI,gBAAgB,CAAC;CAC7D,MAAM,qBAAqB,iBAAiB,IAAI,gBAAgB,CAAC,CAAC,OAAO,OAAO;CAEhF,IAAI,aAAa,MACf,OAAO,mBAAmB,MAAM,YAAY,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI;CAG3E,OAAO,mBAAmB,QAAQ,YAAY,CAAC,QAAQ,IAAI,OAAO,CAAC;AACrE;AAEA,SAAgB,0BACd,aACA,SACgB;CAChB,IAAI,CAAC,aACH,OAAO;CAGT,MAAM,UAAgC,CAAC;CACvC,MAAM,cAAc,YAAY,eAAe,CAAC;CAChD,MAAM,WAAW,YAAY,YAAY,CAAC;CAC1C,MAAM,cAAc,QAAQ,eAAe,QAAQ;CACnD,IAAI,qBAA+B,CAAC;CACpC,IAAI,kBAA4B,CAAC;CAEjC,IAAI,YAAY,gBAAgB,CAAC,QAAQ,gBACvC,QAAQ,KAAK,eAAe;CAG9B,IAAI,YAAY,4BAA4B,CAAC,qBAAqB,WAAW,GAC3E,QAAQ,KAAK,sBAAsB;MAC9B,IAAI,YAAY,6BAA6B,CAAC,qBAAqB,WAAW,GACnF,QAAQ,KAAK,uBAAuB;CAGtC,IAAI,YAAY,SAAS,GAAG;EAE1B,MAAM,SAAS,IADK,kBAAkB,CAAC,GAAI,QAAQ,eAAe,CAAC,CAAE,CAChD,CAAC,CAAC,eAAe,CAAC,GAAG,WAAW,GAAG;GACtD,UAAU,YAAY,sBAAsB;GAC5C,UAAU;EACZ,CAAC;EAED,IAAI,CAAC,OAAO,eAAe;GACzB,qBAAqB,OAAO;GAC5B,QAAQ,KAAK,qBAAqB;EACpC;CACF;CAEA,IAAI,SAAS,SAAS,GAAG;EACvB,kBAAkB,gBAChB,aAAa,eAAe,CAAC,GAC7B,UACA,YAAY,mBAAmB,KACjC;EAEA,IAAI,gBAAgB,SAAS,GAC3B,QAAQ,KAAK,kBAAkB;CAEnC;CAEA,OAAO;EACL,SAAS,QAAQ,WAAW;EAC5B;EACA;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { t as isApiClientError } from "../types-DqCXinAk.mjs";
|
|
2
|
+
import { a as withRetry, c as handleApiError, i as isRetryableError, l as ErrorCodes, n as createRetryFetch, o as ApiError, r as fetchWithTimeout, s as ErrorHandler, t as calculateBackoffDelay } from "../api-client-CB7XZaMg.mjs";
|
|
3
|
+
export { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createRetryFetch, fetchWithTimeout, handleApiError, isApiClientError, isRetryableError, withRetry };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { t as isApiClientError } from "../types-DqCXinAk.mjs";
|
|
2
|
+
import { a as withRetry, c as handleApiError, i as isRetryableError, l as ErrorCodes, n as createRetryFetch, o as ApiError, r as fetchWithTimeout, s as ErrorHandler, t as calculateBackoffDelay } from "../api-client-CB7XZaMg.mjs";
|
|
3
|
+
import { n as createFetchMiddleware, r as createAPIConfigFactory, t as createApiClient } from "../server-ByScGRXA.mjs";
|
|
4
|
+
export { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createAPIConfigFactory, createApiClient, createFetchMiddleware, createRetryFetch, fetchWithTimeout, handleApiError, isApiClientError, isRetryableError, withRetry };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { logger } from "./logging
|
|
2
|
-
|
|
1
|
+
import { logger } from "./logging/index.mjs";
|
|
3
2
|
//#region src/api-client/error-codes.ts
|
|
4
3
|
/**
|
|
5
4
|
* Error Codes
|
|
@@ -30,13 +29,14 @@ const ErrorCodes = {
|
|
|
30
29
|
Forbidden: "Forbidden",
|
|
31
30
|
InternalError: "InternalError"
|
|
32
31
|
};
|
|
33
|
-
|
|
34
32
|
//#endregion
|
|
35
33
|
//#region src/api-client/error-handler.ts
|
|
36
34
|
/**
|
|
37
35
|
* API Error Class
|
|
38
36
|
*/
|
|
39
37
|
var ApiError = class extends Error {
|
|
38
|
+
problemDetails;
|
|
39
|
+
response;
|
|
40
40
|
constructor(problemDetails, response) {
|
|
41
41
|
super(problemDetails.detail);
|
|
42
42
|
this.problemDetails = problemDetails;
|
|
@@ -116,7 +116,6 @@ var ErrorHandler = class {
|
|
|
116
116
|
this.showToast(message, "error");
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
|
-
|
|
120
119
|
//#endregion
|
|
121
120
|
//#region src/api-client/retry-handler.ts
|
|
122
121
|
const DEFAULT_RETRY_CONFIG = {
|
|
@@ -168,15 +167,14 @@ async function withRetry(fn, config) {
|
|
|
168
167
|
* Fetch with timeout
|
|
169
168
|
*/
|
|
170
169
|
async function fetchWithTimeout(input, init) {
|
|
171
|
-
const { timeout = 3e4
|
|
170
|
+
const { timeout = 3e4, ...fetchInit } = init || {};
|
|
172
171
|
const controller = new AbortController();
|
|
173
172
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
174
173
|
try {
|
|
175
|
-
|
|
174
|
+
return await fetch(input, {
|
|
176
175
|
...fetchInit,
|
|
177
176
|
signal: controller.signal
|
|
178
177
|
});
|
|
179
|
-
return response;
|
|
180
178
|
} finally {
|
|
181
179
|
clearTimeout(timeoutId);
|
|
182
180
|
}
|
|
@@ -189,7 +187,7 @@ function createRetryFetch(config) {
|
|
|
189
187
|
return withRetry(() => fetch(input, init), config);
|
|
190
188
|
};
|
|
191
189
|
}
|
|
192
|
-
|
|
193
190
|
//#endregion
|
|
194
|
-
export {
|
|
195
|
-
|
|
191
|
+
export { withRetry as a, handleApiError as c, isRetryableError as i, ErrorCodes as l, createRetryFetch as n, ApiError as o, fetchWithTimeout as r, ErrorHandler as s, calculateBackoffDelay as t };
|
|
192
|
+
|
|
193
|
+
//# sourceMappingURL=api-client-CB7XZaMg.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client-CB7XZaMg.mjs","names":[],"sources":["../src/api-client/error-codes.ts","../src/api-client/error-handler.ts","../src/api-client/retry-handler.ts"],"sourcesContent":["/**\n * Error Codes\n * \n * Generic error codes that apps can use as a starter vocabulary.\n * Domain-specific codes should live in the consuming app.\n */\n\nexport const ErrorCodes = {\n // Validation\n Validation: {\n Failed: 'ValidationFailed',\n },\n\n // Identity\n Identity: {\n InvalidCredentials: 'InvalidCredentials',\n UserInactive: 'UserInactive',\n TenantContextMissing: 'TenantContextMissing',\n InvalidCurrentPassword: 'InvalidCurrentPassword',\n OwnerCannotDisableMembership: 'OwnerCannotDisableMembership',\n CurrentUserMissing: 'CurrentUserMissing',\n UserNotFound: 'UserNotFound',\n EmailAlreadyExists: 'EmailAlreadyExists',\n },\n\n // Access and entitlement\n Access: {\n EntitlementRequired: 'EntitlementRequired',\n EntitlementExpired: 'EntitlementExpired',\n FeatureNotAvailable: 'FeatureNotAvailable',\n TenantContextMissing: 'TenantContextMissing',\n },\n\n // General\n NotFound: 'NotFound',\n Unauthorized: 'Unauthorized',\n Forbidden: 'Forbidden',\n InternalError: 'InternalError',\n} as const;\n","/**\n * Error Handler\n * \n * Centralized error handling with ProblemDetails parsing.\n */\n\nimport type { ProblemDetails, ValidationError } from './types';\nimport { ErrorCodes } from './error-codes';\nimport { logger } from '../logging';\n\n/**\n * API Error Class\n */\nexport class ApiError extends Error {\n constructor(\n public problemDetails: ProblemDetails,\n public response: Response\n ) {\n super(problemDetails.detail);\n this.name = 'ApiError';\n }\n\n get code(): string {\n return this.problemDetails.title;\n }\n\n get status(): number {\n return this.problemDetails.status;\n }\n\n get isValidationError(): boolean {\n return this.code === ErrorCodes.Validation.Failed;\n }\n\n get validationErrors(): ValidationError[] {\n return this.problemDetails.meta?.errors ?? [];\n }\n\n get traceId(): string {\n return this.problemDetails.traceId;\n }\n}\n\n/**\n * Parse API error response\n */\nexport async function handleApiError(response: Response): Promise<never> {\n let problemDetails: ProblemDetails;\n\n try {\n problemDetails = await response.json();\n } catch {\n problemDetails = {\n type: `https://httpstatuses.com/${response.status}`,\n title: 'UnknownError',\n status: response.status,\n detail: response.statusText || 'An unknown error occurred',\n traceId: response.headers.get('x-trace-id') ?? 'unknown',\n };\n }\n\n logger.error('API request failed', undefined, {\n code: problemDetails.title,\n status: problemDetails.status,\n detail: problemDetails.detail,\n traceId: problemDetails.traceId,\n });\n\n throw new ApiError(problemDetails, response);\n}\n\n/**\n * Error Handler Configuration\n */\nexport interface ErrorHandlerConfig {\n handlers?: Partial<Record<string, (error: ApiError) => void>>;\n showToast?: boolean;\n toastMessages?: Partial<Record<string, string>>;\n onError?: (error: ApiError) => void;\n toast?: (message: string, type: 'error' | 'success' | 'info') => void;\n}\n\n/**\n * Error Handler Class\n */\nexport class ErrorHandler {\n private config: ErrorHandlerConfig;\n\n constructor(config: ErrorHandlerConfig = {}) {\n this.config = {\n showToast: true,\n ...config,\n };\n }\n\n handle(error: ApiError): void {\n const code = error.code;\n\n logger.error('Handling API Error', undefined, {\n code,\n status: error.status,\n traceId: error.traceId,\n });\n\n // Try custom handler first\n if (this.config.handlers?.[code]) {\n this.config.handlers[code]!(error);\n return;\n }\n\n // Fallback\n if (this.config.onError) {\n this.config.onError(error);\n } else {\n this.showDefaultToast(error);\n }\n }\n\n private showToast(message: string, type: 'error' | 'success' | 'info'): void {\n if (this.config.showToast && this.config.toast) {\n this.config.toast(message, type);\n }\n }\n\n private showDefaultToast(error: ApiError): void {\n const message =\n this.config.toastMessages?.[error.code] ||\n error.problemDetails.detail ||\n 'An error occurred';\n this.showToast(message, 'error');\n }\n}\n\n/**\n * Create global error handler\n */\nexport function createErrorHandler(config?: ErrorHandlerConfig): ErrorHandler {\n return new ErrorHandler(config);\n}\n\n/**\n * Map validation errors to form fields\n */\nexport function mapValidationErrors(errors: ValidationError[]): Record<string, string> {\n const mapped: Record<string, string> = {};\n for (const error of errors) {\n const fieldName = error.field.charAt(0).toLowerCase() + error.field.slice(1);\n mapped[fieldName] = error.message;\n }\n return mapped;\n}\n","/**\n * Retry Handler\n * \n * Retry logic for API requests.\n */\n\nimport type { RetryConfig } from './types';\n\nconst DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {\n maxRetries: 3,\n baseDelay: 1000,\n maxDelay: 10000,\n retryCondition: (error: unknown) => {\n if (error instanceof Response) {\n return error.status >= 500 || error.status === 429;\n }\n return false;\n },\n};\n\n/**\n * Calculate backoff delay\n */\nexport function calculateBackoffDelay(\n attempt: number,\n baseDelay: number,\n maxDelay: number\n): number {\n const delay = baseDelay * Math.pow(2, attempt);\n const jitter = Math.random() * 0.3 * delay;\n return Math.min(delay + jitter, maxDelay);\n}\n\n/**\n * Check if error is retryable\n */\nexport function isRetryableError(error: unknown): boolean {\n if (error instanceof Response) {\n return error.status >= 500 || error.status === 429;\n }\n if (error instanceof TypeError) {\n // Network errors\n return true;\n }\n return false;\n}\n\n/**\n * Execute function with retry logic\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n config?: RetryConfig\n): Promise<T> {\n const { maxRetries, baseDelay, maxDelay, retryCondition } = {\n ...DEFAULT_RETRY_CONFIG,\n ...config,\n };\n\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt === maxRetries || !retryCondition(error)) {\n throw error;\n }\n\n const delay = calculateBackoffDelay(attempt, baseDelay, maxDelay);\n console.log(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n\n throw lastError;\n}\n\n/**\n * Fetch with timeout\n */\nexport async function fetchWithTimeout(\n input: RequestInfo | URL,\n init?: RequestInit & { timeout?: number }\n): Promise<Response> {\n const { timeout = 30000, ...fetchInit } = init || {};\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(input, {\n ...fetchInit,\n signal: controller.signal,\n });\n return response;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\n/**\n * Create fetch function with retry\n */\nexport function createRetryFetch(config?: RetryConfig) {\n return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n return withRetry(() => fetch(input, init), config);\n };\n}\n"],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { isSessionValid } from './redis-session-storage.server';
|
|
12
12
|
import type { AuthClaimMapping, UserInfo, TokenRefreshResult, LoginOptions, AuthError } from './types';
|
|
13
|
+
export declare function createAuthorizationResponseUrl(requestUrl: string, redirectUri: string): URL;
|
|
13
14
|
/**
|
|
14
15
|
* Configure claim mapping so different providers/backends can project their
|
|
15
16
|
* own claim names onto the shared auth/session primitives.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.server.d.ts","sourceRoot":"","sources":["../../src/auth/auth.server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EAKL,cAAc,EASf,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAEV,gBAAgB,EAChB,QAAQ,EAGR,kBAAkB,EAClB,YAAY,EAKZ,SAAS,EACV,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.server.d.ts","sourceRoot":"","sources":["../../src/auth/auth.server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EAKL,cAAc,EASf,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAEV,gBAAgB,EAChB,QAAQ,EAGR,kBAAkB,EAClB,YAAY,EAKZ,SAAS,EACV,MAAM,SAAS,CAAC;AAgOjB,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,GAAG,CAM3F;AAmVD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAgBzE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C;AAkID;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAChE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,CAkDA;AAyDD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAkB/D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAK/C;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAGtD;AAMD;;;;;GAKG;AACH,wBAAsB,KAAK,CACzB,OAAO,EAAE,OAAO,EAChB,kBAAkB,CAAC,EAAE,MAAM,GAAG,YAAY,GACzC,OAAO,CAAC,QAAQ,CAAC,CAgGnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA2OxE;AAED;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkGhE;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA8CjF;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmClF;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAQxE;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAWrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQ7E;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,kBAAkB,CAAC,CAiD7B;AAGD,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-protection.server.d.ts","sourceRoot":"","sources":["../../src/auth/route-protection.server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAazD;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"route-protection.server.d.ts","sourceRoot":"","sources":["../../src/auth/route-protection.server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { S as updateAuthSession, _ as isSessionValid, a as createOAuthState, b as listCurrentUserAuthSessions, c as destroyAuthSessionById, d as destroyAuthSessionsBySid, f as destroyAuthSessionsForUser, g as getOAuthState, h as getCurrentAuthSessionId, i as createAuthSession, l as destroyAuthSessionsByIdentitySession, m as getAuthSessionById, n as cleanupExpiredOAuthStates, o as deleteOAuthState, p as getAuthSession, r as closeRedisConnection, s as destroyAuthSession, t as AUTH_ERROR_COOKIE_PREFIX, u as destroyAuthSessionsBySessionState, v as listAuthSessionDataForUser, x as requireAuthSession, y as listAuthSessionsForUser } from "../redis-session-storage.server-DGgQcJ9B.mjs";
|
|
2
|
+
import { A as resetAuthClaimMapping, C as handleCallback, D as logout, E as login, O as refreshTokens, S as handleBackChannelLogout, T as hasAuthError, _ as createAuthorizationResponseUrl, a as configureRouteProtection, b as getAuthError, c as protectRoute, d as resetRouteProtectionConfig, f as shouldRefreshToken, g as configureAuthClaimMapping, h as clearAuthServerCache, i as authRoute, k as requireAuth, l as protectedRoute, m as clearAuthErrorHeaders, n as isTokenExpiredResponse, o as onboardingRoute, p as subscriptionRoute, r as shouldAttemptTokenRefresh, s as policyRoute, t as attemptTokenRefresh, u as publicRoute, v as extractUserInfo, w as handleFrontChannelLogout, x as getUser, y as getAccessToken } from "../server-CqX2yCa4.mjs";
|
|
3
|
+
export { AUTH_ERROR_COOKIE_PREFIX, attemptTokenRefresh, authRoute, cleanupExpiredOAuthStates, clearAuthErrorHeaders, clearAuthServerCache, closeRedisConnection, configureAuthClaimMapping, configureRouteProtection, createAuthSession, createAuthorizationResponseUrl, createOAuthState, deleteOAuthState, destroyAuthSession, destroyAuthSessionById, destroyAuthSessionsByIdentitySession, destroyAuthSessionsBySessionState, destroyAuthSessionsBySid, destroyAuthSessionsForUser, extractUserInfo, getAccessToken, getAuthError, getAuthSession, getAuthSessionById, getCurrentAuthSessionId, getOAuthState, getUser, handleBackChannelLogout, handleCallback, handleFrontChannelLogout, hasAuthError, isSessionValid, isTokenExpiredResponse, listAuthSessionDataForUser, listAuthSessionsForUser, listCurrentUserAuthSessions, login, logout, onboardingRoute, policyRoute, protectRoute, protectedRoute, publicRoute, refreshTokens, requireAuth, requireAuthSession, resetAuthClaimMapping, resetRouteProtectionConfig, shouldAttemptTokenRefresh, shouldRefreshToken, subscriptionRoute, updateAuthSession };
|