mcp-twake-mail 0.1.0 → 2.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/README.md +168 -74
- package/build/cli/commands/setup.js +54 -12
- package/build/cli/commands/setup.js.map +1 -1
- package/build/cli/prompts/setup-wizard.d.ts +33 -1
- package/build/cli/prompts/setup-wizard.js +134 -2
- package/build/cli/prompts/setup-wizard.js.map +1 -1
- package/build/cli/prompts/setup-wizard.test.d.ts +1 -0
- package/build/cli/prompts/setup-wizard.test.js +161 -0
- package/build/cli/prompts/setup-wizard.test.js.map +1 -0
- package/build/config/schema.d.ts +2 -0
- package/build/config/schema.js +3 -0
- package/build/config/schema.js.map +1 -1
- package/build/discovery/dns-srv.d.ts +18 -0
- package/build/discovery/dns-srv.js +60 -0
- package/build/discovery/dns-srv.js.map +1 -0
- package/build/discovery/dns-srv.test.d.ts +4 -0
- package/build/discovery/dns-srv.test.js +79 -0
- package/build/discovery/dns-srv.test.js.map +1 -0
- package/build/discovery/index.d.ts +9 -0
- package/build/discovery/index.js +13 -0
- package/build/discovery/index.js.map +1 -0
- package/build/discovery/oauth-discovery.d.ts +34 -0
- package/build/discovery/oauth-discovery.js +160 -0
- package/build/discovery/oauth-discovery.js.map +1 -0
- package/build/discovery/oauth-discovery.test.d.ts +1 -0
- package/build/discovery/oauth-discovery.test.js +198 -0
- package/build/discovery/oauth-discovery.test.js.map +1 -0
- package/build/discovery/orchestrator.d.ts +31 -0
- package/build/discovery/orchestrator.js +87 -0
- package/build/discovery/orchestrator.js.map +1 -0
- package/build/discovery/orchestrator.test.d.ts +4 -0
- package/build/discovery/orchestrator.test.js +242 -0
- package/build/discovery/orchestrator.test.js.map +1 -0
- package/build/discovery/types.d.ts +18 -0
- package/build/discovery/types.js +15 -0
- package/build/discovery/types.js.map +1 -0
- package/build/discovery/well-known.d.ts +21 -0
- package/build/discovery/well-known.js +52 -0
- package/build/discovery/well-known.js.map +1 -0
- package/build/discovery/well-known.test.d.ts +4 -0
- package/build/discovery/well-known.test.js +120 -0
- package/build/discovery/well-known.test.js.map +1 -0
- package/build/jmap/client.js +1 -1
- package/build/jmap/client.js.map +1 -1
- package/build/mcp/server.d.ts +5 -3
- package/build/mcp/server.js +11 -5
- package/build/mcp/server.js.map +1 -1
- package/build/mcp/tools/batch-operations.d.ts +10 -0
- package/build/mcp/tools/batch-operations.js +609 -0
- package/build/mcp/tools/batch-operations.js.map +1 -0
- package/build/mcp/tools/batch-operations.test.d.ts +1 -0
- package/build/mcp/tools/batch-operations.test.js +875 -0
- package/build/mcp/tools/batch-operations.test.js.map +1 -0
- package/build/mcp/tools/email-operations.js +355 -1
- package/build/mcp/tools/email-operations.js.map +1 -1
- package/build/mcp/tools/email-operations.test.js +5 -3
- package/build/mcp/tools/email-operations.test.js.map +1 -1
- package/build/mcp/tools/email-sending.d.ts +10 -1
- package/build/mcp/tools/email-sending.js +481 -14
- package/build/mcp/tools/email-sending.js.map +1 -1
- package/build/mcp/tools/index.d.ts +10 -1
- package/build/mcp/tools/index.js +7 -3
- package/build/mcp/tools/index.js.map +1 -1
- package/build/mcp/tools/mailbox.js +433 -1
- package/build/mcp/tools/mailbox.js.map +1 -1
- package/build/mcp/tools/mailbox.test.js +5 -2
- package/build/mcp/tools/mailbox.test.js.map +1 -1
- package/build/signature/converter.d.ts +2 -0
- package/build/signature/converter.js +23 -0
- package/build/signature/converter.js.map +1 -0
- package/build/signature/converter.test.d.ts +1 -0
- package/build/signature/converter.test.js +84 -0
- package/build/signature/converter.test.js.map +1 -0
- package/build/signature/index.d.ts +2 -0
- package/build/signature/index.js +3 -0
- package/build/signature/index.js.map +1 -0
- package/build/signature/loader.d.ts +6 -0
- package/build/signature/loader.js +31 -0
- package/build/signature/loader.js.map +1 -0
- package/build/signature/loader.test.d.ts +1 -0
- package/build/signature/loader.test.js +85 -0
- package/build/signature/loader.test.js.map +1 -0
- package/build/types/jmap.d.ts +23 -0
- package/docs/auto-discovery.md +210 -0
- package/docs/oidc-configuration.md +261 -0
- package/package.json +3 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth/OIDC discovery from JMAP resource server
|
|
3
|
+
* Implements RFC 9728 Protected Resource Metadata discovery
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Parse WWW-Authenticate header to extract OAuth metadata.
|
|
7
|
+
* Supports Bearer scheme with realm, scope, and issuer parameters.
|
|
8
|
+
*
|
|
9
|
+
* Example header:
|
|
10
|
+
* Bearer realm="example", scope="openid email", issuer="https://auth.example.com"
|
|
11
|
+
*
|
|
12
|
+
* @param header WWW-Authenticate header value
|
|
13
|
+
* @returns Parsed metadata or null if not Bearer auth
|
|
14
|
+
*/
|
|
15
|
+
export function parseWwwAuthenticate(header) {
|
|
16
|
+
// Check if header starts with "Bearer " (case-insensitive)
|
|
17
|
+
if (!header.match(/^Bearer\s+/i)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// Extract the part after "Bearer "
|
|
21
|
+
const params = header.replace(/^Bearer\s+/i, '');
|
|
22
|
+
// Parse key="value" pairs using regex
|
|
23
|
+
const paramRegex = /(\w+)="([^"]+)"/g;
|
|
24
|
+
const result = {};
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = paramRegex.exec(params)) !== null) {
|
|
27
|
+
const [, key, value] = match;
|
|
28
|
+
if (key === 'issuer' || key === 'realm' || key === 'scope') {
|
|
29
|
+
result[key] = value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Return null if no metadata found
|
|
33
|
+
if (Object.keys(result).length === 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Try common SSO subdomain patterns for OIDC discovery.
|
|
40
|
+
* Many organizations use predictable subdomain patterns for their SSO.
|
|
41
|
+
*
|
|
42
|
+
* @param domain The base domain to check
|
|
43
|
+
* @param timeout Request timeout in ms
|
|
44
|
+
* @returns OidcDiscoveryResult or null if no valid OIDC endpoint found
|
|
45
|
+
*/
|
|
46
|
+
async function tryCommonSsoPatterns(domain, timeout) {
|
|
47
|
+
// Extract base domain (remove subdomain like 'jmap.' or 'mail.')
|
|
48
|
+
const parts = domain.split('.');
|
|
49
|
+
const baseDomain = parts.length > 2 ? parts.slice(-2).join('.') : domain;
|
|
50
|
+
// Common SSO subdomain patterns
|
|
51
|
+
const ssoPatterns = ['sso', 'auth', 'login', 'id', 'accounts'];
|
|
52
|
+
for (const pattern of ssoPatterns) {
|
|
53
|
+
const issuerUrl = `https://${pattern}.${baseDomain}`;
|
|
54
|
+
const discoveryUrl = `${issuerUrl}/.well-known/openid-configuration`;
|
|
55
|
+
try {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
58
|
+
const response = await fetch(discoveryUrl, {
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
clearTimeout(timeoutId);
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
const config = await response.json();
|
|
64
|
+
// Verify it's a valid OIDC config with issuer field
|
|
65
|
+
if (config.issuer) {
|
|
66
|
+
return {
|
|
67
|
+
issuer: config.issuer,
|
|
68
|
+
method: 'well-known-oidc',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// This pattern didn't work, try next
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Discover OAuth authorization server from JMAP resource URL.
|
|
81
|
+
* Implements RFC 9728 Protected Resource Metadata discovery with fallbacks.
|
|
82
|
+
*
|
|
83
|
+
* Discovery order:
|
|
84
|
+
* 1. Try /.well-known/oauth-protected-resource at resource origin
|
|
85
|
+
* 2. If that fails, try fetching JMAP URL to trigger 401 with WWW-Authenticate
|
|
86
|
+
* 3. If that fails, try common SSO subdomain patterns (sso., auth., login.)
|
|
87
|
+
*
|
|
88
|
+
* @param jmapUrl The JMAP session or API URL
|
|
89
|
+
* @param timeout Request timeout in ms (default 10000)
|
|
90
|
+
* @returns OidcDiscoveryResult or null if no OAuth info found
|
|
91
|
+
*/
|
|
92
|
+
export async function discoverOAuthFromResource(jmapUrl, timeout = 10000) {
|
|
93
|
+
try {
|
|
94
|
+
// Parse jmapUrl to get origin
|
|
95
|
+
const url = new URL(jmapUrl);
|
|
96
|
+
const origin = `${url.protocol}//${url.host}`;
|
|
97
|
+
// Try RFC 9728 Protected Resource Metadata first
|
|
98
|
+
const metadataUrl = `${origin}/.well-known/oauth-protected-resource`;
|
|
99
|
+
try {
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
102
|
+
const metadataResponse = await fetch(metadataUrl, {
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
});
|
|
105
|
+
clearTimeout(timeoutId);
|
|
106
|
+
if (metadataResponse.ok) {
|
|
107
|
+
const metadata = await metadataResponse.json();
|
|
108
|
+
// Extract issuer from authorization_servers array
|
|
109
|
+
if (metadata.authorization_servers &&
|
|
110
|
+
Array.isArray(metadata.authorization_servers) &&
|
|
111
|
+
metadata.authorization_servers.length > 0) {
|
|
112
|
+
return {
|
|
113
|
+
issuer: metadata.authorization_servers[0],
|
|
114
|
+
method: 'protected-resource',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Protected resource metadata failed, try WWW-Authenticate fallback
|
|
121
|
+
}
|
|
122
|
+
// Fallback 1: Try fetching JMAP URL directly to get 401 with WWW-Authenticate
|
|
123
|
+
try {
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
126
|
+
const jmapResponse = await fetch(jmapUrl, {
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
});
|
|
129
|
+
clearTimeout(timeoutId);
|
|
130
|
+
// Check for 401 response with WWW-Authenticate header
|
|
131
|
+
if (jmapResponse.status === 401) {
|
|
132
|
+
const wwwAuth = jmapResponse.headers.get('WWW-Authenticate');
|
|
133
|
+
if (wwwAuth) {
|
|
134
|
+
const parsed = parseWwwAuthenticate(wwwAuth);
|
|
135
|
+
if (parsed?.issuer) {
|
|
136
|
+
return {
|
|
137
|
+
issuer: parsed.issuer,
|
|
138
|
+
method: 'www-authenticate',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// WWW-Authenticate fallback also failed
|
|
146
|
+
}
|
|
147
|
+
// Fallback 2: Try common SSO subdomain patterns
|
|
148
|
+
const ssoResult = await tryCommonSsoPatterns(url.hostname, timeout);
|
|
149
|
+
if (ssoResult) {
|
|
150
|
+
return ssoResult;
|
|
151
|
+
}
|
|
152
|
+
// No OAuth info found
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// URL parsing or other error
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=oauth-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-discovery.js","sourceRoot":"","sources":["../../src/discovery/oauth-discovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { parseWwwAuthenticate, discoverOAuthFromResource, } from './oauth-discovery.js';
|
|
3
|
+
describe('parseWwwAuthenticate', () => {
|
|
4
|
+
it('should parse valid Bearer header with issuer', () => {
|
|
5
|
+
const header = 'Bearer issuer="https://auth.example.com"';
|
|
6
|
+
const result = parseWwwAuthenticate(header);
|
|
7
|
+
expect(result).toEqual({
|
|
8
|
+
issuer: 'https://auth.example.com',
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
it('should parse Bearer header with multiple parameters', () => {
|
|
12
|
+
const header = 'Bearer realm="example", scope="openid email", issuer="https://auth.example.com"';
|
|
13
|
+
const result = parseWwwAuthenticate(header);
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
realm: 'example',
|
|
16
|
+
scope: 'openid email',
|
|
17
|
+
issuer: 'https://auth.example.com',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it('should be case-insensitive for Bearer scheme', () => {
|
|
21
|
+
const header = 'bearer issuer="https://auth.example.com"';
|
|
22
|
+
const result = parseWwwAuthenticate(header);
|
|
23
|
+
expect(result).toEqual({
|
|
24
|
+
issuer: 'https://auth.example.com',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it('should return null for non-Bearer header', () => {
|
|
28
|
+
const header = 'Basic realm="example"';
|
|
29
|
+
const result = parseWwwAuthenticate(header);
|
|
30
|
+
expect(result).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
it('should return null for malformed header without parameters', () => {
|
|
33
|
+
const header = 'Bearer';
|
|
34
|
+
const result = parseWwwAuthenticate(header);
|
|
35
|
+
expect(result).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it('should ignore unknown parameters', () => {
|
|
38
|
+
const header = 'Bearer issuer="https://auth.example.com", unknown="value"';
|
|
39
|
+
const result = parseWwwAuthenticate(header);
|
|
40
|
+
expect(result).toEqual({
|
|
41
|
+
issuer: 'https://auth.example.com',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('discoverOAuthFromResource', () => {
|
|
46
|
+
const mockFetch = vi.fn();
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
global.fetch = mockFetch;
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
vi.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
it('should discover issuer from protected-resource metadata', async () => {
|
|
55
|
+
mockFetch.mockResolvedValueOnce({
|
|
56
|
+
ok: true,
|
|
57
|
+
json: async () => ({
|
|
58
|
+
authorization_servers: ['https://auth.example.com'],
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
const result = await discoverOAuthFromResource('https://jmap.example.com/api');
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
issuer: 'https://auth.example.com',
|
|
64
|
+
method: 'protected-resource',
|
|
65
|
+
});
|
|
66
|
+
expect(mockFetch).toHaveBeenCalledWith('https://jmap.example.com/.well-known/oauth-protected-resource', expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
67
|
+
});
|
|
68
|
+
it('should fallback to WWW-Authenticate on 401 response', async () => {
|
|
69
|
+
// First call: protected-resource fails
|
|
70
|
+
mockFetch.mockResolvedValueOnce({
|
|
71
|
+
ok: false,
|
|
72
|
+
status: 404,
|
|
73
|
+
});
|
|
74
|
+
// Second call: JMAP URL returns 401 with WWW-Authenticate
|
|
75
|
+
mockFetch.mockResolvedValueOnce({
|
|
76
|
+
ok: false,
|
|
77
|
+
status: 401,
|
|
78
|
+
headers: {
|
|
79
|
+
get: (name) => {
|
|
80
|
+
if (name === 'WWW-Authenticate') {
|
|
81
|
+
return 'Bearer issuer="https://auth.example.com"';
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const result = await discoverOAuthFromResource('https://jmap.example.com/api');
|
|
88
|
+
expect(result).toEqual({
|
|
89
|
+
issuer: 'https://auth.example.com',
|
|
90
|
+
method: 'www-authenticate',
|
|
91
|
+
});
|
|
92
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
93
|
+
});
|
|
94
|
+
it('should return null when no OAuth info available', async () => {
|
|
95
|
+
// First call: protected-resource fails
|
|
96
|
+
mockFetch.mockResolvedValueOnce({
|
|
97
|
+
ok: false,
|
|
98
|
+
status: 404,
|
|
99
|
+
});
|
|
100
|
+
// Second call: JMAP URL returns 200 (no auth required)
|
|
101
|
+
mockFetch.mockResolvedValueOnce({
|
|
102
|
+
ok: true,
|
|
103
|
+
status: 200,
|
|
104
|
+
});
|
|
105
|
+
const result = await discoverOAuthFromResource('https://jmap.example.com/api');
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
it('should return null when WWW-Authenticate has no issuer', async () => {
|
|
109
|
+
// First call: protected-resource fails
|
|
110
|
+
mockFetch.mockResolvedValueOnce({
|
|
111
|
+
ok: false,
|
|
112
|
+
status: 404,
|
|
113
|
+
});
|
|
114
|
+
// Second call: JMAP URL returns 401 with WWW-Authenticate but no issuer
|
|
115
|
+
mockFetch.mockResolvedValueOnce({
|
|
116
|
+
ok: false,
|
|
117
|
+
status: 401,
|
|
118
|
+
headers: {
|
|
119
|
+
get: (name) => {
|
|
120
|
+
if (name === 'WWW-Authenticate') {
|
|
121
|
+
return 'Bearer realm="example"';
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const result = await discoverOAuthFromResource('https://jmap.example.com/api');
|
|
128
|
+
expect(result).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
it('should handle network errors gracefully', async () => {
|
|
131
|
+
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
132
|
+
const result = await discoverOAuthFromResource('https://jmap.example.com/api');
|
|
133
|
+
expect(result).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
it('should handle invalid URL gracefully', async () => {
|
|
136
|
+
const result = await discoverOAuthFromResource('not-a-url');
|
|
137
|
+
expect(result).toBeNull();
|
|
138
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
it('should respect custom timeout', async () => {
|
|
141
|
+
const abortError = new Error('Aborted');
|
|
142
|
+
abortError.name = 'AbortError';
|
|
143
|
+
mockFetch.mockRejectedValue(abortError);
|
|
144
|
+
const result = await discoverOAuthFromResource('https://jmap.example.com/api', 100);
|
|
145
|
+
expect(result).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
it('should handle protected-resource with empty authorization_servers', async () => {
|
|
148
|
+
mockFetch.mockResolvedValueOnce({
|
|
149
|
+
ok: true,
|
|
150
|
+
json: async () => ({
|
|
151
|
+
authorization_servers: [],
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
// Second call: fallback to WWW-Authenticate
|
|
155
|
+
mockFetch.mockResolvedValueOnce({
|
|
156
|
+
ok: false,
|
|
157
|
+
status: 401,
|
|
158
|
+
headers: {
|
|
159
|
+
get: (name) => {
|
|
160
|
+
if (name === 'WWW-Authenticate') {
|
|
161
|
+
return 'Bearer issuer="https://auth.example.com"';
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
const result = await discoverOAuthFromResource('https://jmap.example.com/api');
|
|
168
|
+
expect(result).toEqual({
|
|
169
|
+
issuer: 'https://auth.example.com',
|
|
170
|
+
method: 'www-authenticate',
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it('should handle protected-resource with missing authorization_servers', async () => {
|
|
174
|
+
mockFetch.mockResolvedValueOnce({
|
|
175
|
+
ok: true,
|
|
176
|
+
json: async () => ({}),
|
|
177
|
+
});
|
|
178
|
+
// Second call: fallback to WWW-Authenticate
|
|
179
|
+
mockFetch.mockResolvedValueOnce({
|
|
180
|
+
ok: false,
|
|
181
|
+
status: 401,
|
|
182
|
+
headers: {
|
|
183
|
+
get: (name) => {
|
|
184
|
+
if (name === 'WWW-Authenticate') {
|
|
185
|
+
return 'Bearer issuer="https://auth.example.com"';
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const result = await discoverOAuthFromResource('https://jmap.example.com/api');
|
|
192
|
+
expect(result).toEqual({
|
|
193
|
+
issuer: 'https://auth.example.com',
|
|
194
|
+
method: 'www-authenticate',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
//# sourceMappingURL=oauth-discovery.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-discovery.test.js","sourceRoot":"","sources":["../../src/discovery/oauth-discovery.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EACL,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAE9B,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,MAAM,GAAG,0CAA0C,CAAC;QAC1D,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAE5C,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,0BAA0B;SACnC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GACV,iFAAiF,CAAC;QACpF,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAE5C,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,cAAc;YACrB,MAAM,EAAE,0BAA0B;SACnC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,MAAM,GAAG,0CAA0C,CAAC;QAC1D,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAE5C,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,0BAA0B;SACnC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,MAAM,GAAG,uBAAuB,CAAC;QACvC,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAE5C,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,MAAM,GAAG,QAAQ,CAAC;QACxB,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAE5C,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,MAAM,GACV,2DAA2D,CAAC;QAC9D,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAE5C,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,0BAA0B;SACnC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;IAE1B,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,CAAC,KAAK,GAAG,SAAS,CAAC;QACzB,EAAE,CAAC,aAAa,EAAE,CAAC;IACrB,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACvE,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;gBACjB,qBAAqB,EAAE,CAAC,0BAA0B,CAAC;aACpD,CAAC;SACH,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAC5C,8BAA8B,CAC/B,CAAC;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,0BAA0B;YAClC,MAAM,EAAE,oBAAoB;SAC7B,CAAC,CAAC;QAEH,MAAM,CAAC,SAAS,CAAC,CAAC,oBAAoB,CACpC,+DAA+D,EAC/D,MAAM,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAC7D,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,uCAAuC;QACvC,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;SACZ,CAAC,CAAC;QAEH,0DAA0D;QAC1D,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,OAAO,EAAE;gBACP,GAAG,EAAE,CAAC,IAAY,EAAE,EAAE;oBACpB,IAAI,IAAI,KAAK,kBAAkB,EAAE,CAAC;wBAChC,OAAO,0CAA0C,CAAC;oBACpD,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;aACF;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAC5C,8BAA8B,CAC/B,CAAC;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,0BAA0B;YAClC,MAAM,EAAE,kBAAkB;SAC3B,CAAC,CAAC;QAEH,MAAM,CAAC,SAAS,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,uCAAuC;QACvC,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;SACZ,CAAC,CAAC;QAEH,uDAAuD;QACvD,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,GAAG;SACZ,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAC5C,8BAA8B,CAC/B,CAAC;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,uCAAuC;QACvC,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;SACZ,CAAC,CAAC;QAEH,wEAAwE;QACxE,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,OAAO,EAAE;gBACP,GAAG,EAAE,CAAC,IAAY,EAAE,EAAE;oBACpB,IAAI,IAAI,KAAK,kBAAkB,EAAE,CAAC;wBAChC,OAAO,wBAAwB,CAAC;oBAClC,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;aACF;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAC5C,8BAA8B,CAC/B,CAAC;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,SAAS,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;QAExD,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAC5C,8BAA8B,CAC/B,CAAC;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAAC,WAAW,CAAC,CAAC;QAE5D,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC1B,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QAC7C,MAAM,UAAU,GAAG,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC;QACxC,UAAU,CAAC,IAAI,GAAG,YAAY,CAAC;QAC/B,SAAS,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAExC,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAC5C,8BAA8B,EAC9B,GAAG,CACJ,CAAC;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;QACjF,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;gBACjB,qBAAqB,EAAE,EAAE;aAC1B,CAAC;SACH,CAAC,CAAC;QAEH,4CAA4C;QAC5C,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,OAAO,EAAE;gBACP,GAAG,EAAE,CAAC,IAAY,EAAE,EAAE;oBACpB,IAAI,IAAI,KAAK,kBAAkB,EAAE,CAAC;wBAChC,OAAO,0CAA0C,CAAC;oBACpD,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;aACF;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAC5C,8BAA8B,CAC/B,CAAC;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,0BAA0B;YAClC,MAAM,EAAE,kBAAkB;SAC3B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;QACnF,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;SACvB,CAAC,CAAC;QAEH,4CAA4C;QAC5C,SAAS,CAAC,qBAAqB,CAAC;YAC9B,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,OAAO,EAAE;gBACP,GAAG,EAAE,CAAC,IAAY,EAAE,EAAE;oBACpB,IAAI,IAAI,KAAK,kBAAkB,EAAE,CAAC;wBAChC,OAAO,0CAA0C,CAAC;oBACpD,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;aACF;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAC5C,8BAA8B,CAC/B,CAAC;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,0BAA0B;YAClC,MAAM,EAAE,kBAAkB;SAC3B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery orchestrator - chains DNS SRV, well-known, and OAuth discovery
|
|
3
|
+
* Provides high-level API to discover JMAP and OIDC settings from email address
|
|
4
|
+
*/
|
|
5
|
+
import { JmapDiscoveryResult, OidcDiscoveryResult } from './types.js';
|
|
6
|
+
export interface FullDiscoveryResult {
|
|
7
|
+
jmap: JmapDiscoveryResult;
|
|
8
|
+
oidc?: OidcDiscoveryResult;
|
|
9
|
+
email: string;
|
|
10
|
+
domain: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Extract domain from email address.
|
|
14
|
+
* @throws Error if email format invalid
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractDomain(email: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Discover JMAP and OIDC settings from an email address.
|
|
19
|
+
*
|
|
20
|
+
* Discovery stages:
|
|
21
|
+
* 1. Extract domain from email
|
|
22
|
+
* 2. Try DNS SRV lookup for _jmap._tcp.{domain}
|
|
23
|
+
* 3. If SRV found, construct URL and verify it works
|
|
24
|
+
* 4. If SRV fails, try .well-known/jmap on domain
|
|
25
|
+
* 5. If JMAP found, attempt OAuth discovery on that URL
|
|
26
|
+
*
|
|
27
|
+
* @param email User's email address (e.g., "user@example.com")
|
|
28
|
+
* @returns Full discovery result with JMAP and optional OIDC settings
|
|
29
|
+
* @throws DiscoveryError if JMAP server cannot be discovered
|
|
30
|
+
*/
|
|
31
|
+
export declare function discoverFromEmail(email: string): Promise<FullDiscoveryResult>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery orchestrator - chains DNS SRV, well-known, and OAuth discovery
|
|
3
|
+
* Provides high-level API to discover JMAP and OIDC settings from email address
|
|
4
|
+
*/
|
|
5
|
+
import { resolveSrvRecord } from './dns-srv.js';
|
|
6
|
+
import { fetchWellKnownJmap, verifyJmapUrl } from './well-known.js';
|
|
7
|
+
import { discoverOAuthFromResource } from './oauth-discovery.js';
|
|
8
|
+
import { DiscoveryError, } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Extract domain from email address.
|
|
11
|
+
* @throws Error if email format invalid
|
|
12
|
+
*/
|
|
13
|
+
export function extractDomain(email) {
|
|
14
|
+
// Split on '@' and take second part
|
|
15
|
+
const parts = email.split('@');
|
|
16
|
+
if (parts.length !== 2) {
|
|
17
|
+
throw new Error('Invalid email format');
|
|
18
|
+
}
|
|
19
|
+
const domain = parts[1];
|
|
20
|
+
// Validate domain has at least one '.'
|
|
21
|
+
if (!domain.includes('.')) {
|
|
22
|
+
throw new Error('Invalid email format');
|
|
23
|
+
}
|
|
24
|
+
return domain;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Discover JMAP and OIDC settings from an email address.
|
|
28
|
+
*
|
|
29
|
+
* Discovery stages:
|
|
30
|
+
* 1. Extract domain from email
|
|
31
|
+
* 2. Try DNS SRV lookup for _jmap._tcp.{domain}
|
|
32
|
+
* 3. If SRV found, construct URL and verify it works
|
|
33
|
+
* 4. If SRV fails, try .well-known/jmap on domain
|
|
34
|
+
* 5. If JMAP found, attempt OAuth discovery on that URL
|
|
35
|
+
*
|
|
36
|
+
* @param email User's email address (e.g., "user@example.com")
|
|
37
|
+
* @returns Full discovery result with JMAP and optional OIDC settings
|
|
38
|
+
* @throws DiscoveryError if JMAP server cannot be discovered
|
|
39
|
+
*/
|
|
40
|
+
export async function discoverFromEmail(email) {
|
|
41
|
+
// Extract domain from email
|
|
42
|
+
const domain = extractDomain(email);
|
|
43
|
+
// Stage 1 - DNS SRV discovery
|
|
44
|
+
const srv = await resolveSrvRecord(domain);
|
|
45
|
+
if (srv) {
|
|
46
|
+
// Construct URL from SRV record
|
|
47
|
+
const url = srv.port === 443
|
|
48
|
+
? `https://${srv.hostname}/.well-known/jmap`
|
|
49
|
+
: `https://${srv.hostname}:${srv.port}/.well-known/jmap`;
|
|
50
|
+
// Verify the URL works
|
|
51
|
+
const verified = await verifyJmapUrl(url);
|
|
52
|
+
if (verified) {
|
|
53
|
+
const jmap = {
|
|
54
|
+
sessionUrl: verified,
|
|
55
|
+
method: 'dns-srv',
|
|
56
|
+
};
|
|
57
|
+
// Stage 4 - OAuth discovery
|
|
58
|
+
const oidc = await discoverOAuthFromResource(verified);
|
|
59
|
+
return {
|
|
60
|
+
jmap,
|
|
61
|
+
oidc: oidc ?? undefined,
|
|
62
|
+
email,
|
|
63
|
+
domain,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Stage 2 - Well-known fallback
|
|
68
|
+
const wellKnown = await fetchWellKnownJmap(domain);
|
|
69
|
+
if (wellKnown) {
|
|
70
|
+
const jmap = {
|
|
71
|
+
sessionUrl: wellKnown,
|
|
72
|
+
method: 'well-known-direct',
|
|
73
|
+
};
|
|
74
|
+
// Stage 4 - OAuth discovery
|
|
75
|
+
const oidc = await discoverOAuthFromResource(wellKnown);
|
|
76
|
+
return {
|
|
77
|
+
jmap,
|
|
78
|
+
oidc: oidc ?? undefined,
|
|
79
|
+
email,
|
|
80
|
+
domain,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Stage 3 - Failure
|
|
84
|
+
throw new DiscoveryError(`Could not discover JMAP server for domain "${domain}". ` +
|
|
85
|
+
'The domain does not have a JMAP SRV record or .well-known/jmap endpoint.', domain, 'well-known');
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator.js","sourceRoot":"","sources":["../../src/discovery/orchestrator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAGL,cAAc,GACf,MAAM,YAAY,CAAC;AASpB;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,oCAAoC;IACpC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1C,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAExB,uCAAuC;IACvC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1C,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAa;IAEb,4BAA4B;IAC5B,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAEpC,8BAA8B;IAC9B,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,GAAG,EAAE,CAAC;QACR,gCAAgC;QAChC,MAAM,GAAG,GACP,GAAG,CAAC,IAAI,KAAK,GAAG;YACd,CAAC,CAAC,WAAW,GAAG,CAAC,QAAQ,mBAAmB;YAC5C,CAAC,CAAC,WAAW,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,IAAI,mBAAmB,CAAC;QAE7D,uBAAuB;QACvB,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,IAAI,GAAwB;gBAChC,UAAU,EAAE,QAAQ;gBACpB,MAAM,EAAE,SAAS;aAClB,CAAC;YAEF,4BAA4B;YAC5B,MAAM,IAAI,GAAG,MAAM,yBAAyB,CAAC,QAAQ,CAAC,CAAC;YAEvD,OAAO;gBACL,IAAI;gBACJ,IAAI,EAAE,IAAI,IAAI,SAAS;gBACvB,KAAK;gBACL,MAAM;aACP,CAAC;QACJ,CAAC;IACH,CAAC;IAED,gCAAgC;IAChC,MAAM,SAAS,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,IAAI,GAAwB;YAChC,UAAU,EAAE,SAAS;YACrB,MAAM,EAAE,mBAAmB;SAC5B,CAAC;QAEF,4BAA4B;QAC5B,MAAM,IAAI,GAAG,MAAM,yBAAyB,CAAC,SAAS,CAAC,CAAC;QAExD,OAAO;YACL,IAAI;YACJ,IAAI,EAAE,IAAI,IAAI,SAAS;YACvB,KAAK;YACL,MAAM;SACP,CAAC;IACJ,CAAC;IAED,oBAAoB;IACpB,MAAM,IAAI,cAAc,CACtB,8CAA8C,MAAM,KAAK;QACvD,0EAA0E,EAC5E,MAAM,EACN,YAAY,CACb,CAAC;AACJ,CAAC"}
|