@ontrails/permits 1.0.0-beta.12 → 1.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,17 +1,45 @@
1
1
  # @ontrails/permits
2
2
 
3
- ## 1.0.0-beta.12
3
+ ## 1.0.0-beta.13
4
4
 
5
5
  ### Minor Changes
6
6
 
7
- - Complete trifecta for config, permits, and crumbs (formerly tracks)
7
+ - 6944147: Complete trifecta for config, permits, and tracker (formerly tracks)
8
8
 
9
- - **config**: Add `configService`, `config.layer`, `config.trail`, and `config.workspace` trails with full `defineConfig`, `resolve`, `describe`, `explain`, `doctor`, and code generation support
9
+ - **config**: Add `configProvision`, `configGate`, `config.trail`, and `config.workspace` trails with full `defineConfig`, `resolve`, `describe`, `explain`, `doctor`, and code generation support
10
10
  - **permits**: Add `authService` and `auth.verify` trail for runtime authorization checks
11
- - **crumbs**: Rename tracks to crumbs; add `crumbsService` and `crumbs.status` trail for structured event tracking
11
+ - **tracker**: Rename tracks to tracker; add `trackerProvision` and `tracker.status` trail for structured signal tracking
12
+ - **cli**: Fix build flag handling and improve bootstrap scaffolding
13
+ - **testing**: Expand test context helpers and example-based testing utilities
14
+ - **core/mcp/http**: Internal alignment for provision and composition updates
15
+
16
+ - Trail-native vocabulary cutover. Breaking API field renames across all packages:
17
+
18
+ - Trail spec: `run:` → `blaze:`, `follow:` → `crosses:`, `services:` → `provisions:`, `metadata:` → `meta:`, `emits:` → `signals:`
19
+ - Runtime: `ctx.follow()` → `ctx.cross()`, `ctx.emit()` → `ctx.signal()`, `ctx.signal` (abort) → `ctx.abortSignal`
20
+ - Entry points: `blaze(app)` → `trailhead(app)`
21
+ - Package rename: `@ontrails/crumbs` → `@ontrails/tracker`
22
+ - Wrapper types: `Layer` → `Gate`, `layers`/`middleware` → `gates`
23
+ - Transport: `surface` → `trailhead`, `adapter` → `connector`
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [6944147]
28
+ - Updated dependencies
29
+ - @ontrails/core@1.0.0-beta.13
30
+
31
+ ## 1.0.0-beta.12
32
+
33
+ ### Minor Changes
34
+
35
+ - Complete trifecta for config, permits, and tracker (formerly tracks)
36
+
37
+ - **config**: Add `configProvision`, `config.gate`, `config.trail`, and `config.workspace` trails with full `defineConfig`, `resolve`, `describe`, `explain`, `doctor`, and code generation support
38
+ - **permits**: Add `authProvision` and `auth.verify` trail for runtime authorization checks
39
+ - **tracker**: Rename tracks to tracker; add `trackerProvision` and `tracker.status` trail for structured event tracking
12
40
  - **cli**: Fix build flag handling and improve bootstrap scaffolding
13
41
  - **testing**: Expand test context helpers and example-based testing utilities
14
- - **core/mcp/http**: Internal alignment for service and composition updates
42
+ - **core/mcp/http**: Internal alignment for provision and composition updates
15
43
 
16
44
  ### Patch Changes
17
45
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@ontrails/permits",
3
- "version": "1.0.0-beta.12",
3
+ "version": "1.0.0-beta.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
7
- "./jwt": "./src/adapters/jwt.ts",
7
+ "./jwt": "./src/connectors/jwt.ts",
8
8
  "./package.json": "./package.json"
9
9
  },
