@shipfox/api-auth-dto 19.0.0 → 20.0.0
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-build.log +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/oauth.d.ts +79 -0
- package/dist/schemas/oauth.d.ts.map +1 -0
- package/dist/schemas/oauth.js +67 -0
- package/dist/schemas/oauth.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/schemas/index.ts +13 -0
- package/src/schemas/oauth.test.ts +71 -0
- package/src/schemas/oauth.ts +104 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
package/src/schemas/index.ts
CHANGED
|
@@ -119,6 +119,19 @@ export {
|
|
|
119
119
|
type JobLeaseTokenClaims,
|
|
120
120
|
jobLeaseTokenClaimsSchema,
|
|
121
121
|
} from './job-lease-token.js';
|
|
122
|
+
export {
|
|
123
|
+
OAUTH_READ_SCOPE,
|
|
124
|
+
type OAuthAuthorizationServerMetadataDto,
|
|
125
|
+
type OAuthClientMetadataDocumentDto,
|
|
126
|
+
type OAuthDynamicClientRegistrationRequestDto,
|
|
127
|
+
type OAuthDynamicClientRegistrationResponseDto,
|
|
128
|
+
type OAuthProtectedResourceMetadataDto,
|
|
129
|
+
oauthAuthorizationServerMetadataSchema,
|
|
130
|
+
oauthClientMetadataDocumentSchema,
|
|
131
|
+
oauthDynamicClientRegistrationRequestSchema,
|
|
132
|
+
oauthDynamicClientRegistrationResponseSchema,
|
|
133
|
+
oauthProtectedResourceMetadataSchema,
|
|
134
|
+
} from './oauth.js';
|
|
122
135
|
export {
|
|
123
136
|
RUNNER_SESSION_TOKEN_AUDIENCE,
|
|
124
137
|
type RunnerSessionTokenClaims,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {describe, expect, it} from '@shipfox/vitest/vi';
|
|
2
|
+
import {
|
|
3
|
+
oauthAuthorizationServerMetadataSchema,
|
|
4
|
+
oauthClientMetadataDocumentSchema,
|
|
5
|
+
oauthDynamicClientRegistrationRequestSchema,
|
|
6
|
+
oauthProtectedResourceMetadataSchema,
|
|
7
|
+
} from './oauth.js';
|
|
8
|
+
|
|
9
|
+
describe('OAuth metadata schemas', () => {
|
|
10
|
+
it('accepts the MCP read-only discovery profile', () => {
|
|
11
|
+
expect(
|
|
12
|
+
oauthProtectedResourceMetadataSchema.safeParse({
|
|
13
|
+
resource: 'https://api.example.test/mcp',
|
|
14
|
+
authorization_servers: ['https://api.example.test'],
|
|
15
|
+
scopes_supported: ['read'],
|
|
16
|
+
}).success,
|
|
17
|
+
).toBe(true);
|
|
18
|
+
|
|
19
|
+
expect(
|
|
20
|
+
oauthAuthorizationServerMetadataSchema.safeParse({
|
|
21
|
+
issuer: 'https://api.example.test',
|
|
22
|
+
authorization_endpoint: 'https://api.example.test/oauth/authorize',
|
|
23
|
+
token_endpoint: 'https://api.example.test/oauth/token',
|
|
24
|
+
registration_endpoint: 'https://api.example.test/oauth/register',
|
|
25
|
+
response_types_supported: ['code'],
|
|
26
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
27
|
+
code_challenge_methods_supported: ['S256'],
|
|
28
|
+
token_endpoint_auth_methods_supported: ['none'],
|
|
29
|
+
scopes_supported: ['read'],
|
|
30
|
+
client_id_metadata_document_supported: true,
|
|
31
|
+
}).success,
|
|
32
|
+
).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('requires the bounded public-client registration fields', () => {
|
|
36
|
+
expect(
|
|
37
|
+
oauthDynamicClientRegistrationRequestSchema.safeParse({
|
|
38
|
+
client_name: 'Desktop agent',
|
|
39
|
+
redirect_uris: ['http://127.0.0.1:43123/callback'],
|
|
40
|
+
}).success,
|
|
41
|
+
).toBe(true);
|
|
42
|
+
|
|
43
|
+
expect(
|
|
44
|
+
oauthDynamicClientRegistrationRequestSchema.safeParse({
|
|
45
|
+
client_name: 'Desktop agent',
|
|
46
|
+
redirect_uris: [],
|
|
47
|
+
}).success,
|
|
48
|
+
).toBe(false);
|
|
49
|
+
expect(
|
|
50
|
+
oauthDynamicClientRegistrationRequestSchema.safeParse({
|
|
51
|
+
client_name: 'Desktop agent',
|
|
52
|
+
redirect_uris: ['https://client.example/callback'],
|
|
53
|
+
token_endpoint_auth_method: 'client_secret_basic',
|
|
54
|
+
}).success,
|
|
55
|
+
).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('accepts standard optional CIMD fields without changing the identity fields', () => {
|
|
59
|
+
const result = oauthClientMetadataDocumentSchema.safeParse({
|
|
60
|
+
client_id: 'https://client.example/.well-known/oauth-client',
|
|
61
|
+
client_name: 'Desktop agent',
|
|
62
|
+
redirect_uris: ['https://client.example/callback'],
|
|
63
|
+
token_endpoint_auth_method: 'none',
|
|
64
|
+
client_uri: 'https://client.example',
|
|
65
|
+
contacts: ['security@client.example'],
|
|
66
|
+
custom_metadata: 'ignored by the profile',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.success).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
|
|
3
|
+
const oauthUrlSchema = z.string().url().max(2048);
|
|
4
|
+
const oauthClientNameSchema = z.string().min(1).max(256);
|
|
5
|
+
const oauthRedirectUriSchema = z.string().min(1).max(2048);
|
|
6
|
+
const oauthScopeSchema = z.string().min(1).max(256);
|
|
7
|
+
const oauthGrantTypeSchema = z.enum(['authorization_code', 'refresh_token']);
|
|
8
|
+
const oauthResponseTypeSchema = z.literal('code');
|
|
9
|
+
|
|
10
|
+
/** The only scope exposed by the MCP read-only profile. */
|
|
11
|
+
export const OAUTH_READ_SCOPE = 'read' as const;
|
|
12
|
+
|
|
13
|
+
/** RFC 9728 metadata advertised for the protected MCP resource. */
|
|
14
|
+
export const oauthProtectedResourceMetadataSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
resource: oauthUrlSchema,
|
|
17
|
+
authorization_servers: z.array(oauthUrlSchema).min(1),
|
|
18
|
+
scopes_supported: z.array(z.literal(OAUTH_READ_SCOPE)).min(1),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
|
|
22
|
+
export type OAuthProtectedResourceMetadataDto = z.infer<
|
|
23
|
+
typeof oauthProtectedResourceMetadataSchema
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
/** RFC 8414 metadata for the MCP authorization server. */
|
|
27
|
+
export const oauthAuthorizationServerMetadataSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
issuer: oauthUrlSchema,
|
|
30
|
+
authorization_endpoint: oauthUrlSchema,
|
|
31
|
+
token_endpoint: oauthUrlSchema,
|
|
32
|
+
registration_endpoint: oauthUrlSchema,
|
|
33
|
+
response_types_supported: z.array(oauthResponseTypeSchema).min(1),
|
|
34
|
+
grant_types_supported: z.array(oauthGrantTypeSchema).min(1),
|
|
35
|
+
code_challenge_methods_supported: z.array(z.literal('S256')).min(1),
|
|
36
|
+
token_endpoint_auth_methods_supported: z.array(z.literal('none')).min(1),
|
|
37
|
+
scopes_supported: z.array(z.literal(OAUTH_READ_SCOPE)).min(1),
|
|
38
|
+
client_id_metadata_document_supported: z.literal(true),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
|
|
42
|
+
export type OAuthAuthorizationServerMetadataDto = z.infer<
|
|
43
|
+
typeof oauthAuthorizationServerMetadataSchema
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
const oauthRegistrationProfileFields = {
|
|
47
|
+
client_name: oauthClientNameSchema,
|
|
48
|
+
redirect_uris: z.array(oauthRedirectUriSchema).min(1).max(10),
|
|
49
|
+
grant_types: z.array(oauthGrantTypeSchema).min(1).max(2).optional(),
|
|
50
|
+
response_types: z.array(oauthResponseTypeSchema).min(1).max(1).optional(),
|
|
51
|
+
token_endpoint_auth_method: z.literal('none').optional(),
|
|
52
|
+
scope: oauthScopeSchema.optional(),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** RFC 7591 request shape accepted by the MCP public-client profile. */
|
|
56
|
+
export const oauthDynamicClientRegistrationRequestSchema = z
|
|
57
|
+
.object(oauthRegistrationProfileFields)
|
|
58
|
+
.strict();
|
|
59
|
+
|
|
60
|
+
export type OAuthDynamicClientRegistrationRequestDto = z.infer<
|
|
61
|
+
typeof oauthDynamicClientRegistrationRequestSchema
|
|
62
|
+
>;
|
|
63
|
+
|
|
64
|
+
/** RFC 7591 response shape returned for a newly registered public client. */
|
|
65
|
+
export const oauthDynamicClientRegistrationResponseSchema = z
|
|
66
|
+
.object({
|
|
67
|
+
client_id: z.string().min(1).max(2048),
|
|
68
|
+
client_name: oauthClientNameSchema,
|
|
69
|
+
redirect_uris: z.array(oauthRedirectUriSchema).min(1).max(10),
|
|
70
|
+
grant_types: z.array(oauthGrantTypeSchema).min(1).max(2),
|
|
71
|
+
response_types: z.array(oauthResponseTypeSchema).min(1).max(1),
|
|
72
|
+
token_endpoint_auth_method: z.literal('none'),
|
|
73
|
+
scope: z.literal(OAUTH_READ_SCOPE),
|
|
74
|
+
})
|
|
75
|
+
.strict();
|
|
76
|
+
|
|
77
|
+
export type OAuthDynamicClientRegistrationResponseDto = z.infer<
|
|
78
|
+
typeof oauthDynamicClientRegistrationResponseSchema
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The subset of a Client ID Metadata Document needed before authorization.
|
|
83
|
+
* Optional standard metadata is retained in the schema so a conforming
|
|
84
|
+
* document can be validated without treating unrelated fields as identity.
|
|
85
|
+
*/
|
|
86
|
+
export const oauthClientMetadataDocumentSchema = z
|
|
87
|
+
.object({
|
|
88
|
+
client_id: z.string().min(1).max(2048),
|
|
89
|
+
client_name: oauthClientNameSchema,
|
|
90
|
+
redirect_uris: z.array(oauthRedirectUriSchema).min(1).max(10),
|
|
91
|
+
grant_types: z.array(oauthGrantTypeSchema).min(1).max(2).optional(),
|
|
92
|
+
response_types: z.array(oauthResponseTypeSchema).min(1).max(1).optional(),
|
|
93
|
+
token_endpoint_auth_method: z.string().min(1).max(128).optional(),
|
|
94
|
+
scope: oauthScopeSchema.optional(),
|
|
95
|
+
client_uri: oauthUrlSchema.optional(),
|
|
96
|
+
logo_uri: oauthUrlSchema.optional(),
|
|
97
|
+
tos_uri: oauthUrlSchema.optional(),
|
|
98
|
+
policy_uri: oauthUrlSchema.optional(),
|
|
99
|
+
jwks_uri: oauthUrlSchema.optional(),
|
|
100
|
+
contacts: z.array(z.string().min(1).max(256)).max(10).optional(),
|
|
101
|
+
})
|
|
102
|
+
.passthrough();
|
|
103
|
+
|
|
104
|
+
export type OAuthClientMetadataDocumentDto = z.infer<typeof oauthClientMetadataDocumentSchema>;
|