@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.
- package/CHANGELOG.md +76 -0
- package/README.md +35 -27
- package/package.json +12 -3
- package/src/adapters/adapter.ts +41 -0
- package/src/{connectors → adapters}/jwt.ts +148 -21
- package/src/auth-resource.ts +89 -0
- package/src/boundary.ts +200 -0
- package/src/errors.ts +1 -18
- package/src/extraction.ts +22 -16
- package/src/index.ts +22 -10
- package/src/permit.ts +2 -2
- package/src/rules.ts +2 -2
- package/src/testing.ts +5 -5
- package/src/trails/auth-verify.ts +13 -15
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/adapter.d.ts +0 -26
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js +0 -2
- package/dist/adapter.js.map +0 -1
- package/dist/adapters/jwt.d.ts +0 -25
- package/dist/adapters/jwt.d.ts.map +0 -1
- package/dist/adapters/jwt.js +0 -148
- package/dist/adapters/jwt.js.map +0 -1
- package/dist/auth-gate.d.ts +0 -18
- package/dist/auth-gate.d.ts.map +0 -1
- package/dist/auth-gate.js +0 -56
- package/dist/auth-gate.js.map +0 -1
- package/dist/auth-layer.d.ts +0 -18
- package/dist/auth-layer.d.ts.map +0 -1
- package/dist/auth-layer.js +0 -56
- package/dist/auth-layer.js.map +0 -1
- package/dist/auth-provision.d.ts +0 -11
- package/dist/auth-provision.d.ts.map +0 -1
- package/dist/auth-provision.js +0 -22
- package/dist/auth-provision.js.map +0 -1
- package/dist/auth-service.d.ts +0 -10
- package/dist/auth-service.d.ts.map +0 -1
- package/dist/auth-service.js +0 -21
- package/dist/auth-service.js.map +0 -1
- package/dist/connectors/connector.d.ts +0 -26
- package/dist/connectors/connector.d.ts.map +0 -1
- package/dist/connectors/connector.js +0 -2
- package/dist/connectors/connector.js.map +0 -1
- package/dist/connectors/jwt.d.ts +0 -25
- package/dist/connectors/jwt.d.ts.map +0 -1
- package/dist/connectors/jwt.js +0 -148
- package/dist/connectors/jwt.js.map +0 -1
- package/dist/errors.d.ts +0 -15
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -15
- package/dist/errors.js.map +0 -1
- package/dist/extraction.d.ts +0 -20
- package/dist/extraction.d.ts.map +0 -1
- package/dist/extraction.js +0 -2
- package/dist/extraction.js.map +0 -1
- package/dist/index.d.ts +0 -11
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -11
- package/dist/index.js.map +0 -1
- package/dist/permit.d.ts +0 -26
- package/dist/permit.d.ts.map +0 -1
- package/dist/permit.js +0 -17
- package/dist/permit.js.map +0 -1
- package/dist/rules.d.ts +0 -47
- package/dist/rules.d.ts.map +0 -1
- package/dist/rules.js +0 -127
- package/dist/rules.js.map +0 -1
- package/dist/testing.d.ts +0 -20
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -22
- package/dist/testing.js.map +0 -1
- package/dist/trails/auth-verify.d.ts +0 -22
- package/dist/trails/auth-verify.d.ts.map +0 -1
- package/dist/trails/auth-verify.js +0 -85
- package/dist/trails/auth-verify.js.map +0 -1
- package/src/__tests__/auth-gate.test.ts +0 -130
- package/src/__tests__/auth-provision.test.ts +0 -62
- package/src/__tests__/auth-verify.test.ts +0 -278
- package/src/__tests__/connector.test.ts +0 -338
- package/src/__tests__/permit.test.ts +0 -122
- package/src/__tests__/rules.test.ts +0 -239
- package/src/__tests__/testing.test.ts +0 -57
- package/src/auth-gate.ts +0 -80
- package/src/auth-provision.ts +0 -26
- package/src/connectors/connector.ts +0 -35
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import type { Permit, PermitExtractionInput } from '../index';
|
|
4
|
-
import { getPermit } from '../index';
|
|
5
|
-
|
|
6
|
-
describe('Permit type', () => {
|
|
7
|
-
test('accepts a valid permit with required fields only', () => {
|
|
8
|
-
const permit: Permit = {
|
|
9
|
-
id: 'usr_abc123',
|
|
10
|
-
scopes: ['user:read', 'user:write'],
|
|
11
|
-
};
|
|
12
|
-
expect(permit.id).toBe('usr_abc123');
|
|
13
|
-
expect(permit.scopes).toEqual(['user:read', 'user:write']);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('accepts a permit with all optional fields', () => {
|
|
17
|
-
const permit: Permit = {
|
|
18
|
-
id: 'usr_full',
|
|
19
|
-
metadata: { plan: 'pro', provider: 'clerk' },
|
|
20
|
-
roles: ['admin', 'editor'],
|
|
21
|
-
scopes: ['entity:read'],
|
|
22
|
-
tenantId: 'tenant_xyz',
|
|
23
|
-
};
|
|
24
|
-
expect(permit.roles).toEqual(['admin', 'editor']);
|
|
25
|
-
expect(permit.tenantId).toBe('tenant_xyz');
|
|
26
|
-
expect(permit.metadata).toEqual({ plan: 'pro', provider: 'clerk' });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('scopes and roles arrays are readonly', () => {
|
|
30
|
-
const permit: Permit = {
|
|
31
|
-
id: 'usr_ro',
|
|
32
|
-
roles: ['viewer'],
|
|
33
|
-
scopes: ['read'],
|
|
34
|
-
};
|
|
35
|
-
// Structural check: readonly arrays are assignable to readonly string[]
|
|
36
|
-
const { scopes } = permit;
|
|
37
|
-
const { roles } = permit;
|
|
38
|
-
expect(scopes).toEqual(['read']);
|
|
39
|
-
expect(roles).toEqual(['viewer']);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('getPermit()', () => {
|
|
44
|
-
test('returns Permit from a context with a permit', () => {
|
|
45
|
-
const permit: Permit = { id: 'usr_1', scopes: ['user:read'] };
|
|
46
|
-
const ctx = { permit, requestId: 'req-1' };
|
|
47
|
-
const result = getPermit(ctx);
|
|
48
|
-
expect(result).toEqual(permit);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('returns undefined when context has no permit', () => {
|
|
52
|
-
const ctx = { requestId: 'req-2' };
|
|
53
|
-
const result = getPermit(ctx);
|
|
54
|
-
expect(result).toBeUndefined();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('returns undefined when permit is explicitly undefined', () => {
|
|
58
|
-
const ctx = { permit: undefined, requestId: 'req-3' };
|
|
59
|
-
const result = getPermit(ctx);
|
|
60
|
-
expect(result).toBeUndefined();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('preserves extended permit fields from the auth gate', () => {
|
|
64
|
-
const ctx = {
|
|
65
|
-
permit: {
|
|
66
|
-
id: 'usr_2',
|
|
67
|
-
metadata: { plan: 'pro' },
|
|
68
|
-
roles: ['admin'],
|
|
69
|
-
scopes: ['user:read'],
|
|
70
|
-
tenantId: 'tenant-1',
|
|
71
|
-
} satisfies Permit,
|
|
72
|
-
requestId: 'req-4',
|
|
73
|
-
};
|
|
74
|
-
const result = getPermit(ctx);
|
|
75
|
-
expect(result).toEqual(ctx.permit);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('PermitExtractionInput', () => {
|
|
80
|
-
test('accepts HTTP trailhead extraction', () => {
|
|
81
|
-
const input: PermitExtractionInput = {
|
|
82
|
-
bearerToken: 'eyJhbGciOiJSUzI1NiJ9.test',
|
|
83
|
-
headers: new Headers({
|
|
84
|
-
authorization: 'Bearer eyJhbGciOiJSUzI1NiJ9.test',
|
|
85
|
-
}),
|
|
86
|
-
requestId: 'req-http-1',
|
|
87
|
-
trailhead: 'http',
|
|
88
|
-
};
|
|
89
|
-
expect(input.trailhead).toBe('http');
|
|
90
|
-
expect(input.bearerToken).toBeDefined();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('accepts MCP trailhead extraction', () => {
|
|
94
|
-
const input: PermitExtractionInput = {
|
|
95
|
-
requestId: 'req-mcp-1',
|
|
96
|
-
sessionId: 'mcp-session-abc',
|
|
97
|
-
trailhead: 'mcp',
|
|
98
|
-
};
|
|
99
|
-
expect(input.trailhead).toBe('mcp');
|
|
100
|
-
expect(input.sessionId).toBe('mcp-session-abc');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('accepts CLI trailhead extraction', () => {
|
|
104
|
-
const input: PermitExtractionInput = {
|
|
105
|
-
bearerToken: 'cli-token-from-keyring',
|
|
106
|
-
requestId: 'req-cli-1',
|
|
107
|
-
trailhead: 'cli',
|
|
108
|
-
};
|
|
109
|
-
expect(input.trailhead).toBe('cli');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('accepts minimal extraction with only required fields', () => {
|
|
113
|
-
const input: PermitExtractionInput = {
|
|
114
|
-
requestId: 'req-minimal',
|
|
115
|
-
trailhead: 'http',
|
|
116
|
-
};
|
|
117
|
-
expect(input.requestId).toBe('req-minimal');
|
|
118
|
-
expect(input.bearerToken).toBeUndefined();
|
|
119
|
-
expect(input.sessionId).toBeUndefined();
|
|
120
|
-
expect(input.headers).toBeUndefined();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { trail, Result } from '@ontrails/core';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
destroyWithoutPermit,
|
|
7
|
-
writeWithoutPermit,
|
|
8
|
-
scopeNamingConsistency,
|
|
9
|
-
orphanScopeDetection,
|
|
10
|
-
validatePermits,
|
|
11
|
-
} from '../rules.js';
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Helpers
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
const emptyInput = z.object({});
|
|
18
|
-
const noopRun = () => Result.ok({});
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// destroyWithoutPermit
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
describe('destroyWithoutPermit', () => {
|
|
25
|
-
test('error when destroy trail has no permit', () => {
|
|
26
|
-
const t = trail('user.delete', {
|
|
27
|
-
blaze: noopRun,
|
|
28
|
-
input: emptyInput,
|
|
29
|
-
intent: 'destroy',
|
|
30
|
-
});
|
|
31
|
-
const diagnostics = destroyWithoutPermit([t]);
|
|
32
|
-
expect(diagnostics).toHaveLength(1);
|
|
33
|
-
expect(diagnostics[0]).toMatchObject({
|
|
34
|
-
message: expect.stringContaining('destroy'),
|
|
35
|
-
rule: 'destroyWithoutPermit',
|
|
36
|
-
severity: 'error',
|
|
37
|
-
trailId: 'user.delete',
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('no diagnostic when destroy trail has a scoped permit', () => {
|
|
42
|
-
const t = trail('user.delete', {
|
|
43
|
-
blaze: noopRun,
|
|
44
|
-
input: emptyInput,
|
|
45
|
-
intent: 'destroy',
|
|
46
|
-
permit: { scopes: ['user:delete'] },
|
|
47
|
-
});
|
|
48
|
-
const diagnostics = destroyWithoutPermit([t]);
|
|
49
|
-
expect(diagnostics).toHaveLength(0);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('error when destroy trail has permit: public', () => {
|
|
53
|
-
const t = trail('user.delete', {
|
|
54
|
-
blaze: noopRun,
|
|
55
|
-
input: emptyInput,
|
|
56
|
-
intent: 'destroy',
|
|
57
|
-
permit: 'public',
|
|
58
|
-
});
|
|
59
|
-
const diagnostics = destroyWithoutPermit([t]);
|
|
60
|
-
expect(diagnostics).toHaveLength(1);
|
|
61
|
-
expect(diagnostics[0]).toMatchObject({
|
|
62
|
-
rule: 'destroyWithoutPermit',
|
|
63
|
-
severity: 'error',
|
|
64
|
-
trailId: 'user.delete',
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// writeWithoutPermit
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
describe('writeWithoutPermit', () => {
|
|
74
|
-
test('warning when write trail has no permit', () => {
|
|
75
|
-
const t = trail('user.create', {
|
|
76
|
-
blaze: noopRun,
|
|
77
|
-
input: emptyInput,
|
|
78
|
-
});
|
|
79
|
-
const diagnostics = writeWithoutPermit([t]);
|
|
80
|
-
expect(diagnostics).toHaveLength(1);
|
|
81
|
-
expect(diagnostics[0]).toMatchObject({
|
|
82
|
-
rule: 'writeWithoutPermit',
|
|
83
|
-
severity: 'warning',
|
|
84
|
-
trailId: 'user.create',
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('no warning when write trail has permit: public', () => {
|
|
89
|
-
const t = trail('user.create', {
|
|
90
|
-
blaze: noopRun,
|
|
91
|
-
input: emptyInput,
|
|
92
|
-
permit: 'public',
|
|
93
|
-
});
|
|
94
|
-
const diagnostics = writeWithoutPermit([t]);
|
|
95
|
-
expect(diagnostics).toHaveLength(0);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('warning when trail has no intent (defaults to write)', () => {
|
|
99
|
-
const t = trail('user.update', {
|
|
100
|
-
blaze: noopRun,
|
|
101
|
-
input: emptyInput,
|
|
102
|
-
});
|
|
103
|
-
// Override intent to undefined to simulate a manually constructed trail
|
|
104
|
-
const noIntent = { ...t, intent: undefined } as unknown as ReturnType<
|
|
105
|
-
typeof trail
|
|
106
|
-
>;
|
|
107
|
-
const diagnostics = writeWithoutPermit([noIntent]);
|
|
108
|
-
expect(diagnostics).toHaveLength(1);
|
|
109
|
-
expect(diagnostics[0]).toMatchObject({
|
|
110
|
-
rule: 'writeWithoutPermit',
|
|
111
|
-
severity: 'warning',
|
|
112
|
-
trailId: 'user.update',
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('no diagnostic for read trail without permit', () => {
|
|
117
|
-
const t = trail('user.list', {
|
|
118
|
-
blaze: noopRun,
|
|
119
|
-
input: emptyInput,
|
|
120
|
-
intent: 'read',
|
|
121
|
-
});
|
|
122
|
-
const diagnostics = writeWithoutPermit([t]);
|
|
123
|
-
expect(diagnostics).toHaveLength(0);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// scopeNamingConsistency
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
describe('scopeNamingConsistency', () => {
|
|
132
|
-
test('scope user:write passes naming check', () => {
|
|
133
|
-
const t = trail('user.update', {
|
|
134
|
-
blaze: noopRun,
|
|
135
|
-
input: emptyInput,
|
|
136
|
-
permit: { scopes: ['user:write'] },
|
|
137
|
-
});
|
|
138
|
-
const diagnostics = scopeNamingConsistency([t]);
|
|
139
|
-
expect(diagnostics).toHaveLength(0);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('warning for scope without colon', () => {
|
|
143
|
-
const t = trail('admin.panel', {
|
|
144
|
-
blaze: noopRun,
|
|
145
|
-
input: emptyInput,
|
|
146
|
-
permit: { scopes: ['admin'] },
|
|
147
|
-
});
|
|
148
|
-
const diagnostics = scopeNamingConsistency([t]);
|
|
149
|
-
expect(diagnostics).toHaveLength(1);
|
|
150
|
-
expect(diagnostics[0]).toMatchObject({
|
|
151
|
-
message: expect.stringContaining('admin'),
|
|
152
|
-
rule: 'scopeNamingConsistency',
|
|
153
|
-
severity: 'warning',
|
|
154
|
-
trailId: 'admin.panel',
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
// orphanScopeDetection
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
|
|
163
|
-
describe('orphanScopeDetection', () => {
|
|
164
|
-
test('warning for orphan scope (typo)', () => {
|
|
165
|
-
const t1 = trail('user.read', {
|
|
166
|
-
blaze: noopRun,
|
|
167
|
-
input: emptyInput,
|
|
168
|
-
permit: { scopes: ['user:read'] },
|
|
169
|
-
});
|
|
170
|
-
const t2 = trail('user.write', {
|
|
171
|
-
blaze: noopRun,
|
|
172
|
-
input: emptyInput,
|
|
173
|
-
permit: { scopes: ['user:wirte'] },
|
|
174
|
-
});
|
|
175
|
-
const diagnostics = orphanScopeDetection([t1, t2]);
|
|
176
|
-
// Both scopes are unique (appear in only 1 trail each)
|
|
177
|
-
expect(diagnostics).toHaveLength(2);
|
|
178
|
-
const messages = diagnostics.map((d) => d.message);
|
|
179
|
-
expect(messages.some((m) => m.includes('user:wirte'))).toBe(true);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('no warning for shared scopes', () => {
|
|
183
|
-
const t1 = trail('user.read', {
|
|
184
|
-
blaze: noopRun,
|
|
185
|
-
input: emptyInput,
|
|
186
|
-
permit: { scopes: ['user:read'] },
|
|
187
|
-
});
|
|
188
|
-
const t2 = trail('user.profile', {
|
|
189
|
-
blaze: noopRun,
|
|
190
|
-
input: emptyInput,
|
|
191
|
-
permit: { scopes: ['user:read'] },
|
|
192
|
-
});
|
|
193
|
-
const diagnostics = orphanScopeDetection([t1, t2]);
|
|
194
|
-
expect(diagnostics).toHaveLength(0);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
// validatePermits
|
|
200
|
-
// ---------------------------------------------------------------------------
|
|
201
|
-
|
|
202
|
-
/* oxlint-disable max-statements -- integration test validates all rules fire */
|
|
203
|
-
describe('validatePermits', () => {
|
|
204
|
-
test('runs all rules and aggregates diagnostics', () => {
|
|
205
|
-
const destroyNoPerm = trail('user.delete', {
|
|
206
|
-
blaze: noopRun,
|
|
207
|
-
input: emptyInput,
|
|
208
|
-
intent: 'destroy',
|
|
209
|
-
});
|
|
210
|
-
const writeNoPerm = trail('user.create', {
|
|
211
|
-
blaze: noopRun,
|
|
212
|
-
input: emptyInput,
|
|
213
|
-
});
|
|
214
|
-
const badScope = trail('admin.panel', {
|
|
215
|
-
blaze: noopRun,
|
|
216
|
-
input: emptyInput,
|
|
217
|
-
permit: { scopes: ['admin'] },
|
|
218
|
-
});
|
|
219
|
-
const orphanScope = trail('analytics.export', {
|
|
220
|
-
blaze: noopRun,
|
|
221
|
-
input: emptyInput,
|
|
222
|
-
permit: { scopes: ['analytics:exportt'] },
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const diagnostics = validatePermits([
|
|
226
|
-
destroyNoPerm,
|
|
227
|
-
writeNoPerm,
|
|
228
|
-
badScope,
|
|
229
|
-
orphanScope,
|
|
230
|
-
]);
|
|
231
|
-
|
|
232
|
-
const rules = diagnostics.map((d) => d.rule);
|
|
233
|
-
expect(rules).toContain('destroyWithoutPermit');
|
|
234
|
-
expect(rules).toContain('writeWithoutPermit');
|
|
235
|
-
expect(rules).toContain('scopeNamingConsistency');
|
|
236
|
-
expect(rules).toContain('orphanScopeDetection');
|
|
237
|
-
expect(diagnostics.length).toBeGreaterThanOrEqual(4);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { mintPermitForTrail, mintTestPermit } from '../testing';
|
|
4
|
-
|
|
5
|
-
describe('mintTestPermit()', () => {
|
|
6
|
-
test('returns a Permit with the given scopes', () => {
|
|
7
|
-
const permit = mintTestPermit({ scopes: ['user:read', 'user:write'] });
|
|
8
|
-
expect(permit.scopes).toEqual(['user:read', 'user:write']);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test('generates a unique id when not specified', () => {
|
|
12
|
-
const a = mintTestPermit();
|
|
13
|
-
const b = mintTestPermit();
|
|
14
|
-
expect(a.id).not.toBe(b.id);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('uses the provided id when specified', () => {
|
|
18
|
-
const permit = mintTestPermit({ id: 'custom-id' });
|
|
19
|
-
expect(permit.id).toBe('custom-id');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('returns empty scopes when no options provided', () => {
|
|
23
|
-
const permit = mintTestPermit();
|
|
24
|
-
expect(permit.scopes).toEqual([]);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('includes roles when provided', () => {
|
|
28
|
-
const permit = mintTestPermit({ roles: ['admin', 'editor'] });
|
|
29
|
-
expect(permit.roles).toEqual(['admin', 'editor']);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('includes tenantId when provided', () => {
|
|
33
|
-
const permit = mintTestPermit({ tenantId: 'tenant_abc' });
|
|
34
|
-
expect(permit.tenantId).toBe('tenant_abc');
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('mintPermitForTrail()', () => {
|
|
39
|
-
test('extracts scopes from trail permit requirement', () => {
|
|
40
|
-
const trail = { permit: { scopes: ['entity:read', 'entity:write'] } };
|
|
41
|
-
const permit = mintPermitForTrail(trail);
|
|
42
|
-
expect(permit).toBeDefined();
|
|
43
|
-
expect(permit?.scopes).toEqual(['entity:read', 'entity:write']);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('returns undefined for public trails', () => {
|
|
47
|
-
const trail = { permit: 'public' as const };
|
|
48
|
-
const permit = mintPermitForTrail(trail);
|
|
49
|
-
expect(permit).toBeUndefined();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('returns undefined when no permit declared', () => {
|
|
53
|
-
const trail = {};
|
|
54
|
-
const permit = mintPermitForTrail(trail);
|
|
55
|
-
expect(permit).toBeUndefined();
|
|
56
|
-
});
|
|
57
|
-
});
|
package/src/auth-gate.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Result } from '@ontrails/core';
|
|
2
|
-
import type { Gate } from '@ontrails/core';
|
|
3
|
-
|
|
4
|
-
import { PermitError } from './errors.js';
|
|
5
|
-
import { getPermit } from './permit.js';
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Helpers (defined before callers — no use-before-define)
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Returns `true` when the permit requirement means "no enforcement needed."
|
|
13
|
-
* Either the trail hasn't declared a permit posture or has explicitly
|
|
14
|
-
* opted out with `'public'`.
|
|
15
|
-
*/
|
|
16
|
-
const isPassThrough = (
|
|
17
|
-
requirement: unknown
|
|
18
|
-
): requirement is undefined | 'public' =>
|
|
19
|
-
requirement === undefined || requirement === 'public';
|
|
20
|
-
|
|
21
|
-
/** Returns scopes present in `required` but absent from `held`. */
|
|
22
|
-
const findMissing = (
|
|
23
|
-
required: readonly string[],
|
|
24
|
-
held: readonly string[]
|
|
25
|
-
): readonly string[] => required.filter((s) => !held.includes(s));
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Auth gate
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A {@link Gate} that enforces permit scopes declared on trails.
|
|
33
|
-
*
|
|
34
|
-
* The gate reads the trail's `permit` field (a `PermitRequirement`):
|
|
35
|
-
*
|
|
36
|
-
* - If `permit` is `'public'` or `undefined` the gate passes through.
|
|
37
|
-
* - If `permit` has `scopes`, the gate checks that `ctx.permit` contains
|
|
38
|
-
* all required scopes. A superset is fine; missing scopes produce a
|
|
39
|
-
* `PermitError`.
|
|
40
|
-
*
|
|
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
|
-
* architecture.
|
|
45
|
-
*/
|
|
46
|
-
export const authGate: Gate = {
|
|
47
|
-
description: 'Enforces permit scopes declared on trails',
|
|
48
|
-
name: 'auth',
|
|
49
|
-
wrap: (_trail, impl) => {
|
|
50
|
-
const requirement = _trail.permit;
|
|
51
|
-
|
|
52
|
-
if (isPassThrough(requirement)) {
|
|
53
|
-
return impl;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return (input, ctx) => {
|
|
57
|
-
const permit = getPermit(ctx);
|
|
58
|
-
|
|
59
|
-
if (!permit) {
|
|
60
|
-
return Promise.resolve(
|
|
61
|
-
Result.err(new PermitError('No permit provided'))
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const missing = findMissing(requirement.scopes, permit.scopes);
|
|
66
|
-
|
|
67
|
-
if (missing.length > 0) {
|
|
68
|
-
return Promise.resolve(
|
|
69
|
-
Result.err(
|
|
70
|
-
new PermitError(`Missing scopes: ${missing.join(', ')}`, {
|
|
71
|
-
context: { missing, required: requirement.scopes },
|
|
72
|
-
})
|
|
73
|
-
)
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return Promise.resolve(impl(input, ctx));
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
};
|
package/src/auth-provision.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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,35 +0,0 @@
|
|
|
1
|
-
import type { Result } from '@ontrails/core';
|
|
2
|
-
|
|
3
|
-
import type { PermitExtractionInput } from '../extraction.js';
|
|
4
|
-
import type { Permit } from '../permit.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @deprecated Use {@link PermitExtractionInput} instead. Kept as an alias
|
|
8
|
-
* for backward compatibility during migration.
|
|
9
|
-
*/
|
|
10
|
-
export type AuthCredentials = PermitExtractionInput;
|
|
11
|
-
|
|
12
|
-
/** Errors from auth connectors. */
|
|
13
|
-
export interface AuthError {
|
|
14
|
-
readonly code:
|
|
15
|
-
| 'expired_token'
|
|
16
|
-
| 'insufficient_scope'
|
|
17
|
-
| 'invalid_token'
|
|
18
|
-
| 'missing_credentials';
|
|
19
|
-
readonly message: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Auth connector port. Given extraction input, produce a permit or an error.
|
|
24
|
-
*
|
|
25
|
-
* The connector receives the full {@link PermitExtractionInput} — trailhead,
|
|
26
|
-
* headers, requestId, and credential fields — so it can make richer
|
|
27
|
-
* decisions (e.g., rate-limit by trailhead or correlate via requestId).
|
|
28
|
-
*
|
|
29
|
-
* Deliberately narrow — no session management, no token refresh.
|
|
30
|
-
*/
|
|
31
|
-
export interface AuthConnector {
|
|
32
|
-
readonly authenticate: (
|
|
33
|
-
input: PermitExtractionInput
|
|
34
|
-
) => Promise<Result<Permit | null, AuthError>>;
|
|
35
|
-
}
|
package/tsconfig.json
DELETED
package/tsconfig.tsbuildinfo
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["./src/auth-gate.ts","./src/auth-provision.ts","./src/errors.ts","./src/extraction.ts","./src/index.ts","./src/permit.ts","./src/rules.ts","./src/testing.ts","./src/connectors/connector.ts","./src/connectors/jwt.ts","./src/trails/auth-verify.ts"],"version":"5.9.3"}
|