10
10
  "scripts": {
@@ -1,11 +1,11 @@
1
- /* oxlint-disable require-await -- layer wrappers satisfy async interfaces without awaiting */
1
+ /* oxlint-disable require-await -- gate wrappers satisfy async interfaces without awaiting */
2
2
  import { describe, expect, test } from 'bun:test';
3
3
 
4
4
  import { Result, trail } from '@ontrails/core';
5
5
  import type { TrailContext } from '@ontrails/core';
6
6
  import { z } from 'zod';
7
7
 
8
- import { authLayer } from '../auth-layer';
8
+ import { authGate } from '../auth-gate';
9
9
  import { PermitError } from '../errors';
10
10
 
11
11
  // ---------------------------------------------------------------------------
@@ -16,9 +16,9 @@ const makeCtx = (permit?: {
16
16
  id: string;
17
17
  scopes: readonly string[];
18
18
  }): TrailContext => ({
19
+ abortSignal: AbortSignal.timeout(5000),
19
20
  permit,
20
21
  requestId: 'test-auth',
21
- signal: AbortSignal.timeout(5000),
22
22
  });
23
23
 
24
24
  const okImpl = async () => Result.ok({ done: true });
@@ -27,21 +27,21 @@ const okImpl = async () => Result.ok({ done: true });
27
27
  // Tests
28
28
  // ---------------------------------------------------------------------------
29
29
 
30
- describe('authLayer', () => {
30
+ describe('authGate', () => {
31
31
  test('has correct name and description', () => {
32
- expect(authLayer.name).toBe('auth');
33
- expect(authLayer.description).toBeDefined();
32
+ expect(authGate.name).toBe('auth');
33
+ expect(authGate.description).toBeDefined();
34
34
  });
35
35
 
36
36
  describe('pass-through cases', () => {
37
37
  test('passes through when trail has no permit field', async () => {
38
38
  const t = trail('test.nopermit', {
39
+ blaze: okImpl,
39
40
  input: z.object({}),
40
41
  output: z.object({ done: z.boolean() }),
41
- run: okImpl,
42
42
  });
43
43
 
44
- const wrapped = authLayer.wrap(t, okImpl);
44
+ const wrapped = authGate.wrap(t, okImpl);
45
45
  const result = await wrapped({}, makeCtx());
46
46
 
47
47
  expect(result.isOk()).toBe(true);
@@ -50,13 +50,13 @@ describe('authLayer', () => {
50
50
 
51
51
  test('passes through when trail permit is public', async () => {
52
52
  const t = trail('test.public', {
53
+ blaze: okImpl,
53
54
  input: z.object({}),
54
55
  output: z.object({ done: z.boolean() }),
55
56
  permit: 'public',
56
- run: okImpl,
57
57
  });
58
58
 
59
- const wrapped = authLayer.wrap(t, okImpl);
59
+ const wrapped = authGate.wrap(t, okImpl);
60
60
  const result = await wrapped({}, makeCtx());
61
61
 
62
62
  expect(result.isOk()).toBe(true);
@@ -66,14 +66,14 @@ describe('authLayer', () => {
66
66
 
67
67
  describe('scope enforcement', () => {
68
68
  const scopedTrail = trail('test.scoped', {
69
+ blaze: okImpl,
69
70
  input: z.object({}),
70
71
  output: z.object({ done: z.boolean() }),
71
72
  permit: { scopes: ['user:read'] },
72
- run: okImpl,
73
73
  });
74
74
 
75
75
  test('passes when ctx.permit has matching scopes', async () => {
76
- const wrapped = authLayer.wrap(scopedTrail, okImpl);
76
+ const wrapped = authGate.wrap(scopedTrail, okImpl);
77
77
  const result = await wrapped(
78
78
  {},
79
79
  makeCtx({ id: 'usr-1', scopes: ['user:read'] })
@@ -84,7 +84,7 @@ describe('authLayer', () => {
84
84
  });
85
85
 
86
86
  test('returns error when ctx has no permit', async () => {
87
- const wrapped = authLayer.wrap(scopedTrail, okImpl);
87
+ const wrapped = authGate.wrap(scopedTrail, okImpl);
88
88
  const result = await wrapped({}, makeCtx());
89
89
 
90
90
  expect(result.isErr()).toBe(true);
@@ -95,13 +95,13 @@ describe('authLayer', () => {
95
95
 
96
96
  test('returns error when permit is missing required scopes', async () => {
97
97
  const multiScopeTrail = trail('test.multi', {
98
+ blaze: okImpl,
98
99
  input: z.object({}),
99
100
  output: z.object({ done: z.boolean() }),
100
101
  permit: { scopes: ['user:read', 'user:write'] },
101
- run: okImpl,
102
102
  });
103
103
 
104
- const wrapped = authLayer.wrap(multiScopeTrail, okImpl);
104
+ const wrapped = authGate.wrap(multiScopeTrail, okImpl);
105
105
  const result = await wrapped(
106
106
  {},
107
107
  makeCtx({ id: 'usr-1', scopes: ['user:read'] })
@@ -114,7 +114,7 @@ describe('authLayer', () => {
114
114
  });
115
115
 
116
116
  test('passes when permit has superset of required scopes', async () => {
117
- const wrapped = authLayer.wrap(scopedTrail, okImpl);
117
+ const wrapped = authGate.wrap(scopedTrail, okImpl);
118
118
  const result = await wrapped(
119
119
  {},
120
120
  makeCtx({
@@ -1,9 +1,9 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
 
3
- import type { ServiceContext } from '@ontrails/core';
3
+ import type { ProvisionContext } from '@ontrails/core';
4
4
 
5
- import type { AuthAdapter } from '../adapter.js';
6
- import { authService } from '../auth-service.js';
5
+ import type { AuthConnector } from '../connectors/connector.js';
6
+ import { authProvision } from '../auth-provision.js';
7
7
  import type { PermitExtractionInput } from '../extraction.js';
8
8
 
9
9
  /** Minimal extraction input for tests. */
@@ -11,7 +11,7 @@ const testInput = (
11
11
  overrides?: Partial<PermitExtractionInput>
12
12
  ): PermitExtractionInput => ({
13
13
  requestId: 'test-svc-req',
14
- surface: 'http',
14
+ trailhead: 'http',
15
15
  ...overrides,
16
16
  });
17
17
 
@@ -19,7 +19,7 @@ const testInput = (
19
19
  // Helpers
20
20
  // ---------------------------------------------------------------------------
21
21
 
22
- const testSvcCtx: ServiceContext = {
22
+ const testSvcCtx: ProvisionContext = {
23
23
  config: undefined,
24
24
  cwd: '/tmp',
25
25
  env: {},
@@ -30,32 +30,32 @@ const testSvcCtx: ServiceContext = {
30
30
  // Tests
31
31
  // ---------------------------------------------------------------------------
32
32
 
33
- describe('authService', () => {
33
+ describe('authProvision', () => {
34
34
  test('has correct id and kind', () => {
35
- expect(authService.id).toBe('auth');
36
- expect(authService.kind).toBe('service');
35
+ expect(authProvision.id).toBe('auth');
36
+ expect(authProvision.kind).toBe('provision');
37
37
  });
38
38
 
39
- test('has infrastructure metadata', () => {
40
- expect(authService.metadata).toEqual({ category: 'infrastructure' });
39
+ test('has infrastructure meta', () => {
40
+ expect(authProvision.meta).toEqual({ category: 'infrastructure' });
41
41
  });
42
42
 
43
- test('mock returns an AuthAdapter', async () => {
44
- const mock = authService.mock?.();
43
+ test('mock returns an AuthConnector', async () => {
44
+ const mock = authProvision.mock?.();
45
45
  expect(mock).toBeDefined();
46
46
 
47
- const adapter = mock as AuthAdapter;
48
- const result = await adapter.authenticate(testInput());
47
+ const connector = mock as AuthConnector;
48
+ const result = await connector.authenticate(testInput());
49
49
  expect(result.isOk()).toBe(true);
50
50
  expect(result.unwrap()).toBeNull();
51
51
  });
52
52
 
53
- test('create returns Result.ok with an AuthAdapter', async () => {
54
- const result = await authService.create(testSvcCtx);
53
+ test('create returns Result.ok with an AuthConnector', async () => {
54
+ const result = await authProvision.create(testSvcCtx);
55
55
  expect(result.isOk()).toBe(true);
56
56
 
57
- const adapter = result.unwrap() as AuthAdapter;
58
- const authResult = await adapter.authenticate(testInput());
57
+ const connector = result.unwrap() as AuthConnector;
58
+ const authResult = await connector.authenticate(testInput());
59
59
  expect(authResult.isOk()).toBe(true);
60
60
  expect(authResult.unwrap()).toBeNull();
61
61
  });
@@ -2,14 +2,14 @@ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import {
4
4
  Result,
5
- SURFACE_KEY,
5
+ TRAILHEAD_KEY,
6
6
  ValidationError,
7
7
  executeTrail,
8
8
  } from '@ontrails/core';
9
9
 
10
- import type { AuthAdapter } from '../adapter.js';
11
- import { authService } from '../auth-service.js';
12
- import { createJwtAdapter } from '../adapters/jwt.js';
10
+ import type { AuthConnector } from '../connectors/connector.js';
11
+ import { authProvision } from '../auth-provision.js';
12
+ import { createJwtConnector } from '../connectors/jwt.js';
13
13
  import type { Permit } from '../permit.js';
14
14
  import { authVerify } from '../trails/auth-verify.js';
15
15
 
@@ -55,15 +55,16 @@ const signJwt = async (
55
55
 
56
56
  const TEST_SECRET = 'test-secret-for-hmac-256';
57
57
 
58
- /** Create an AuthAdapter wired to a JWT secret. */
59
- const jwtAdapter = (): AuthAdapter => createJwtAdapter({ secret: TEST_SECRET });
58
+ /** Create an AuthConnector wired to a JWT secret. */
59
+ const jwtConnector = (): AuthConnector =>
60
+ createJwtConnector({ secret: TEST_SECRET });
60
61
 
61
- /** Execute auth.verify with a given adapter injected as the auth service. */
62
+ /** Execute auth.verify with a given connector injected as the auth provision. */
62
63
  const runVerify = async (
63
64
  token: string,
64
- adapter: AuthAdapter,
65
+ connector: AuthConnector,
65
66
  options?: {
66
- surface?: 'http' | 'mcp' | 'cli';
67
+ trailhead?: 'http' | 'mcp' | 'cli';
67
68
  }
68
69
  ): Promise<
69
70
  Result<
@@ -91,10 +92,10 @@ const runVerify = async (
91
92
  { token },
92
93
  {
93
94
  ctx:
94
- options?.surface === undefined
95
+ options?.trailhead === undefined
95
96
  ? undefined
96
- : { extensions: { [SURFACE_KEY]: options.surface } },
97
- services: { [authService.id]: adapter },
97
+ : { extensions: { [TRAILHEAD_KEY]: options.trailhead } },
98
+ provisions: { [authProvision.id]: connector },
98
99
  }
99
100
  );
100
101
  return result as Result<
@@ -124,8 +125,8 @@ describe('auth.verify trail', () => {
124
125
  expect(authVerify.intent).toBe('read');
125
126
  });
126
127
 
127
- test('has infrastructure metadata', () => {
128
- expect(authVerify.metadata).toEqual({ category: 'infrastructure' });
128
+ test('has infrastructure meta', () => {
129
+ expect(authVerify.meta).toEqual({ category: 'infrastructure' });
129
130
  });
130
131
 
131
132
  test('has examples', () => {
@@ -133,20 +134,20 @@ describe('auth.verify trail', () => {
133
134
  expect(authVerify.examples?.length).toBeGreaterThan(0);
134
135
  });
135
136
 
136
- test('declares authService dependency', () => {
137
- expect(authVerify.services).toHaveLength(1);
138
- expect(authVerify.services[0]?.id).toBe('auth');
137
+ test('declares authProvision dependency', () => {
138
+ expect(authVerify.provisions).toHaveLength(1);
139
+ expect(authVerify.provisions[0]?.id).toBe('auth');
139
140
  });
140
141
  });
141
142
 
142
- describe('with mock adapter (no credentials)', () => {
143
+ describe('with mock connector (no credentials)', () => {
143
144
  test('returns valid: false with error message', async () => {
144
- const noopAdapter: AuthAdapter = {
145
+ const noopConnector: AuthConnector = {
145
146
  // oxlint-disable-next-line require-await -- satisfies async interface
146
147
  authenticate: async () => Result.ok(null),
147
148
  };
148
149
 
149
- const result = await runVerify('some-token', noopAdapter);
150
+ const result = await runVerify('some-token', noopConnector);
150
151
 
151
152
  expect(result.isOk()).toBe(true);
152
153
  const value = result.unwrap();
@@ -165,7 +166,7 @@ describe('auth.verify trail', () => {
165
166
  TEST_SECRET
166
167
  );
167
168
 
168
- const result = await runVerify(token, jwtAdapter());
169
+ const result = await runVerify(token, jwtConnector());
169
170
 
170
171
  expect(result.isOk()).toBe(true);
171
172
  const value = result.unwrap();
@@ -177,7 +178,7 @@ describe('auth.verify trail', () => {
177
178
  expect(value.error).toBeUndefined();
178
179
  });
179
180
 
180
- test('returns the full permit payload from the adapter', async () => {
181
+ test('returns the full permit payload from the connector', async () => {
181
182
  const permit: Permit = {
182
183
  id: 'user-42',
183
184
  metadata: { plan: 'pro' },
@@ -185,12 +186,12 @@ describe('auth.verify trail', () => {
185
186
  scopes: ['read', 'write'],
186
187
  tenantId: 'tenant-1',
187
188
  };
188
- const adapter: AuthAdapter = {
189
+ const connector: AuthConnector = {
189
190
  // oxlint-disable-next-line require-await -- satisfies async interface
190
191
  authenticate: async () => Result.ok(permit),
191
192
  };
192
193
 
193
- const result = await runVerify('full-permit-token', adapter);
194
+ const result = await runVerify('full-permit-token', connector);
194
195
 
195
196
  expect(result.isOk()).toBe(true);
196
197
  expect(result.unwrap().permit).toEqual({
@@ -202,12 +203,12 @@ describe('auth.verify trail', () => {
202
203
  });
203
204
  });
204
205
 
205
- test('forwards the invoking surface from trail context', async () => {
206
- let seenSurface: string | undefined;
207
- const adapter: AuthAdapter = {
208
- // oxlint-disable-next-line require-await -- captures adapter input
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
209
210
  authenticate: async (input) => {
210
- seenSurface = input.surface;
211
+ seenTrailhead = input.trailhead;
211
212
  return Result.ok({
212
213
  id: 'user-42',
213
214
  scopes: ['read'],
@@ -215,12 +216,12 @@ describe('auth.verify trail', () => {
215
216
  },
216
217
  };
217
218
 
218
- const result = await runVerify('surface-aware-token', adapter, {
219
- surface: 'mcp',
219
+ const result = await runVerify('trailhead-aware-token', connector, {
220
+ trailhead: 'mcp',
220
221
  });
221
222
 
222
223
  expect(result.isOk()).toBe(true);
223
- expect(seenSurface).toBe('mcp');
224
+ expect(seenTrailhead).toBe('mcp');
224
225
  });
225
226
  });
226
227
 
@@ -232,7 +233,7 @@ describe('auth.verify trail', () => {
232
233
  'wrong-secret'
233
234
  );
234
235
 
235
- const result = await runVerify(token, jwtAdapter());
236
+ const result = await runVerify(token, jwtConnector());
236
237
 
237
238
  expect(result.isOk()).toBe(true);
238
239
  const value = result.unwrap();
@@ -249,7 +250,7 @@ describe('auth.verify trail', () => {
249
250
  TEST_SECRET
250
251
  );
251
252
 
252
- const result = await runVerify(token, jwtAdapter());
253
+ const result = await runVerify(token, jwtConnector());
253
254
 
254
255
  expect(result.isOk()).toBe(true);
255
256
  const value = result.unwrap();
@@ -266,7 +267,7 @@ describe('auth.verify trail', () => {
266
267
  authVerify,
267
268
  { token: '' },
268
269
  {
269
- services: { [authService.id]: jwtAdapter() },
270
+ provisions: { [authProvision.id]: jwtConnector() },
270
271
  }
271
272
  );
272
273
 
@@ -2,10 +2,10 @@ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import { Result } from '@ontrails/core';
4
4
 
5
- import type { AuthAdapter, AuthError } from '../adapter.js';
5
+ import type { AuthConnector, AuthError } from '../connectors/connector.js';
6
6
  import type { PermitExtractionInput } from '../extraction.js';
7
7
  import type { Permit } from '../permit.js';
8
- import { createJwtAdapter } from '../adapters/jwt.js';
8
+ import { createJwtConnector } from '../connectors/jwt.js';
9
9
 
10
10
  // ---------------------------------------------------------------------------
11
11
  // Test helper: sign a JWT with HMAC-SHA256 using crypto.subtle
@@ -58,37 +58,37 @@ const testInput = (
58
58
  overrides?: Partial<PermitExtractionInput>
59
59
  ): PermitExtractionInput => ({
60
60
  requestId: 'test-req',
61
- surface: 'http',
61
+ trailhead: 'http',
62
62
  ...overrides,
63
63
  });
64
64
 
65
- describe('AuthAdapter interface', () => {
66
- test('accepts a valid adapter implementation', async () => {
67
- const adapter: AuthAdapter = {
68
- // oxlint-disable-next-line require-await -- stub adapter for type test
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
69
  authenticate: async (_input: PermitExtractionInput) => Result.ok(null),
70
70
  };
71
- const result = await adapter.authenticate(testInput());
71
+ const result = await connector.authenticate(testInput());
72
72
  expect(result.isOk()).toBe(true);
73
73
  });
74
74
  });
75
75
 
76
76
  /* oxlint-disable max-statements -- test suite with multiple concern groups */
77
- describe('createJwtAdapter', () => {
78
- test('returns an AuthAdapter', () => {
79
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
80
- expect(adapter).toBeDefined();
81
- expect(adapter.authenticate).toBeInstanceOf(Function);
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
82
  });
83
83
 
84
84
  test('verifies a valid HS256 token and returns a Permit', async () => {
85
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
85
+ const connector = createJwtConnector({ secret: TEST_SECRET });
86
86
  const now = Math.floor(Date.now() / 1000);
87
87
  const token = await signJwt(
88
88
  { exp: now + 3600, scope: 'read write', sub: 'user-123' },
89
89
  TEST_SECRET
90
90
  );
91
- const result = await adapter.authenticate(
91
+ const result = await connector.authenticate(
92
92
  testInput({ bearerToken: token })
93
93
  );
94
94
  expect(result.isOk()).toBe(true);
@@ -99,13 +99,13 @@ describe('createJwtAdapter', () => {
99
99
  });
100
100
 
101
101
  test('rejects an expired token', async () => {
102
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
102
+ const connector = createJwtConnector({ secret: TEST_SECRET });
103
103
  const past = Math.floor(Date.now() / 1000) - 3600;
104
104
  const token = await signJwt(
105
105
  { exp: past, sub: 'user-expired' },
106
106
  TEST_SECRET
107
107
  );
108
- const result = await adapter.authenticate(
108
+ const result = await connector.authenticate(
109
109
  testInput({ bearerToken: token })
110
110
  );
111
111
  expect(result.isErr()).toBe(true);
@@ -114,13 +114,13 @@ describe('createJwtAdapter', () => {
114
114
  });
115
115
 
116
116
  test('rejects an invalid signature', async () => {
117
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
117
+ const connector = createJwtConnector({ secret: TEST_SECRET });
118
118
  const now = Math.floor(Date.now() / 1000);
119
119
  const token = await signJwt(
120
120
  { exp: now + 3600, sub: 'user-bad-sig' },
121
121
  'wrong-secret'
122
122
  );
123
- const result = await adapter.authenticate(
123
+ const result = await connector.authenticate(
124
124
  testInput({ bearerToken: token })
125
125
  );
126
126
  expect(result.isErr()).toBe(true);
@@ -129,13 +129,13 @@ describe('createJwtAdapter', () => {
129
129
  });
130
130
 
131
131
  test('rejects tokens with a missing subject claim', async () => {
132
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
132
+ const connector = createJwtConnector({ secret: TEST_SECRET });
133
133
  const now = Math.floor(Date.now() / 1000);
134
134
  const token = await signJwt(
135
135
  { exp: now + 3600, scope: 'read' },
136
136
  TEST_SECRET
137
137
  );
138
- const result = await adapter.authenticate(
138
+ const result = await connector.authenticate(
139
139
  testInput({ bearerToken: token })
140
140
  );
141
141
  expect(result.isErr()).toBe(true);
@@ -145,7 +145,7 @@ describe('createJwtAdapter', () => {
145
145
  });
146
146
 
147
147
  test('extracts scopes from token claims', async () => {
148
- const adapter = createJwtAdapter({
148
+ const connector = createJwtConnector({
149
149
  scopesClaim: 'permissions',
150
150
  secret: TEST_SECRET,
151
151
  });
@@ -158,7 +158,7 @@ describe('createJwtAdapter', () => {
158
158
  },
159
159
  TEST_SECRET
160
160
  );
161
- const result = await adapter.authenticate(
161
+ const result = await connector.authenticate(
162
162
  testInput({ bearerToken: token })
163
163
  );
164
164
  expect(result.isOk()).toBe(true);
@@ -167,13 +167,13 @@ describe('createJwtAdapter', () => {
167
167
  });
168
168
 
169
169
  test('extracts roles from token claims', async () => {
170
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
170
+ const connector = createJwtConnector({ secret: TEST_SECRET });
171
171
  const now = Math.floor(Date.now() / 1000);
172
172
  const token = await signJwt(
173
173
  { exp: now + 3600, roles: ['admin', 'editor'], sub: 'user-roles' },
174
174
  TEST_SECRET
175
175
  );
176
- const result = await adapter.authenticate(
176
+ const result = await connector.authenticate(
177
177
  testInput({ bearerToken: token })
178
178
  );
179
179
  expect(result.isOk()).toBe(true);
@@ -182,14 +182,14 @@ describe('createJwtAdapter', () => {
182
182
  });
183
183
 
184
184
  test('returns null for missing credentials', async () => {
185
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
186
- const result = await adapter.authenticate(testInput());
185
+ const connector = createJwtConnector({ secret: TEST_SECRET });
186
+ const result = await connector.authenticate(testInput());
187
187
  expect(result.isOk()).toBe(true);
188
188
  expect(result.unwrap()).toBeNull();
189
189
  });
190
190
 
191
191
  test('checks issuer claim when configured', async () => {
192
- const adapter = createJwtAdapter({
192
+ const connector = createJwtConnector({
193
193
  issuer: 'https://auth.example.com',
194
194
  secret: TEST_SECRET,
195
195
  });
@@ -198,7 +198,7 @@ describe('createJwtAdapter', () => {
198
198
  { exp: now + 3600, iss: 'https://evil.example.com', sub: 'user-iss' },
199
199
  TEST_SECRET
200
200
  );
201
- const result = await adapter.authenticate(
201
+ const result = await connector.authenticate(
202
202
  testInput({ bearerToken: token })
203
203
  );
204
204
  expect(result.isErr()).toBe(true);
@@ -207,7 +207,7 @@ describe('createJwtAdapter', () => {
207
207
  });
208
208
 
209
209
  test('checks audience claim when configured', async () => {
210
- const adapter = createJwtAdapter({
210
+ const connector = createJwtConnector({
211
211
  audience: 'my-api',
212
212
  secret: TEST_SECRET,
213
213
  });
@@ -216,7 +216,7 @@ describe('createJwtAdapter', () => {
216
216
  { aud: 'other-api', exp: now + 3600, sub: 'user-aud' },
217
217
  TEST_SECRET
218
218
  );
219
- const result = await adapter.authenticate(
219
+ const result = await connector.authenticate(
220
220
  testInput({ bearerToken: token })
221
221
  );
222
222
  expect(result.isErr()).toBe(true);
@@ -225,7 +225,7 @@ describe('createJwtAdapter', () => {
225
225
  });
226
226
 
227
227
  test('accepts array-format audience containing configured value', async () => {
228
- const adapter = createJwtAdapter({
228
+ const connector = createJwtConnector({
229
229
  audience: 'my-api',
230
230
  secret: TEST_SECRET,
231
231
  });
@@ -234,7 +234,7 @@ describe('createJwtAdapter', () => {
234
234
  { aud: ['my-api', 'account'], exp: now + 3600, sub: 'user-arr-aud' },
235
235
  TEST_SECRET
236
236
  );
237
- const result = await adapter.authenticate(
237
+ const result = await connector.authenticate(
238
238
  testInput({ bearerToken: token })
239
239
  );
240
240
  expect(result.isOk()).toBe(true);
@@ -243,7 +243,7 @@ describe('createJwtAdapter', () => {
243
243
  });
244
244
 
245
245
  test('rejects array-format audience not containing configured value', async () => {
246
- const adapter = createJwtAdapter({
246
+ const connector = createJwtConnector({
247
247
  audience: 'my-api',
248
248
  secret: TEST_SECRET,
249
249
  });
@@ -252,7 +252,7 @@ describe('createJwtAdapter', () => {
252
252
  { aud: ['other-api', 'account'], exp: now + 3600, sub: 'user-no-aud' },
253
253
  TEST_SECRET
254
254
  );
255
- const result = await adapter.authenticate(
255
+ const result = await connector.authenticate(
256
256
  testInput({ bearerToken: token })
257
257
  );
258
258
  expect(result.isErr()).toBe(true);
@@ -261,13 +261,13 @@ describe('createJwtAdapter', () => {
261
261
  });
262
262
 
263
263
  test('extracts scopes from array-format claim', async () => {
264
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
264
+ const connector = createJwtConnector({ secret: TEST_SECRET });
265
265
  const now = Math.floor(Date.now() / 1000);
266
266
  const token = await signJwt(
267
267
  { exp: now + 3600, scope: ['read', 'write'], sub: 'user-arr-scope' },
268
268
  TEST_SECRET
269
269
  );
270
- const result = await adapter.authenticate(
270
+ const result = await connector.authenticate(
271
271
  testInput({ bearerToken: token })
272
272
  );
273
273
  expect(result.isOk()).toBe(true);
@@ -276,7 +276,7 @@ describe('createJwtAdapter', () => {
276
276
  });
277
277
 
278
278
  test('filters empty strings from array-format scope claims', async () => {
279
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
279
+ const connector = createJwtConnector({ secret: TEST_SECRET });
280
280
  const now = Math.floor(Date.now() / 1000);
281
281
  const token = await signJwt(
282
282
  {
@@ -286,7 +286,7 @@ describe('createJwtAdapter', () => {
286
286
  },
287
287
  TEST_SECRET
288
288
  );
289
- const result = await adapter.authenticate(
289
+ const result = await connector.authenticate(
290
290
  testInput({ bearerToken: token })
291
291
  );
292
292
  expect(result.isOk()).toBe(true);
@@ -295,7 +295,7 @@ describe('createJwtAdapter', () => {
295
295
  });
296
296
 
297
297
  test('returns error for malformed signature bytes', async () => {
298
- const adapter = createJwtAdapter({ secret: TEST_SECRET });
298
+ const connector = createJwtConnector({ secret: TEST_SECRET });
299
299
  const now = Math.floor(Date.now() / 1000);
300
300
  const token = await signJwt(
301
301
  { exp: now + 3600, sub: 'user-bad' },
@@ -303,7 +303,7 @@ describe('createJwtAdapter', () => {
303
303
  );
304
304
  const parts = token.split('.');
305
305
  const malformed = `${parts[0]}.${parts[1]}.!!!invalid-base64!!!`;
306
- const result = await adapter.authenticate(
306
+ const result = await connector.authenticate(
307
307
  testInput({ bearerToken: malformed })
308
308
  );
309
309
  expect(result.isErr()).toBe(true);
@@ -312,7 +312,7 @@ describe('createJwtAdapter', () => {
312
312
  });
313
313
 
314
314
  test('accepts token with matching issuer and audience', async () => {
315
- const adapter = createJwtAdapter({
315
+ const connector = createJwtConnector({
316
316
  audience: 'my-api',
317
317
  issuer: 'https://auth.example.com',
318
318
  secret: TEST_SECRET,
@@ -328,7 +328,7 @@ describe('createJwtAdapter', () => {
328
328
  },
329
329
  TEST_SECRET
330
330
  );
331
- const result = await adapter.authenticate(
331
+ const result = await connector.authenticate(
332
332
  testInput({ bearerToken: token })
333
333
  );
334
334
  expect(result.isOk()).toBe(true);
@@ -60,7 +60,7 @@ describe('getPermit()', () => {
60
60
  expect(result).toBeUndefined();
61
61
  });
62
62
 
63
- test('preserves extended permit fields from the auth layer', () => {
63
+ test('preserves extended permit fields from the auth gate', () => {
64
64
  const ctx = {
65
65
  permit: {
66
66
  id: 'usr_2',
@@ -77,42 +77,42 @@ describe('getPermit()', () => {
77
77
  });
78
78
 
79
79
  describe('PermitExtractionInput', () => {
80
- test('accepts HTTP surface extraction', () => {
80
+ test('accepts HTTP trailhead extraction', () => {
81
81
  const input: PermitExtractionInput = {
82
82
  bearerToken: 'eyJhbGciOiJSUzI1NiJ9.test',
83
83
  headers: new Headers({
84
84
  authorization: 'Bearer eyJhbGciOiJSUzI1NiJ9.test',
85
85
  }),
86
86
  requestId: 'req-http-1',
87
- surface: 'http',
87
+ trailhead: 'http',
88
88
  };
89
- expect(input.surface).toBe('http');
89
+ expect(input.trailhead).toBe('http');
90
90
  expect(input.bearerToken).toBeDefined();
91
91
  });
92
92
 
93
- test('accepts MCP surface extraction', () => {
93
+ test('accepts MCP trailhead extraction', () => {
94
94
  const input: PermitExtractionInput = {
95
95
  requestId: 'req-mcp-1',
96
96
  sessionId: 'mcp-session-abc',
97
- surface: 'mcp',
97
+ trailhead: 'mcp',
98
98
  };
99
- expect(input.surface).toBe('mcp');
99
+ expect(input.trailhead).toBe('mcp');
100
100
  expect(input.sessionId).toBe('mcp-session-abc');
101
101
  });
102
102
 
103
- test('accepts CLI surface extraction', () => {
103
+ test('accepts CLI trailhead extraction', () => {
104
104
  const input: PermitExtractionInput = {
105
105
  bearerToken: 'cli-token-from-keyring',
106
106
  requestId: 'req-cli-1',
107
- surface: 'cli',
107
+ trailhead: 'cli',
108
108
  };
109
- expect(input.surface).toBe('cli');
109
+ expect(input.trailhead).toBe('cli');
110
110
  });
111
111
 
112
112
  test('accepts minimal extraction with only required fields', () => {
113
113
  const input: PermitExtractionInput = {
114
114
  requestId: 'req-minimal',
115
- surface: 'http',
115
+ trailhead: 'http',
116
116
  };
117
117
  expect(input.requestId).toBe('req-minimal');
118
118
  expect(input.bearerToken).toBeUndefined();
@@ -24,9 +24,9 @@ const noopRun = () => Result.ok({});
24
24
  describe('destroyWithoutPermit', () => {
25
25
  test('error when destroy trail has no permit', () => {
26
26
  const t = trail('user.delete', {
27
+ blaze: noopRun,
27
28
  input: emptyInput,
28
29
  intent: 'destroy',
29
- run: noopRun,
30
30
  });
31
31
  const diagnostics = destroyWithoutPermit([t]);
32
32
  expect(diagnostics).toHaveLength(1);
@@ -40,10 +40,10 @@ describe('destroyWithoutPermit', () => {
40
40
 
41
41
  test('no diagnostic when destroy trail has a scoped permit', () => {
42
42
  const t = trail('user.delete', {
43
+ blaze: noopRun,
43
44
  input: emptyInput,
44
45
  intent: 'destroy',
45
46
  permit: { scopes: ['user:delete'] },
46
- run: noopRun,
47
47
  });
48
48
  const diagnostics = destroyWithoutPermit([t]);
49
49
  expect(diagnostics).toHaveLength(0);
@@ -51,10 +51,10 @@ describe('destroyWithoutPermit', () => {
51
51
 
52
52
  test('error when destroy trail has permit: public', () => {
53
53
  const t = trail('user.delete', {
54
+ blaze: noopRun,
54
55
  input: emptyInput,
55
56
  intent: 'destroy',
56
57
  permit: 'public',
57
- run: noopRun,
58
58
  });
59
59
  const diagnostics = destroyWithoutPermit([t]);
60
60
  expect(diagnostics).toHaveLength(1);
@@ -73,8 +73,8 @@ describe('destroyWithoutPermit', () => {
73
73
  describe('writeWithoutPermit', () => {
74
74
  test('warning when write trail has no permit', () => {
75
75
  const t = trail('user.create', {
76
+ blaze: noopRun,
76
77
  input: emptyInput,
77
- run: noopRun,
78
78
  });
79
79
  const diagnostics = writeWithoutPermit([t]);
80
80
  expect(diagnostics).toHaveLength(1);
@@ -87,9 +87,9 @@ describe('writeWithoutPermit', () => {
87
87
 
88
88
  test('no warning when write trail has permit: public', () => {
89
89
  const t = trail('user.create', {
90
+ blaze: noopRun,
90
91
  input: emptyInput,
91
92
  permit: 'public',
92
- run: noopRun,
93
93
  });
94
94
  const diagnostics = writeWithoutPermit([t]);
95
95
  expect(diagnostics).toHaveLength(0);
@@ -97,8 +97,8 @@ describe('writeWithoutPermit', () => {
97
97
 
98
98
  test('warning when trail has no intent (defaults to write)', () => {
99
99
  const t = trail('user.update', {
100
+ blaze: noopRun,
100
101
  input: emptyInput,
101
- run: noopRun,
102
102
  });
103
103
  // Override intent to undefined to simulate a manually constructed trail
104
104
  const noIntent = { ...t, intent: undefined } as unknown as ReturnType<
@@ -115,9 +115,9 @@ describe('writeWithoutPermit', () => {
115
115
 
116
116
  test('no diagnostic for read trail without permit', () => {
117
117
  const t = trail('user.list', {
118
+ blaze: noopRun,
118
119
  input: emptyInput,
119
120
  intent: 'read',
120
- run: noopRun,
121
121
  });
122
122
  const diagnostics = writeWithoutPermit([t]);
123
123
  expect(diagnostics).toHaveLength(0);
@@ -131,9 +131,9 @@ describe('writeWithoutPermit', () => {
131
131
  describe('scopeNamingConsistency', () => {
132
132
  test('scope user:write passes naming check', () => {
133
133
  const t = trail('user.update', {
134
+ blaze: noopRun,
134
135
  input: emptyInput,
135
136
  permit: { scopes: ['user:write'] },
136
- run: noopRun,
137
137
  });
138
138
  const diagnostics = scopeNamingConsistency([t]);
139
139
  expect(diagnostics).toHaveLength(0);
@@ -141,9 +141,9 @@ describe('scopeNamingConsistency', () => {
141
141
 
142
142
  test('warning for scope without colon', () => {
143
143
  const t = trail('admin.panel', {
144
+ blaze: noopRun,
144
145
  input: emptyInput,
145
146
  permit: { scopes: ['admin'] },
146
- run: noopRun,
147
147
  });
148
148
  const diagnostics = scopeNamingConsistency([t]);
149
149
  expect(diagnostics).toHaveLength(1);
@@ -163,14 +163,14 @@ describe('scopeNamingConsistency', () => {
163
163
  describe('orphanScopeDetection', () => {
164
164
  test('warning for orphan scope (typo)', () => {
165
165
  const t1 = trail('user.read', {
166
+ blaze: noopRun,
166
167
  input: emptyInput,
167
168
  permit: { scopes: ['user:read'] },
168
- run: noopRun,
169
169
  });
170
170
  const t2 = trail('user.write', {
171
+ blaze: noopRun,
171
172
  input: emptyInput,
172
173
  permit: { scopes: ['user:wirte'] },
173
- run: noopRun,
174
174
  });
175
175
  const diagnostics = orphanScopeDetection([t1, t2]);
176
176
  // Both scopes are unique (appear in only 1 trail each)
@@ -181,14 +181,14 @@ describe('orphanScopeDetection', () => {
181
181
 
182
182
  test('no warning for shared scopes', () => {
183
183
  const t1 = trail('user.read', {
184
+ blaze: noopRun,
184
185
  input: emptyInput,
185
186
  permit: { scopes: ['user:read'] },
186
- run: noopRun,
187
187
  });
188
188
  const t2 = trail('user.profile', {
189
+ blaze: noopRun,
189
190
  input: emptyInput,
190
191
  permit: { scopes: ['user:read'] },
191
- run: noopRun,
192
192
  });
193
193
  const diagnostics = orphanScopeDetection([t1, t2]);
194
194
  expect(diagnostics).toHaveLength(0);
@@ -203,23 +203,23 @@ describe('orphanScopeDetection', () => {
203
203
  describe('validatePermits', () => {
204
204
  test('runs all rules and aggregates diagnostics', () => {
205
205
  const destroyNoPerm = trail('user.delete', {
206
+ blaze: noopRun,
206
207
  input: emptyInput,
207
208
  intent: 'destroy',
208
- run: noopRun,
209
209
  });
210
210
  const writeNoPerm = trail('user.create', {
211
+ blaze: noopRun,
211
212
  input: emptyInput,
212
- run: noopRun,
213
213
  });
214
214
  const badScope = trail('admin.panel', {
215
+ blaze: noopRun,
215
216
  input: emptyInput,
216
217
  permit: { scopes: ['admin'] },
217
- run: noopRun,
218
218
  });
219
219
  const orphanScope = trail('analytics.export', {
220
+ blaze: noopRun,
220
221
  input: emptyInput,
221
222
  permit: { scopes: ['analytics:exportt'] },
222
- run: noopRun,
223
223
  });
224
224
 
225
225
  const diagnostics = validatePermits([
@@ -1,5 +1,5 @@
1
1
  import { Result } from '@ontrails/core';
2
- import type { Layer } from '@ontrails/core';
2
+ import type { Gate } from '@ontrails/core';
3
3
 
4
4
  import { PermitError } from './errors.js';
5
5
  import { getPermit } from './permit.js';
@@ -25,25 +25,25 @@ const findMissing = (
25
25
  ): readonly string[] => required.filter((s) => !held.includes(s));
26
26
 
27
27
  // ---------------------------------------------------------------------------
28
- // Auth layer
28
+ // Auth gate
29
29
  // ---------------------------------------------------------------------------
30
30
 
31
31
  /**
32
- * A {@link Layer} that enforces permit scopes declared on trails.
32
+ * A {@link Gate} that enforces permit scopes declared on trails.
33
33
  *
34
- * The layer reads the trail's `permit` field (a `PermitRequirement`):
34
+ * The gate reads the trail's `permit` field (a `PermitRequirement`):
35
35
  *
36
- * - If `permit` is `'public'` or `undefined` the layer passes through.
37
- * - If `permit` has `scopes`, the layer checks that `ctx.permit` contains
36
+ * - If `permit` is `'public'` or `undefined` the gate passes through.
37
+ * - If `permit` has `scopes`, the gate checks that `ctx.permit` contains
38
38
  * all required scopes. A superset is fine; missing scopes produce a
39
39
  * `PermitError`.
40
40
  *
41
- * Because `ctx.follow()` re-enters `executeTrail` (which applies layers),
42
- * this layer automatically re-checks on every invocation in a follow chain.
43
- * No special follow-chain handling is needed — it is built into the
41
+ * Because `ctx.cross()` re-enters `executeTrail` (which applies gates),
42
+ * this gate automatically re-checks on every invocation in a crossing chain.
43
+ * No special crossing-chain handling is needed — it is built into the
44
44
  * architecture.
45
45
  */
46
- export const authLayer: Layer = {
46
+ export const authGate: Gate = {
47
47
  description: 'Enforces permit scopes declared on trails',
48
48
  name: 'auth',
49
49
  wrap: (_trail, impl) => {
@@ -0,0 +1,26 @@
1
+ import { Result, provision } from '@ontrails/core';
2
+
3
+ import type { AuthConnector } from './connectors/connector.js';
4
+
5
+ /**
6
+ * Auth provision — manages the auth connector lifecycle.
7
+ *
8
+ * The v1 factory returns a no-op connector that always succeeds (null permit).
9
+ * Real connector configuration will come through `ProvisionSpec.config`
10
+ * (TRL-91). The mock factory provides a synthetic connector that always
11
+ * succeeds.
12
+ */
13
+ export const authProvision = provision<AuthConnector>('auth', {
14
+ create: (_svc) =>
15
+ Result.ok({
16
+ // oxlint-disable-next-line require-await -- stub connector satisfies async interface
17
+ authenticate: async () => Result.ok(null),
18
+ } satisfies AuthConnector),
19
+ description: 'Authentication connector',
20
+ meta: { category: 'infrastructure' },
21
+ mock: () =>
22
+ ({
23
+ // oxlint-disable-next-line require-await -- mock connector satisfies async interface
24
+ authenticate: async () => Result.ok(null),
25
+ }) satisfies AuthConnector,
26
+ });
@@ -1,7 +1,7 @@
1
1
  import type { Result } from '@ontrails/core';
2
2
 
3
- import type { PermitExtractionInput } from './extraction.js';
4
- import type { Permit } from './permit.js';
3
+ import type { PermitExtractionInput } from '../extraction.js';
4
+ import type { Permit } from '../permit.js';
5
5
 
6
6
  /**
7
7
  * @deprecated Use {@link PermitExtractionInput} instead. Kept as an alias
@@ -9,7 +9,7 @@ import type { Permit } from './permit.js';
9
9
  */
10
10
  export type AuthCredentials = PermitExtractionInput;
11
11
 
12
- /** Errors from auth adapters. */
12
+ /** Errors from auth connectors. */
13
13
  export interface AuthError {
14
14
  readonly code:
15
15
  | 'expired_token'
@@ -20,15 +20,15 @@ export interface AuthError {
20
20
  }
21
21
 
22
22
  /**
23
- * Auth adapter port. Given extraction input, produce a permit or an error.
23
+ * Auth connector port. Given extraction input, produce a permit or an error.
24
24
  *
25
- * The adapter receives the full {@link PermitExtractionInput} — surface,
25
+ * The connector receives the full {@link PermitExtractionInput} — trailhead,
26
26
  * headers, requestId, and credential fields — so it can make richer
27
- * decisions (e.g., rate-limit by surface or correlate via requestId).
27
+ * decisions (e.g., rate-limit by trailhead or correlate via requestId).
28
28
  *
29
29
  * Deliberately narrow — no session management, no token refresh.
30
30
  */
31
- export interface AuthAdapter {
31
+ export interface AuthConnector {
32
32
  readonly authenticate: (
33
33
  input: PermitExtractionInput
34
34
  ) => Promise<Result<Permit | null, AuthError>>;
@@ -1,11 +1,11 @@
1
1
  import { Result } from '@ontrails/core';
2
2
 
3
- import type { AuthAdapter, AuthError } from '../adapter.js';
3
+ import type { AuthConnector, AuthError } from './connector.js';
4
4
  import type { PermitExtractionInput } from '../extraction.js';
5
5
  import type { Permit } from '../permit.js';
6
6
 
7
- /** Configuration for the JWT auth adapter. */
8
- export interface JwtAdapterOptions {
7
+ /** Configuration for the JWT auth connector. */
8
+ export interface JwtConnectorOptions {
9
9
  /** HMAC secret for HS256 verification. */
10
10
  readonly secret?: string;
11
11
  /** JWKS endpoint for RS256/ES256 (not yet implemented). */
@@ -101,7 +101,7 @@ const verifyHmacSignature = (
101
101
  /** Validate standard claims (exp, iss, aud). */
102
102
  const validateClaims = (
103
103
  payload: JwtPayload,
104
- options: JwtAdapterOptions
104
+ options: JwtConnectorOptions
105
105
  ): AuthError | undefined => {
106
106
  if (
107
107
  payload.exp !== undefined &&
@@ -156,7 +156,7 @@ const extractRoles = (
156
156
  /** Build a Permit from a validated JWT payload. */
157
157
  const buildPermit = (
158
158
  payload: JwtPayload,
159
- options: JwtAdapterOptions
159
+ options: JwtConnectorOptions
160
160
  ): Result<Permit, AuthError> => {
161
161
  if (!payload.sub) {
162
162
  return authErr('invalid_token', 'Missing subject claim (sub)');
@@ -192,7 +192,7 @@ const decodeAndVerify = async (
192
192
  /** Validate claims and build a permit from a verified payload. */
193
193
  const payloadToPermit = (
194
194
  payload: JwtPayload,
195
- options: JwtAdapterOptions
195
+ options: JwtConnectorOptions
196
196
  ): Result<Permit, AuthError> => {
197
197
  const claimError = validateClaims(payload, options);
198
198
  if (claimError) {
@@ -206,13 +206,15 @@ const payloadToPermit = (
206
206
  // ---------------------------------------------------------------------------
207
207
 
208
208
  /**
209
- * Create a JWT auth adapter using Bun's native crypto.
209
+ * Create a JWT auth connector using Bun's native crypto.
210
210
  *
211
211
  * Verifies HS256-signed JWTs, extracts claims into a Permit, and checks
212
212
  * issuer/audience when configured. Returns `Result.ok(null)` when no
213
213
  * credentials are provided.
214
214
  */
215
- export const createJwtAdapter = (options: JwtAdapterOptions): AuthAdapter => {
215
+ export const createJwtConnector = (
216
+ options: JwtConnectorOptions
217
+ ): AuthConnector => {
216
218
  const authenticate = async (
217
219
  input: PermitExtractionInput
218
220
  ): Promise<Result<Permit | null, AuthError>> => {
package/src/extraction.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  /**
2
- * Normalized input for auth adapters.
2
+ * Normalized input for auth connectors.
3
3
  *
4
- * Each surface extracts raw credentials from its transport and normalizes
5
- * them into this shape. No surface types (Request, McpSession, etc.) cross
4
+ * Each trailhead extracts raw credentials from its transport and normalizes
5
+ * them into this shape. No trailhead types (Request, McpSession, etc.) cross
6
6
  * into core — only this interface.
7
7
  */
8
8
  export interface PermitExtractionInput {
9
- /** Which surface produced this extraction */
10
- readonly surface: 'http' | 'mcp' | 'cli';
9
+ /** Which trailhead produced this extraction */
10
+ readonly trailhead: 'http' | 'mcp' | 'cli';
11
11
  /** Bearer token from Authorization header or equivalent */
12
12
  readonly bearerToken?: string;
13
13
  /** Session identifier from transport handshake */
14
14
  readonly sessionId?: string;
15
- /** Raw headers (HTTP surface only, typically) */
15
+ /** Raw headers (HTTP trailhead only, typically) */
16
16
  readonly headers?: Headers;
17
17
  /** Correlation ID for tracing */
18
18
  readonly requestId: string;
package/src/index.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  export {
2
- type AuthAdapter,
2
+ type AuthConnector,
3
3
  type AuthCredentials,
4
4
  type AuthError,
5
- } from './adapter.js';
6
- export { createJwtAdapter, type JwtAdapterOptions } from './adapters/jwt.js';
7
- export { authLayer } from './auth-layer.js';
8
- export { authService } from './auth-service.js';
5
+ } from './connectors/connector.js';
6
+ export {
7
+ createJwtConnector,
8
+ type JwtConnectorOptions,
9
+ } from './connectors/jwt.js';
10
+ export { authGate } from './auth-gate.js';
11
+ export { authProvision } from './auth-provision.js';
9
12
  export { authVerify } from './trails/auth-verify.js';
10
13
  export { PermitError } from './errors.js';
11
14
  export { type PermitExtractionInput } from './extraction.js';
package/src/permit.ts CHANGED
@@ -11,8 +11,8 @@ export interface Permit extends BasePermit {
11
11
  * Type-safe accessor for `ctx.permit` with a downcast to `Permit`.
12
12
  *
13
13
  * `TrailContext.permit` is typed as `BasePermit` (id + scopes). This accessor
14
- * returns the full `Permit` when the auth layer has set one. Safe because
15
- * the auth layer is the only writer and always sets a full `Permit`.
14
+ * returns the full `Permit` when the auth gate has set one. Safe because
15
+ * the auth gate is the only writer and always sets a full `Permit`.
16
16
  *
17
17
  * @example
18
18
  * ```typescript
@@ -1,8 +1,8 @@
1
- import { Result, SURFACE_KEY, trail } from '@ontrails/core';
1
+ import { Result, TRAILHEAD_KEY, trail } from '@ontrails/core';
2
2
  import type { TrailContext } from '@ontrails/core';
3
3
  import { z } from 'zod';
4
4
 
5
- import { authService } from '../auth-service.js';
5
+ import { authProvision } from '../auth-provision.js';
6
6
  import type { PermitExtractionInput } from '../extraction.js';
7
7
  import type { Permit } from '../permit.js';
8
8
 
@@ -30,45 +30,32 @@ const toOutputPermit = (permit: Permit) => ({
30
30
  scopes: [...permit.scopes],
31
31
  });
32
32
 
33
- const isSurface = (value: unknown): value is PermitExtractionInput['surface'] =>
33
+ const isTrailhead = (
34
+ value: unknown
35
+ ): value is PermitExtractionInput['trailhead'] =>
34
36
  value === 'http' || value === 'mcp' || value === 'cli';
35
37
 
36
- const getSurface = (ctx: TrailContext): PermitExtractionInput['surface'] => {
37
- const surface = ctx.extensions?.[SURFACE_KEY];
38
- return isSurface(surface) ? surface : 'http';
38
+ const getTrailhead = (
39
+ ctx: TrailContext
40
+ ): PermitExtractionInput['trailhead'] => {
41
+ const trailhead = ctx.extensions?.[TRAILHEAD_KEY];
42
+ return isTrailhead(trailhead) ? trailhead : 'http';
39
43
  };
40
44
 
41
45
  /**
42
46
  * Infrastructure trail that verifies a bearer token and returns the resolved permit.
43
47
  *
44
- * Reads the auth adapter from `authService` — the adapter is configured at
45
- * bootstrap (e.g. JWT with HMAC secret). The mock adapter always succeeds with
46
- * a null permit, so `testAll(app)` works without configuration.
48
+ * Reads the auth connector from `authProvision` — the connector is configured
49
+ * at bootstrap (e.g. JWT with HMAC secret). The mock connector always
50
+ * succeeds with a null permit, so `testAll(app)` works without configuration.
47
51
  */
48
52
  export const authVerify = trail('auth.verify', {
49
- examples: [
50
- {
51
- input: { token: 'test-token' },
52
- name: 'Verify a token',
53
- },
54
- ],
55
- input: z.object({
56
- token: z.string().min(1).describe('Bearer token to verify'),
57
- }),
58
- intent: 'read',
59
- metadata: { category: 'infrastructure' },
60
- output: z.object({
61
- error: z.string().optional(),
62
- errorCode: authErrorCodeSchema.optional(),
63
- permit: permitSchema.optional(),
64
- valid: z.boolean(),
65
- }),
66
- run: async (input, ctx) => {
67
- const adapter = authService.from(ctx);
68
- const result = await adapter.authenticate({
53
+ blaze: async (input, ctx) => {
54
+ const connector = authProvision.from(ctx);
55
+ const result = await connector.authenticate({
69
56
  bearerToken: input.token,
70
57
  requestId: ctx.requestId,
71
- surface: getSurface(ctx),
58
+ trailhead: getTrailhead(ctx),
72
59
  });
73
60
 
74
61
  if (result.isErr()) {
@@ -93,5 +80,22 @@ export const authVerify = trail('auth.verify', {
93
80
  valid: true,
94
81
  });
95
82
  },
96
- services: [authService],
83
+ examples: [
84
+ {
85
+ input: { token: 'test-token' },
86
+ name: 'Verify a token',
87
+ },
88
+ ],
89
+ input: z.object({
90
+ token: z.string().min(1).describe('Bearer token to verify'),
91
+ }),
92
+ intent: 'read',
93
+ meta: { category: 'infrastructure' },
94
+ output: z.object({
95
+ error: z.string().optional(),
96
+ errorCode: authErrorCodeSchema.optional(),
97
+ permit: permitSchema.optional(),
98
+ valid: z.boolean(),
99
+ }),
100
+ provisions: [authProvision],
97
101
  });
@@ -1,25 +0,0 @@
1
- import { Result, service } from '@ontrails/core';
2
-
3
- import type { AuthAdapter } from './adapter.js';
4
-
5
- /**
6
- * Auth service — manages the auth adapter lifecycle.
7
- *
8
- * The v1 factory returns a no-op adapter that always succeeds (null permit).
9
- * Real adapter configuration will come through `ServiceSpec.config` (TRL-91).
10
- * The mock factory provides a synthetic adapter that always succeeds.
11
- */
12
- export const authService = service<AuthAdapter>('auth', {
13
- create: (_svc) =>
14
- Result.ok({
15
- // oxlint-disable-next-line require-await -- stub adapter satisfies async interface
16
- authenticate: async () => Result.ok(null),
17
- } satisfies AuthAdapter),
18
- description: 'Authentication adapter',
19
- metadata: { category: 'infrastructure' },
20
- mock: () =>
21
- ({
22
- // oxlint-disable-next-line require-await -- mock adapter satisfies async interface
23
- authenticate: async () => Result.ok(null),
24
- }) satisfies AuthAdapter,
25
- });