@ontrails/permits 1.0.0-beta.15 → 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 (70) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +30 -22
  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 +81 -18
  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/trails/auth-verify.ts +11 -13
  12. package/.turbo/turbo-build.log +0 -1
  13. package/.turbo/turbo-lint.log +0 -3
  14. package/.turbo/turbo-typecheck.log +0 -1
  15. package/dist/auth-layer.d.ts +0 -18
  16. package/dist/auth-layer.d.ts.map +0 -1
  17. package/dist/auth-layer.js +0 -56
  18. package/dist/auth-layer.js.map +0 -1
  19. package/dist/auth-resource.d.ts +0 -11
  20. package/dist/auth-resource.d.ts.map +0 -1
  21. package/dist/auth-resource.js +0 -22
  22. package/dist/auth-resource.js.map +0 -1
  23. package/dist/connectors/connector.d.ts +0 -26
  24. package/dist/connectors/connector.d.ts.map +0 -1
  25. package/dist/connectors/connector.js +0 -2
  26. package/dist/connectors/connector.js.map +0 -1
  27. package/dist/connectors/jwt.d.ts +0 -25
  28. package/dist/connectors/jwt.d.ts.map +0 -1
  29. package/dist/connectors/jwt.js +0 -148
  30. package/dist/connectors/jwt.js.map +0 -1
  31. package/dist/errors.d.ts +0 -15
  32. package/dist/errors.d.ts.map +0 -1
  33. package/dist/errors.js +0 -15
  34. package/dist/errors.js.map +0 -1
  35. package/dist/extraction.d.ts +0 -20
  36. package/dist/extraction.d.ts.map +0 -1
  37. package/dist/extraction.js +0 -2
  38. package/dist/extraction.js.map +0 -1
  39. package/dist/index.d.ts +0 -11
  40. package/dist/index.d.ts.map +0 -1
  41. package/dist/index.js +0 -11
  42. package/dist/index.js.map +0 -1
  43. package/dist/permit.d.ts +0 -26
  44. package/dist/permit.d.ts.map +0 -1
  45. package/dist/permit.js +0 -17
  46. package/dist/permit.js.map +0 -1
  47. package/dist/rules.d.ts +0 -47
  48. package/dist/rules.d.ts.map +0 -1
  49. package/dist/rules.js +0 -127
  50. package/dist/rules.js.map +0 -1
  51. package/dist/testing.d.ts +0 -20
  52. package/dist/testing.d.ts.map +0 -1
  53. package/dist/testing.js +0 -22
  54. package/dist/testing.js.map +0 -1
  55. package/dist/trails/auth-verify.d.ts +0 -22
  56. package/dist/trails/auth-verify.d.ts.map +0 -1
  57. package/dist/trails/auth-verify.js +0 -85
  58. package/dist/trails/auth-verify.js.map +0 -1
  59. package/src/__tests__/auth-layer.test.ts +0 -130
  60. package/src/__tests__/auth-resource.test.ts +0 -62
  61. package/src/__tests__/auth-verify.test.ts +0 -278
  62. package/src/__tests__/connector.test.ts +0 -338
  63. package/src/__tests__/permit.test.ts +0 -122
  64. package/src/__tests__/rules.test.ts +0 -239
  65. package/src/__tests__/testing.test.ts +0 -57
  66. package/src/auth-layer.ts +0 -80
  67. package/src/connectors/connector.ts +0 -35
  68. package/tsconfig.json +0 -9
  69. package/tsconfig.tests.json +0 -10
  70. package/tsconfig.tsbuildinfo +0 -1
