@zshn-dev/auth-server 0.2.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 (42) hide show
  1. package/README.md +152 -0
  2. package/dist/__tests__/crypto.test.d.ts +2 -0
  3. package/dist/__tests__/crypto.test.d.ts.map +1 -0
  4. package/dist/__tests__/crypto.test.js +16 -0
  5. package/dist/__tests__/crypto.test.js.map +1 -0
  6. package/dist/__tests__/jwt.test.d.ts +2 -0
  7. package/dist/__tests__/jwt.test.d.ts.map +1 -0
  8. package/dist/__tests__/jwt.test.js +41 -0
  9. package/dist/__tests__/jwt.test.js.map +1 -0
  10. package/dist/__tests__/sanitize.test.d.ts +2 -0
  11. package/dist/__tests__/sanitize.test.d.ts.map +1 -0
  12. package/dist/__tests__/sanitize.test.js +31 -0
  13. package/dist/__tests__/sanitize.test.js.map +1 -0
  14. package/dist/index.d.ts +30 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +85 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/middleware/verify-jwt.d.ts +12 -0
  19. package/dist/middleware/verify-jwt.d.ts.map +1 -0
  20. package/dist/middleware/verify-jwt.js +21 -0
  21. package/dist/middleware/verify-jwt.js.map +1 -0
  22. package/dist/providers/github.d.ts +5 -0
  23. package/dist/providers/github.d.ts.map +1 -0
  24. package/dist/providers/github.js +47 -0
  25. package/dist/providers/github.js.map +1 -0
  26. package/dist/types.d.ts +32 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +3 -0
  29. package/dist/types.js.map +1 -0
  30. package/dist/utils/crypto.d.ts +3 -0
  31. package/dist/utils/crypto.d.ts.map +1 -0
  32. package/dist/utils/crypto.js +42 -0
  33. package/dist/utils/crypto.js.map +1 -0
  34. package/dist/utils/jwt.d.ts +8 -0
  35. package/dist/utils/jwt.d.ts.map +1 -0
  36. package/dist/utils/jwt.js +15 -0
  37. package/dist/utils/jwt.js.map +1 -0
  38. package/dist/utils/sanitize.d.ts +4 -0
  39. package/dist/utils/sanitize.d.ts.map +1 -0
  40. package/dist/utils/sanitize.js +20 -0
  41. package/dist/utils/sanitize.js.map +1 -0
  42. package/package.json +43 -0
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # @your-scope/auth-server
2
+
3
+ > GitHub OAuth + JWT authentication middleware for Express.
4
+
5
+ > **Note:** `@your-scope` is a placeholder — replace it with your actual npm scope.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @your-scope/auth-server
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Quick Start
18
+
19
+ ```ts
20
+ import express from 'express';
21
+ import { createAuthRouter, verifyJwt } from '@your-scope/auth-server';
22
+
23
+ const config = {
24
+ clientId: process.env.GITHUB_CLIENT_ID!,
25
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
26
+ jwtSecret: process.env.JWT_SECRET!,
27
+ callbackUrl: 'http://localhost:3000/auth/github/callback',
28
+ afterLoginUrl: 'http://localhost:4200/auth/callback',
29
+ };
30
+
31
+ const app = express();
32
+
33
+ // Mount the auth router at /auth
34
+ app.use('/auth', createAuthRouter(config));
35
+
36
+ // Protect routes with JWT middleware
37
+ app.get('/api/me', verifyJwt(config), (req, res) => {
38
+ res.json({ user: req.user });
39
+ });
40
+
41
+ app.listen(3000, () => console.log('Server running on http://localhost:3000'));
42
+ ```
43
+
44
+ After a successful login, GitHub redirects to `callbackUrl`, which exchanges the OAuth code for a JWT and then redirects the user to:
45
+
46
+ ```
47
+ <afterLoginUrl>?token=<jwt>
48
+ ```
49
+
50
+ ---
51
+
52
+ ## API Reference
53
+
54
+ ### `createAuthRouter(config: AuthServerConfig): Router`
55
+
56
+ Returns an Express `Router`. Mount it at `/auth`:
57
+
58
+ ```ts
59
+ app.use('/auth', createAuthRouter(config));
60
+ ```
61
+
62
+ **Routes:**
63
+
64
+ | Method | Path | Description |
65
+ | ------ | ----------------- | ------------------------------------------------------------------------ |
66
+ | GET | `/github` | Redirects the user to the GitHub OAuth authorization page |
67
+ | GET | `/github/callback`| Exchanges the OAuth code for a JWT; redirects to `afterLoginUrl?token=…` |
68
+
69
+ ---
70
+
71
+ ### `AuthServerConfig`
72
+
73
+ | Property | Type | Required | Default | Description |
74
+ | ----------------- | ----------------------------------------------- | -------- | -------------- | -------------------------------------------------------------- |
75
+ | `clientId` | `string` | ✅ | — | GitHub OAuth App client ID |
76
+ | `clientSecret` | `string` | ✅ | — | GitHub OAuth App client secret |
77
+ | `jwtSecret` | `string` | ✅ | — | Secret used to sign/verify JWTs — **must be 32+ characters** |
78
+ | `callbackUrl` | `string` | ✅ | — | Full URL GitHub redirects to after authorization |
79
+ | `afterLoginUrl` | `string` | ✅ | — | URL of your frontend app; receives `?token=<jwt>` on success |
80
+ | `transformUser` | `(profile: GitHubProfile) => Partial<User>` | ❌ | — | Optional hook to customize the JWT payload from the GitHub profile |
81
+ | `stateCookieName` | `string` | ❌ | `oauth_state` | Name of the cookie used to store the OAuth CSRF state value |
82
+
83
+ ---
84
+
85
+ ### `verifyJwt(config: AuthServerConfig): RequestHandler`
86
+
87
+ Returns an Express middleware that validates a JWT on every request.
88
+
89
+ - Reads the `Authorization: Bearer <token>` header
90
+ - Verifies the token against `config.jwtSecret`
91
+ - Sets `req.user` (typed as `JwtPayload`) on success
92
+ - Returns `401 Unauthorized` if the token is missing, invalid, or expired
93
+
94
+ ```ts
95
+ app.get('/api/profile', verifyJwt(config), (req, res) => {
96
+ res.json(req.user);
97
+ });
98
+ ```
99
+
100
+ ---
101
+
102
+ ## `transformUser` Hook
103
+
104
+ Use `transformUser` to control which fields from the GitHub profile are included in the JWT payload:
105
+
106
+ ```ts
107
+ import { createAuthRouter, GitHubProfile } from '@your-scope/auth-server';
108
+
109
+ app.use('/auth', createAuthRouter({
110
+ ...config,
111
+ transformUser: (profile: GitHubProfile) => ({
112
+ id: profile.id,
113
+ login: profile.login,
114
+ name: profile.name,
115
+ avatarUrl: profile.avatar_url,
116
+ email: profile.email,
117
+ }),
118
+ }));
119
+ ```
120
+
121
+ The returned object is merged into the JWT payload and later available on `req.user`.
122
+
123
+ ---
124
+
125
+ ## Environment Variables
126
+
127
+ Never hardcode secrets. Use a `.env` file (via [dotenv](https://github.com/motdotla/dotenv)) or your deployment platform's secret management:
128
+
129
+ ```env
130
+ GITHUB_CLIENT_ID=your_github_client_id
131
+ GITHUB_CLIENT_SECRET=your_github_client_secret
132
+ # Must be at least 32 characters
133
+ JWT_SECRET=a_very_long_random_secret_at_least_32_chars
134
+ ```
135
+
136
+ ```ts
137
+ import 'dotenv/config';
138
+
139
+ const config = {
140
+ clientId: process.env.GITHUB_CLIENT_ID!,
141
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
142
+ jwtSecret: process.env.JWT_SECRET!,
143
+ callbackUrl: process.env.CALLBACK_URL ?? 'http://localhost:3000/auth/github/callback',
144
+ afterLoginUrl: process.env.AFTER_LOGIN_URL ?? 'http://localhost:4200/auth/callback',
145
+ };
146
+ ```
147
+
148
+ Add `.env` to your `.gitignore` to avoid committing secrets:
149
+
150
+ ```
151
+ .env
152
+ ```
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=crypto.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/crypto.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const crypto_js_1 = require("../utils/crypto.js");
5
+ (0, vitest_1.describe)('generateState', () => {
6
+ (0, vitest_1.it)('returns a 64-character lowercase hex string', () => {
7
+ const state = (0, crypto_js_1.generateState)();
8
+ (0, vitest_1.expect)(state).toMatch(/^[0-9a-f]{64}$/);
9
+ });
10
+ (0, vitest_1.it)('returns a different value on each call', () => {
11
+ const a = (0, crypto_js_1.generateState)();
12
+ const b = (0, crypto_js_1.generateState)();
13
+ (0, vitest_1.expect)(a).not.toBe(b);
14
+ });
15
+ });
16
+ //# sourceMappingURL=crypto.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.test.js","sourceRoot":"","sources":["../../src/__tests__/crypto.test.ts"],"names":[],"mappings":";;AAAA,mCAA8C;AAC9C,kDAAmD;AAEnD,IAAA,iBAAQ,EAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,IAAA,WAAE,EAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,KAAK,GAAG,IAAA,yBAAa,GAAE,CAAC;QAC9B,IAAA,eAAM,EAAC,KAAK,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,GAAG,IAAA,yBAAa,GAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,IAAA,yBAAa,GAAE,CAAC;QAC1B,IAAA,eAAM,EAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=jwt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/jwt.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const jwt_js_1 = require("../utils/jwt.js");
5
+ const SECRET = 'a-test-secret-that-is-at-least-32-characters-long';
6
+ const USER = {
7
+ id: '42',
8
+ email: 'user@example.com',
9
+ name: 'Test User',
10
+ avatar_url: 'https://example.com/avatar.png',
11
+ };
12
+ (0, vitest_1.describe)('signJwt', () => {
13
+ (0, vitest_1.it)('returns a three-part JWT string', () => {
14
+ const token = (0, jwt_js_1.signJwt)(USER, SECRET);
15
+ const parts = token.split('.');
16
+ (0, vitest_1.expect)(parts).toHaveLength(3);
17
+ });
18
+ (0, vitest_1.it)('embeds the user payload in the token', () => {
19
+ const token = (0, jwt_js_1.signJwt)(USER, SECRET);
20
+ const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
21
+ (0, vitest_1.expect)(payload.id).toBe('42');
22
+ (0, vitest_1.expect)(payload.email).toBe('user@example.com');
23
+ });
24
+ });
25
+ (0, vitest_1.describe)('verifyJwtToken', () => {
26
+ (0, vitest_1.it)('round-trips: verify returns the original user payload', () => {
27
+ const token = (0, jwt_js_1.signJwt)(USER, SECRET);
28
+ const payload = (0, jwt_js_1.verifyJwtToken)(token, SECRET);
29
+ (0, vitest_1.expect)(payload.id).toBe(USER.id);
30
+ (0, vitest_1.expect)(payload.email).toBe(USER.email);
31
+ (0, vitest_1.expect)(payload.name).toBe(USER.name);
32
+ });
33
+ (0, vitest_1.it)('throws when the secret is wrong', () => {
34
+ const token = (0, jwt_js_1.signJwt)(USER, SECRET);
35
+ (0, vitest_1.expect)(() => (0, jwt_js_1.verifyJwtToken)(token, 'wrong-secret')).toThrow();
36
+ });
37
+ (0, vitest_1.it)('throws for a malformed token', () => {
38
+ (0, vitest_1.expect)(() => (0, jwt_js_1.verifyJwtToken)('not.a.token', SECRET)).toThrow();
39
+ });
40
+ });
41
+ //# sourceMappingURL=jwt.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.test.js","sourceRoot":"","sources":["../../src/__tests__/jwt.test.ts"],"names":[],"mappings":";;AAAA,mCAA8C;AAC9C,4CAA0D;AAE1D,MAAM,MAAM,GAAG,mDAAmD,CAAC;AACnE,MAAM,IAAI,GAAG;IACX,EAAE,EAAE,IAAI;IACR,KAAK,EAAE,kBAAkB;IACzB,IAAI,EAAE,WAAW;IACjB,UAAU,EAAE,gCAAgC;CAC7C,CAAC;AAEF,IAAA,iBAAQ,EAAC,SAAS,EAAE,GAAG,EAAE;IACvB,IAAA,WAAE,EAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,KAAK,GAAG,IAAA,gBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAA,eAAM,EAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,KAAK,GAAG,IAAA,gBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClF,IAAA,eAAM,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAA,eAAM,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,IAAA,WAAE,EAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,KAAK,GAAG,IAAA,gBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,IAAA,uBAAc,EAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC9C,IAAA,eAAM,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,IAAA,eAAM,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,KAAK,GAAG,IAAA,gBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,IAAA,eAAM,EAAC,GAAG,EAAE,CAAC,IAAA,uBAAc,EAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,IAAA,eAAM,EAAC,GAAG,EAAE,CAAC,IAAA,uBAAc,EAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sanitize.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sanitize.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const sanitize_js_1 = require("../utils/sanitize.js");
5
+ (0, vitest_1.describe)('isSafeUrl', () => {
6
+ (0, vitest_1.it)('accepts http URLs', () => {
7
+ (0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('http://localhost:3000')).toBe(true);
8
+ });
9
+ (0, vitest_1.it)('accepts https URLs', () => {
10
+ (0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('https://example.com/callback')).toBe(true);
11
+ });
12
+ (0, vitest_1.it)('rejects javascript: protocol', () => {
13
+ (0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('javascript:alert(1)')).toBe(false);
14
+ });
15
+ (0, vitest_1.it)('rejects ftp: protocol', () => {
16
+ (0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('ftp://example.com')).toBe(false);
17
+ });
18
+ (0, vitest_1.it)('rejects plain strings that are not URLs', () => {
19
+ (0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('not-a-url')).toBe(false);
20
+ (0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('')).toBe(false);
21
+ });
22
+ });
23
+ (0, vitest_1.describe)('assertConfig', () => {
24
+ (0, vitest_1.it)('does not throw when condition is true', () => {
25
+ (0, vitest_1.expect)(() => (0, sanitize_js_1.assertConfig)(true, 'should not throw')).not.toThrow();
26
+ });
27
+ (0, vitest_1.it)('throws an Error with the given message when condition is false', () => {
28
+ (0, vitest_1.expect)(() => (0, sanitize_js_1.assertConfig)(false, 'clientId is required')).toThrowError('[auth-server] Invalid config: clientId is required');
29
+ });
30
+ });
31
+ //# sourceMappingURL=sanitize.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.test.js","sourceRoot":"","sources":["../../src/__tests__/sanitize.test.ts"],"names":[],"mappings":";;AAAA,mCAA8C;AAC9C,sDAA+D;AAE/D,IAAA,iBAAQ,EAAC,WAAW,EAAE,GAAG,EAAE;IACzB,IAAA,WAAE,EAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,8BAA8B,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,IAAA,WAAE,EAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,IAAA,eAAM,EAAC,GAAG,EAAE,CAAC,IAAA,0BAAY,EAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,IAAA,eAAM,EAAC,GAAG,EAAE,CAAC,IAAA,0BAAY,EAAC,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,YAAY,CACpE,oDAAoD,CACrD,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { Router, RequestHandler } from 'express';
2
+ import { AuthServerConfig } from './types.js';
3
+ import { JwtPayload } from './middleware/verify-jwt.js';
4
+ export type { JwtPayload };
5
+ declare global {
6
+ namespace Express {
7
+ interface Request {
8
+ user?: JwtPayload;
9
+ }
10
+ }
11
+ }
12
+ /**
13
+ * Creates an Express Router that handles the GitHub OAuth flow.
14
+ *
15
+ * Mount it at a path like `/auth`:
16
+ * ```ts
17
+ * app.use('/auth', createAuthRouter({ ... }));
18
+ * ```
19
+ *
20
+ * Routes provided:
21
+ * - `GET /github` — redirects to GitHub's authorization page
22
+ * - `GET /github/callback` — handles the OAuth callback, signs a JWT, and redirects to the Angular app
23
+ */
24
+ export declare function createAuthRouter(config: AuthServerConfig): Router;
25
+ /**
26
+ * Express middleware that validates a Bearer JWT and attaches the decoded payload to `req.user`.
27
+ * Returns 401 if the token is missing, malformed, or expired.
28
+ */
29
+ export declare function verifyJwt(config: Pick<AuthServerConfig, 'jwtSecret'>): RequestHandler;
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAQ,MAAM,YAAY,CAAC;AAKpD,OAAO,EAA6B,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAInF,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,UAAU,CAAC;SACnB;KACF;CACF;AAUD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAoDjE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,GAAG,cAAc,CAErF"}
package/dist/index.js ADDED
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createAuthRouter = createAuthRouter;
7
+ exports.verifyJwt = verifyJwt;
8
+ const express_1 = require("express");
9
+ const cookie_parser_1 = __importDefault(require("cookie-parser"));
10
+ const crypto_js_1 = require("./utils/crypto.js");
11
+ const jwt_js_1 = require("./utils/jwt.js");
12
+ const sanitize_js_1 = require("./utils/sanitize.js");
13
+ const github_js_1 = require("./providers/github.js");
14
+ const verify_jwt_js_1 = require("./middleware/verify-jwt.js");
15
+ function validateConfig(config) {
16
+ (0, sanitize_js_1.assertConfig)(!!config.clientId, 'clientId is required');
17
+ (0, sanitize_js_1.assertConfig)(!!config.clientSecret, 'clientSecret is required');
18
+ (0, sanitize_js_1.assertConfig)(!!config.jwtSecret && config.jwtSecret.length >= 32, 'jwtSecret must be at least 32 characters');
19
+ (0, sanitize_js_1.assertConfig)((0, sanitize_js_1.isSafeUrl)(config.callbackUrl), 'callbackUrl must be a valid http/https URL');
20
+ (0, sanitize_js_1.assertConfig)((0, sanitize_js_1.isSafeUrl)(config.afterLoginUrl), 'afterLoginUrl must be a valid http/https URL');
21
+ }
22
+ /**
23
+ * Creates an Express Router that handles the GitHub OAuth flow.
24
+ *
25
+ * Mount it at a path like `/auth`:
26
+ * ```ts
27
+ * app.use('/auth', createAuthRouter({ ... }));
28
+ * ```
29
+ *
30
+ * Routes provided:
31
+ * - `GET /github` — redirects to GitHub's authorization page
32
+ * - `GET /github/callback` — handles the OAuth callback, signs a JWT, and redirects to the Angular app
33
+ */
34
+ function createAuthRouter(config) {
35
+ validateConfig(config);
36
+ const stateCookieName = config.stateCookieName ?? 'oauth_state';
37
+ const router = (0, express_1.Router)();
38
+ router.use((0, cookie_parser_1.default)());
39
+ // Step 1: redirect to GitHub
40
+ router.get('/github', (_req, res) => {
41
+ const state = (0, crypto_js_1.generateState)();
42
+ res.cookie(stateCookieName, state, { httpOnly: true, sameSite: 'lax', maxAge: 10 * 60 * 1000 });
43
+ res.redirect((0, github_js_1.buildAuthUrl)(config.clientId, state, config.callbackUrl));
44
+ });
45
+ // Step 2: handle callback
46
+ router.get('/github/callback', async (req, res) => {
47
+ const { code, state } = req.query;
48
+ const savedState = req.cookies[stateCookieName];
49
+ if (!code || !state || state !== savedState) {
50
+ res.status(400).send('Invalid OAuth state');
51
+ return;
52
+ }
53
+ res.clearCookie(stateCookieName);
54
+ try {
55
+ const accessToken = await (0, github_js_1.exchangeCode)(config.clientId, config.clientSecret, code);
56
+ const profile = await (0, github_js_1.fetchGitHubUser)(accessToken);
57
+ const baseUser = {
58
+ id: String(profile.id),
59
+ email: profile.email ?? '',
60
+ name: profile.name ?? profile.login,
61
+ avatar_url: profile.avatar_url,
62
+ };
63
+ const user = config.transformUser
64
+ ? { ...baseUser, ...config.transformUser(profile) }
65
+ : baseUser;
66
+ const token = (0, jwt_js_1.signJwt)(user, config.jwtSecret);
67
+ const redirectUrl = new URL(config.afterLoginUrl);
68
+ redirectUrl.searchParams.set('token', token);
69
+ res.redirect(redirectUrl.toString());
70
+ }
71
+ catch (err) {
72
+ console.error('[auth-server] OAuth callback error:', err);
73
+ res.status(500).send('Authentication failed');
74
+ }
75
+ });
76
+ return router;
77
+ }
78
+ /**
79
+ * Express middleware that validates a Bearer JWT and attaches the decoded payload to `req.user`.
80
+ * Returns 401 if the token is missing, malformed, or expired.
81
+ */
82
+ function verifyJwt(config) {
83
+ return (0, verify_jwt_js_1.createVerifyJwtMiddleware)(config.jwtSecret);
84
+ }
85
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { RequestHandler } from 'express';
2
+ import { JwtPayload } from '../utils/jwt.js';
3
+ export type { JwtPayload };
4
+ declare global {
5
+ namespace Express {
6
+ interface Request {
7
+ user?: JwtPayload;
8
+ }
9
+ }
10
+ }
11
+ export declare function createVerifyJwtMiddleware(jwtSecret: string): RequestHandler;
12
+ //# sourceMappingURL=verify-jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-jwt.d.ts","sourceRoot":"","sources":["../../src/middleware/verify-jwt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAkB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,UAAU,CAAC;SACnB;KACF;CACF;AAED,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAc3E"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createVerifyJwtMiddleware = createVerifyJwtMiddleware;
4
+ const jwt_js_1 = require("../utils/jwt.js");
5
+ function createVerifyJwtMiddleware(jwtSecret) {
6
+ return (req, res, next) => {
7
+ const authHeader = req.headers['authorization'];
8
+ if (!authHeader?.startsWith('Bearer ')) {
9
+ res.status(401).json({ error: 'Missing or invalid Authorization header' });
10
+ return;
11
+ }
12
+ try {
13
+ req.user = (0, jwt_js_1.verifyJwtToken)(authHeader.slice(7), jwtSecret);
14
+ next();
15
+ }
16
+ catch {
17
+ res.status(401).json({ error: 'Invalid or expired token' });
18
+ }
19
+ };
20
+ }
21
+ //# sourceMappingURL=verify-jwt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-jwt.js","sourceRoot":"","sources":["../../src/middleware/verify-jwt.ts"],"names":[],"mappings":";;AAcA,8DAcC;AA3BD,4CAA6D;AAa7D,SAAgB,yBAAyB,CAAC,SAAiB;IACzD,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACvC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,GAAG,IAAA,uBAAc,EAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;YAC1D,IAAI,EAAE,CAAC;QACT,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { GitHubProfile } from '../types.js';
2
+ export declare function buildAuthUrl(clientId: string, state: string, callbackUrl: string): string;
3
+ export declare function exchangeCode(clientId: string, clientSecret: string, code: string): Promise<string>;
4
+ export declare function fetchGitHubUser(accessToken: string): Promise<GitHubProfile>;
5
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/providers/github.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAK5C,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAQzF;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAyBjF"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAuthUrl = buildAuthUrl;
4
+ exports.exchangeCode = exchangeCode;
5
+ exports.fetchGitHubUser = fetchGitHubUser;
6
+ const GITHUB_API = 'https://api.github.com';
7
+ const GITHUB_OAUTH = 'https://github.com';
8
+ function buildAuthUrl(clientId, state, callbackUrl) {
9
+ const params = new URLSearchParams({
10
+ client_id: clientId,
11
+ redirect_uri: callbackUrl,
12
+ scope: 'read:user user:email',
13
+ state,
14
+ });
15
+ return `${GITHUB_OAUTH}/login/oauth/authorize?${params}`;
16
+ }
17
+ async function exchangeCode(clientId, clientSecret, code) {
18
+ const res = await fetch(`${GITHUB_OAUTH}/login/oauth/access_token`, {
19
+ method: 'POST',
20
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code }),
22
+ });
23
+ const data = (await res.json());
24
+ if (!data.access_token) {
25
+ throw new Error(`GitHub token exchange failed: ${data.error ?? 'unknown error'}`);
26
+ }
27
+ return data.access_token;
28
+ }
29
+ async function fetchGitHubUser(accessToken) {
30
+ const headers = {
31
+ Authorization: `Bearer ${accessToken}`,
32
+ Accept: 'application/vnd.github+json',
33
+ 'X-GitHub-Api-Version': '2022-11-28',
34
+ };
35
+ const [profileRes, emailsRes] = await Promise.all([
36
+ fetch(`${GITHUB_API}/user`, { headers }),
37
+ fetch(`${GITHUB_API}/user/emails`, { headers }),
38
+ ]);
39
+ const profile = (await profileRes.json());
40
+ if (!profile.email) {
41
+ const emails = (await emailsRes.json());
42
+ const primary = emails.find((e) => e.primary && e.verified);
43
+ profile.email = primary?.email ?? null;
44
+ }
45
+ return profile;
46
+ }
47
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/providers/github.ts"],"names":[],"mappings":";;AAKA,oCAQC;AAED,oCAeC;AAED,0CAyBC;AAvDD,MAAM,UAAU,GAAG,wBAAwB,CAAC;AAC5C,MAAM,YAAY,GAAG,oBAAoB,CAAC;AAE1C,SAAgB,YAAY,CAAC,QAAgB,EAAE,KAAa,EAAE,WAAmB;IAC/E,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,QAAQ;QACnB,YAAY,EAAE,WAAW;QACzB,KAAK,EAAE,sBAAsB;QAC7B,KAAK;KACN,CAAC,CAAC;IACH,OAAO,GAAG,YAAY,0BAA0B,MAAM,EAAE,CAAC;AAC3D,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,QAAgB,EAChB,YAAoB,EACpB,IAAY;IAEZ,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,YAAY,2BAA2B,EAAE;QAClE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;KACjF,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA8C,CAAC;IAC7E,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,KAAK,IAAI,eAAe,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,IAAI,CAAC,YAAY,CAAC;AAC3B,CAAC;AAEM,KAAK,UAAU,eAAe,CAAC,WAAmB;IACvD,MAAM,OAAO,GAAG;QACd,aAAa,EAAE,UAAU,WAAW,EAAE;QACtC,MAAM,EAAE,6BAA6B;QACrC,sBAAsB,EAAE,YAAY;KACrC,CAAC;IAEF,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAChD,KAAK,CAAC,GAAG,UAAU,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC;QACxC,KAAK,CAAC,GAAG,UAAU,cAAc,EAAE,EAAE,OAAO,EAAE,CAAC;KAChD,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,CAAC,MAAM,UAAU,CAAC,IAAI,EAAE,CAAkB,CAAC;IAE3D,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,CAIpC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC5D,OAAO,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC;IACzC,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,32 @@
1
+ export interface AuthServerConfig {
2
+ /** GitHub OAuth client ID */
3
+ clientId: string;
4
+ /** GitHub OAuth client secret */
5
+ clientSecret: string;
6
+ /** JWT signing secret */
7
+ jwtSecret: string;
8
+ /** URL to redirect to after successful login (e.g. http://localhost:4200/auth/callback) */
9
+ callbackUrl: string;
10
+ /** URL the Angular app sends users back to after the OAuth callback completes */
11
+ afterLoginUrl: string;
12
+ /** Optional: transform the raw GitHub profile before it is signed into the JWT */
13
+ transformUser?: (profile: GitHubProfile) => Partial<User>;
14
+ /** Cookie name used for the CSRF state param (default: "oauth_state") */
15
+ stateCookieName?: string;
16
+ }
17
+ export interface GitHubProfile {
18
+ id: number;
19
+ login: string;
20
+ name: string | null;
21
+ email: string | null;
22
+ avatar_url: string;
23
+ }
24
+ /** Normalised user stored in the JWT */
25
+ export interface User {
26
+ id: string;
27
+ email: string;
28
+ name: string;
29
+ avatar_url: string;
30
+ [key: string]: unknown;
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wCAAwC;AACxC,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ /** Generate a cryptographically-random state string for CSRF protection */
2
+ export declare function generateState(): string;
3
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/utils/crypto.ts"],"names":[],"mappings":"AAEA,2EAA2E;AAC3E,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.generateState = generateState;
37
+ const crypto = __importStar(require("crypto"));
38
+ /** Generate a cryptographically-random state string for CSRF protection */
39
+ function generateState() {
40
+ return crypto.randomBytes(32).toString('hex');
41
+ }
42
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/utils/crypto.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,sCAEC;AALD,+CAAiC;AAEjC,2EAA2E;AAC3E,SAAgB,aAAa;IAC3B,OAAO,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAChD,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { User } from '../types.js';
2
+ export interface JwtPayload extends User {
3
+ iat?: number;
4
+ exp?: number;
5
+ }
6
+ export declare function signJwt(user: User, secret: string, expiresIn?: string): string;
7
+ export declare function verifyJwtToken(token: string, secret: string): JwtPayload;
8
+ //# sourceMappingURL=jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/utils/jwt.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,MAAM,WAAW,UAAW,SAAQ,IAAI;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,MAAM,CAE5E;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAExE"}
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.signJwt = signJwt;
7
+ exports.verifyJwtToken = verifyJwtToken;
8
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
+ function signJwt(user, secret, expiresIn = '7d') {
10
+ return jsonwebtoken_1.default.sign(user, secret, { expiresIn });
11
+ }
12
+ function verifyJwtToken(token, secret) {
13
+ return jsonwebtoken_1.default.verify(token, secret);
14
+ }
15
+ //# sourceMappingURL=jwt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../../src/utils/jwt.ts"],"names":[],"mappings":";;;;;AAQA,0BAEC;AAED,wCAEC;AAdD,gEAA+B;AAQ/B,SAAgB,OAAO,CAAC,IAAU,EAAE,MAAc,EAAE,SAAS,GAAG,IAAI;IAClE,OAAO,sBAAG,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,EAAqB,CAAC,CAAC;AAClE,CAAC;AAED,SAAgB,cAAc,CAAC,KAAa,EAAE,MAAc;IAC1D,OAAO,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAe,CAAC;AACjD,CAAC"}
@@ -0,0 +1,4 @@
1
+ /** Validate that a string is a safe URL (http/https only, no javascript: etc.) */
2
+ export declare function isSafeUrl(value: string): boolean;
3
+ export declare function assertConfig(condition: boolean, message: string): void;
4
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOhD;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAItE"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSafeUrl = isSafeUrl;
4
+ exports.assertConfig = assertConfig;
5
+ /** Validate that a string is a safe URL (http/https only, no javascript: etc.) */
6
+ function isSafeUrl(value) {
7
+ try {
8
+ const url = new URL(value);
9
+ return url.protocol === 'http:' || url.protocol === 'https:';
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ function assertConfig(condition, message) {
16
+ if (!condition) {
17
+ throw new Error(`[auth-server] Invalid config: ${message}`);
18
+ }
19
+ }
20
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":";;AACA,8BAOC;AAED,oCAIC;AAdD,kFAAkF;AAClF,SAAgB,SAAS,CAAC,KAAa;IACrC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,OAAO,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAgB,YAAY,CAAC,SAAkB,EAAE,OAAe;IAC9D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,iCAAiC,OAAO,EAAE,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@zshn-dev/auth-server",
3
+ "version": "0.2.0",
4
+ "description": "Angular-first auth server SDK — Express router for GitHub OAuth",
5
+ "keywords": [
6
+ "github",
7
+ "oauth",
8
+ "jwt",
9
+ "express",
10
+ "angular",
11
+ "auth"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/zshn-dev/ngx-gh-auth.git",
17
+ "directory": "packages/auth-server"
18
+ },
19
+ "homepage": "https://github.com/zshn-dev/ngx-gh-auth#readme",
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "dev": "tsc -p tsconfig.json --watch",
28
+ "test": "vitest run"
29
+ },
30
+ "dependencies": {
31
+ "cookie-parser": "^1.4.7",
32
+ "express": "^4.21.2",
33
+ "jsonwebtoken": "^9.0.2"
34
+ },
35
+ "devDependencies": {
36
+ "@types/cookie-parser": "^1.4.8",
37
+ "@types/express": "^5.0.1",
38
+ "@types/jsonwebtoken": "^9.0.9",
39
+ "@types/node": "^22.15.0",
40
+ "typescript": "~5.9.2",
41
+ "vitest": "^4.0.8"
42
+ }
43
+ }