@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-auth-dto",
3
3
  "license": "MIT",
4
- "version": "19.0.0",
4
+ "version": "20.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -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>;