@relyper/sp-auth 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonas Esser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # @relyper/sp-auth
2
+
3
+ Service-provider side of Relyper identity: turn gateway headers into a typed
4
+ principal, gate access by role, and keep the identity layer out of your
5
+ application code.
6
+
7
+ Built for services that sit behind the Relyper auth gateway. The package covers
8
+ the parts every service provider repeats — header parsing, role gating, the
9
+ `/me` contract, dev login — and leaves persistence to you.
10
+
11
+ ```bash
12
+ npm install @relyper/sp-auth
13
+ ```
14
+
15
+ Requires Node 20+. ESM only.
16
+
17
+ ## Quick start (Fastify)
18
+
19
+ ```ts
20
+ import Fastify from 'fastify';
21
+ import { relyperAuth } from '@relyper/sp-auth/fastify';
22
+
23
+ const app = Fastify();
24
+
25
+ await app.register(relyperAuth, {
26
+ requiredRole: 'my_service_user',
27
+ protect: (request) => request.url.startsWith('/api/'),
28
+ meRoute: '/api/me',
29
+ // Turn the IdP identity into your own user record.
30
+ resolveUser: async (identity) => prisma.user.upsert({
31
+ where: { idpSubject: identity.subject },
32
+ create: { idpSubject: identity.subject, email: identity.email, displayName: identity.displayName, roles: identity.roles },
33
+ update: { email: identity.email, displayName: identity.displayName, roles: identity.roles, lastSeenAt: new Date() }
34
+ })
35
+ });
36
+
37
+ app.get('/api/cases', async (request) => {
38
+ request.relyperIdentity; // { subject, email, displayName, roles }
39
+ request.principal; // whatever resolveUser returned
40
+ });
41
+ ```
42
+
43
+ Declare the type of your own principal once:
44
+
45
+ ```ts
46
+ declare module 'fastify' {
47
+ interface FastifyRequest {
48
+ principal: { id: string; email: string };
49
+ }
50
+ }
51
+ ```
52
+
53
+ ## Without Fastify
54
+
55
+ The core is a pure function over headers — no framework, no I/O:
56
+
57
+ ```ts
58
+ import { createRelyperAuth } from '@relyper/sp-auth';
59
+
60
+ const auth = createRelyperAuth({ requiredRole: 'my_service_user' });
61
+ const result = auth.authenticate(request.headers); // Node headers or a fetch Headers object
62
+
63
+ if (!result.ok) {
64
+ return new Response(JSON.stringify({ error: result.message }), { status: result.status });
65
+ }
66
+ result.identity.subject;
67
+ ```
68
+
69
+ `authenticate` never throws and never does I/O, which makes it easy to unit test
70
+ and safe to call on every request.
71
+
72
+ ## Browser client
73
+
74
+ ```ts
75
+ import { fetchRelyperSession } from '@relyper/sp-auth/client';
76
+
77
+ const session = await fetchRelyperSession<{ id: string; email: string }>();
78
+
79
+ switch (session.status) {
80
+ case 'authenticated': return session.user;
81
+ case 'unauthenticated': return redirectToLogin();
82
+ case 'forbidden': return showNoAccessScreen();
83
+ case 'error': return showError();
84
+ }
85
+ ```
86
+
87
+ No framework dependency. A Vue composable or React hook around it is a few lines.
88
+
89
+ ## Options
90
+
91
+ | Option | Default | Purpose |
92
+ | --- | --- | --- |
93
+ | `requiredRole` | – | Role(s) required for this service. Omit to only require an identity. |
94
+ | `roleMatch` | `'any'` | With several required roles: one is enough, or all are needed. |
95
+ | `requireEmail` | `true` | Set to `false` if your IdP does not send an address. |
96
+ | `headerNames` | Relyper headers | Override individual header names for another gateway. |
97
+ | `acceptForwardedHeaders` | `false` | Also accept `x-forwarded-*`. Off by default on purpose. |
98
+ | `devAuth` | `false` | Local login without a gateway. Never enable in production. |
99
+ | `unauthenticatedStatus` | `401` | Status when no identity arrives. |
100
+ | `forbiddenStatus` | `403` | Status when the role is missing. |
101
+ | `message` | per code | Fixed string or a function for the error message. |
102
+ | `parseRoles` | comma-separated | Custom splitting of the roles header. |
103
+
104
+ Fastify adapter additions: `protect`, `hook`, `resolveUser`, `principalKey`,
105
+ `meRoute`, `meResponse`, `errorBody`, `onAuthFailure`, `warnOnDevAuth`.
106
+
107
+ ## Headers
108
+
109
+ | Header | Meaning |
110
+ | --- | --- |
111
+ | `x-relyper-subject` | Stable user ID at the IdP |
112
+ | `x-relyper-email` | E-mail address |
113
+ | `x-relyper-name` | Display name |
114
+ | `x-relyper-roles` | Comma-separated roles |
115
+
116
+ Fallbacks when `acceptForwardedHeaders` is on: `x-forwarded-user`,
117
+ `x-forwarded-email`, `x-forwarded-preferred-username`, `x-forwarded-groups`.
118
+
119
+ ## Security model — read this
120
+
121
+ This package trusts headers. It does **not** verify a token or a signature. That
122
+ is only safe when your service is unreachable except through a gateway that
123
+ strips client-supplied `x-relyper-*` headers and sets them itself.
124
+
125
+ If your service can be reached directly, anyone can send
126
+ `x-relyper-roles: my_service_user` and be admitted. Two rules follow:
127
+
128
+ - Never expose the service port publicly without the gateway in front of it.
129
+ - Never enable `devAuth` in production. It is off by default, and the Fastify
130
+ adapter logs a warning the first time it is used.
131
+
132
+ Token verification against the Relyper IdP (JWT/JWKS) is planned for a later
133
+ version behind the same API, so switching should not require changes in calling
134
+ code.
135
+
136
+ ## Design
137
+
138
+ - `authenticate` is pure: headers in, result out. No database, no fetch, no throw.
139
+ - Identity and application user stay separate. The package hands you a
140
+ `RelyperIdentity`; `resolveUser` maps it to your own record. That boundary is
141
+ what makes the package reusable across service providers.
142
+ - `subject` is the IdP ID and never the primary key of your database.
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,38 @@
1
+ import type { RelyperIdentity } from './types.js';
2
+ /**
3
+ * Framework-free browser client for a service provider's /me route.
4
+ * Deliberately without a Vue/React dependency: a composable or hook wrapping
5
+ * it is a ten-liner in the application.
6
+ */
7
+ export type RelyperSession<TUser = RelyperIdentity> = {
8
+ status: 'authenticated';
9
+ user: TUser;
10
+ }
11
+ /** No identity passed through the gateway. Typically: login required. */
12
+ | {
13
+ status: 'unauthenticated';
14
+ response: Response;
15
+ }
16
+ /** Signed in, but without the role required for this service provider. */
17
+ | {
18
+ status: 'forbidden';
19
+ response: Response;
20
+ } | {
21
+ status: 'error';
22
+ response: Response;
23
+ };
24
+ export type FetchSessionOptions = {
25
+ /** Default: '/api/me'. */
26
+ path?: string;
27
+ /** Custom fetch implementation, e.g. for tests or SSR. */
28
+ fetch?: typeof globalThis.fetch;
29
+ headers?: Record<string, string>;
30
+ signal?: AbortSignal;
31
+ /** Default: 'same-origin'. */
32
+ credentials?: RequestCredentials;
33
+ };
34
+ export declare function fetchRelyperSession<TUser = RelyperIdentity>(options?: FetchSessionOptions): Promise<RelyperSession<TUser>>;
35
+ /** true if the identity has at least one of the given roles. */
36
+ export declare function hasAnyRole(identity: Pick<RelyperIdentity, 'roles'>, roles: string[]): boolean;
37
+ export type { RelyperIdentity };
38
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;GAIG;AAEH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,eAAe,IAC9C;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE;AAC1C,yEAAyE;GACvE;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE;AACnD,0EAA0E;GACxE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAC3C;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC,CAAC;AAEF,wBAAsB,mBAAmB,CAAC,KAAK,GAAG,eAAe,EAC/D,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAkBhC;AAED,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAE7F;AAED,YAAY,EAAE,eAAe,EAAE,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,25 @@
1
+ export async function fetchRelyperSession(options = {}) {
2
+ const doFetch = options.fetch ?? globalThis.fetch;
3
+ if (!doFetch)
4
+ throw new Error('@relyper/sp-auth/client: no fetch implementation available.');
5
+ const response = await doFetch(options.path ?? '/api/me', {
6
+ method: 'GET',
7
+ credentials: options.credentials ?? 'same-origin',
8
+ headers: { accept: 'application/json', ...(options.headers ?? {}) },
9
+ signal: options.signal
10
+ });
11
+ if (response.status === 401)
12
+ return { status: 'unauthenticated', response };
13
+ if (response.status === 403)
14
+ return { status: 'forbidden', response };
15
+ if (!response.ok)
16
+ return { status: 'error', response };
17
+ const body = (await response.json());
18
+ const user = body.user ?? body;
19
+ return { status: 'authenticated', user };
20
+ }
21
+ /** true if the identity has at least one of the given roles. */
22
+ export function hasAnyRole(identity, roles) {
23
+ return roles.some((role) => identity.roles.includes(role));
24
+ }
25
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AA2BA,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAO,GAAwB,EAAE;IAEjC,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IAClD,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IAE7F,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS,EAAE;QACxD,MAAM,EAAE,KAAK;QACb,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,aAAa;QACjD,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE;QACnE,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC;IAC5E,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;IACtE,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAEvD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA6B,CAAC;IACjE,MAAM,IAAI,GAAI,IAAyB,CAAC,IAAI,IAAK,IAAc,CAAC;IAChE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC;AAC3C,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,UAAU,CAAC,QAAwC,EAAE,KAAe;IAClF,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;AAC7D,CAAC"}
package/dist/core.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { RelyperAuthOptions, RelyperAuthResult, RelyperHeaderNames, RelyperHeaderSource, RelyperIdentity, ResolvedRelyperAuthOptions } from './types.js';
2
+ export declare const DEFAULT_HEADER_NAMES: RelyperHeaderNames;
3
+ /** Common headers of a generic auth proxy, only active on explicit request. */
4
+ export declare const FORWARDED_HEADER_NAMES: RelyperHeaderNames;
5
+ export declare function parseRoleList(raw: string): string[];
6
+ export declare function readHeader(source: RelyperHeaderSource, name: string): string;
7
+ export type RelyperAuth = {
8
+ readonly options: ResolvedRelyperAuthOptions;
9
+ /** Checks headers and returns either an identity or a failure with an HTTP status. */
10
+ authenticate(headers: RelyperHeaderSource): RelyperAuthResult;
11
+ /** Role check for additional gates inside the application. */
12
+ hasRole(identity: RelyperIdentity, role: string | string[], match?: 'any' | 'all'): boolean;
13
+ };
14
+ export declare function createRelyperAuth(options?: RelyperAuthOptions): RelyperAuth;
15
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,0BAA0B,EAC3B,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,oBAAoB,EAAE,kBAKlC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,EAAE,kBAKpC,CAAC;AAQF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAKnD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAS5E;AAkCD,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,OAAO,EAAE,0BAA0B,CAAC;IAC7C,sFAAsF;IACtF,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,iBAAiB,CAAC;IAC9D,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC;CAC7F,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAgD/E"}
package/dist/core.js ADDED
@@ -0,0 +1,123 @@
1
+ export const DEFAULT_HEADER_NAMES = {
2
+ subject: 'x-relyper-subject',
3
+ email: 'x-relyper-email',
4
+ displayName: 'x-relyper-name',
5
+ roles: 'x-relyper-roles'
6
+ };
7
+ /** Common headers of a generic auth proxy, only active on explicit request. */
8
+ export const FORWARDED_HEADER_NAMES = {
9
+ subject: 'x-forwarded-user',
10
+ email: 'x-forwarded-email',
11
+ displayName: 'x-forwarded-preferred-username',
12
+ roles: 'x-forwarded-groups'
13
+ };
14
+ const DEV_AUTH_DEFAULTS = {
15
+ subject: 'dev-user',
16
+ email: 'dev@relyper.local',
17
+ displayName: 'Relyper Dev User'
18
+ };
19
+ export function parseRoleList(raw) {
20
+ return raw
21
+ .split(',')
22
+ .map((role) => role.trim())
23
+ .filter(Boolean);
24
+ }
25
+ export function readHeader(source, name) {
26
+ if (typeof source.get === 'function') {
27
+ const value = source.get(name);
28
+ return typeof value === 'string' ? value.trim() : '';
29
+ }
30
+ const record = source;
31
+ const value = record[name] ?? record[name.toLowerCase()];
32
+ if (Array.isArray(value))
33
+ return (value[0] ?? '').trim();
34
+ return typeof value === 'string' ? value.trim() : '';
35
+ }
36
+ function resolveOptions(options) {
37
+ const requiredRoles = options.requiredRole === undefined
38
+ ? []
39
+ : (Array.isArray(options.requiredRole) ? options.requiredRole : [options.requiredRole]).filter(Boolean);
40
+ const forwarded = options.acceptForwardedHeaders;
41
+ const forwardedHeaderNames = forwarded
42
+ ? { ...FORWARDED_HEADER_NAMES, ...(typeof forwarded === 'object' ? forwarded : {}) }
43
+ : null;
44
+ const devAuthOption = options.devAuth;
45
+ const devAuthEnabled = Boolean(devAuthOption) && devAuthOption.enabled !== false;
46
+ return {
47
+ requiredRoles,
48
+ roleMatch: options.roleMatch ?? 'any',
49
+ requireEmail: options.requireEmail ?? true,
50
+ headerNames: { ...DEFAULT_HEADER_NAMES, ...(options.headerNames ?? {}) },
51
+ forwardedHeaderNames,
52
+ devAuth: devAuthEnabled
53
+ ? {
54
+ subject: devAuthOption.subject ?? DEV_AUTH_DEFAULTS.subject,
55
+ email: devAuthOption.email ?? DEV_AUTH_DEFAULTS.email,
56
+ displayName: devAuthOption.displayName ?? DEV_AUTH_DEFAULTS.displayName,
57
+ roles: devAuthOption.roles ?? requiredRoles
58
+ }
59
+ : null,
60
+ unauthenticatedStatus: options.unauthenticatedStatus ?? 401,
61
+ forbiddenStatus: options.forbiddenStatus ?? 403
62
+ };
63
+ }
64
+ export function createRelyperAuth(options = {}) {
65
+ const resolved = resolveOptions(options);
66
+ const parseRoles = options.parseRoles ?? parseRoleList;
67
+ function fail(code, status, presentedRoles) {
68
+ const base = { ok: false, status, code, presentedRoles };
69
+ const message = typeof options.message === 'function'
70
+ ? options.message(base)
71
+ : options.message ?? defaultMessage(code);
72
+ return { ...base, message };
73
+ }
74
+ return {
75
+ options: resolved,
76
+ authenticate(headers) {
77
+ const pick = (key) => {
78
+ const primary = readHeader(headers, resolved.headerNames[key]);
79
+ if (primary)
80
+ return primary;
81
+ if (!resolved.forwardedHeaderNames)
82
+ return '';
83
+ return readHeader(headers, resolved.forwardedHeaderNames[key]);
84
+ };
85
+ const presentedRoles = parseRoles(pick('roles'));
86
+ const dev = resolved.devAuth;
87
+ const subject = pick('subject') || (dev ? dev.subject : '');
88
+ const email = pick('email') || (dev ? dev.email : '');
89
+ const roles = presentedRoles.length ? presentedRoles : dev ? [...dev.roles] : [];
90
+ const displayName = pick('displayName') || email || (dev ? dev.displayName : '') || 'Relyper User';
91
+ if (!subject)
92
+ return fail('missing_subject', resolved.unauthenticatedStatus, presentedRoles);
93
+ if (resolved.requireEmail && !email)
94
+ return fail('missing_email', resolved.unauthenticatedStatus, presentedRoles);
95
+ if (resolved.requiredRoles.length && !matchesRoles(roles, resolved.requiredRoles, resolved.roleMatch)) {
96
+ return fail('missing_role', resolved.forbiddenStatus, presentedRoles);
97
+ }
98
+ const identity = { subject, email, displayName, roles };
99
+ const viaDevAuth = Boolean(dev) && !readHeader(headers, resolved.headerNames.subject);
100
+ return { ok: true, identity, viaDevAuth };
101
+ },
102
+ hasRole(identity, role, match = 'any') {
103
+ const wanted = Array.isArray(role) ? role : [role];
104
+ return matchesRoles(identity.roles, wanted, match);
105
+ }
106
+ };
107
+ }
108
+ function matchesRoles(actual, required, match) {
109
+ return match === 'all'
110
+ ? required.every((role) => actual.includes(role))
111
+ : required.some((role) => actual.includes(role));
112
+ }
113
+ function defaultMessage(code) {
114
+ switch (code) {
115
+ case 'missing_subject':
116
+ return 'Authentication required.';
117
+ case 'missing_email':
118
+ return 'Authentication required: the identity provider did not supply an e-mail address.';
119
+ case 'missing_role':
120
+ return 'Access requires an authenticated user with the required role.';
121
+ }
122
+ }
123
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import { type RelyperAuth } from './core.js';
3
+ import type { RelyperAuthFailure, RelyperAuthOptions, RelyperIdentity } from './types.js';
4
+ declare module 'fastify' {
5
+ interface FastifyRequest {
6
+ /** Identity from the Relyper IdP. Set by the plugin once the request is protected. */
7
+ relyperIdentity: RelyperIdentity;
8
+ }
9
+ interface FastifyInstance {
10
+ relyperAuth: RelyperAuth;
11
+ }
12
+ }
13
+ export type RelyperFastifyOptions = RelyperAuthOptions & {
14
+ /**
15
+ * Decides which requests are protected. Default: all.
16
+ * Typical: `(request) => request.url.startsWith('/api/')`.
17
+ */
18
+ protect?: (request: FastifyRequest) => boolean;
19
+ /** Hook the check runs in. Default: onRequest, i.e. before the body is parsed. */
20
+ hook?: 'onRequest' | 'preHandler';
21
+ /**
22
+ * Translates the IdP identity into the application's own user object,
23
+ * typically an upsert into its own database.
24
+ * Without this hook, the IdP identity lands on the request unchanged.
25
+ */
26
+ resolveUser?: (identity: RelyperIdentity, request: FastifyRequest) => unknown | Promise<unknown>;
27
+ /** Property on the request that holds the result. Default: 'principal'. */
28
+ principalKey?: string;
29
+ /** Path for a /me route. Default: false, the plugin does not register a route on its own. */
30
+ meRoute?: string | false;
31
+ /** Response of the /me route. Default: `{ user: request[principalKey] }`. */
32
+ meResponse?: (request: FastifyRequest) => unknown;
33
+ /** Error body. Default: `{ error: failure.message }`. */
34
+ errorBody?: (failure: RelyperAuthFailure, request: FastifyRequest) => unknown;
35
+ /** Hook for audit-logging failed access attempts. */
36
+ onAuthFailure?: (failure: RelyperAuthFailure, request: FastifyRequest) => void | Promise<void>;
37
+ /** Warns once in the log when dev auth kicks in. Default: true. */
38
+ warnOnDevAuth?: boolean;
39
+ };
40
+ declare function plugin(app: FastifyInstance, options: RelyperFastifyOptions): Promise<void>;
41
+ export declare const relyperAuth: typeof plugin;
42
+ export default relyperAuth;
43
+ //# sourceMappingURL=fastify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../src/fastify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAgB,cAAc,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1F,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,sFAAsF;QACtF,eAAe,EAAE,eAAe,CAAC;KAClC;IACD,UAAU,eAAe;QACvB,WAAW,EAAE,WAAW,CAAC;KAC1B;CACF;AAED,MAAM,MAAM,qBAAqB,GAAG,kBAAkB,GAAG;IACvD;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC;IAC/C,kFAAkF;IAClF,IAAI,CAAC,EAAE,WAAW,GAAG,YAAY,CAAC;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjG,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACzB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC;IAClD,yDAAyD;IACzD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC;IAC9E,qDAAqD;IACrD,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/F,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,iBAAe,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDzF;AAED,eAAO,MAAM,WAAW,eAGtB,CAAC;eAEY,WAAW"}
@@ -0,0 +1,51 @@
1
+ import fp from 'fastify-plugin';
2
+ import { createRelyperAuth } from './core.js';
3
+ async function plugin(app, options) {
4
+ const auth = createRelyperAuth(options);
5
+ const principalKey = options.principalKey ?? 'principal';
6
+ const hook = options.hook ?? 'onRequest';
7
+ const protect = options.protect ?? (() => true);
8
+ const errorBody = options.errorBody ?? ((failure) => ({ error: failure.message }));
9
+ const warnOnDevAuth = options.warnOnDevAuth ?? true;
10
+ let devAuthWarned = false;
11
+ if (!app.hasRequestDecorator('relyperIdentity')) {
12
+ // Fastify recommends reserving the slot on the request upfront. The value stays
13
+ // empty until the hook runs; the declared type is deliberately non-nullable
14
+ // because a protected route always has an identity by the time it runs.
15
+ app.decorateRequest('relyperIdentity', null);
16
+ }
17
+ if (!app.hasRequestDecorator(principalKey)) {
18
+ app.decorateRequest(principalKey, null);
19
+ }
20
+ if (!app.hasDecorator('relyperAuth')) {
21
+ app.decorate('relyperAuth', auth);
22
+ }
23
+ app.addHook(hook, async (request, reply) => {
24
+ if (!protect(request))
25
+ return;
26
+ const result = auth.authenticate(request.headers);
27
+ if (!result.ok) {
28
+ if (options.onAuthFailure)
29
+ await options.onAuthFailure(result, request);
30
+ return reply.code(result.status).send(errorBody(result, request));
31
+ }
32
+ if (result.viaDevAuth && warnOnDevAuth && !devAuthWarned) {
33
+ devAuthWarned = true;
34
+ app.log.warn('@relyper/sp-auth: dev auth is active, requests without gateway headers are treated as an authenticated user. Never enable this in production.');
35
+ }
36
+ request.relyperIdentity = result.identity;
37
+ const principal = options.resolveUser ? await options.resolveUser(result.identity, request) : result.identity;
38
+ request[principalKey] = principal;
39
+ });
40
+ if (options.meRoute) {
41
+ const meResponse = options.meResponse
42
+ ?? ((request) => ({ user: request[principalKey] }));
43
+ app.get(options.meRoute, async (request) => meResponse(request));
44
+ }
45
+ }
46
+ export const relyperAuth = fp(plugin, {
47
+ name: '@relyper/sp-auth',
48
+ fastify: '5.x'
49
+ });
50
+ export default relyperAuth;
51
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.js","sourceRoot":"","sources":["../src/fastify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { createRelyperAuth, parseRoleList, readHeader, DEFAULT_HEADER_NAMES, FORWARDED_HEADER_NAMES, type RelyperAuth } from './core.js';
2
+ export type { RelyperAuthFailure, RelyperAuthFailureCode, RelyperAuthOptions, RelyperAuthResult, RelyperAuthSuccess, RelyperDevAuthOptions, RelyperHeaderNames, RelyperHeaderSource, RelyperIdentity, ResolvedRelyperAuthOptions } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,WAAW,EACjB,MAAM,WAAW,CAAC;AAEnB,YAAY,EACV,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,0BAA0B,EAC3B,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createRelyperAuth, parseRoleList, readHeader, DEFAULT_HEADER_NAMES, FORWARDED_HEADER_NAMES } from './core.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,oBAAoB,EACpB,sBAAsB,EAEvB,MAAM,WAAW,CAAC"}
@@ -0,0 +1,88 @@
1
+ /** Identity that the Relyper IdP passes to a service provider via the gateway. */
2
+ export type RelyperIdentity = {
3
+ /** Stable, unique identifier of the user at the IdP. Never the local database ID. */
4
+ subject: string;
5
+ email: string;
6
+ displayName: string;
7
+ roles: string[];
8
+ };
9
+ export type RelyperAuthFailureCode = 'missing_subject' | 'missing_email' | 'missing_role';
10
+ export type RelyperAuthFailure = {
11
+ ok: false;
12
+ /** HTTP status the adapter should send. */
13
+ status: number;
14
+ code: RelyperAuthFailureCode;
15
+ message: string;
16
+ /**
17
+ * Roles the gateway sent along. Intended solely for logging and
18
+ * debugging, never as a basis for authorization.
19
+ */
20
+ presentedRoles: string[];
21
+ };
22
+ export type RelyperAuthSuccess = {
23
+ ok: true;
24
+ identity: RelyperIdentity;
25
+ /** true if the identity came from dev auth rather than the gateway. */
26
+ viaDevAuth: boolean;
27
+ };
28
+ export type RelyperAuthResult = RelyperAuthSuccess | RelyperAuthFailure;
29
+ export type RelyperHeaderNames = {
30
+ subject: string;
31
+ email: string;
32
+ displayName: string;
33
+ roles: string;
34
+ };
35
+ /**
36
+ * Header source. Supports both the plain object from Node/Fastify and
37
+ * anything with a `get` method, such as the Fetch API's Headers.
38
+ */
39
+ export type RelyperHeaderSource = Record<string, string | string[] | undefined> | {
40
+ get(name: string): string | null | undefined;
41
+ };
42
+ export type RelyperDevAuthOptions = {
43
+ /** Default: true, as soon as an object is passed. */
44
+ enabled?: boolean;
45
+ subject?: string;
46
+ email?: string;
47
+ displayName?: string;
48
+ roles?: string[];
49
+ };
50
+ export type RelyperAuthOptions = {
51
+ /** Role(s) the user needs for this service provider. Empty means: no role check. */
52
+ requiredRole?: string | string[];
53
+ /** With multiple required roles: one is enough ('any', default) or all are needed ('all'). */
54
+ roleMatch?: 'any' | 'all';
55
+ /** Default: true. Set to false if the IdP does not supply an email address. */
56
+ requireEmail?: boolean;
57
+ /** Custom header names, e.g. for a different gateway prefix. */
58
+ headerNames?: Partial<RelyperHeaderNames>;
59
+ /**
60
+ * Accept `x-forwarded-*` as a fallback. Default: false.
61
+ * Deliberately off because these headers can come from generic proxies.
62
+ */
63
+ acceptForwardedHeaders?: boolean | Partial<RelyperHeaderNames>;
64
+ /**
65
+ * Development login without a gateway. Default: off.
66
+ * Must stay off in production, otherwise any caller authenticates itself.
67
+ */
68
+ devAuth?: RelyperDevAuthOptions | false;
69
+ /** Status when no identity arrives at all. Default: 401. */
70
+ unauthenticatedStatus?: number;
71
+ /** Status when the identity is valid but the role is missing. Default: 403. */
72
+ forbiddenStatus?: number;
73
+ /** Fixed text or function for the error message. */
74
+ message?: string | ((failure: Omit<RelyperAuthFailure, 'message'>) => string);
75
+ /** Custom parsing of the roles header, in case the gateway does not use a comma. */
76
+ parseRoles?: (raw: string) => string[];
77
+ };
78
+ export type ResolvedRelyperAuthOptions = {
79
+ requiredRoles: string[];
80
+ roleMatch: 'any' | 'all';
81
+ requireEmail: boolean;
82
+ headerNames: RelyperHeaderNames;
83
+ forwardedHeaderNames: RelyperHeaderNames | null;
84
+ devAuth: Required<Omit<RelyperDevAuthOptions, 'enabled'>> | null;
85
+ unauthenticatedStatus: number;
86
+ forbiddenStatus: number;
87
+ };
88
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@relyper/sp-auth",
3
+ "version": "0.1.0",
4
+ "description": "Service-provider side of Relyper identity: principal resolution from gateway headers, role gating, a Fastify adapter, and a framework-free browser client.",
5
+ "license": "MIT",
6
+ "author": "Jonas Esser",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "keywords": [
13
+ "relyper",
14
+ "identity",
15
+ "authentication",
16
+ "authorization",
17
+ "service-provider",
18
+ "fastify",
19
+ "rbac"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/jonasesser/relyper-sp-auth.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/jonasesser/relyper-sp-auth/issues"
27
+ },
28
+ "homepage": "https://github.com/jonasesser/relyper-sp-auth#readme",
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ },
36
+ "./fastify": {
37
+ "types": "./dist/fastify.d.ts",
38
+ "import": "./dist/fastify.js"
39
+ },
40
+ "./client": {
41
+ "types": "./dist/client.d.ts",
42
+ "import": "./dist/client.js"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsc -p tsconfig.build.json",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "prepublishOnly": "npm run build"
56
+ },
57
+ "dependencies": {
58
+ "fastify-plugin": "^5.1.0"
59
+ },
60
+ "peerDependencies": {
61
+ "fastify": ">=5"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "fastify": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^26.3.0",
70
+ "fastify": "^5.12.1",
71
+ "typescript": "^7.0.2",
72
+ "vitest": "^4.1.11"
73
+ },
74
+ "publishConfig": {
75
+ "access": "public"
76
+ }
77
+ }