@eminuckan/spine 0.2.4 → 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 +13 -0
- package/CODE_OF_CONDUCT.md +39 -0
- package/CONTRIBUTING.md +78 -0
- package/MAINTAINERS.md +43 -0
- package/README.md +66 -49
- package/ROADMAP.md +58 -0
- package/SECURITY.md +37 -0
- package/dist/access/evaluator.d.ts.map +1 -1
- package/dist/access/index.js +1 -1
- package/dist/access/types.d.ts +10 -2
- package/dist/access/types.d.ts.map +1 -1
- package/dist/{access-DlAqsVGQ.js → access-CGrsDMwu.js} +9 -7
- 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 +2 -2
- 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 +3 -3
- package/dist/{api-client-CZapXIF8.js → api-client-CgZUMukO.js} +9 -26
- package/dist/api-client-CgZUMukO.js.map +1 -0
- 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/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 +24 -2
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/{auth.server-B3ODGIX4.js → auth.server-D5oiz9aF.js} +62 -53
- package/dist/auth.server-D5oiz9aF.js.map +1 -0
- package/dist/{auth.server-wdKsjQsn.js → auth.server-lC94lmlx.js} +3 -3
- 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 +2 -2
- package/dist/identity/types.d.ts +3 -1
- package/dist/identity/types.d.ts.map +1 -1
- package/dist/{identity-yT2ijZF0.js → identity-C5un-xAs.js} +104 -60
- package/dist/identity-C5un-xAs.js.map +1 -0
- package/dist/index.js +8 -8
- 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/permissions/constants.d.ts +3 -0
- package/dist/permissions/constants.d.ts.map +1 -1
- package/dist/permissions/index.js +1 -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-OUK8Eql2.js → permissions-_n1hIG7Q.js} +6 -12
- package/dist/permissions-_n1hIG7Q.js.map +1 -0
- package/dist/query-client/types.d.ts +9 -9
- package/dist/query-client/types.d.ts.map +1 -1
- package/dist/query-client-Bh3TyB8W.js.map +1 -1
- package/dist/react-router/index.js +8 -8
- package/dist/react-router/server.js +14 -14
- package/dist/{redis-session-storage.server-D0-72FV-.js → redis-session-storage.server-v2VcpFsQ.js} +2 -2
- package/dist/{redis-session-storage.server-D0-72FV-.js.map → redis-session-storage.server-v2VcpFsQ.js.map} +1 -1
- package/dist/{server-B7JN86_0.js → server-B6PCxc-T.js} +31 -83
- package/dist/server-B6PCxc-T.js.map +1 -0
- package/dist/{server-DeBlxoaW.js → server-C96NMFBy.js} +17 -9
- package/dist/server-C96NMFBy.js.map +1 -0
- package/dist/{server-Ce9jQRdL.js → server-Ca-3DJf2.js} +44 -11
- package/dist/server-Ca-3DJf2.js.map +1 -0
- package/dist/{server-BmiuV28c.js → server-Dxsww9v9.js} +5 -4
- package/dist/server-Dxsww9v9.js.map +1 -0
- package/dist/server.js +14 -14
- package/dist/signalr/index.js +1 -1
- package/dist/signalr/types.d.ts +1 -1
- package/dist/{signalr-CSfCz1Ik.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/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 +21 -3
- package/dist/access-DlAqsVGQ.js.map +0 -1
- package/dist/api-client-CZapXIF8.js.map +0 -1
- package/dist/auth.server-B3ODGIX4.js.map +0 -1
- package/dist/identity-yT2ijZF0.js.map +0 -1
- package/dist/permissions-OUK8Eql2.js.map +0 -1
- package/dist/server-B7JN86_0.js.map +0 -1
- package/dist/server-BmiuV28c.js.map +0 -1
- package/dist/server-Ce9jQRdL.js.map +0 -1
- package/dist/server-DeBlxoaW.js.map +0 -1
- package/dist/signalr-CSfCz1Ik.js.map +0 -1
- package/dist/tenant-BErqEENp.js +0 -256
- package/dist/tenant-BErqEENp.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,18 +4,31 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
No unreleased changes.
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-06-01
|
|
10
|
+
|
|
7
11
|
### Added
|
|
8
12
|
|
|
13
|
+
- Added a public-surface check that blocks private project names and app-specific authorization hints from the public package.
|
|
14
|
+
- Added per-login allowlists for app-specific OIDC authorization parameters and public state UI context.
|
|
9
15
|
- Added Keycloak-compatible back-channel and front-channel logout handlers.
|
|
10
16
|
- Added Redis session indexes by user, OIDC `sid`, and legacy `session_state`.
|
|
11
17
|
- Added session listing and destroy primitives for current-user, all-user, `sid`, and `session_state` cleanup.
|
|
12
18
|
- Added OIDC client authentication method configuration for public and confidential clients.
|
|
19
|
+
- Added generic tenant, identity, permission, access, and API configuration surfaces for custom SaaS backend contracts.
|
|
20
|
+
- Added Vitest coverage for authorization parameters, access evaluation, permissions, tenant state, identity state, and API header configuration.
|
|
21
|
+
- Added React Router SaaS example app, installation docs, adapter docs, roadmap, maintainer notes, and Codex maintenance plan.
|
|
22
|
+
- Added an adapter request issue template for framework and provider integrations.
|
|
13
23
|
|
|
14
24
|
### Changed
|
|
15
25
|
|
|
16
26
|
- Switched server-side auth from direct `oauth4webapi` calls to `openid-client`.
|
|
27
|
+
- Removed built-in product-flow redirects from route protection; consuming apps now own all setup, billing, and tenant workflow routing through `configureRouteProtection`.
|
|
28
|
+
- Changed built-in package defaults from legacy project branding to Spine-oriented defaults.
|
|
17
29
|
- Made default logout RP-Initiated Logout and kept app-local cleanup explicit with `logout=local`.
|
|
18
30
|
- Removed `offline_access` from the default scope; apps can opt in explicitly if they need long-lived refresh tokens.
|
|
31
|
+
- Updated CI and release validation to run the full `pnpm check` gate.
|
|
19
32
|
|
|
20
33
|
## [0.2.4] - 2026-04-25
|
|
21
34
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We want Spine to be a welcoming project for contributors of all experience levels. We commit to making participation respectful, constructive, and harassment-free.
|
|
6
|
+
|
|
7
|
+
## Our Standards
|
|
8
|
+
|
|
9
|
+
Examples of behavior that help create a positive environment:
|
|
10
|
+
|
|
11
|
+
- Being respectful in language and tone
|
|
12
|
+
- Giving actionable, kind feedback
|
|
13
|
+
- Accepting good-faith disagreement
|
|
14
|
+
- Prioritizing shared understanding over ego
|
|
15
|
+
- Helping newer contributors learn the codebase
|
|
16
|
+
|
|
17
|
+
Examples of unacceptable behavior:
|
|
18
|
+
|
|
19
|
+
- Harassment or discrimination
|
|
20
|
+
- Personal attacks
|
|
21
|
+
- Trolling or deliberately hostile behavior
|
|
22
|
+
- Publishing private information without permission
|
|
23
|
+
- Repeatedly derailing technical discussions
|
|
24
|
+
|
|
25
|
+
## Enforcement
|
|
26
|
+
|
|
27
|
+
Project maintainers are responsible for clarifying and enforcing this code of conduct. They may remove, edit, or reject comments, commits, issues, pull requests, and other contributions that violate these standards.
|
|
28
|
+
|
|
29
|
+
## Reporting
|
|
30
|
+
|
|
31
|
+
If you experience or witness unacceptable behavior, open a private report to the maintainer at `eminuckan@gmail.com`.
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
This code of conduct applies within project spaces and in public spaces when someone is representing the project.
|
|
36
|
+
|
|
37
|
+
## Attribution
|
|
38
|
+
|
|
39
|
+
This document is inspired by the Contributor Covenant and adapted for this project.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Contributing to Spine
|
|
2
|
+
|
|
3
|
+
Thanks for contributing.
|
|
4
|
+
|
|
5
|
+
Spine is intended to be a reusable foundation for SaaS infrastructure, so changes should optimize for clarity, composability, and portability across products.
|
|
6
|
+
|
|
7
|
+
## Before You Start
|
|
8
|
+
|
|
9
|
+
- Read [README.md](README.md)
|
|
10
|
+
- Read [docs/architecture.md](docs/architecture.md)
|
|
11
|
+
- Read [docs/backend-adaptation.md](docs/backend-adaptation.md)
|
|
12
|
+
- Check existing issues and pull requests first
|
|
13
|
+
|
|
14
|
+
## Local Setup
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm install
|
|
18
|
+
pnpm check
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Project Structure
|
|
22
|
+
|
|
23
|
+
| Path | Purpose |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `src/auth` | OAuth, sessions, token refresh, route protection |
|
|
26
|
+
| `src/tenant` | Tenant store, provider, cookie, and server helpers |
|
|
27
|
+
| `src/identity` | Identity cache, client store, and provider |
|
|
28
|
+
| `src/permissions` | Permission store, hooks, components, and server protection |
|
|
29
|
+
| `src/api-client` | API config factory, fetch middleware, and retry/error helpers |
|
|
30
|
+
| `src/query-client` | TanStack Query defaults and utilities |
|
|
31
|
+
| `src/signalr` | Realtime client primitives |
|
|
32
|
+
| `src/logging` | Logging utilities |
|
|
33
|
+
| `src/react-router` | React Router adapter entry points |
|
|
34
|
+
| `docs` | Project documentation |
|
|
35
|
+
|
|
36
|
+
## Contribution Rules
|
|
37
|
+
|
|
38
|
+
- Keep core modules framework-agnostic unless the file is inside an adapter namespace
|
|
39
|
+
- Prefer configuration hooks over hardcoded product behavior
|
|
40
|
+
- Avoid shipping product-specific routes, claim names, endpoint paths, or UI assumptions into core
|
|
41
|
+
- Keep public APIs small and explicit
|
|
42
|
+
- Update documentation when behavior or public APIs change
|
|
43
|
+
- Add or update changelog entries for user-visible changes
|
|
44
|
+
|
|
45
|
+
## Adapter Philosophy
|
|
46
|
+
|
|
47
|
+
When deciding where code should live:
|
|
48
|
+
|
|
49
|
+
- Put it in core if it can work across multiple apps with configuration
|
|
50
|
+
- Put it in an adapter if it depends on framework primitives
|
|
51
|
+
- Keep it in the consuming app if it expresses product policy
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
|
|
55
|
+
- Good for core: token refresh, tenant cookie utilities, permission checks, identity caching
|
|
56
|
+
- Good for adapters: framework-specific redirects, loader wrappers, request helpers
|
|
57
|
+
- Good for apps: setup redirects, entitlement gating, domain permission constants
|
|
58
|
+
|
|
59
|
+
## Pull Request Checklist
|
|
60
|
+
|
|
61
|
+
- `pnpm check` passes
|
|
62
|
+
- Public API changes are documented
|
|
63
|
+
- Changelog is updated when needed
|
|
64
|
+
- No product-specific behavior leaked into core without a configuration escape hatch
|
|
65
|
+
|
|
66
|
+
## Commit Messages
|
|
67
|
+
|
|
68
|
+
Use concise, English commit messages.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
|
|
72
|
+
- `docs: rewrite README for open source release`
|
|
73
|
+
- `feat: add configurable permission route protection`
|
|
74
|
+
- `refactor: move tenant cookie policy into core config`
|
|
75
|
+
|
|
76
|
+
## Release Notes
|
|
77
|
+
|
|
78
|
+
Maintainers should also read [docs/releasing.md](docs/releasing.md).
|
package/MAINTAINERS.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Maintainers
|
|
2
|
+
|
|
3
|
+
## Current Maintainer
|
|
4
|
+
|
|
5
|
+
- Muhammet Emin Uckan, primary maintainer
|
|
6
|
+
|
|
7
|
+
## Maintainer Responsibilities
|
|
8
|
+
|
|
9
|
+
The maintainer owns:
|
|
10
|
+
|
|
11
|
+
- reviewing issues and pull requests
|
|
12
|
+
- keeping the package installable and documented
|
|
13
|
+
- preserving the boundary between Spine core, framework adapters, and consuming apps
|
|
14
|
+
- coordinating security reports
|
|
15
|
+
- preparing releases and changelog entries
|
|
16
|
+
- validating changes with `pnpm check` and package dry runs
|
|
17
|
+
|
|
18
|
+
## Review Policy
|
|
19
|
+
|
|
20
|
+
Changes should be reviewed against these questions:
|
|
21
|
+
|
|
22
|
+
- Does this solve a repeated SaaS infrastructure problem?
|
|
23
|
+
- Can it work across multiple apps without product-specific naming?
|
|
24
|
+
- Is the core still framework-agnostic?
|
|
25
|
+
- Does this belong in core, a framework adapter, provider guidance, or the app?
|
|
26
|
+
- Are public API changes documented and covered by tests?
|
|
27
|
+
|
|
28
|
+
## Release Policy
|
|
29
|
+
|
|
30
|
+
Spine follows semver. While the package is on a `0.x` line, breaking changes may still happen, but they must be documented and kept migration-friendly where practical.
|
|
31
|
+
|
|
32
|
+
Before publishing:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm check
|
|
36
|
+
npm pack --dry-run
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Codex-Assisted Maintenance
|
|
40
|
+
|
|
41
|
+
Codex may be used to help with issue triage, pull request review, release checks, documentation updates, and security hardening. The maintainer remains responsible for all final decisions, shipped code, and release artifacts.
|
|
42
|
+
|
|
43
|
+
See [docs/codex-maintenance.md](docs/codex-maintenance.md) for the operating plan.
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Spine
|
|
2
2
|
|
|
3
|
+
[](https://github.com/eminuckan/spine/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@eminuckan/spine)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
Framework-agnostic SaaS primitives for authentication, identity, permissions, multi-tenancy, API access, realtime events, query configuration, and logging.
|
|
4
8
|
|
|
5
9
|
Spine is designed around one idea: the reusable parts of a SaaS platform should live in a single package, while application policy and framework quirks should stay in thin adapters. Today the package ships a framework-agnostic core plus a React Router adapter surface. Future adapters can follow the same pattern.
|
|
@@ -10,6 +14,7 @@ Spine is designed around one idea: the reusable parts of a SaaS platform should
|
|
|
10
14
|
- Current adapter: `@eminuckan/spine/react-router`
|
|
11
15
|
- Server primitives use Web `Request`/`Response` objects, not framework-specific response helpers
|
|
12
16
|
- React Router adapter is intentionally thin today so other adapters can be added without changing the core contract
|
|
17
|
+
- Extracted from repeated production-facing SaaS frontend infrastructure needs, then rebooted as an independent OSS package
|
|
13
18
|
|
|
14
19
|
## Why Spine
|
|
15
20
|
|
|
@@ -28,7 +33,7 @@ Spine aims to be:
|
|
|
28
33
|
- Core before adapter: framework-specific behavior belongs in dedicated adapter entry points
|
|
29
34
|
- Configure, do not fork: backend claim names, cookie behavior, endpoints, and redirects should be configured first
|
|
30
35
|
- Request/Response first: server modules should work anywhere a standard Web `Request` and `Response` exist
|
|
31
|
-
- App policy stays local:
|
|
36
|
+
- App policy stays local: setup flows, entitlement gates, product-specific redirects, and permission taxonomies should live in the consuming app
|
|
32
37
|
- Open for composition: apps should be able to wrap Spine primitives instead of rewriting them
|
|
33
38
|
|
|
34
39
|
## Package Surfaces
|
|
@@ -68,8 +73,8 @@ Spine aims to be:
|
|
|
68
73
|
|
|
69
74
|
## What Spine Does Not Include
|
|
70
75
|
|
|
71
|
-
- Your product's
|
|
72
|
-
- Your product's
|
|
76
|
+
- Your product's setup rules
|
|
77
|
+
- Your product's billing or entitlement rules
|
|
73
78
|
- Your product's permission vocabulary
|
|
74
79
|
- Your backend DTOs or generated API clients
|
|
75
80
|
- Your page structure, layouts, or UI system
|
|
@@ -108,6 +113,19 @@ If you use client-side React features, install peer dependencies too:
|
|
|
108
113
|
pnpm add react @tanstack/react-query
|
|
109
114
|
```
|
|
110
115
|
|
|
116
|
+
For a fuller setup path, see [docs/installation.md](docs/installation.md).
|
|
117
|
+
|
|
118
|
+
## Try the Example App
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd examples/react-router-saas
|
|
122
|
+
pnpm install
|
|
123
|
+
cp .env.example .env
|
|
124
|
+
pnpm dev
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The example includes React Router auth routes, a protected dashboard loader, client provider wiring, and local app adapters for backend identity and tenant conventions.
|
|
128
|
+
|
|
111
129
|
## Environment Variables
|
|
112
130
|
|
|
113
131
|
The built-in auth/session layer currently reads these environment variables:
|
|
@@ -126,7 +144,7 @@ The built-in auth/session layer currently reads these environment variables:
|
|
|
126
144
|
| `OIDC_ALLOW_INSECURE_REQUESTS` | No | Allows non-HTTPS OIDC issuer calls only outside production |
|
|
127
145
|
| `REDIS_URL` | No | Redis connection string for sessions and OAuth state |
|
|
128
146
|
| `REDIS_KEY_PREFIX` | No | Prefix for Redis keys |
|
|
129
|
-
| `API_BASE_URL` | No |
|
|
147
|
+
| `API_BASE_URL` | No | Optional fallback API base URL for `createAPIConfigFactory` |
|
|
130
148
|
|
|
131
149
|
## OIDC Session Lifecycle
|
|
132
150
|
|
|
@@ -172,7 +190,7 @@ Back-channel logout verifies the signed `logout_token` against the provider JWKS
|
|
|
172
190
|
Spine's server modules are generic, so your app should provide backend-specific fetchers once.
|
|
173
191
|
|
|
174
192
|
```ts
|
|
175
|
-
// app/lib/
|
|
193
|
+
// app/lib/spine/identity.server.ts
|
|
176
194
|
import {
|
|
177
195
|
configureIdentityAPIFetcher,
|
|
178
196
|
configurePermissionFetcher,
|
|
@@ -182,7 +200,6 @@ import {
|
|
|
182
200
|
import { createAPIConfigFactory } from '@eminuckan/spine/api-client/server';
|
|
183
201
|
import { getAccessToken } from '@eminuckan/spine/react-router/server';
|
|
184
202
|
import { getCurrentTenant } from '@eminuckan/spine/tenant/server';
|
|
185
|
-
import { Configuration, IdentityApi } from '~/lib/api-clients/api';
|
|
186
203
|
|
|
187
204
|
const { createAPIConfig } = createAPIConfigFactory(getAccessToken, getCurrentTenant);
|
|
188
205
|
|
|
@@ -192,47 +209,38 @@ configureIdentityAPIFetcher(async (request) => {
|
|
|
192
209
|
includeAuth: true,
|
|
193
210
|
});
|
|
194
211
|
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
accessToken: config.accessToken,
|
|
199
|
-
baseOptions: { headers: config.headers },
|
|
200
|
-
})
|
|
201
|
-
);
|
|
212
|
+
const response = await fetch(`${config.basePath}/api/me/context`, {
|
|
213
|
+
headers: config.headers,
|
|
214
|
+
});
|
|
202
215
|
|
|
203
|
-
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
throw new Error(`Failed to load identity context: ${response.status}`);
|
|
218
|
+
}
|
|
204
219
|
|
|
205
|
-
return
|
|
206
|
-
...response.data,
|
|
207
|
-
contextVersion: Date.now(),
|
|
208
|
-
hasSubscription: Boolean(
|
|
209
|
-
response.data.onboarding?.subscription?.exists &&
|
|
210
|
-
['active', 'trialing'].includes(response.data.onboarding?.subscription?.status || '')
|
|
211
|
-
),
|
|
212
|
-
};
|
|
220
|
+
return response.json();
|
|
213
221
|
});
|
|
214
222
|
|
|
215
223
|
configurePermissionFetcher(async (request, tenantId) => {
|
|
216
224
|
const config = await createAPIConfig(request, { requireTenant: false });
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
accessToken: config.accessToken,
|
|
221
|
-
baseOptions: { headers: config.headers },
|
|
222
|
-
})
|
|
223
|
-
);
|
|
225
|
+
const response = await fetch(`${config.basePath}/api/me/permissions?tenantId=${tenantId}`, {
|
|
226
|
+
headers: config.headers,
|
|
227
|
+
});
|
|
224
228
|
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const payload = await response.json();
|
|
234
|
+
return payload.permissions || [];
|
|
227
235
|
});
|
|
228
236
|
|
|
229
237
|
export { contextToUserInfo, getIdentityContext };
|
|
230
238
|
```
|
|
231
239
|
|
|
232
240
|
```ts
|
|
233
|
-
// app/lib/
|
|
241
|
+
// app/lib/spine/tenant.server.ts
|
|
234
242
|
import {
|
|
235
|
-
|
|
243
|
+
configureTenantResolution,
|
|
236
244
|
configureTenantCookie,
|
|
237
245
|
getAvailableTenants,
|
|
238
246
|
getCurrentTenant,
|
|
@@ -245,7 +253,9 @@ configureTenantCookie({
|
|
|
245
253
|
sameSite: 'Lax',
|
|
246
254
|
});
|
|
247
255
|
|
|
248
|
-
|
|
256
|
+
configureTenantResolution({
|
|
257
|
+
identityContextFetcher: fetchIdentityContext,
|
|
258
|
+
});
|
|
249
259
|
|
|
250
260
|
export {
|
|
251
261
|
getAvailableTenants,
|
|
@@ -257,7 +267,7 @@ export {
|
|
|
257
267
|
### 2. Configure Server-Side Permission Protection
|
|
258
268
|
|
|
259
269
|
```ts
|
|
260
|
-
// app/lib/
|
|
270
|
+
// app/lib/spine/permissions.server.ts
|
|
261
271
|
import {
|
|
262
272
|
configurePermissionRouteProtection,
|
|
263
273
|
requirePermission,
|
|
@@ -295,8 +305,8 @@ export { requirePermission };
|
|
|
295
305
|
```ts
|
|
296
306
|
// app/routes/_protected.tsx
|
|
297
307
|
import { authRoute, getAccessToken } from '@eminuckan/spine/react-router/server';
|
|
298
|
-
import { getCurrentTenant, initializeTenant } from '~/lib/
|
|
299
|
-
import { contextToUserInfo, getIdentityContext } from '~/lib/
|
|
308
|
+
import { getCurrentTenant, initializeTenant } from '~/lib/spine/tenant.server';
|
|
309
|
+
import { contextToUserInfo, getIdentityContext } from '~/lib/spine/identity.server';
|
|
300
310
|
|
|
301
311
|
export async function loader({ request }: { request: Request }) {
|
|
302
312
|
return authRoute(request, async (user) => {
|
|
@@ -388,10 +398,10 @@ import {
|
|
|
388
398
|
} from '@eminuckan/spine/server';
|
|
389
399
|
|
|
390
400
|
configureAuthClaimMapping({
|
|
391
|
-
tenantIds: ['
|
|
392
|
-
tenantRoles: ['
|
|
401
|
+
tenantIds: ['tenant_ids'],
|
|
402
|
+
tenantRoles: ['tenant_roles'],
|
|
393
403
|
permissions: ['permissions', 'scope'],
|
|
394
|
-
isOnboarded: ['
|
|
404
|
+
isOnboarded: ['is_onboarded'],
|
|
395
405
|
});
|
|
396
406
|
|
|
397
407
|
configureIdentityStore({
|
|
@@ -405,17 +415,24 @@ configureRouteProtection({
|
|
|
405
415
|
});
|
|
406
416
|
```
|
|
407
417
|
|
|
418
|
+
Client contracts are configurable too. Simple apps can use endpoint configuration; apps with workspace/account/customer-specific contracts can provide `fetchTenantData`, `switchTenant`, `fetchContext`, and `fetchPermissions` functions. API header names are configurable through `createAPIConfigFactory`.
|
|
419
|
+
|
|
408
420
|
More adaptation examples live in [docs/backend-adaptation.md](docs/backend-adaptation.md).
|
|
409
421
|
|
|
410
422
|
## Module Guides
|
|
411
423
|
|
|
412
424
|
- [Architecture](docs/architecture.md)
|
|
425
|
+
- [Installation](docs/installation.md)
|
|
413
426
|
- [Adapters](docs/adapters.md)
|
|
414
427
|
- [Backend Adaptation](docs/backend-adaptation.md)
|
|
415
428
|
- [Module Reference](docs/module-reference.md)
|
|
429
|
+
- [Roadmap](ROADMAP.md)
|
|
430
|
+
- [Codex Maintenance Plan](docs/codex-maintenance.md)
|
|
416
431
|
- [Releasing](docs/releasing.md)
|
|
417
432
|
- [Contributing](CONTRIBUTING.md)
|
|
433
|
+
- [Maintainers](MAINTAINERS.md)
|
|
418
434
|
- [Security](SECURITY.md)
|
|
435
|
+
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
419
436
|
|
|
420
437
|
## Current Boundaries
|
|
421
438
|
|
|
@@ -430,27 +447,27 @@ Spine already owns the reusable infrastructure for:
|
|
|
430
447
|
|
|
431
448
|
Consuming apps should still own:
|
|
432
449
|
|
|
433
|
-
- Product-specific
|
|
434
|
-
- Product-specific
|
|
450
|
+
- Product-specific setup pages and redirects
|
|
451
|
+
- Product-specific billing or entitlement policies
|
|
435
452
|
- Permission constants and domain vocabulary
|
|
436
453
|
- Generated API clients
|
|
437
454
|
- UI-specific wrappers and design system components
|
|
438
455
|
|
|
439
456
|
## Roadmap
|
|
440
457
|
|
|
458
|
+
The active roadmap lives in [ROADMAP.md](ROADMAP.md). Current tracks include:
|
|
459
|
+
|
|
441
460
|
- First-class Next.js adapter surface
|
|
442
|
-
-
|
|
443
|
-
-
|
|
444
|
-
-
|
|
445
|
-
-
|
|
461
|
+
- Clerk and Supabase integration guidance
|
|
462
|
+
- More adapter authoring tests and examples
|
|
463
|
+
- Cleaner migration from deprecated compatibility aliases
|
|
464
|
+
- Public API stability review before `1.0`
|
|
446
465
|
|
|
447
466
|
## Development
|
|
448
467
|
|
|
449
468
|
```bash
|
|
450
469
|
pnpm install
|
|
451
|
-
pnpm
|
|
452
|
-
pnpm build
|
|
453
|
-
pnpm lint
|
|
470
|
+
pnpm check
|
|
454
471
|
```
|
|
455
472
|
|
|
456
473
|
Detailed contributor guidance lives in [CONTRIBUTING.md](CONTRIBUTING.md).
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Spine Roadmap
|
|
2
|
+
|
|
3
|
+
Spine is a small, adapter-friendly foundation for SaaS frontend infrastructure. The roadmap favors reusable primitives, clear framework adapters, and backend-contract flexibility over product-specific features.
|
|
4
|
+
|
|
5
|
+
## Current Focus
|
|
6
|
+
|
|
7
|
+
- Keep the core framework-agnostic and based on Web `Request` and `Response` primitives.
|
|
8
|
+
- Keep application policy, permission vocabularies, setup flows, and entitlement rules in consuming apps.
|
|
9
|
+
- Make every backend convention configurable before adding a new abstraction.
|
|
10
|
+
- Preserve compatibility for existing consumers while moving toward a cleaner `1.0` API.
|
|
11
|
+
|
|
12
|
+
## Near-Term
|
|
13
|
+
|
|
14
|
+
- Publish the `0.3.x` open source reboot with generic tenant, identity, permission, API, realtime, and adapter surfaces.
|
|
15
|
+
- Expand the React Router example into a fuller SaaS shell with auth, tenant switching, permission gates, and API configuration.
|
|
16
|
+
- Add adapter authoring tests that prove new adapters can stay thin.
|
|
17
|
+
- Add more normalization examples for account, workspace, organization, and custom tenant contracts.
|
|
18
|
+
- Document migration notes from deprecated organization and subscription aliases to tenant and entitlement terminology.
|
|
19
|
+
|
|
20
|
+
## Adapter Track
|
|
21
|
+
|
|
22
|
+
- First-class Next.js adapter entry points.
|
|
23
|
+
- React Router adapter refinements where framework-specific helpers remove real boilerplate.
|
|
24
|
+
- Research notes for Vue and Svelte adapter shapes.
|
|
25
|
+
- Adapter request template and contribution checklist for community proposals.
|
|
26
|
+
|
|
27
|
+
## Provider Track
|
|
28
|
+
|
|
29
|
+
- Clerk identity/session adapter guidance.
|
|
30
|
+
- Supabase auth and Postgres-backed tenant examples.
|
|
31
|
+
- Auth0 and generic OpenID Connect recipes.
|
|
32
|
+
- Optional examples for API clients generated from OpenAPI without making any generated client mandatory.
|
|
33
|
+
|
|
34
|
+
## Core Hardening
|
|
35
|
+
|
|
36
|
+
- Broader tests for route protection and OIDC/session lifecycle behavior.
|
|
37
|
+
- More explicit storage abstraction boundaries for sessions and OAuth state.
|
|
38
|
+
- Runtime compatibility notes for Node, edge-like runtimes, and serverless deployments.
|
|
39
|
+
- Public API stability review before `1.0`.
|
|
40
|
+
|
|
41
|
+
## Not Planned For Core
|
|
42
|
+
|
|
43
|
+
- Product-specific route names.
|
|
44
|
+
- Product-specific permission constants.
|
|
45
|
+
- Billing plan vocabulary.
|
|
46
|
+
- Backend DTOs or generated SDKs.
|
|
47
|
+
- UI components tied to one design system.
|
|
48
|
+
|
|
49
|
+
Those belong in consuming apps or app-specific adapter packages.
|
|
50
|
+
|
|
51
|
+
## How To Propose Work
|
|
52
|
+
|
|
53
|
+
Open an issue using the relevant template and describe:
|
|
54
|
+
|
|
55
|
+
- the repeated SaaS infrastructure problem
|
|
56
|
+
- whether it belongs in core, a framework adapter, provider guidance, or the consuming app
|
|
57
|
+
- how the API can remain generic across multiple projects
|
|
58
|
+
- what tests or docs would prove the behavior
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Security fixes are applied to the latest published release line.
|
|
6
|
+
|
|
7
|
+
## Reporting a Vulnerability
|
|
8
|
+
|
|
9
|
+
Please do not open public GitHub issues for security problems.
|
|
10
|
+
|
|
11
|
+
Instead, report vulnerabilities privately to:
|
|
12
|
+
|
|
13
|
+
- Email: `eminuckan@gmail.com`
|
|
14
|
+
|
|
15
|
+
When possible, include:
|
|
16
|
+
|
|
17
|
+
- A clear description of the issue
|
|
18
|
+
- Affected versions
|
|
19
|
+
- Reproduction steps
|
|
20
|
+
- Potential impact
|
|
21
|
+
- Any suggested mitigation
|
|
22
|
+
|
|
23
|
+
## Response Expectations
|
|
24
|
+
|
|
25
|
+
The goal is to acknowledge reports quickly, validate impact, and coordinate a fix before public disclosure.
|
|
26
|
+
|
|
27
|
+
## Scope Notes
|
|
28
|
+
|
|
29
|
+
Spine provides infrastructure primitives, but consuming applications are still responsible for:
|
|
30
|
+
|
|
31
|
+
- Backend authorization enforcement
|
|
32
|
+
- Secure secret management
|
|
33
|
+
- Correct adapter configuration
|
|
34
|
+
- Safe redirect and cookie policies
|
|
35
|
+
- Protecting product-specific APIs and workflows
|
|
36
|
+
|
|
37
|
+
Frontend permission checks and route guards in Spine improve UX, but they are not a substitute for backend authorization.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/access/evaluator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EAGd,iBAAiB,EACjB,aAAa,EAEd,MAAM,SAAS,CAAC;AA0CjB,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EACjD,OAAO,EAAE,aAAa,GACrB,cAAc,
|
|
1
|
+
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/access/evaluator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EAGd,iBAAiB,EACjB,aAAa,EAEd,MAAM,SAAS,CAAC;AA0CjB,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EACjD,OAAO,EAAE,aAAa,GACrB,cAAc,CAqDhB"}
|
package/dist/access/index.js
CHANGED
package/dist/access/types.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { PermissionCode } from '../permissions/types';
|
|
2
2
|
export type AccessOperator = 'AND' | 'OR';
|
|
3
|
-
export interface
|
|
3
|
+
export interface EntitlementAccessSnapshot {
|
|
4
4
|
exists?: boolean;
|
|
5
5
|
status?: string | null;
|
|
6
6
|
planCode?: string | null;
|
|
7
7
|
featureKeys?: readonly string[] | null;
|
|
8
8
|
limits?: Record<string, unknown> | null;
|
|
9
9
|
}
|
|
10
|
+
/** @deprecated Use EntitlementAccessSnapshot instead. */
|
|
11
|
+
export type SubscriptionAccessSnapshot = EntitlementAccessSnapshot;
|
|
10
12
|
export interface AccessSubject {
|
|
11
13
|
permissions?: readonly PermissionCode[] | null;
|
|
14
|
+
entitlement?: EntitlementAccessSnapshot | null;
|
|
15
|
+
/** @deprecated Use entitlement instead. */
|
|
12
16
|
subscription?: SubscriptionAccessSnapshot | null;
|
|
13
17
|
isInternalUser?: boolean;
|
|
14
18
|
}
|
|
@@ -17,10 +21,14 @@ export interface AccessRequirement {
|
|
|
17
21
|
permissionOperator?: AccessOperator;
|
|
18
22
|
features?: readonly string[];
|
|
19
23
|
featureOperator?: AccessOperator;
|
|
24
|
+
requireActiveEntitlement?: boolean;
|
|
25
|
+
/** @deprecated Use requireActiveEntitlement instead. */
|
|
20
26
|
requireActiveSubscription?: boolean;
|
|
21
27
|
internalOnly?: boolean;
|
|
22
28
|
}
|
|
23
|
-
export type AccessDeniedReason = 'internal-only' | 'inactive-
|
|
29
|
+
export type AccessDeniedReason = 'internal-only' | 'inactive-entitlement'
|
|
30
|
+
/** @deprecated Use inactive-entitlement instead. */
|
|
31
|
+
| 'inactive-subscription' | 'missing-permissions' | 'missing-features';
|
|
24
32
|
export interface AccessDecision {
|
|
25
33
|
allowed: boolean;
|
|
26
34
|
reasons: AccessDeniedReason[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/access/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,IAAI,CAAC;AAE1C,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/access/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,IAAI,CAAC;AAE1C,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzC;AAED,yDAAyD;AACzD,MAAM,MAAM,0BAA0B,GAAG,yBAAyB,CAAC;AAEnE,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,SAAS,cAAc,EAAE,GAAG,IAAI,CAAC;IAC/C,WAAW,CAAC,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAC/C,2CAA2C;IAC3C,YAAY,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAAC;IACjD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACxC,kBAAkB,CAAC,EAAE,cAAc,CAAC;IACpC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,wDAAwD;IACxD,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,kBAAkB,GAC1B,eAAe,GACf,sBAAsB;AACxB,oDAAoD;GAClD,uBAAuB,GACvB,qBAAqB,GACrB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,kBAAkB,EAAE,cAAc,EAAE,CAAC;IACrC,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PermissionChecker } from "./permission-service-CvGpwI0d.js";
|
|
2
2
|
|
|
3
3
|
//#region src/access/evaluator.ts
|
|
4
|
-
const
|
|
4
|
+
const ACTIVE_ENTITLEMENT_STATUSES = new Set(["active", "trialing"]);
|
|
5
5
|
const EMPTY_DECISION = Object.freeze({
|
|
6
6
|
allowed: true,
|
|
7
7
|
reasons: [],
|
|
@@ -11,9 +11,9 @@ const EMPTY_DECISION = Object.freeze({
|
|
|
11
11
|
function normalizeFeature(value) {
|
|
12
12
|
return value.trim().toLowerCase();
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
if (!
|
|
16
|
-
return
|
|
14
|
+
function hasActiveEntitlement(entitlement) {
|
|
15
|
+
if (!entitlement?.exists) return false;
|
|
16
|
+
return ACTIVE_ENTITLEMENT_STATUSES.has(entitlement.status?.toLowerCase() ?? "");
|
|
17
17
|
}
|
|
18
18
|
function checkFeatureSet(grantedFeatures, requiredFeatures, operator) {
|
|
19
19
|
if (requiredFeatures.length === 0) return [];
|
|
@@ -27,10 +27,12 @@ function evaluateAccessRequirement(requirement, subject) {
|
|
|
27
27
|
const reasons = [];
|
|
28
28
|
const permissions = requirement.permissions ?? [];
|
|
29
29
|
const features = requirement.features ?? [];
|
|
30
|
+
const entitlement = subject.entitlement ?? subject.subscription;
|
|
30
31
|
let missingPermissions = [];
|
|
31
32
|
let missingFeatures = [];
|
|
32
33
|
if (requirement.internalOnly && !subject.isInternalUser) reasons.push("internal-only");
|
|
33
|
-
if (requirement.
|
|
34
|
+
if (requirement.requireActiveEntitlement && !hasActiveEntitlement(entitlement)) reasons.push("inactive-entitlement");
|
|
35
|
+
else if (requirement.requireActiveSubscription && !hasActiveEntitlement(entitlement)) reasons.push("inactive-subscription");
|
|
34
36
|
if (permissions.length > 0) {
|
|
35
37
|
const checker = new PermissionChecker([...subject.permissions ?? []]);
|
|
36
38
|
const result = checker.hasPermissions([...permissions], {
|
|
@@ -43,7 +45,7 @@ function evaluateAccessRequirement(requirement, subject) {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
if (features.length > 0) {
|
|
46
|
-
missingFeatures = checkFeatureSet(
|
|
48
|
+
missingFeatures = checkFeatureSet(entitlement?.featureKeys ?? [], features, requirement.featureOperator ?? "AND");
|
|
47
49
|
if (missingFeatures.length > 0) reasons.push("missing-features");
|
|
48
50
|
}
|
|
49
51
|
return {
|
|
@@ -56,4 +58,4 @@ function evaluateAccessRequirement(requirement, subject) {
|
|
|
56
58
|
|
|
57
59
|
//#endregion
|
|
58
60
|
export { evaluateAccessRequirement };
|
|
59
|
-
//# sourceMappingURL=access-
|
|
61
|
+
//# sourceMappingURL=access-CGrsDMwu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-CGrsDMwu.js","names":["EMPTY_DECISION: AccessDecision","value: string","entitlement?: EntitlementAccessSnapshot | null","grantedFeatures: readonly string[]","requiredFeatures: readonly string[]","operator: AccessOperator","requirement: AccessRequirement | null | undefined","subject: AccessSubject","reasons: AccessDeniedReason[]","missingPermissions: string[]","missingFeatures: string[]"],"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,8BAA8B,IAAI,IAAI,CAAC,UAAU,UAAW;AAElE,MAAMA,iBAAiC,OAAO,OAAO;CACnD,SAAS;CACT,SAAS,CAAE;CACX,oBAAoB,CAAE;CACtB,iBAAiB,CAAE;AACpB,EAAC;AAEF,SAAS,iBAAiBC,OAAuB;AAC/C,QAAO,MAAM,MAAM,CAAC,aAAa;AAClC;AAED,SAAS,qBAAqBC,aAAyD;AACrF,MAAK,aAAa,OAChB,QAAO;AAGT,QAAO,4BAA4B,IAAI,YAAY,QAAQ,aAAa,IAAI,GAAG;AAChF;AAED,SAAS,gBACPC,iBACAC,kBACAC,UACU;AACV,KAAI,iBAAiB,WAAW,EAC9B,QAAO,CAAE;CAGX,MAAM,UAAU,IAAI,IAAI,gBAAgB,IAAI,iBAAiB;CAC7D,MAAM,qBAAqB,iBAAiB,IAAI,iBAAiB,CAAC,OAAO,QAAQ;AAEjF,KAAI,aAAa,KACf,QAAO,mBAAmB,KAAK,CAAC,YAAY,QAAQ,IAAI,QAAQ,CAAC,GAAG,CAAE,IAAG;AAG3E,QAAO,mBAAmB,OAAO,CAAC,aAAa,QAAQ,IAAI,QAAQ,CAAC;AACrE;AAED,SAAgB,0BACdC,aACAC,SACgB;AAChB,MAAK,YACH,QAAO;CAGT,MAAMC,UAAgC,CAAE;CACxC,MAAM,cAAc,YAAY,eAAe,CAAE;CACjD,MAAM,WAAW,YAAY,YAAY,CAAE;CAC3C,MAAM,cAAc,QAAQ,eAAe,QAAQ;CACnD,IAAIC,qBAA+B,CAAE;CACrC,IAAIC,kBAA4B,CAAE;AAElC,KAAI,YAAY,iBAAiB,QAAQ,eACvC,SAAQ,KAAK,gBAAgB;AAG/B,KAAI,YAAY,6BAA6B,qBAAqB,YAAY,CAC5E,SAAQ,KAAK,uBAAuB;UAC3B,YAAY,8BAA8B,qBAAqB,YAAY,CACpF,SAAQ,KAAK,wBAAwB;AAGvC,KAAI,YAAY,SAAS,GAAG;EAC1B,MAAM,UAAU,IAAI,kBAAkB,CAAC,GAAI,QAAQ,eAAe,CAAE,CAAE;EACtE,MAAM,SAAS,QAAQ,eAAe,CAAC,GAAG,WAAY,GAAE;GACtD,UAAU,YAAY,sBAAsB;GAC5C,UAAU;EACX,EAAC;AAEF,OAAK,OAAO,eAAe;AACzB,wBAAqB,OAAO;AAC5B,WAAQ,KAAK,sBAAsB;EACpC;CACF;AAED,KAAI,SAAS,SAAS,GAAG;AACvB,oBAAkB,gBAChB,aAAa,eAAe,CAAE,GAC9B,UACA,YAAY,mBAAmB,MAChC;AAED,MAAI,gBAAgB,SAAS,EAC3B,SAAQ,KAAK,mBAAmB;CAEnC;AAED,QAAO;EACL,SAAS,QAAQ,WAAW;EAC5B;EACA;EACA;CACD;AACF"}
|