@ontrails/permits 1.0.0-beta.12 → 1.0.0-beta.14
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +44 -5
- package/README.md +175 -0
- package/dist/auth-gate.d.ts +18 -0
- package/dist/auth-gate.d.ts.map +1 -0
- package/dist/auth-gate.js +56 -0
- package/dist/auth-gate.js.map +1 -0
- package/dist/auth-provision.d.ts +11 -0
- package/dist/auth-provision.d.ts.map +1 -0
- package/dist/auth-provision.js +22 -0
- package/dist/auth-provision.js.map +1 -0
- package/dist/connectors/connector.d.ts +26 -0
- package/dist/connectors/connector.d.ts.map +1 -0
- package/dist/connectors/connector.js +2 -0
- package/dist/connectors/connector.js.map +1 -0
- package/dist/connectors/jwt.d.ts +25 -0
- package/dist/connectors/jwt.d.ts.map +1 -0
- package/dist/connectors/jwt.js +148 -0
- package/dist/connectors/jwt.js.map +1 -0
- package/dist/extraction.d.ts +6 -6
- package/dist/extraction.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/permit.d.ts +2 -2
- package/dist/permit.js +2 -2
- package/dist/trails/auth-verify.d.ts +3 -3
- package/dist/trails/auth-verify.d.ts.map +1 -1
- package/dist/trails/auth-verify.js +31 -31
- package/dist/trails/auth-verify.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/{auth-layer.test.ts → auth-gate.test.ts} +16 -16
- package/src/__tests__/{auth-service.test.ts → auth-provision.test.ts} +18 -18
- package/src/__tests__/auth-verify.test.ts +36 -35
- package/src/__tests__/{adapter.test.ts → connector.test.ts} +43 -43
- package/src/__tests__/permit.test.ts +11 -11
- package/src/__tests__/rules.test.ts +17 -17
- package/src/{auth-layer.ts → auth-gate.ts} +10 -10
- package/src/auth-provision.ts +26 -0
- package/src/{adapter.ts → connectors/connector.ts} +7 -7
- package/src/{adapters → connectors}/jwt.ts +10 -8
- package/src/extraction.ts +6 -6
- package/src/index.ts +8 -5
- package/src/permit.ts +2 -2
- package/src/trails/auth-verify.ts +35 -31
- package/tsconfig.tsbuildinfo +1 -1
- package/src/auth-service.ts +0 -25
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export {} from './
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
1
|
+
export {} from './connectors/connector.js';
|
|
2
|
+
export { createJwtConnector, } from './connectors/jwt.js';
|
|
3
|
+
export { authGate } from './auth-gate.js';
|
|
4
|
+
export { authProvision } from './auth-provision.js';
|
|
5
5
|
export { authVerify } from './trails/auth-verify.js';
|
|
6
6
|
export { PermitError } from './errors.js';
|
|
7
7
|
export {} from './extraction.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,kBAAkB,GAEnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAA8B,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAe,SAAS,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,eAAe,EAAyB,MAAM,YAAY,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/permit.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export interface Permit extends BasePermit {
|
|
|
9
9
|
* Type-safe accessor for `ctx.permit` with a downcast to `Permit`.
|
|
10
10
|
*
|
|
11
11
|
* `TrailContext.permit` is typed as `BasePermit` (id + scopes). This accessor
|
|
12
|
-
* returns the full `Permit` when the auth
|
|
13
|
-
* the auth
|
|
12
|
+
* returns the full `Permit` when the auth gate has set one. Safe because
|
|
13
|
+
* the auth gate is the only writer and always sets a full `Permit`.
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
16
|
* ```typescript
|
package/dist/permit.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Type-safe accessor for `ctx.permit` with a downcast to `Permit`.
|
|
3
3
|
*
|
|
4
4
|
* `TrailContext.permit` is typed as `BasePermit` (id + scopes). This accessor
|
|
5
|
-
* returns the full `Permit` when the auth
|
|
6
|
-
* the auth
|
|
5
|
+
* returns the full `Permit` when the auth gate has set one. Safe because
|
|
6
|
+
* the auth gate is the only writer and always sets a full `Permit`.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```typescript
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Infrastructure trail that verifies a bearer token and returns the resolved permit.
|
|
3
3
|
*
|
|
4
|
-
* Reads the auth
|
|
5
|
-
* bootstrap (e.g. JWT with HMAC secret). The mock
|
|
6
|
-
* a null permit, so `testAll(app)` works without configuration.
|
|
4
|
+
* Reads the auth connector from `authProvision` — the connector is configured
|
|
5
|
+
* at bootstrap (e.g. JWT with HMAC secret). The mock connector always
|
|
6
|
+
* succeeds with a null permit, so `testAll(app)` works without configuration.
|
|
7
7
|
*/
|
|
8
8
|
export declare const authVerify: import("@ontrails/core").Trail<{
|
|
9
9
|
token: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-verify.d.ts","sourceRoot":"","sources":["../../src/trails/auth-verify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth-verify.d.ts","sourceRoot":"","sources":["../../src/trails/auth-verify.ts"],"names":[],"mappings":"AA4CA;;;;;;GAMG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;EAiDrB,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Result,
|
|
1
|
+
import { Result, TRAILHEAD_KEY, trail } from '@ontrails/core';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { authProvision } from '../auth-provision.js';
|
|
4
4
|
const permitSchema = z.object({
|
|
5
5
|
id: z.string(),
|
|
6
6
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -23,42 +23,25 @@ const toOutputPermit = (permit) => ({
|
|
|
23
23
|
id: permit.id,
|
|
24
24
|
scopes: [...permit.scopes],
|
|
25
25
|
});
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
return
|
|
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
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* Infrastructure trail that verifies a bearer token and returns the resolved permit.
|
|
33
33
|
*
|
|
34
|
-
* Reads the auth
|
|
35
|
-
* bootstrap (e.g. JWT with HMAC secret). The mock
|
|
36
|
-
* a null permit, so `testAll(app)` works without configuration.
|
|
34
|
+
* Reads the auth connector from `authProvision` — 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
37
|
*/
|
|
38
38
|
export const authVerify = trail('auth.verify', {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
name: 'Verify a token',
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
input: z.object({
|
|
46
|
-
token: z.string().min(1).describe('Bearer token to verify'),
|
|
47
|
-
}),
|
|
48
|
-
intent: 'read',
|
|
49
|
-
metadata: { category: 'infrastructure' },
|
|
50
|
-
output: z.object({
|
|
51
|
-
error: z.string().optional(),
|
|
52
|
-
errorCode: authErrorCodeSchema.optional(),
|
|
53
|
-
permit: permitSchema.optional(),
|
|
54
|
-
valid: z.boolean(),
|
|
55
|
-
}),
|
|
56
|
-
run: async (input, ctx) => {
|
|
57
|
-
const adapter = authService.from(ctx);
|
|
58
|
-
const result = await adapter.authenticate({
|
|
39
|
+
blaze: async (input, ctx) => {
|
|
40
|
+
const connector = authProvision.from(ctx);
|
|
41
|
+
const result = await connector.authenticate({
|
|
59
42
|
bearerToken: input.token,
|
|
60
43
|
requestId: ctx.requestId,
|
|
61
|
-
|
|
44
|
+
trailhead: getTrailhead(ctx),
|
|
62
45
|
});
|
|
63
46
|
if (result.isErr()) {
|
|
64
47
|
return Result.ok({
|
|
@@ -80,6 +63,23 @@ export const authVerify = trail('auth.verify', {
|
|
|
80
63
|
valid: true,
|
|
81
64
|
});
|
|
82
65
|
},
|
|
83
|
-
|
|
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
|
+
provisions: [authProvision],
|
|
84
84
|
});
|
|
85
85
|
//# sourceMappingURL=auth-verify.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-verify.js","sourceRoot":"","sources":["../../src/trails/auth-verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"auth-verify.js","sourceRoot":"","sources":["../../src/trails/auth-verify.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/permits",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
7
|
-
"./jwt": "./src/
|
|
7
|
+
"./jwt": "./src/connectors/jwt.ts",
|
|
8
8
|
"./package.json": "./package.json"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
18
|
+
"@ontrails/core": "^1.0.0-beta.13",
|
|
19
19
|
"zod": "^4.3.5"
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/* oxlint-disable require-await --
|
|
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 {
|
|
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('
|
|
30
|
+
describe('authGate', () => {
|
|
31
31
|
test('has correct name and description', () => {
|
|
32
|
-
expect(
|
|
33
|
-
expect(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
3
|
+
import type { ProvisionContext } from '@ontrails/core';
|
|
4
4
|
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
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
|
-
|
|
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:
|
|
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('
|
|
33
|
+
describe('authProvision', () => {
|
|
34
34
|
test('has correct id and kind', () => {
|
|
35
|
-
expect(
|
|
36
|
-
expect(
|
|
35
|
+
expect(authProvision.id).toBe('auth');
|
|
36
|
+
expect(authProvision.kind).toBe('provision');
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
test('has infrastructure
|
|
40
|
-
expect(
|
|
39
|
+
test('has infrastructure meta', () => {
|
|
40
|
+
expect(authProvision.meta).toEqual({ category: 'infrastructure' });
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
test('mock returns an
|
|
44
|
-
const mock =
|
|
43
|
+
test('mock returns an AuthConnector', async () => {
|
|
44
|
+
const mock = authProvision.mock?.();
|
|
45
45
|
expect(mock).toBeDefined();
|
|
46
46
|
|
|
47
|
-
const
|
|
48
|
-
const result = await
|
|
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
|
|
54
|
-
const result = await
|
|
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
|
|
58
|
-
const authResult = await
|
|
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
|
-
|
|
5
|
+
TRAILHEAD_KEY,
|
|
6
6
|
ValidationError,
|
|
7
7
|
executeTrail,
|
|
8
8
|
} from '@ontrails/core';
|
|
9
9
|
|
|
10
|
-
import type {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
|
59
|
-
const
|
|
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
|
|
62
|
+
/** Execute auth.verify with a given connector injected as the auth provision. */
|
|
62
63
|
const runVerify = async (
|
|
63
64
|
token: string,
|
|
64
|
-
|
|
65
|
+
connector: AuthConnector,
|
|
65
66
|
options?: {
|
|
66
|
-
|
|
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?.
|
|
95
|
+
options?.trailhead === undefined
|
|
95
96
|
? undefined
|
|
96
|
-
: { extensions: { [
|
|
97
|
-
|
|
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
|
|
128
|
-
expect(authVerify.
|
|
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
|
|
137
|
-
expect(authVerify.
|
|
138
|
-
expect(authVerify.
|
|
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
|
|
143
|
+
describe('with mock connector (no credentials)', () => {
|
|
143
144
|
test('returns valid: false with error message', async () => {
|
|
144
|
-
const
|
|
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',
|
|
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,
|
|
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
|
|
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
|
|
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',
|
|
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
|
|
206
|
-
let
|
|
207
|
-
const
|
|
208
|
-
// oxlint-disable-next-line require-await -- captures
|
|
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
|
-
|
|
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('
|
|
219
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
270
|
+
provisions: { [authProvision.id]: jwtConnector() },
|
|
270
271
|
}
|
|
271
272
|
);
|
|
272
273
|
|