@hmp-global/payload-cas-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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/dist/handlers/admin-session.d.ts +18 -0
  4. package/dist/handlers/admin-session.d.ts.map +1 -0
  5. package/dist/handlers/admin-session.js +111 -0
  6. package/dist/handlers/admin-session.js.map +1 -0
  7. package/dist/handlers/callback.d.ts +9 -0
  8. package/dist/handlers/callback.d.ts.map +1 -0
  9. package/dist/handlers/callback.js +63 -0
  10. package/dist/handlers/callback.js.map +1 -0
  11. package/dist/handlers/login.d.ts +4 -0
  12. package/dist/handlers/login.d.ts.map +1 -0
  13. package/dist/handlers/login.js +13 -0
  14. package/dist/handlers/login.js.map +1 -0
  15. package/dist/handlers/logout.d.ts +8 -0
  16. package/dist/handlers/logout.d.ts.map +1 -0
  17. package/dist/handlers/logout.js +21 -0
  18. package/dist/handlers/logout.js.map +1 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +9 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/middleware.d.ts +22 -0
  24. package/dist/middleware.d.ts.map +1 -0
  25. package/dist/middleware.js +80 -0
  26. package/dist/middleware.js.map +1 -0
  27. package/dist/plugin.d.ts +30 -0
  28. package/dist/plugin.d.ts.map +1 -0
  29. package/dist/plugin.js +65 -0
  30. package/dist/plugin.js.map +1 -0
  31. package/dist/roles.d.ts +11 -0
  32. package/dist/roles.d.ts.map +1 -0
  33. package/dist/roles.js +42 -0
  34. package/dist/roles.js.map +1 -0
  35. package/dist/session.d.ts +6 -0
  36. package/dist/session.d.ts.map +1 -0
  37. package/dist/session.js +38 -0
  38. package/dist/session.js.map +1 -0
  39. package/dist/types.d.ts +107 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +7 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/xml-parser.d.ts +4 -0
  44. package/dist/xml-parser.d.ts.map +1 -0
  45. package/dist/xml-parser.js +17 -0
  46. package/dist/xml-parser.js.map +1 -0
  47. package/package.json +55 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 HMP Global
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,166 @@
1
+ # `@hmp-global/payload-cas-auth`
2
+
3
+ CAS SSO authentication plugin for **Payload CMS v3** + **Next.js 16**.
4
+
5
+ - Registers CAS login/callback/logout as Payload API endpoints (no per-app route files needed)
6
+ - JWT session management (signed with your own secret)
7
+ - Role-based access control derived from CAS attributes
8
+ - Seamless Payload admin panel bridging — users go straight to `/admin` after CAS login
9
+ - Configurable role → capability mapping (`cdn`, `analytics`, `admin`)
10
+ - Dev bypass mode with injected role for local development
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @hmp-global/payload-cas-auth
18
+ # or
19
+ pnpm add @hmp-global/payload-cas-auth
20
+ ```
21
+
22
+ Peer dependencies (install if not already present): `next >= 16`, `payload >= 3`, `jose >= 5`
23
+
24
+ ---
25
+
26
+ ## Publishing a new version
27
+
28
+ 1. Bump the `version` in `package.json`
29
+ 2. Add an `NPM_TOKEN` secret to the GitHub repo (create one at npmjs.com → Access Tokens → Automation token)
30
+ 3. Create a **GitHub Release** — the `publish.yml` workflow builds and publishes to npm automatically
31
+
32
+ ---
33
+
34
+ ## Setup — two files
35
+
36
+ ### 1. `payload.config.ts`
37
+
38
+ ```ts
39
+ import { buildConfig } from 'payload'
40
+ import { casAuthPlugin } from '@hmp-global/payload-cas-auth'
41
+
42
+ export default buildConfig({
43
+ // ...
44
+ plugins: [
45
+ casAuthPlugin({
46
+ casBaseUrl: process.env.CAS_BASE_URL!,
47
+ sessionSecret: process.env.CAS_SESSION_SECRET!,
48
+ publicBaseUrl: process.env.PUBLIC_BASE_URL, // e.g. https://app.example.com
49
+ enabled: process.env.CAS_ENABLED !== 'false',
50
+ devRole: (process.env.DEV_ROLE ?? 'development') as AppRole,
51
+ }),
52
+ ],
53
+ })
54
+ ```
55
+
56
+ This registers four endpoints automatically:
57
+ | Method | Path | Purpose |
58
+ |--------|------|---------|
59
+ | GET | `/api/auth/cas/login` | Redirects to CAS server |
60
+ | GET | `/api/auth/cas/callback` | Validates ticket, sets session cookie |
61
+ | GET | `/api/auth/logout` | Clears session cookie |
62
+ | GET | `/api/auth/admin-session` | Bridges CAS session → Payload admin cookie |
63
+
64
+ ### 2. `src/middleware.ts`
65
+
66
+ ```ts
67
+ import { createCasMiddleware } from '@hmp-global/payload-cas-auth/middleware'
68
+ import type { AppRole } from '@hmp-global/payload-cas-auth'
69
+
70
+ export const { middleware, config } = createCasMiddleware({
71
+ casBaseUrl: process.env.CAS_BASE_URL!,
72
+ sessionSecret: process.env.CAS_SESSION_SECRET!,
73
+ publicBaseUrl: process.env.PUBLIC_BASE_URL,
74
+ enabled: process.env.CAS_ENABLED !== 'false',
75
+ devRole: (process.env.DEV_ROLE ?? 'development') as AppRole,
76
+
77
+ // Paths that require CAS login (default: ['/dashboard', '/chat', '/admin'])
78
+ protectedPaths: ['/dashboard', '/chat', '/admin'],
79
+
80
+ // Paths that require the 'cdn' capability — others are redirected to /dashboard
81
+ cdnRestrictedPaths: ['/dashboard/cdn'],
82
+ })
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Environment variables
88
+
89
+ | Variable | Required | Description |
90
+ |----------|----------|-------------|
91
+ | `CAS_BASE_URL` | ✅ | CAS server base URL, e.g. `https://login.example.com/cas` |
92
+ | `CAS_SESSION_SECRET` | ✅ | Secret for signing session JWTs (32+ random chars) |
93
+ | `PUBLIC_BASE_URL` | Recommended | Public hostname for CAS redirect URIs |
94
+ | `CAS_ENABLED` | — | Set to `false` to bypass CAS in dev. Default: `true` |
95
+ | `DEV_ROLE` | — | Role to inject when `CAS_ENABLED=false`. Default: `development` |
96
+
97
+ ---
98
+
99
+ ## Role configuration
100
+
101
+ The plugin reads a CAS attribute to determine each user's role:
102
+
103
+ ```ts
104
+ casAuthPlugin({
105
+ // ...
106
+ roleAttribute: 'department', // CAS attribute name (default: 'role')
107
+ adminGroups: ['admin', 'it-admin'],
108
+ devGroups: ['engineering', 'development'],
109
+ marketingGroups: ['marketing', 'content', 'seo'],
110
+ })
111
+ ```
112
+
113
+ ### Role → capability mapping
114
+
115
+ | Role | Can see CDN | Can see Analytics | Can access Admin |
116
+ |------|-------------|-------------------|-----------------|
117
+ | `admin` | ✅ | ✅ | ✅ |
118
+ | `development` | ✅ | ✅ | ✅ |
119
+ | `marketing` | ✗ | ✅ | ✗ (no Payload account) |
120
+ | `unknown` | ✗ | ✗ | ✗ |
121
+
122
+ Users with `unknown` role are authenticated by CAS but shown a "request access" message — no data is visible.
123
+
124
+ ---
125
+
126
+ ## Using the role in your app
127
+
128
+ The middleware injects the resolved role as an `x-user-role` request header:
129
+
130
+ ```ts
131
+ // In a server component or API route:
132
+ import { headers } from 'next/headers'
133
+ import { can, ROLE_HEADER } from '@hmp-global/payload-cas-auth'
134
+ import type { AppRole } from '@hmp-global/payload-cas-auth'
135
+
136
+ const reqHeaders = await headers()
137
+ const role = (reqHeaders.get(ROLE_HEADER) ?? 'unknown') as AppRole
138
+ const canSeeCdn = can(role, 'cdn')
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Admin panel auto-login
144
+
145
+ When a CAS-authenticated user with `admin` or `development` role visits `/admin`:
146
+
147
+ 1. Middleware sees the `_cas_admin` marker cookie is absent → routes to `/api/auth/admin-session`
148
+ 2. The bridge finds/creates their Payload user account by email
149
+ 3. Syncs a deterministic server-side password (`HMAC(email, sessionSecret)`)
150
+ 4. Calls `payload.login()` with that password → gets a verified Payload JWT
151
+ 5. Sets `payload-token` + `_cas_admin` cookies → redirects to `/admin`
152
+ 6. User lands directly in the admin panel — no separate login prompt
153
+
154
+ Existing Payload accounts are matched by email. If you already created an admin account via the Payload email/password flow, the bridge will find it and update its password to the deterministic value (the original password is replaced — Payload admin login is intentionally replaced by CAS).
155
+
156
+ ---
157
+
158
+ ## Finding your CAS attribute name
159
+
160
+ After first login with `CAS_ENABLED=true`, the server logs will print:
161
+
162
+ ```
163
+ [cas-auth] login user=jsmith email=jsmith@example.com role=unknown attrs={"department":"Engineering","uid":"jsmith"}
164
+ ```
165
+
166
+ If `role=unknown`, look at the `attrs` keys and set `roleAttribute` to the right key (e.g. `department`). Then add the values to `devGroups`/`marketingGroups` etc.
@@ -0,0 +1,18 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { CasAuthConfig } from '../types.js';
3
+ /**
4
+ * Returns a Payload-compatible endpoint handler for GET /api/auth/admin-session.
5
+ *
6
+ * Exchanges a valid CAS session cookie for a Payload admin cookie by:
7
+ * 1. Verifying the CAS session JWT.
8
+ * 2. Finding or creating the Payload user by email.
9
+ * 3. Syncing a deterministic server-side password for that user.
10
+ * 4. Calling payload.login() — uses Payload's own signing path (guaranteed valid token).
11
+ * 5. Setting both the Payload token cookie and a `_cas_admin` marker cookie.
12
+ * 6. Redirecting to the original admin path.
13
+ *
14
+ * The `_cas_admin` marker tells the middleware the bridge has already run, preventing
15
+ * infinite redirect loops while still handling stale/expired Payload tokens.
16
+ */
17
+ export declare function createAdminSessionHandler(config: CasAuthConfig): (req: PayloadRequest) => Promise<Response>;
18
+ //# sourceMappingURL=admin-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-session.d.ts","sourceRoot":"","sources":["../../src/handlers/admin-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAchD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,IAC/B,KAAK,cAAc,KAAG,OAAO,CAAC,QAAQ,CAAC,CA2FtE"}
@@ -0,0 +1,111 @@
1
+ import { verifySession, getCookieName } from '../session.js';
2
+ import crypto from 'crypto';
3
+ /**
4
+ * Derive a deterministic server-side password from an email address.
5
+ * Never exposed to users — only the server calls payload.login() with it.
6
+ * The password changes if `bridgeSecret` changes, invalidating all existing
7
+ * Payload sessions (intentional — rotate bridgeSecret to force re-auth).
8
+ */
9
+ function derivePassword(email, config) {
10
+ const secret = config.bridgeSecret ?? config.sessionSecret;
11
+ return crypto.createHmac('sha256', secret).update(email.toLowerCase()).digest('hex');
12
+ }
13
+ /**
14
+ * Returns a Payload-compatible endpoint handler for GET /api/auth/admin-session.
15
+ *
16
+ * Exchanges a valid CAS session cookie for a Payload admin cookie by:
17
+ * 1. Verifying the CAS session JWT.
18
+ * 2. Finding or creating the Payload user by email.
19
+ * 3. Syncing a deterministic server-side password for that user.
20
+ * 4. Calling payload.login() — uses Payload's own signing path (guaranteed valid token).
21
+ * 5. Setting both the Payload token cookie and a `_cas_admin` marker cookie.
22
+ * 6. Redirecting to the original admin path.
23
+ *
24
+ * The `_cas_admin` marker tells the middleware the bridge has already run, preventing
25
+ * infinite redirect loops while still handling stale/expired Payload tokens.
26
+ */
27
+ export function createAdminSessionHandler(config) {
28
+ return async function handler(req) {
29
+ const url = new URL(req.url ?? "http://localhost");
30
+ const base = config.publicBaseUrl || url.origin;
31
+ const returnTo = url.searchParams.get('returnTo') ?? '/admin';
32
+ const loginUrl = new URL('/api/auth/cas/login', base).toString();
33
+ // 1. Verify CAS session
34
+ const cookieName = getCookieName(config);
35
+ const cookies = Object.fromEntries((req.headers.get('cookie') ?? '').split(';').map((c) => {
36
+ const [k, ...rest] = c.trim().split('=');
37
+ return [k.trim(), rest.join('=')];
38
+ }));
39
+ const sessionToken = cookies[cookieName];
40
+ if (!sessionToken)
41
+ return Response.redirect(loginUrl, 302);
42
+ const sessionUser = await verifySession(sessionToken, config);
43
+ if (!sessionUser)
44
+ return Response.redirect(loginUrl, 302);
45
+ // 2. Get the Payload instance from the request (injected by Payload's endpoint system)
46
+ const payload = req.payload;
47
+ if (!payload) {
48
+ return new Response('Payload instance not available on request.', { status: 500 });
49
+ }
50
+ const collection = config.userCollection ?? 'users';
51
+ const cookiePrefix = config.payloadCookiePrefix ?? payload.config.cookiePrefix ?? 'payload';
52
+ const deterministicPwd = derivePassword(sessionUser.email, config);
53
+ // 3. Find or create Payload user
54
+ const existing = await payload.find({
55
+ collection,
56
+ where: { email: { equals: sessionUser.email } },
57
+ limit: 1,
58
+ depth: 0,
59
+ });
60
+ if (existing.docs.length > 0) {
61
+ await payload.update({
62
+ collection,
63
+ id: existing.docs[0].id,
64
+ data: { password: deterministicPwd },
65
+ overrideAccess: true,
66
+ });
67
+ }
68
+ else {
69
+ await payload.create({
70
+ collection,
71
+ data: { email: sessionUser.email, password: deterministicPwd },
72
+ });
73
+ }
74
+ // 4. Login via Payload's own operation to get a guaranteed-valid token
75
+ let loginToken;
76
+ try {
77
+ const result = await payload.login({
78
+ collection,
79
+ data: { email: sessionUser.email, password: deterministicPwd },
80
+ });
81
+ loginToken = result.token;
82
+ }
83
+ catch (err) {
84
+ console.error('[cas-auth] admin-session: payload.login failed:', err);
85
+ return new Response('Admin login failed — check server logs.', { status: 500 });
86
+ }
87
+ // 5. Build cookies and redirect
88
+ const secure = config.cookies?.secure ?? url.protocol === 'https:';
89
+ const maxAge = 7200; // 2 hours
90
+ const sameSite = 'Lax';
91
+ const payloadCookie = [
92
+ `${cookiePrefix}-token=${loginToken}`,
93
+ 'Path=/', 'HttpOnly', `Max-Age=${maxAge}`, `SameSite=${sameSite}`,
94
+ secure ? 'Secure' : '',
95
+ ].filter(Boolean).join('; ');
96
+ const markerCookie = [
97
+ `_cas_admin=1`,
98
+ 'Path=/', `Max-Age=${maxAge}`, `SameSite=${sameSite}`,
99
+ secure ? 'Secure' : '',
100
+ ].filter(Boolean).join('; ');
101
+ return new Response(null, {
102
+ status: 302,
103
+ headers: new Headers([
104
+ ['Location', new URL(returnTo, base).toString()],
105
+ ['Set-Cookie', payloadCookie],
106
+ ['Set-Cookie', markerCookie],
107
+ ]),
108
+ });
109
+ };
110
+ }
111
+ //# sourceMappingURL=admin-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-session.js","sourceRoot":"","sources":["../../src/handlers/admin-session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { CasAuthConfig } from '../types.js';
3
+ /**
4
+ * Returns a Payload-compatible endpoint handler for GET /api/auth/cas/callback.
5
+ * Validates the CAS ticket, resolves the user's role, signs a session JWT,
6
+ * sets the session cookie, and redirects to /dashboard.
7
+ */
8
+ export declare function createCallbackHandler(config: CasAuthConfig): (req: PayloadRequest) => Promise<Response>;
9
+ //# sourceMappingURL=callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../src/handlers/callback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAI7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,aAAa,IAC3B,KAAK,cAAc,KAAG,OAAO,CAAC,QAAQ,CAAC,CAgEtE"}
@@ -0,0 +1,63 @@
1
+ import { signSession, getCookieName, getCookieMaxAge } from '../session.js';
2
+ import { roleFromCasAttributes } from '../roles.js';
3
+ import { parseCasXml } from '../xml-parser.js';
4
+ /**
5
+ * Returns a Payload-compatible endpoint handler for GET /api/auth/cas/callback.
6
+ * Validates the CAS ticket, resolves the user's role, signs a session JWT,
7
+ * sets the session cookie, and redirects to /dashboard.
8
+ */
9
+ export function createCallbackHandler(config) {
10
+ return async function handler(req) {
11
+ const url = new URL(req.url ?? "http://localhost");
12
+ const base = config.publicBaseUrl || url.origin;
13
+ const ticket = url.searchParams.get('ticket');
14
+ if (!ticket) {
15
+ return new Response('Missing ticket parameter.', { status: 400 });
16
+ }
17
+ // Build the service URL — must exactly match what was sent to CAS in /login
18
+ const serviceUrl = new URL('/api/auth/cas/callback', base);
19
+ serviceUrl.searchParams.delete('ticket');
20
+ const validateUrl = `${config.casBaseUrl}/serviceValidate` +
21
+ `?service=${encodeURIComponent(serviceUrl.toString())}` +
22
+ `&ticket=${encodeURIComponent(ticket)}`;
23
+ let xml;
24
+ try {
25
+ const resp = await fetch(validateUrl);
26
+ xml = await resp.text();
27
+ }
28
+ catch (err) {
29
+ console.error('[cas-auth] serviceValidate fetch error:', err);
30
+ return new Response('Failed to reach CAS server.', { status: 502 });
31
+ }
32
+ const result = parseCasXml(xml);
33
+ if (!result.ok) {
34
+ console.error('[cas-auth] ticket validation failed:', result);
35
+ return new Response(`CAS error ${result.code}: ${result.message}`, { status: 401 });
36
+ }
37
+ const email = result.email ??
38
+ (result.user.includes('@') ? result.user : `${result.user}@${url.hostname}`);
39
+ const role = roleFromCasAttributes(result.attributes, config);
40
+ console.log(`[cas-auth] login user=${result.user} email=${email} role=${role}`);
41
+ const token = await signSession({ username: result.user, email, role }, config);
42
+ const cookieName = getCookieName(config);
43
+ const maxAge = getCookieMaxAge(config);
44
+ const secure = config.cookies?.secure ?? url.protocol === 'https:';
45
+ const sameSite = config.cookies?.sameSite ?? 'Lax';
46
+ const cookieStr = [
47
+ `${cookieName}=${token}`,
48
+ `Path=/`,
49
+ `HttpOnly`,
50
+ `Max-Age=${maxAge}`,
51
+ `SameSite=${sameSite}`,
52
+ secure ? 'Secure' : '',
53
+ ].filter(Boolean).join('; ');
54
+ return new Response(null, {
55
+ status: 302,
56
+ headers: {
57
+ Location: new URL('/dashboard', base).toString(),
58
+ 'Set-Cookie': cookieStr,
59
+ },
60
+ });
61
+ };
62
+ }
63
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../../src/handlers/callback.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAG9C;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAqB;IACzD,OAAO,KAAK,UAAU,OAAO,CAAC,GAAmB;QAC/C,MAAM,GAAG,GAAI,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,kBAAkB,CAAC,CAAA;QACnD,MAAM,IAAI,GAAG,MAAM,CAAC,aAAa,IAAI,GAAG,CAAC,MAAM,CAAA;QAE/C,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAC7C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,IAAI,QAAQ,CAAC,2BAA2B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACnE,CAAC;QAED,4EAA4E;QAC5E,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAA;QAC1D,UAAU,CAAC,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QAExC,MAAM,WAAW,GACf,GAAG,MAAM,CAAC,UAAU,kBAAkB;YACtC,YAAY,kBAAkB,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,EAAE;YACvD,WAAW,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAA;QAEzC,IAAI,GAAW,CAAA;QACf,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,CAAA;YACrC,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;QACzB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE,GAAG,CAAC,CAAA;YAC7D,OAAO,IAAI,QAAQ,CAAC,6BAA6B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACrE,CAAC;QAED,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;QAC/B,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,sCAAsC,EAAE,MAAM,CAAC,CAAA;YAC7D,OAAO,IAAI,QAAQ,CAAC,aAAa,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACrF,CAAC;QAED,MAAM,KAAK,GACT,MAAM,CAAC,KAAK;YACZ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAA;QAE9E,MAAM,IAAI,GAAG,qBAAqB,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;QAC7D,OAAO,CAAC,GAAG,CAAC,yBAAyB,MAAM,CAAC,IAAI,UAAU,KAAK,SAAS,IAAI,EAAE,CAAC,CAAA;QAE/E,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,MAAM,CAAC,CAAA;QAE/E,MAAM,UAAU,GAAI,aAAa,CAAC,MAAM,CAAC,CAAA;QACzC,MAAM,MAAM,GAAQ,eAAe,CAAC,MAAM,CAAC,CAAA;QAC3C,MAAM,MAAM,GAAQ,MAAM,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAA;QACvE,MAAM,QAAQ,GAAM,MAAM,CAAC,OAAO,EAAE,QAAQ,IAAI,KAAK,CAAA;QAErD,MAAM,SAAS,GAAG;YAChB,GAAG,UAAU,IAAI,KAAK,EAAE;YACxB,QAAQ;YACR,UAAU;YACV,WAAW,MAAM,EAAE;YACnB,YAAY,QAAQ,EAAE;YACtB,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;SACvB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAE5B,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;YACxB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE;gBACP,QAAQ,EAAI,IAAI,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,QAAQ,EAAE;gBAClD,YAAY,EAAE,SAAS;aACxB;SACF,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { CasAuthConfig } from '../types.js';
3
+ export declare function createLoginHandler(config: CasAuthConfig): (req: PayloadRequest) => Promise<Response>;
4
+ //# sourceMappingURL=login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/handlers/login.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAOhD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,IACxB,KAAK,cAAc,KAAG,OAAO,CAAC,QAAQ,CAAC,CAMtE"}
@@ -0,0 +1,13 @@
1
+ function buildLoginUrl(casBase, callbackUrl, template) {
2
+ const tpl = template ?? `${casBase}/login?service=__SERVICE__`;
3
+ return tpl.replace('__SERVICE__', encodeURIComponent(callbackUrl));
4
+ }
5
+ export function createLoginHandler(config) {
6
+ return async function handler(req) {
7
+ const url = new URL(req.url ?? "http://localhost");
8
+ const base = config.publicBaseUrl || url.origin;
9
+ const callback = new URL('/api/auth/cas/callback', base).toString();
10
+ return Response.redirect(buildLoginUrl(config.casBaseUrl, callback, config.loginUrlTemplate), 302);
11
+ };
12
+ }
13
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/handlers/login.ts"],"names":[],"mappings":"AAGA,SAAS,aAAa,CAAC,OAAe,EAAE,WAAmB,EAAE,QAAiB;IAC5E,MAAM,GAAG,GAAG,QAAQ,IAAI,GAAG,OAAO,4BAA4B,CAAA;IAC9D,OAAO,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,CAAA;AACpE,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,OAAO,KAAK,UAAU,OAAO,CAAC,GAAmB;QAC/C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,kBAAkB,CAAC,CAAA;QAClD,MAAM,IAAI,GAAG,MAAM,CAAC,aAAa,IAAI,GAAG,CAAC,MAAM,CAAA;QAC/C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAA;QACnE,OAAO,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,gBAAgB,CAAC,EAAE,GAAG,CAAC,CAAA;IACpG,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { CasAuthConfig } from '../types.js';
3
+ /**
4
+ * Returns a Payload-compatible endpoint handler for GET /api/auth/logout.
5
+ * Clears the session cookie and redirects to the home page.
6
+ */
7
+ export declare function createLogoutHandler(config: CasAuthConfig): (req: PayloadRequest) => Promise<Response>;
8
+ //# sourceMappingURL=logout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../src/handlers/logout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,IACzB,KAAK,cAAc,KAAG,OAAO,CAAC,QAAQ,CAAC,CAetE"}
@@ -0,0 +1,21 @@
1
+ import { getCookieName } from '../session.js';
2
+ /**
3
+ * Returns a Payload-compatible endpoint handler for GET /api/auth/logout.
4
+ * Clears the session cookie and redirects to the home page.
5
+ */
6
+ export function createLogoutHandler(config) {
7
+ return async function handler(req) {
8
+ const url = new URL(req.url ?? "http://localhost");
9
+ const base = config.publicBaseUrl || url.origin;
10
+ const cookieName = getCookieName(config);
11
+ const clearCookie = `${cookieName}=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`;
12
+ return new Response(null, {
13
+ status: 302,
14
+ headers: {
15
+ Location: new URL('/', base).toString(),
16
+ 'Set-Cookie': clearCookie,
17
+ },
18
+ });
19
+ };
20
+ }
21
+ //# sourceMappingURL=logout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout.js","sourceRoot":"","sources":["../../src/handlers/logout.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAG7C;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAqB;IACvD,OAAO,KAAK,UAAU,OAAO,CAAC,GAAmB;QAC/C,MAAM,GAAG,GAAI,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,kBAAkB,CAAC,CAAA;QACnD,MAAM,IAAI,GAAG,MAAM,CAAC,aAAa,IAAI,GAAG,CAAC,MAAM,CAAA;QAE/C,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;QACxC,MAAM,WAAW,GAAG,GAAG,UAAU,8CAA8C,CAAA;QAE/E,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;YACxB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE;gBACP,QAAQ,EAAM,IAAI,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,QAAQ,EAAE;gBAC3C,YAAY,EAAE,WAAW;aAC1B;SACF,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { casAuthPlugin } from './plugin.js';
2
+ export { createCasMiddleware } from './middleware.js';
3
+ export { verifySession, signSession, getCookieName } from './session.js';
4
+ export { can, getCapabilities, resolveRole, roleFromCasAttributes } from './roles.js';
5
+ export type { CasAuthConfig, AppRole, Capability, SessionUser, CasResult } from './types.js';
6
+ export { CAS_ADMIN_MARKER, ROLE_HEADER } from './types.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAqB,aAAa,CAAA;AAG1D,OAAO,EAAE,mBAAmB,EAAE,MAAe,iBAAiB,CAAA;AAG9D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACxE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAGrF,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC5F,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // Main plugin export
2
+ export { casAuthPlugin } from './plugin.js';
3
+ // Middleware factory (also available via the 'middleware' subpath export)
4
+ export { createCasMiddleware } from './middleware.js';
5
+ // Utilities for use in app code (API routes, server components)
6
+ export { verifySession, signSession, getCookieName } from './session.js';
7
+ export { can, getCapabilities, resolveRole, roleFromCasAttributes } from './roles.js';
8
+ export { CAS_ADMIN_MARKER, ROLE_HEADER } from './types.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,OAAO,EAAE,aAAa,EAAE,MAAqB,aAAa,CAAA;AAE1D,0EAA0E;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAe,iBAAiB,CAAA;AAE9D,gEAAgE;AAChE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACxE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAIrF,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,22 @@
1
+ import type { CasAuthConfig } from './types.js';
2
+ import type { NextRequest, NextResponse } from 'next/server';
3
+ type MiddlewareFn = (req: NextRequest) => Promise<NextResponse> | NextResponse;
4
+ interface MiddlewareOptions extends CasAuthConfig {
5
+ /**
6
+ * Paths that require CAS authentication. Default: `['/dashboard', '/chat', '/admin']`
7
+ */
8
+ protectedPaths?: string[];
9
+ /**
10
+ * Paths that require the `cdn` capability. Users without it are redirected to `/dashboard`.
11
+ * Default: `['/dashboard/cdn']`
12
+ */
13
+ cdnRestrictedPaths?: string[];
14
+ }
15
+ export declare function createCasMiddleware(options: MiddlewareOptions): {
16
+ middleware: MiddlewareFn;
17
+ config: {
18
+ matcher: string[];
19
+ };
20
+ };
21
+ export {};
22
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,aAAa,EAAW,MAAO,YAAY,CAAA;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE5D,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAA;AAE9E,UAAU,iBAAkB,SAAQ,aAAa;IAC/C;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IAEzB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC9B;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB,GAAG;IAC/D,UAAU,EAAE,YAAY,CAAA;IACxB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;CAC9B,CAkEA"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Next.js Edge middleware factory for CAS authentication.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * // middleware.ts
7
+ * import { createCasMiddleware } from '@nexus-core/payload-cas-auth/middleware'
8
+ *
9
+ * const { middleware, config } = createCasMiddleware({
10
+ * casBaseUrl: process.env.CAS_BASE_URL!,
11
+ * sessionSecret: process.env.CAS_SESSION_SECRET!,
12
+ * publicBaseUrl: process.env.PUBLIC_BASE_URL,
13
+ * enabled: process.env.CAS_ENABLED !== 'false',
14
+ * devRole: (process.env.DEV_ROLE ?? 'development') as AppRole,
15
+ * protectedPaths: ['/dashboard', '/chat', '/admin'],
16
+ * cdnRestrictedPaths: ['/dashboard/cdn'],
17
+ * })
18
+ *
19
+ * export { middleware, config }
20
+ * ```
21
+ */
22
+ import { verifySession, getCookieName } from './session.js';
23
+ import { can } from './roles.js';
24
+ import { CAS_ADMIN_MARKER, ROLE_HEADER } from './types.js';
25
+ export function createCasMiddleware(options) {
26
+ const protectedPaths = options.protectedPaths ?? ['/dashboard', '/chat', '/admin'];
27
+ const cdnRestrictedPaths = options.cdnRestrictedPaths ?? ['/dashboard/cdn'];
28
+ const loginPath = '/api/auth/cas/login';
29
+ const cookieName = getCookieName(options);
30
+ async function middleware(req) {
31
+ const { NextResponse } = await import('next/server');
32
+ const { pathname } = req.nextUrl;
33
+ const isProtected = protectedPaths.some((p) => pathname === p || pathname.startsWith(p + '/'));
34
+ const isCdnRoute = cdnRestrictedPaths.some((r) => pathname === r || pathname.startsWith(r + '/'));
35
+ const isAdminRoute = pathname === '/admin' || pathname.startsWith('/admin/');
36
+ // ── Dev bypass ──────────────────────────────────────────────────────────
37
+ if (options.enabled === false) {
38
+ const devRole = (options.devRole ?? 'development');
39
+ if (isCdnRoute && !can(devRole, 'cdn')) {
40
+ return NextResponse.redirect(new URL('/dashboard', req.url));
41
+ }
42
+ return NextResponse.next({
43
+ request: { headers: new Headers({ ...Object.fromEntries(req.headers), [ROLE_HEADER]: devRole }) },
44
+ });
45
+ }
46
+ if (!isProtected)
47
+ return NextResponse.next();
48
+ // ── CAS session check ───────────────────────────────────────────────────
49
+ const token = req.cookies.get(cookieName)?.value;
50
+ if (!token) {
51
+ return NextResponse.redirect(new URL(loginPath, req.url));
52
+ }
53
+ const user = await verifySession(token, options);
54
+ if (!user) {
55
+ return NextResponse.redirect(new URL(loginPath, req.url));
56
+ }
57
+ // ── CDN capability gate ─────────────────────────────────────────────────
58
+ if (isCdnRoute && !can(user.role, 'cdn')) {
59
+ return NextResponse.redirect(new URL('/dashboard', req.url));
60
+ }
61
+ // ── Admin bridge ────────────────────────────────────────────────────────
62
+ // Route through admin-session if the CAS-admin marker is absent.
63
+ // The bridge mints a Payload token via payload.login() and sets the marker.
64
+ if (isAdminRoute && !req.cookies.get(CAS_ADMIN_MARKER)?.value) {
65
+ const bridge = new URL('/api/auth/admin-session', req.url);
66
+ bridge.searchParams.set('returnTo', pathname + req.nextUrl.search);
67
+ return NextResponse.redirect(bridge);
68
+ }
69
+ // ── Forward role header ─────────────────────────────────────────────────
70
+ return NextResponse.next({
71
+ request: { headers: new Headers({ ...Object.fromEntries(req.headers), [ROLE_HEADER]: user.role }) },
72
+ });
73
+ }
74
+ // Build a matcher that covers all protected paths + their sub-paths
75
+ const matcher = [
76
+ ...new Set(protectedPaths.flatMap((p) => [p, `${p}/:path*`])),
77
+ ];
78
+ return { middleware, config: { matcher } };
79
+ }
80
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { Config } from 'payload';
2
+ import type { CasAuthConfig } from './types.js';
3
+ /**
4
+ * Payload CMS plugin that adds CAS SSO authentication.
5
+ *
6
+ * Registers four API endpoints on the Payload app:
7
+ * GET /api/auth/cas/login — redirects to CAS login page
8
+ * GET /api/auth/cas/callback — validates ticket, sets session cookie
9
+ * GET /api/auth/logout — clears session cookie
10
+ * GET /api/auth/admin-session — bridges CAS session → Payload admin cookie
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // payload.config.ts
15
+ * import { casAuthPlugin } from '@nexus-core/payload-cas-auth'
16
+ *
17
+ * export default buildConfig({
18
+ * plugins: [
19
+ * casAuthPlugin({
20
+ * casBaseUrl: process.env.CAS_BASE_URL!,
21
+ * sessionSecret: process.env.CAS_SESSION_SECRET!,
22
+ * publicBaseUrl: process.env.PUBLIC_BASE_URL,
23
+ * enabled: process.env.CAS_ENABLED !== 'false',
24
+ * }),
25
+ * ],
26
+ * })
27
+ * ```
28
+ */
29
+ export declare function casAuthPlugin(config: CasAuthConfig): (incomingConfig: Config) => Config;
30
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAKrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAa,YAAY,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,IAC1B,gBAAgB,MAAM,KAAG,MAAM,CAkCvD"}
package/dist/plugin.js ADDED
@@ -0,0 +1,65 @@
1
+ import { createLoginHandler } from './handlers/login.js';
2
+ import { createCallbackHandler } from './handlers/callback.js';
3
+ import { createLogoutHandler } from './handlers/logout.js';
4
+ import { createAdminSessionHandler } from './handlers/admin-session.js';
5
+ /**
6
+ * Payload CMS plugin that adds CAS SSO authentication.
7
+ *
8
+ * Registers four API endpoints on the Payload app:
9
+ * GET /api/auth/cas/login — redirects to CAS login page
10
+ * GET /api/auth/cas/callback — validates ticket, sets session cookie
11
+ * GET /api/auth/logout — clears session cookie
12
+ * GET /api/auth/admin-session — bridges CAS session → Payload admin cookie
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * // payload.config.ts
17
+ * import { casAuthPlugin } from '@nexus-core/payload-cas-auth'
18
+ *
19
+ * export default buildConfig({
20
+ * plugins: [
21
+ * casAuthPlugin({
22
+ * casBaseUrl: process.env.CAS_BASE_URL!,
23
+ * sessionSecret: process.env.CAS_SESSION_SECRET!,
24
+ * publicBaseUrl: process.env.PUBLIC_BASE_URL,
25
+ * enabled: process.env.CAS_ENABLED !== 'false',
26
+ * }),
27
+ * ],
28
+ * })
29
+ * ```
30
+ */
31
+ export function casAuthPlugin(config) {
32
+ return function plugin(incomingConfig) {
33
+ const loginHandler = createLoginHandler(config);
34
+ const callbackHandler = createCallbackHandler(config);
35
+ const logoutHandler = createLogoutHandler(config);
36
+ const adminSessionHandler = createAdminSessionHandler(config);
37
+ const newEndpoints = [
38
+ {
39
+ path: '/auth/cas/login',
40
+ method: 'get',
41
+ handler: loginHandler,
42
+ },
43
+ {
44
+ path: '/auth/cas/callback',
45
+ method: 'get',
46
+ handler: callbackHandler,
47
+ },
48
+ {
49
+ path: '/auth/logout',
50
+ method: 'get',
51
+ handler: logoutHandler,
52
+ },
53
+ {
54
+ path: '/auth/admin-session',
55
+ method: 'get',
56
+ handler: adminSessionHandler,
57
+ },
58
+ ];
59
+ return {
60
+ ...incomingConfig,
61
+ endpoints: [...(incomingConfig.endpoints ?? []), ...newEndpoints],
62
+ };
63
+ };
64
+ }
65
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAa,qBAAqB,CAAA;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAU,wBAAwB,CAAA;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAY,sBAAsB,CAAA;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAA;AAGvE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,aAAa,CAAC,MAAqB;IACjD,OAAO,SAAS,MAAM,CAAC,cAAsB;QAC3C,MAAM,YAAY,GAAU,kBAAkB,CAAC,MAAM,CAAC,CAAA;QACtD,MAAM,eAAe,GAAO,qBAAqB,CAAC,MAAM,CAAC,CAAA;QACzD,MAAM,aAAa,GAAS,mBAAmB,CAAC,MAAM,CAAC,CAAA;QACvD,MAAM,mBAAmB,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAA;QAE7D,MAAM,YAAY,GAAqC;YACrD;gBACE,IAAI,EAAK,iBAAiB;gBAC1B,MAAM,EAAG,KAAK;gBACd,OAAO,EAAE,YAAY;aACtB;YACD;gBACE,IAAI,EAAK,oBAAoB;gBAC7B,MAAM,EAAG,KAAK;gBACd,OAAO,EAAE,eAAe;aACzB;YACD;gBACE,IAAI,EAAK,cAAc;gBACvB,MAAM,EAAG,KAAK;gBACd,OAAO,EAAE,aAAa;aACvB;YACD;gBACE,IAAI,EAAK,qBAAqB;gBAC9B,MAAM,EAAG,KAAK;gBACd,OAAO,EAAE,mBAAmB;aAC7B;SACF,CAAA;QAED,OAAO;YACL,GAAG,cAAc;YACjB,SAAS,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,GAAG,YAAY,CAAC;SAClE,CAAA;IACH,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { AppRole, CasAuthConfig, Capability } from './types.js';
2
+ export declare function can(role: AppRole, capability: Capability): boolean;
3
+ export declare function getCapabilities(role: AppRole): Capability[];
4
+ /**
5
+ * Resolve an AppRole from a raw CAS attribute value.
6
+ * Supports multi-valued attributes separated by comma, pipe, semicolon, or space.
7
+ */
8
+ export declare function resolveRole(value: string | undefined, config: CasAuthConfig): AppRole;
9
+ /** Extract and resolve role from a CAS attributes map. */
10
+ export declare function roleFromCasAttributes(attrs: Record<string, string>, config: CasAuthConfig): AppRole;
11
+ //# sourceMappingURL=roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AASpE,wBAAgB,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAElE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,EAAE,CAE3D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAgBrF;AAED,0DAA0D;AAC1D,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAKnG"}
package/dist/roles.js ADDED
@@ -0,0 +1,42 @@
1
+ const ROLE_CAPABILITIES = {
2
+ admin: ['cdn', 'analytics', 'admin'],
3
+ development: ['cdn', 'analytics', 'admin'],
4
+ marketing: ['analytics'],
5
+ unknown: [],
6
+ };
7
+ export function can(role, capability) {
8
+ return ROLE_CAPABILITIES[role]?.includes(capability) ?? false;
9
+ }
10
+ export function getCapabilities(role) {
11
+ return ROLE_CAPABILITIES[role] ?? [];
12
+ }
13
+ /**
14
+ * Resolve an AppRole from a raw CAS attribute value.
15
+ * Supports multi-valued attributes separated by comma, pipe, semicolon, or space.
16
+ */
17
+ export function resolveRole(value, config) {
18
+ if (!value)
19
+ return 'unknown';
20
+ const v = value.toLowerCase().trim();
21
+ const vals = v.split(/[,|;\s]+/);
22
+ const adminGroups = (config.adminGroups ?? ['admin', 'administrator', 'administrators']).map((s) => s.toLowerCase());
23
+ const devGroups = (config.devGroups ?? ['development', 'dev', 'engineering', 'it', 'technology']).map((s) => s.toLowerCase());
24
+ const mktGroups = (config.marketingGroups ?? ['marketing', 'content', 'seo', 'analytics', 'communications']).map((s) => s.toLowerCase());
25
+ for (const v of vals) {
26
+ if (adminGroups.includes(v))
27
+ return 'admin';
28
+ if (devGroups.includes(v))
29
+ return 'development';
30
+ if (mktGroups.includes(v))
31
+ return 'marketing';
32
+ }
33
+ return 'unknown';
34
+ }
35
+ /** Extract and resolve role from a CAS attributes map. */
36
+ export function roleFromCasAttributes(attrs, config) {
37
+ const attrName = (config.roleAttribute ?? 'role').toLowerCase();
38
+ const val = attrs[attrName]
39
+ ?? Object.entries(attrs).find(([k]) => k.toLowerCase() === attrName)?.[1];
40
+ return resolveRole(val, config);
41
+ }
42
+ //# sourceMappingURL=roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.js","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAEA,MAAM,iBAAiB,GAAkC;IACvD,KAAK,EAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC;IAC1C,WAAW,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC;IAC1C,SAAS,EAAI,CAAC,WAAW,CAAC;IAC1B,OAAO,EAAM,EAAE;CAChB,CAAA;AAED,MAAM,UAAU,GAAG,CAAC,IAAa,EAAE,UAAsB;IACvD,OAAO,iBAAiB,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,IAAI,KAAK,CAAA;AAC/D,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAa;IAC3C,OAAO,iBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAyB,EAAE,MAAqB;IAC1E,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAE5B,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAA;IACpC,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IAEhC,MAAM,WAAW,GAAI,CAAC,MAAM,CAAC,WAAW,IAAK,CAAC,OAAO,EAAE,eAAe,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAA;IACtH,MAAM,SAAS,GAAM,CAAC,MAAM,CAAC,SAAS,IAAO,CAAC,aAAa,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAA;IACnI,MAAM,SAAS,GAAM,CAAC,MAAM,CAAC,eAAe,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAA;IAE3I,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAG,OAAO,OAAO,CAAA;QAC5C,IAAI,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAK,OAAO,aAAa,CAAA;QAClD,IAAI,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAK,OAAO,WAAW,CAAA;IAClD,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,qBAAqB,CAAC,KAA6B,EAAE,MAAqB;IACxF,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,CAAA;IAC/D,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC;WACtB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAC3E,OAAO,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;AACjC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { CasAuthConfig, SessionUser } from './types.js';
2
+ export declare function getCookieName(config: CasAuthConfig): string;
3
+ export declare function getCookieMaxAge(config: CasAuthConfig): number;
4
+ export declare function signSession(user: SessionUser, config: CasAuthConfig): Promise<string>;
5
+ export declare function verifySession(token: string, config: CasAuthConfig): Promise<SessionUser | null>;
6
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAS5D,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAE3D;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAE7D;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAW3F;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAWrG"}
@@ -0,0 +1,38 @@
1
+ import { SignJWT, jwtVerify } from 'jose';
2
+ const DEFAULT_COOKIE_NAME = 'hms-session';
3
+ const DEFAULT_MAX_AGE = 60 * 60 * 8; // 8 hours
4
+ function getSecret(config) {
5
+ return new TextEncoder().encode(config.sessionSecret || 'dev-fallback-secret-32-chars-min');
6
+ }
7
+ export function getCookieName(config) {
8
+ return config.cookies?.name ?? DEFAULT_COOKIE_NAME;
9
+ }
10
+ export function getCookieMaxAge(config) {
11
+ return config.cookies?.maxAgeSecs ?? DEFAULT_MAX_AGE;
12
+ }
13
+ export async function signSession(user, config) {
14
+ return new SignJWT({
15
+ username: user.username,
16
+ email: user.email,
17
+ role: user.role,
18
+ sub: user.username,
19
+ })
20
+ .setProtectedHeader({ alg: 'HS256' })
21
+ .setIssuedAt()
22
+ .setExpirationTime(`${getCookieMaxAge(config)}s`)
23
+ .sign(getSecret(config));
24
+ }
25
+ export async function verifySession(token, config) {
26
+ try {
27
+ const { payload } = await jwtVerify(token, getSecret(config));
28
+ return {
29
+ username: String(payload.username ?? payload.sub ?? ''),
30
+ email: String(payload.email ?? ''),
31
+ role: payload.role ?? 'unknown',
32
+ };
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAA;AAGzC,MAAM,mBAAmB,GAAG,aAAa,CAAA;AACzC,MAAM,eAAe,GAAO,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA,CAAG,UAAU;AAEpD,SAAS,SAAS,CAAC,MAAqB;IACtC,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,IAAI,kCAAkC,CAAC,CAAA;AAC7F,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAqB;IACjD,OAAO,MAAM,CAAC,OAAO,EAAE,IAAI,IAAI,mBAAmB,CAAA;AACpD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAqB;IACnD,OAAO,MAAM,CAAC,OAAO,EAAE,UAAU,IAAI,eAAe,CAAA;AACtD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAiB,EAAE,MAAqB;IACxE,OAAO,IAAI,OAAO,CAAC;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,KAAK,EAAK,IAAI,CAAC,KAAK;QACpB,IAAI,EAAM,IAAI,CAAC,IAAI;QACnB,GAAG,EAAO,IAAI,CAAC,QAAQ;KACxB,CAAC;SACC,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;SACpC,WAAW,EAAE;SACb,iBAAiB,CAAC,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC;SAChD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAa,EAAE,MAAqB;IACtE,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;QAC7D,OAAO;YACL,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;YACvD,KAAK,EAAK,MAAM,CAAC,OAAO,CAAC,KAAK,IAAO,EAAE,CAAC;YACxC,IAAI,EAAO,OAAO,CAAC,IAA4B,IAAI,SAAS;SAC7D,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC"}
@@ -0,0 +1,107 @@
1
+ /** Application roles derived from CAS attributes. */
2
+ export type AppRole = 'admin' | 'development' | 'marketing' | 'unknown';
3
+ /** Data capabilities gated by role. */
4
+ export type Capability = 'cdn' | 'analytics' | 'admin';
5
+ export interface CasAuthConfig {
6
+ /**
7
+ * CAS server base URL, e.g. `https://login.example.com/cas`
8
+ * Required.
9
+ */
10
+ casBaseUrl: string;
11
+ /**
12
+ * Secret used to sign session JWTs. Should be at least 32 random chars.
13
+ * Typically `process.env.CAS_SESSION_SECRET`.
14
+ */
15
+ sessionSecret: string;
16
+ /**
17
+ * Secret used to derive admin bridge passwords.
18
+ * Falls back to `sessionSecret` if not provided.
19
+ * Typically `process.env.CAS_SESSION_SECRET`.
20
+ */
21
+ bridgeSecret?: string;
22
+ /**
23
+ * Public-facing hostname of this app (used to build CAS redirect URIs).
24
+ * e.g. `https://app.example.com`. Defaults to the request origin.
25
+ * Typically `process.env.PUBLIC_BASE_URL`.
26
+ */
27
+ publicBaseUrl?: string;
28
+ /**
29
+ * When `false`, CAS is bypassed and `devRole` is injected instead.
30
+ * Defaults to `true`. Set to `false` for local dev.
31
+ */
32
+ enabled?: boolean;
33
+ /**
34
+ * Role injected when `enabled` is false. Defaults to `'development'`.
35
+ */
36
+ devRole?: AppRole;
37
+ /**
38
+ * CAS attribute name that carries the user's department/group.
39
+ * Default: `'role'`. Check raw CAS XML logs after first login if unknown.
40
+ */
41
+ roleAttribute?: string;
42
+ /**
43
+ * CAS group values that map to the `admin` role.
44
+ * Default: `['admin', 'administrator', 'administrators']`
45
+ */
46
+ adminGroups?: string[];
47
+ /**
48
+ * CAS group values that map to the `development` role.
49
+ * Default: `['development', 'dev', 'engineering', 'it', 'technology']`
50
+ */
51
+ devGroups?: string[];
52
+ /**
53
+ * CAS group values that map to the `marketing` role.
54
+ * Default: `['marketing', 'content', 'seo', 'analytics', 'communications']`
55
+ */
56
+ marketingGroups?: string[];
57
+ /**
58
+ * Custom CAS login URL template. Use `__SERVICE__` as a placeholder for
59
+ * the encoded callback URL. Useful when CAS is fronted by SAML:
60
+ * `https://login.example.com/saml/login?destination=/cas/login%3Fservice=__SERVICE__`
61
+ *
62
+ * Falls back to `{casBaseUrl}/login?service=__SERVICE__`.
63
+ */
64
+ loginUrlTemplate?: string;
65
+ /**
66
+ * Payload users collection slug. Default: `'users'`.
67
+ */
68
+ userCollection?: string;
69
+ /**
70
+ * Payload admin cookie prefix. Default: `'payload'`.
71
+ * Must match the `cookiePrefix` in your Payload config.
72
+ */
73
+ payloadCookiePrefix?: string;
74
+ /**
75
+ * Session cookie settings.
76
+ */
77
+ cookies?: {
78
+ /** Default: `'hms-session'` */
79
+ name?: string;
80
+ /** Default: `true` in production */
81
+ secure?: boolean;
82
+ /** Default: `'lax'` */
83
+ sameSite?: 'lax' | 'strict' | 'none';
84
+ /** Default: 8 hours (28800 seconds) */
85
+ maxAgeSecs?: number;
86
+ };
87
+ }
88
+ export interface SessionUser {
89
+ username: string;
90
+ email: string;
91
+ role: AppRole;
92
+ }
93
+ export type CasResult = {
94
+ ok: true;
95
+ user: string;
96
+ email?: string;
97
+ attributes: Record<string, string>;
98
+ } | {
99
+ ok: false;
100
+ code: string;
101
+ message: string;
102
+ };
103
+ /** Cookie set by the admin-session bridge — tells middleware the bridge ran. */
104
+ export declare const CAS_ADMIN_MARKER = "_cas_admin";
105
+ /** Header injected by middleware carrying the user's resolved role. */
106
+ export declare const ROLE_HEADER = "x-user-role";
107
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,qDAAqD;AACrD,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,CAAA;AAEvE,uCAAuC;AACvC,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,WAAW,GAAG,OAAO,CAAA;AAItD,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;IAElB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAA;IAErB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IAEtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IAEpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAE1B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAEzB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IAEvB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B;;OAEG;IACH,OAAO,CAAC,EAAE;QACR,+BAA+B;QAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,oCAAoC;QACpC,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,uBAAuB;QACvB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;QACpC,uCAAuC;QACvC,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAID,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,OAAO,CAAA;CACd;AAID,MAAM,MAAM,SAAS,GACjB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC9E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAIhD,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,eAAe,CAAA;AAE5C,uEAAuE;AACvE,eAAO,MAAM,WAAW,gBAAgB,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ // ─── Role model ───────────────────────────────────────────────────────────────
2
+ // ─── Internal constants ───────────────────────────────────────────────────────
3
+ /** Cookie set by the admin-session bridge — tells middleware the bridge ran. */
4
+ export const CAS_ADMIN_MARKER = '_cas_admin';
5
+ /** Header injected by middleware carrying the user's resolved role. */
6
+ export const ROLE_HEADER = 'x-user-role';
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAyHjF,iFAAiF;AAEjF,gFAAgF;AAChF,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAA;AAE5C,uEAAuE;AACvE,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { CasResult } from './types.js';
2
+ /** Parse a CAS 2.0/3.0 serviceValidate XML response. No external dependencies. */
3
+ export declare function parseCasXml(xml: string): CasResult;
4
+ //# sourceMappingURL=xml-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xml-parser.d.ts","sourceRoot":"","sources":["../src/xml-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAiBlD"}
@@ -0,0 +1,17 @@
1
+ /** Parse a CAS 2.0/3.0 serviceValidate XML response. No external dependencies. */
2
+ export function parseCasXml(xml) {
3
+ if (/<cas:authenticationSuccess[\s>]/.test(xml)) {
4
+ const user = xml.match(/<cas:user>(.*?)<\/cas:user>/s)?.[1]?.trim() ?? '';
5
+ const email = xml.match(/<cas:email>(.*?)<\/cas:email>/s)?.[1]?.trim();
6
+ const attrBlock = xml.match(/<cas:attributes>(.*?)<\/cas:attributes>/s)?.[1] ?? '';
7
+ const attributes = {};
8
+ for (const m of attrBlock.matchAll(/<cas:(\w+)>(.*?)<\/cas:\1>/gs)) {
9
+ attributes[m[1]] = m[2].trim();
10
+ }
11
+ return { ok: true, user, email: email ?? attributes['email'], attributes };
12
+ }
13
+ const code = xml.match(/code="([^"]+)"/)?.[1] ?? 'UNKNOWN';
14
+ const message = xml.match(/<cas:authenticationFailure[^>]*>(.*?)<\/cas:authenticationFailure>/s)?.[1]?.trim() ?? 'Authentication failed';
15
+ return { ok: false, code, message };
16
+ }
17
+ //# sourceMappingURL=xml-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xml-parser.js","sourceRoot":"","sources":["../src/xml-parser.ts"],"names":[],"mappings":"AAEA,kFAAkF;AAClF,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,iCAAiC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,GAAI,GAAG,CAAC,KAAK,CAAC,8BAA8B,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;QAC1E,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,gCAAgC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAA;QAEtE,MAAM,SAAS,GAAI,GAAG,CAAC,KAAK,CAAC,0CAA0C,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACnF,MAAM,UAAU,GAA2B,EAAE,CAAA;QAC7C,KAAK,MAAM,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EAAE,CAAC;YACnE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAChC,CAAC;QAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;IAC5E,CAAC;IAED,MAAM,IAAI,GAAM,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,CAAA;IAC7D,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,qEAAqE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,uBAAuB,CAAA;IACxI,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AACrC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@hmp-global/payload-cas-auth",
3
+ "version": "0.1.0",
4
+ "description": "CAS SSO authentication plugin for Payload CMS v3 + Next.js 16.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ },
17
+ "./middleware": {
18
+ "import": "./dist/middleware.js",
19
+ "types": "./dist/middleware.d.ts"
20
+ }
21
+ },
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "scripts": {
25
+ "prepare": "tsc",
26
+ "build": "tsc",
27
+ "build:watch": "tsc --watch",
28
+ "type-check": "tsc --noEmit"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.0",
32
+ "jose": "^5.0.0",
33
+ "next": "^16.0.0",
34
+ "payload": "^3.0.0",
35
+ "typescript": "^5.8.3"
36
+ },
37
+ "peerDependencies": {
38
+ "jose": ">=5",
39
+ "next": ">=16",
40
+ "payload": ">=3"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "next": { "optional": false },
44
+ "payload": { "optional": false },
45
+ "jose": { "optional": false }
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/hmpglobal/payload-cas-auth"
53
+ },
54
+ "keywords": ["payload", "cms", "cas", "sso", "authentication", "plugin", "nextjs"]
55
+ }