@@ -1,85 +0,0 @@
1
- import { Result, TRAILHEAD_KEY, trail } from '@ontrails/core';
2
- import { z } from 'zod';
3
- import { authResource } from '../auth-resource.js';
4
- const permitSchema = z.object({
5
- id: z.string(),
6
- metadata: z.record(z.string(), z.unknown()).optional(),
7
- roles: z.array(z.string()).optional(),
8
- scopes: z.array(z.string()),
9
- tenantId: z.string().optional(),
10
- });
11
- const authErrorCodeSchema = z.enum([
12
- 'expired_token',
13
- 'insufficient_scope',
14
- 'invalid_token',
15
- 'missing_credentials',
16
- ]);
17
- const toOutputPermit = (permit) => ({
18
- ...(permit.metadata === undefined
19
- ? {}
20
- : { metadata: { ...permit.metadata } }),
21
- ...(permit.roles === undefined ? {} : { roles: [...permit.roles] }),
22
- ...(permit.tenantId === undefined ? {} : { tenantId: permit.tenantId }),
23
- id: permit.id,
24
- scopes: [...permit.scopes],
25
- });
26
- const isTrailhead = (value) => value === 'http' || value === 'mcp' || value === 'cli';
27
- const getTrailhead = (ctx) => {
28
- const trailhead = ctx.extensions?.[TRAILHEAD_KEY];
29
- return isTrailhead(trailhead) ? trailhead : 'http';
30
- };
31
- /**
32
- * Infrastructure trail that verifies a bearer token and returns the resolved permit.
33
- *
34
- * Reads the auth connector from `authResource` — the connector is configured
35
- * at bootstrap (e.g. JWT with HMAC secret). The mock connector always
36
- * succeeds with a null permit, so `testAll(app)` works without configuration.
37
- */
38
- export const authVerify = trail('auth.verify', {
39
- blaze: async (input, ctx) => {
40
- const connector = authResource.from(ctx);
41
- const result = await connector.authenticate({
42
- bearerToken: input.token,
43
- requestId: ctx.requestId,
44
- trailhead: getTrailhead(ctx),
45
- });
46
- if (result.isErr()) {
47
- return Result.ok({
48
- error: result.error.message,
49
- errorCode: result.error.code,
50
- valid: false,
51
- });
52
- }
53
- const permit = result.value;
54
- if (!permit) {
55
- return Result.ok({
56
- error: 'No credentials',
57
- errorCode: 'missing_credentials',
58
- valid: false,
59
- });
60
- }
61
- return Result.ok({
62
- permit: toOutputPermit(permit),
63
- valid: true,
64
- });
65
- },
66
- examples: [
67
- {
68
- input: { token: 'test-token' },
69
- name: 'Verify a token',
70
- },
71
- ],
72
- input: z.object({
73
- token: z.string().min(1).describe('Bearer token to verify'),
74
- }),
75
- intent: 'read',
76
- meta: { category: 'infrastructure' },
77
- output: z.object({
78
- error: z.string().optional(),
79
- errorCode: authErrorCodeSchema.optional(),
80
- permit: permitSchema.optional(),
81
- valid: z.boolean(),
82
- }),
83
- resources: [authResource],
84
- });
85
- //# sourceMappingURL=auth-verify.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-verify.js","sourceRoot":"","sources":["../../src/trails/auth-verify.ts"],"names":[],"mappings":"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"}
@@ -1,130 +0,0 @@
1
- /* oxlint-disable require-await -- layer wrappers satisfy async interfaces without awaiting */
2
- import { describe, expect, test } from 'bun:test';
3
-
4
- import { Result, trail } from '@ontrails/core';
5
- import type { TrailContext } from '@ontrails/core';
6
- import { z } from 'zod';
7
-
8
- import { authLayer } from '../auth-layer';
9
- import { PermitError } from '../errors';
10
-
11
- // ---------------------------------------------------------------------------
12
- // Helpers
13
- // ---------------------------------------------------------------------------
14
-
15
- const makeCtx = (permit?: {
16
- id: string;
17
- scopes: readonly string[];
18
- }): TrailContext => ({
19
- abortSignal: AbortSignal.timeout(5000),
20
- permit,
21
- requestId: 'test-auth',
22
- });
23
-
24
- const okImpl = async () => Result.ok({ done: true });
25
-
26
- // ---------------------------------------------------------------------------
27
- // Tests
28
- // ---------------------------------------------------------------------------
29
-
30
- describe('authLayer', () => {
31
- test('has correct name and description', () => {
32
- expect(authLayer.name).toBe('auth');
33
- expect(authLayer.description).toBeDefined();
34
- });
35
-
36
- describe('pass-through cases', () => {
37
- test('passes through when trail has no permit field', async () => {
38
- const t = trail('test.nopermit', {
39
- blaze: okImpl,
40
- input: z.object({}),
41
- output: z.object({ done: z.boolean() }),
42
- });
43
-
44
- const wrapped = authLayer.wrap(t, okImpl);
45
- const result = await wrapped({}, makeCtx());
46
-
47
- expect(result.isOk()).toBe(true);
48
- expect(result.unwrap()).toEqual({ done: true });
49
- });
50
-
51
- test('passes through when trail permit is public', async () => {
52
- const t = trail('test.public', {
53
- blaze: okImpl,
54
- input: z.object({}),
55
- output: z.object({ done: z.boolean() }),
56
- permit: 'public',
57
- });
58
-
59
- const wrapped = authLayer.wrap(t, okImpl);
60
- const result = await wrapped({}, makeCtx());
61
-
62
- expect(result.isOk()).toBe(true);
63
- expect(result.unwrap()).toEqual({ done: true });
64
- });
65
- });
66
-
67
- describe('scope enforcement', () => {
68
- const scopedTrail = trail('test.scoped', {
69
- blaze: okImpl,
70
- input: z.object({}),
71
- output: z.object({ done: z.boolean() }),
72
- permit: { scopes: ['user:read'] },
73
- });
74
-
75
- test('passes when ctx.permit has matching scopes', async () => {
76
- const wrapped = authLayer.wrap(scopedTrail, okImpl);
77
- const result = await wrapped(
78
- {},
79
- makeCtx({ id: 'usr-1', scopes: ['user:read'] })
80
- );
81
-
82
- expect(result.isOk()).toBe(true);
83
- expect(result.unwrap()).toEqual({ done: true });
84
- });
85
-
86
- test('returns error when ctx has no permit', async () => {
87
- const wrapped = authLayer.wrap(scopedTrail, okImpl);
88
- const result = await wrapped({}, makeCtx());
89
-
90
- expect(result.isErr()).toBe(true);
91
- const err = (result as unknown as { error: PermitError }).error;
92
- expect(err).toBeInstanceOf(PermitError);
93
- expect(err.message).toContain('No permit');
94
- });
95
-
96
- test('returns error when permit is missing required scopes', async () => {
97
- const multiScopeTrail = trail('test.multi', {
98
- blaze: okImpl,
99
- input: z.object({}),
100
- output: z.object({ done: z.boolean() }),
101
- permit: { scopes: ['user:read', 'user:write'] },
102
- });
103
-
104
- const wrapped = authLayer.wrap(multiScopeTrail, okImpl);
105
- const result = await wrapped(
106
- {},
107
- makeCtx({ id: 'usr-1', scopes: ['user:read'] })
108
- );
109
-
110
- expect(result.isErr()).toBe(true);
111
- const err = (result as unknown as { error: PermitError }).error;
112
- expect(err).toBeInstanceOf(PermitError);
113
- expect(err.message).toContain('user:write');
114
- });
115
-
116
- test('passes when permit has superset of required scopes', async () => {
117
- const wrapped = authLayer.wrap(scopedTrail, okImpl);
118
- const result = await wrapped(
119
- {},
120
- makeCtx({
121
- id: 'usr-1',
122
- scopes: ['user:read', 'user:write', 'admin'],
123
- })
124
- );
125
-
126
- expect(result.isOk()).toBe(true);
127
- expect(result.unwrap()).toEqual({ done: true });
128
- });
129
- });
130
- });
@@ -1,62 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import type { ResourceContext } from '@ontrails/core';
4
-
5
- import type { AuthConnector } from '../connectors/connector.js';
6
- import { authResource } from '../auth-resource.js';
7
- import type { PermitExtractionInput } from '../extraction.js';
8
-
9
- /** Minimal extraction input for tests. */
10
- const testInput = (
11
- overrides?: Partial<PermitExtractionInput>
12
- ): PermitExtractionInput => ({
13
- requestId: 'test-svc-req',
14
- trailhead: 'http',
15
- ...overrides,
16
- });
17
-
18
- // ---------------------------------------------------------------------------
19
- // Helpers
20
- // ---------------------------------------------------------------------------
21
-
22
- const testSvcCtx: ResourceContext = {
23
- config: undefined,
24
- cwd: '/tmp',
25
- env: {},
26
- workspaceRoot: '/tmp',
27
- };
28
-
29
- // ---------------------------------------------------------------------------
30
- // Tests
31
- // ---------------------------------------------------------------------------
32
-
33
- describe('authResource', () => {
34
- test('has correct id and kind', () => {
35
- expect(authResource.id).toBe('auth');
36
- expect(authResource.kind).toBe('resource');
37
- });
38
-
39
- test('has infrastructure meta', () => {
40
- expect(authResource.meta).toEqual({ category: 'infrastructure' });
41
- });
42
-
43
- test('mock returns an AuthConnector', async () => {
44
- const mock = authResource.mock?.();
45
- expect(mock).toBeDefined();
46
-
47
- const connector = mock as AuthConnector;
48
- const result = await connector.authenticate(testInput());
49
- expect(result.isOk()).toBe(true);
50
- expect(result.unwrap()).toBeNull();
51
- });
52
-
53
- test('create returns Result.ok with an AuthConnector', async () => {
54
- const result = await authResource.create(testSvcCtx);
55
- expect(result.isOk()).toBe(true);
56
-
57
- const connector = result.unwrap() as AuthConnector;
58
- const authResult = await connector.authenticate(testInput());
59
- expect(authResult.isOk()).toBe(true);
60
- expect(authResult.unwrap()).toBeNull();
61
- });
62
- });
@@ -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 { authResource } from '../auth-resource.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 resource. */
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
- resources: { [authResource.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 authResource dependency', () => {
138
- expect(authVerify.resources).toHaveLength(1);
139
- expect(authVerify.resources[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
- resources: { [authResource.id]: jwtConnector() },
271
- }
272
- );
273
-
274
- expect(result.isErr()).toBe(true);
275
- expect(result.error).toBeInstanceOf(ValidationError);
276
- });
277
- });
278
- });