@ontrails/permits 1.0.0-beta.14 → 1.0.0-beta.16

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 (89) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +35 -27
  3. package/package.json +12 -3
  4. package/src/adapters/adapter.ts +41 -0
  5. package/src/{connectors → adapters}/jwt.ts +148 -21
  6. package/src/auth-resource.ts +89 -0
  7. package/src/boundary.ts +200 -0
  8. package/src/errors.ts +1 -18
  9. package/src/extraction.ts +22 -16
  10. package/src/index.ts +22 -10
  11. package/src/permit.ts +2 -2
  12. package/src/rules.ts +2 -2
  13. package/src/testing.ts +5 -5
  14. package/src/trails/auth-verify.ts +13 -15
  15. package/.turbo/turbo-build.log +0 -1
  16. package/.turbo/turbo-lint.log +0 -3
  17. package/.turbo/turbo-typecheck.log +0 -1
  18. package/dist/adapter.d.ts +0 -26
  19. package/dist/adapter.d.ts.map +0 -1
  20. package/dist/adapter.js +0 -2
  21. package/dist/adapter.js.map +0 -1
  22. package/dist/adapters/jwt.d.ts +0 -25
  23. package/dist/adapters/jwt.d.ts.map +0 -1
  24. package/dist/adapters/jwt.js +0 -148
  25. package/dist/adapters/jwt.js.map +0 -1
  26. package/dist/auth-gate.d.ts +0 -18
  27. package/dist/auth-gate.d.ts.map +0 -1
  28. package/dist/auth-gate.js +0 -56
  29. package/dist/auth-gate.js.map +0 -1
  30. package/dist/auth-layer.d.ts +0 -18
  31. package/dist/auth-layer.d.ts.map +0 -1
  32. package/dist/auth-layer.js +0 -56
  33. package/dist/auth-layer.js.map +0 -1
  34. package/dist/auth-provision.d.ts +0 -11
  35. package/dist/auth-provision.d.ts.map +0 -1
  36. package/dist/auth-provision.js +0 -22
  37. package/dist/auth-provision.js.map +0 -1
  38. package/dist/auth-service.d.ts +0 -10
  39. package/dist/auth-service.d.ts.map +0 -1
  40. package/dist/auth-service.js +0 -21
  41. package/dist/auth-service.js.map +0 -1
  42. package/dist/connectors/connector.d.ts +0 -26
  43. package/dist/connectors/connector.d.ts.map +0 -1
  44. package/dist/connectors/connector.js +0 -2
  45. package/dist/connectors/connector.js.map +0 -1
  46. package/dist/connectors/jwt.d.ts +0 -25
  47. package/dist/connectors/jwt.d.ts.map +0 -1
  48. package/dist/connectors/jwt.js +0 -148
  49. package/dist/connectors/jwt.js.map +0 -1
  50. package/dist/errors.d.ts +0 -15
  51. package/dist/errors.d.ts.map +0 -1
  52. package/dist/errors.js +0 -15
  53. package/dist/errors.js.map +0 -1
  54. package/dist/extraction.d.ts +0 -20
  55. package/dist/extraction.d.ts.map +0 -1
  56. package/dist/extraction.js +0 -2
  57. package/dist/extraction.js.map +0 -1
  58. package/dist/index.d.ts +0 -11
  59. package/dist/index.d.ts.map +0 -1
  60. package/dist/index.js +0 -11
  61. package/dist/index.js.map +0 -1
  62. package/dist/permit.d.ts +0 -26
  63. package/dist/permit.d.ts.map +0 -1
  64. package/dist/permit.js +0 -17
  65. package/dist/permit.js.map +0 -1
  66. package/dist/rules.d.ts +0 -47
  67. package/dist/rules.d.ts.map +0 -1
  68. package/dist/rules.js +0 -127
  69. package/dist/rules.js.map +0 -1
  70. package/dist/testing.d.ts +0 -20
  71. package/dist/testing.d.ts.map +0 -1
  72. package/dist/testing.js +0 -22
  73. package/dist/testing.js.map +0 -1
  74. package/dist/trails/auth-verify.d.ts +0 -22
  75. package/dist/trails/auth-verify.d.ts.map +0 -1
  76. package/dist/trails/auth-verify.js +0 -85
  77. package/dist/trails/auth-verify.js.map +0 -1
  78. package/src/__tests__/auth-gate.test.ts +0 -130
  79. package/src/__tests__/auth-provision.test.ts +0 -62
  80. package/src/__tests__/auth-verify.test.ts +0 -278
  81. package/src/__tests__/connector.test.ts +0 -338
  82. package/src/__tests__/permit.test.ts +0 -122
  83. package/src/__tests__/rules.test.ts +0 -239
  84. package/src/__tests__/testing.test.ts +0 -57
  85. package/src/auth-gate.ts +0 -80
  86. package/src/auth-provision.ts +0 -26
  87. package/src/connectors/connector.ts +0 -35
  88. package/tsconfig.json +0 -9
  89. package/tsconfig.tsbuildinfo +0 -1
@@ -1,278 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import {
4
- Result,
5
- TRAILHEAD_KEY,
6
- ValidationError,
7
- executeTrail,
8
- } from '@ontrails/core';
9
-
10
- import type { AuthConnector } from '../connectors/connector.js';
11
- import { authProvision } from '../auth-provision.js';
12
- import { createJwtConnector } from '../connectors/jwt.js';
13
- import type { Permit } from '../permit.js';
14
- import { authVerify } from '../trails/auth-verify.js';
15
-
16
- // ---------------------------------------------------------------------------
17
- // Test helper: sign a JWT with HMAC-SHA256 using crypto.subtle
18
- // ---------------------------------------------------------------------------
19
-
20
- const base64url = (buf: ArrayBuffer): string => {
21
- const bytes = new Uint8Array(buf);
22
- let binary = '';
23
- for (const b of bytes) {
24
- binary += String.fromCodePoint(b);
25
- }
26
- return btoa(binary)
27
- .replaceAll('+', '-')
28
- .replaceAll('/', '_')
29
- .replace(/=+$/, '');
30
- };
31
-
32
- const base64urlEncode = (str: string): string => {
33
- const encoder = new TextEncoder();
34
- return base64url(encoder.encode(str).buffer as ArrayBuffer);
35
- };
36
-
37
- const signJwt = async (
38
- payload: Record<string, unknown>,
39
- secret: string
40
- ): Promise<string> => {
41
- const header = base64urlEncode(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
42
- const body = base64urlEncode(JSON.stringify(payload));
43
- const data = `${header}.${body}`;
44
- const encoder = new TextEncoder();
45
- const key = await crypto.subtle.importKey(
46
- 'raw',
47
- encoder.encode(secret),
48
- { hash: 'SHA-256', name: 'HMAC' },
49
- false,
50
- ['sign']
51
- );
52
- const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
53
- return `${data}.${base64url(sig)}`;
54
- };
55
-
56
- const TEST_SECRET = 'test-secret-for-hmac-256';
57
-
58
- /** Create an AuthConnector wired to a JWT secret. */
59
- const jwtConnector = (): AuthConnector =>
60
- createJwtConnector({ secret: TEST_SECRET });
61
-
62
- /** Execute auth.verify with a given connector injected as the auth provision. */
63
- const runVerify = async (
64
- token: string,
65
- connector: AuthConnector,
66
- options?: {
67
- trailhead?: 'http' | 'mcp' | 'cli';
68
- }
69
- ): Promise<
70
- Result<
71
- {
72
- error?: string;
73
- errorCode?:
74
- | 'expired_token'
75
- | 'insufficient_scope'
76
- | 'invalid_token'
77
- | 'missing_credentials';
78
- permit?: {
79
- id: string;
80
- metadata?: Record<string, unknown>;
81
- roles?: string[];
82
- scopes: string[];
83
- tenantId?: string;
84
- };
85
- valid: boolean;
86
- },
87
- Error
88
- >
89
- > => {
90
- const result = await executeTrail(
91
- authVerify,
92
- { token },
93
- {
94
- ctx:
95
- options?.trailhead === undefined
96
- ? undefined
97
- : { extensions: { [TRAILHEAD_KEY]: options.trailhead } },
98
- provisions: { [authProvision.id]: connector },
99
- }
100
- );
101
- return result as Result<
102
- {
103
- error?: string;
104
- permit?: {
105
- id: string;
106
- metadata?: Record<string, unknown>;
107
- roles?: string[];
108
- scopes: string[];
109
- tenantId?: string;
110
- };
111
- valid: boolean;
112
- },
113
- Error
114
- >;
115
- };
116
-
117
- // ---------------------------------------------------------------------------
118
- // Tests
119
- // ---------------------------------------------------------------------------
120
-
121
- describe('auth.verify trail', () => {
122
- describe('contract', () => {
123
- test('has correct id and intent', () => {
124
- expect(authVerify.id).toBe('auth.verify');
125
- expect(authVerify.intent).toBe('read');
126
- });
127
-
128
- test('has infrastructure meta', () => {
129
- expect(authVerify.meta).toEqual({ category: 'infrastructure' });
130
- });
131
-
132
- test('has examples', () => {
133
- expect(authVerify.examples).toBeDefined();
134
- expect(authVerify.examples?.length).toBeGreaterThan(0);
135
- });
136
-
137
- test('declares authProvision dependency', () => {
138
- expect(authVerify.provisions).toHaveLength(1);
139
- expect(authVerify.provisions[0]?.id).toBe('auth');
140
- });
141
- });
142
-
143
- describe('with mock connector (no credentials)', () => {
144
- test('returns valid: false with error message', async () => {
145
- const noopConnector: AuthConnector = {
146
- // oxlint-disable-next-line require-await -- satisfies async interface
147
- authenticate: async () => Result.ok(null),
148
- };
149
-
150
- const result = await runVerify('some-token', noopConnector);
151
-
152
- expect(result.isOk()).toBe(true);
153
- const value = result.unwrap();
154
- expect(value.valid).toBe(false);
155
- expect(value.error).toBe('No credentials');
156
- expect(value.errorCode).toBe('missing_credentials');
157
- expect(value.permit).toBeUndefined();
158
- });
159
- });
160
-
161
- describe('with valid token and secret', () => {
162
- test('returns valid: true with permit', async () => {
163
- const now = Math.floor(Date.now() / 1000);
164
- const token = await signJwt(
165
- { exp: now + 3600, scope: 'read write', sub: 'user-42' },
166
- TEST_SECRET
167
- );
168
-
169
- const result = await runVerify(token, jwtConnector());
170
-
171
- expect(result.isOk()).toBe(true);
172
- const value = result.unwrap();
173
- expect(value.valid).toBe(true);
174
- expect(value.permit).toEqual({
175
- id: 'user-42',
176
- scopes: ['read', 'write'],
177
- });
178
- expect(value.error).toBeUndefined();
179
- });
180
-
181
- test('returns the full permit payload from the connector', async () => {
182
- const permit: Permit = {
183
- id: 'user-42',
184
- metadata: { plan: 'pro' },
185
- roles: ['admin'],
186
- scopes: ['read', 'write'],
187
- tenantId: 'tenant-1',
188
- };
189
- const connector: AuthConnector = {
190
- // oxlint-disable-next-line require-await -- satisfies async interface
191
- authenticate: async () => Result.ok(permit),
192
- };
193
-
194
- const result = await runVerify('full-permit-token', connector);
195
-
196
- expect(result.isOk()).toBe(true);
197
- expect(result.unwrap().permit).toEqual({
198
- id: 'user-42',
199
- metadata: { plan: 'pro' },
200
- roles: ['admin'],
201
- scopes: ['read', 'write'],
202
- tenantId: 'tenant-1',
203
- });
204
- });
205
-
206
- test('forwards the invoking trailhead from trail context', async () => {
207
- let seenTrailhead: string | undefined;
208
- const connector: AuthConnector = {
209
- // oxlint-disable-next-line require-await -- captures connector input
210
- authenticate: async (input) => {
211
- seenTrailhead = input.trailhead;
212
- return Result.ok({
213
- id: 'user-42',
214
- scopes: ['read'],
215
- });
216
- },
217
- };
218
-
219
- const result = await runVerify('trailhead-aware-token', connector, {
220
- trailhead: 'mcp',
221
- });
222
-
223
- expect(result.isOk()).toBe(true);
224
- expect(seenTrailhead).toBe('mcp');
225
- });
226
- });
227
-
228
- describe('with invalid token', () => {
229
- test('returns valid: false with error for bad signature', async () => {
230
- const now = Math.floor(Date.now() / 1000);
231
- const token = await signJwt(
232
- { exp: now + 3600, sub: 'user-bad' },
233
- 'wrong-secret'
234
- );
235
-
236
- const result = await runVerify(token, jwtConnector());
237
-
238
- expect(result.isOk()).toBe(true);
239
- const value = result.unwrap();
240
- expect(value.valid).toBe(false);
241
- expect(value.error).toBeDefined();
242
- expect(value.errorCode).toBe('invalid_token');
243
- expect(value.permit).toBeUndefined();
244
- });
245
-
246
- test('returns valid: false for expired token', async () => {
247
- const past = Math.floor(Date.now() / 1000) - 3600;
248
- const token = await signJwt(
249
- { exp: past, sub: 'user-expired' },
250
- TEST_SECRET
251
- );
252
-
253
- const result = await runVerify(token, jwtConnector());
254
-
255
- expect(result.isOk()).toBe(true);
256
- const value = result.unwrap();
257
- expect(value.valid).toBe(false);
258
- expect(value.error).toBeDefined();
259
- expect(value.errorCode).toBe('expired_token');
260
- expect(value.permit).toBeUndefined();
261
- });
262
- });
263
-
264
- describe('input validation', () => {
265
- test('rejects empty bearer tokens at the boundary', async () => {
266
- const result = await executeTrail(
267
- authVerify,
268
- { token: '' },
269
- {
270
- provisions: { [authProvision.id]: jwtConnector() },
271
- }
272
- );
273
-
274
- expect(result.isErr()).toBe(true);
275
- expect(result.error).toBeInstanceOf(ValidationError);
276
- });
277
- });
278
- });
@@ -1,338 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import { Result } from '@ontrails/core';
4
-
5
- import type { AuthConnector, AuthError } from '../connectors/connector.js';
6
- import type { PermitExtractionInput } from '../extraction.js';
7
- import type { Permit } from '../permit.js';
8
- import { createJwtConnector } from '../connectors/jwt.js';
9
-
10
- // ---------------------------------------------------------------------------
11
- // Test helper: sign a JWT with HMAC-SHA256 using crypto.subtle
12
- // ---------------------------------------------------------------------------
13
-
14
- const base64url = (buf: ArrayBuffer): string => {
15
- const bytes = new Uint8Array(buf);
16
- let binary = '';
17
- for (const b of bytes) {
18
- binary += String.fromCodePoint(b);
19
- }
20
- return btoa(binary)
21
- .replaceAll('+', '-')
22
- .replaceAll('/', '_')
23
- .replace(/=+$/, '');
24
- };
25
-
26
- const base64urlEncode = (str: string): string => {
27
- const encoder = new TextEncoder();
28
- return base64url(encoder.encode(str).buffer as ArrayBuffer);
29
- };
30
-
31
- const signJwt = async (
32
- payload: Record<string, unknown>,
33
- secret: string
34
- ): Promise<string> => {
35
- const header = base64urlEncode(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
36
- const body = base64urlEncode(JSON.stringify(payload));
37
- const data = `${header}.${body}`;
38
- const encoder = new TextEncoder();
39
- const key = await crypto.subtle.importKey(
40
- 'raw',
41
- encoder.encode(secret),
42
- { hash: 'SHA-256', name: 'HMAC' },
43
- false,
44
- ['sign']
45
- );
46
- const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
47
- return `${data}.${base64url(sig)}`;
48
- };
49
-
50
- const TEST_SECRET = 'test-secret-for-hmac-256';
51
-
52
- // ---------------------------------------------------------------------------
53
- // Tests
54
- // ---------------------------------------------------------------------------
55
-
56
- /** Minimal extraction input for tests that don't need all fields. */
57
- const testInput = (
58
- overrides?: Partial<PermitExtractionInput>
59
- ): PermitExtractionInput => ({
60
- requestId: 'test-req',
61
- trailhead: 'http',
62
- ...overrides,
63
- });
64
-
65
- describe('AuthConnector interface', () => {
66
- test('accepts a valid connector implementation', async () => {
67
- const connector: AuthConnector = {
68
- // oxlint-disable-next-line require-await -- stub connector for type test
69
- authenticate: async (_input: PermitExtractionInput) => Result.ok(null),
70
- };
71
- const result = await connector.authenticate(testInput());
72
- expect(result.isOk()).toBe(true);
73
- });
74
- });
75
-
76
- /* oxlint-disable max-statements -- test suite with multiple concern groups */
77
- describe('createJwtConnector', () => {
78
- test('returns an AuthConnector', () => {
79
- const connector = createJwtConnector({ secret: TEST_SECRET });
80
- expect(connector).toBeDefined();
81
- expect(connector.authenticate).toBeInstanceOf(Function);
82
- });
83
-
84
- test('verifies a valid HS256 token and returns a Permit', async () => {
85
- const connector = createJwtConnector({ secret: TEST_SECRET });
86
- const now = Math.floor(Date.now() / 1000);
87
- const token = await signJwt(
88
- { exp: now + 3600, scope: 'read write', sub: 'user-123' },
89
- TEST_SECRET
90
- );
91
- const result = await connector.authenticate(
92
- testInput({ bearerToken: token })
93
- );
94
- expect(result.isOk()).toBe(true);
95
- const permit = result.unwrap() as Permit;
96
- expect(permit).not.toBeNull();
97
- expect(permit.id).toBe('user-123');
98
- expect(permit.scopes).toEqual(['read', 'write']);
99
- });
100
-
101
- test('rejects an expired token', async () => {
102
- const connector = createJwtConnector({ secret: TEST_SECRET });
103
- const past = Math.floor(Date.now() / 1000) - 3600;
104
- const token = await signJwt(
105
- { exp: past, sub: 'user-expired' },
106
- TEST_SECRET
107
- );
108
- const result = await connector.authenticate(
109
- testInput({ bearerToken: token })
110
- );
111
- expect(result.isErr()).toBe(true);
112
- const err = (result as ReturnType<typeof Result.err<AuthError>>).error;
113
- expect(err.code).toBe('expired_token');
114
- });
115
-
116
- test('rejects an invalid signature', async () => {
117
- const connector = createJwtConnector({ secret: TEST_SECRET });
118
- const now = Math.floor(Date.now() / 1000);
119
- const token = await signJwt(
120
- { exp: now + 3600, sub: 'user-bad-sig' },
121
- 'wrong-secret'
122
- );
123
- const result = await connector.authenticate(
124
- testInput({ bearerToken: token })
125
- );
126
- expect(result.isErr()).toBe(true);
127
- const err = (result as ReturnType<typeof Result.err<AuthError>>).error;
128
- expect(err.code).toBe('invalid_token');
129
- });
130
-
131
- test('rejects tokens with a missing subject claim', async () => {
132
- const connector = createJwtConnector({ secret: TEST_SECRET });
133
- const now = Math.floor(Date.now() / 1000);
134
- const token = await signJwt(
135
- { exp: now + 3600, scope: 'read' },
136
- TEST_SECRET
137
- );
138
- const result = await connector.authenticate(
139
- testInput({ bearerToken: token })
140
- );
141
- expect(result.isErr()).toBe(true);
142
- const err = (result as ReturnType<typeof Result.err<AuthError>>).error;
143
- expect(err.code).toBe('invalid_token');
144
- expect(err.message).toContain('Missing subject claim');
145
- });
146
-
147
- test('extracts scopes from token claims', async () => {
148
- const connector = createJwtConnector({
149
- scopesClaim: 'permissions',
150
- secret: TEST_SECRET,
151
- });
152
- const now = Math.floor(Date.now() / 1000);
153
- const token = await signJwt(
154
- {
155
- exp: now + 3600,
156
- permissions: 'admin:read admin:write',
157
- sub: 'user-scopes',
158
- },
159
- TEST_SECRET
160
- );
161
- const result = await connector.authenticate(
162
- testInput({ bearerToken: token })
163
- );
164
- expect(result.isOk()).toBe(true);
165
- const permit = result.unwrap() as Permit;
166
- expect(permit.scopes).toEqual(['admin:read', 'admin:write']);
167
- });
168
-
169
- test('extracts roles from token claims', async () => {
170
- const connector = createJwtConnector({ secret: TEST_SECRET });
171
- const now = Math.floor(Date.now() / 1000);
172
- const token = await signJwt(
173
- { exp: now + 3600, roles: ['admin', 'editor'], sub: 'user-roles' },
174
- TEST_SECRET
175
- );
176
- const result = await connector.authenticate(
177
- testInput({ bearerToken: token })
178
- );
179
- expect(result.isOk()).toBe(true);
180
- const permit = result.unwrap() as Permit;
181
- expect(permit.roles).toEqual(['admin', 'editor']);
182
- });
183
-
184
- test('returns null for missing credentials', async () => {
185
- const connector = createJwtConnector({ secret: TEST_SECRET });
186
- const result = await connector.authenticate(testInput());
187
- expect(result.isOk()).toBe(true);
188
- expect(result.unwrap()).toBeNull();
189
- });
190
-
191
- test('checks issuer claim when configured', async () => {
192
- const connector = createJwtConnector({
193
- issuer: 'https://auth.example.com',
194
- secret: TEST_SECRET,
195
- });
196
- const now = Math.floor(Date.now() / 1000);
197
- const token = await signJwt(
198
- { exp: now + 3600, iss: 'https://evil.example.com', sub: 'user-iss' },
199
- TEST_SECRET
200
- );
201
- const result = await connector.authenticate(
202
- testInput({ bearerToken: token })
203
- );
204
- expect(result.isErr()).toBe(true);
205
- const err = (result as ReturnType<typeof Result.err<AuthError>>).error;
206
- expect(err.code).toBe('invalid_token');
207
- });
208
-
209
- test('checks audience claim when configured', async () => {
210
- const connector = createJwtConnector({
211
- audience: 'my-api',
212
- secret: TEST_SECRET,
213
- });
214
- const now = Math.floor(Date.now() / 1000);
215
- const token = await signJwt(
216
- { aud: 'other-api', exp: now + 3600, sub: 'user-aud' },
217
- TEST_SECRET
218
- );
219
- const result = await connector.authenticate(
220
- testInput({ bearerToken: token })
221
- );
222
- expect(result.isErr()).toBe(true);
223
- const err = (result as ReturnType<typeof Result.err<AuthError>>).error;
224
- expect(err.code).toBe('invalid_token');
225
- });
226
-
227
- test('accepts array-format audience containing configured value', async () => {
228
- const connector = createJwtConnector({
229
- audience: 'my-api',
230
- secret: TEST_SECRET,
231
- });
232
- const now = Math.floor(Date.now() / 1000);
233
- const token = await signJwt(
234
- { aud: ['my-api', 'account'], exp: now + 3600, sub: 'user-arr-aud' },
235
- TEST_SECRET
236
- );
237
- const result = await connector.authenticate(
238
- testInput({ bearerToken: token })
239
- );
240
- expect(result.isOk()).toBe(true);
241
- const permit = result.unwrap() as Permit;
242
- expect(permit.id).toBe('user-arr-aud');
243
- });
244
-
245
- test('rejects array-format audience not containing configured value', async () => {
246
- const connector = createJwtConnector({
247
- audience: 'my-api',
248
- secret: TEST_SECRET,
249
- });
250
- const now = Math.floor(Date.now() / 1000);
251
- const token = await signJwt(
252
- { aud: ['other-api', 'account'], exp: now + 3600, sub: 'user-no-aud' },
253
- TEST_SECRET
254
- );
255
- const result = await connector.authenticate(
256
- testInput({ bearerToken: token })
257
- );
258
- expect(result.isErr()).toBe(true);
259
- const err = (result as ReturnType<typeof Result.err<AuthError>>).error;
260
- expect(err.code).toBe('invalid_token');
261
- });
262
-
263
- test('extracts scopes from array-format claim', async () => {
264
- const connector = createJwtConnector({ secret: TEST_SECRET });
265
- const now = Math.floor(Date.now() / 1000);
266
- const token = await signJwt(
267
- { exp: now + 3600, scope: ['read', 'write'], sub: 'user-arr-scope' },
268
- TEST_SECRET
269
- );
270
- const result = await connector.authenticate(
271
- testInput({ bearerToken: token })
272
- );
273
- expect(result.isOk()).toBe(true);
274
- const permit = result.unwrap() as Permit;
275
- expect(permit.scopes).toEqual(['read', 'write']);
276
- });
277
-
278
- test('filters empty strings from array-format scope claims', async () => {
279
- const connector = createJwtConnector({ secret: TEST_SECRET });
280
- const now = Math.floor(Date.now() / 1000);
281
- const token = await signJwt(
282
- {
283
- exp: now + 3600,
284
- scope: ['', 'read', '', 'write'],
285
- sub: 'user-arr-scope-empty',
286
- },
287
- TEST_SECRET
288
- );
289
- const result = await connector.authenticate(
290
- testInput({ bearerToken: token })
291
- );
292
- expect(result.isOk()).toBe(true);
293
- const permit = result.unwrap() as Permit;
294
- expect(permit.scopes).toEqual(['read', 'write']);
295
- });
296
-
297
- test('returns error for malformed signature bytes', async () => {
298
- const connector = createJwtConnector({ secret: TEST_SECRET });
299
- const now = Math.floor(Date.now() / 1000);
300
- const token = await signJwt(
301
- { exp: now + 3600, sub: 'user-bad' },
302
- TEST_SECRET
303
- );
304
- const parts = token.split('.');
305
- const malformed = `${parts[0]}.${parts[1]}.!!!invalid-base64!!!`;
306
- const result = await connector.authenticate(
307
- testInput({ bearerToken: malformed })
308
- );
309
- expect(result.isErr()).toBe(true);
310
- const err = (result as ReturnType<typeof Result.err<AuthError>>).error;
311
- expect(err.code).toBe('invalid_token');
312
- });
313
-
314
- test('accepts token with matching issuer and audience', async () => {
315
- const connector = createJwtConnector({
316
- audience: 'my-api',
317
- issuer: 'https://auth.example.com',
318
- secret: TEST_SECRET,
319
- });
320
- const now = Math.floor(Date.now() / 1000);
321
- const token = await signJwt(
322
- {
323
- aud: 'my-api',
324
- exp: now + 3600,
325
- iss: 'https://auth.example.com',
326
- scope: 'read',
327
- sub: 'user-valid',
328
- },
329
- TEST_SECRET
330
- );
331
- const result = await connector.authenticate(
332
- testInput({ bearerToken: token })
333
- );
334
- expect(result.isOk()).toBe(true);
335
- const permit = result.unwrap() as Permit;
336
- expect(permit.id).toBe('user-valid');
337
- });
338
- });