@airdraft/plugin-auth 0.1.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.
@@ -0,0 +1,224 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { signAccessToken, signRefreshToken, verifyAccessToken, } from '@airdraft/auth';
3
+ import { refreshTokenBlocklist } from './blocklist.js';
4
+ async function getGitHubUser(accessToken) {
5
+ const res = await fetch('https://api.github.com/user', {
6
+ headers: {
7
+ Authorization: `Bearer ${accessToken}`,
8
+ Accept: 'application/vnd.github+json',
9
+ 'X-GitHub-Api-Version': '2022-11-28',
10
+ },
11
+ });
12
+ return res.ok ? (await res.json()) : null;
13
+ }
14
+ async function getGitHubEmail(accessToken) {
15
+ const res = await fetch('https://api.github.com/user/emails', {
16
+ headers: {
17
+ Authorization: `Bearer ${accessToken}`,
18
+ Accept: 'application/vnd.github+json',
19
+ 'X-GitHub-Api-Version': '2022-11-28',
20
+ },
21
+ });
22
+ if (!res.ok)
23
+ return null;
24
+ const emails = (await res.json());
25
+ return emails.find((e) => e.primary && e.verified)?.email ?? null;
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Cookie helpers (shared with CredentialsProvider)
29
+ // ---------------------------------------------------------------------------
30
+ function parseCookie(header, name) {
31
+ if (!header)
32
+ return null;
33
+ for (const part of header.split(';')) {
34
+ const [key, ...rest] = part.trim().split('=');
35
+ if (key?.trim() === name)
36
+ return decodeURIComponent(rest.join('='));
37
+ }
38
+ return null;
39
+ }
40
+ function isSecure(req) {
41
+ try {
42
+ return new URL(req.url).protocol === 'https:';
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ function setCookieHeader(name, value, attrs) {
49
+ return [`${name}=${encodeURIComponent(value)}`, ...attrs].join('; ');
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // GitHubOAuthProvider (C12)
53
+ // ---------------------------------------------------------------------------
54
+ /**
55
+ * GitHub OAuth2 provider for Airdraft CMS.
56
+ *
57
+ * Flow:
58
+ * 1. Browser hits `GET /auth/github` → redirected to GitHub OAuth
59
+ * 2. GitHub redirects back to `GET /auth/github/callback?code=...&state=...`
60
+ * 3. Server exchanges code for token, fetches GitHub user, issues JWT
61
+ *
62
+ * CSRF protection: `state` param is a random nonce stored in a session cookie.
63
+ *
64
+ * ```ts
65
+ * withAuth({
66
+ * provider: GitHubOAuthProvider({
67
+ * clientId: process.env.GITHUB_CLIENT_ID!,
68
+ * clientSecret: process.env.GITHUB_CLIENT_SECRET!,
69
+ * secret: process.env.AIRDRAFT_JWT_SECRET!,
70
+ * roles: { 'alice@example.com': 'admin' },
71
+ * })
72
+ * })
73
+ * ```
74
+ */
75
+ export function GitHubOAuthProvider(opts) {
76
+ const secret = opts.secret ?? process.env['AIRDRAFT_JWT_SECRET'] ?? '';
77
+ if (!secret || secret.length < 32) {
78
+ throw new Error('[airdraft] AIRDRAFT_JWT_SECRET must be at least 32 characters. ' +
79
+ 'Generate one with: npx airdraft generate-secret');
80
+ }
81
+ const configRoles = opts.roles ?? {};
82
+ const basePath = opts.basePath ?? '/api/cms';
83
+ const defaultRole = opts.defaultRole ?? 'editor';
84
+ const ACCESS_TTL = 15 * 60;
85
+ const REFRESH_TTL = 7 * 24 * 60 * 60;
86
+ function resolveRole(email) {
87
+ return configRoles[email.toLowerCase()] ?? defaultRole;
88
+ }
89
+ async function verify(req) {
90
+ const cookie = req.headers.get('cookie');
91
+ const token = parseCookie(cookie, 'airdraft_session');
92
+ if (!token)
93
+ return null;
94
+ return verifyAccessToken(token, secret);
95
+ }
96
+ /** GET /auth/github — redirect to GitHub OAuth */
97
+ async function githubInitHandler(req) {
98
+ const callbackUrl = opts.callbackUrl ?? `${new URL(req.url).origin}${basePath}/auth/github/callback`;
99
+ const state = randomBytes(16).toString('hex');
100
+ const params = new URLSearchParams({
101
+ client_id: opts.clientId,
102
+ redirect_uri: callbackUrl,
103
+ scope: 'user:email read:org',
104
+ state,
105
+ });
106
+ const headers = new Headers({
107
+ Location: `https://github.com/login/oauth/authorize?${params.toString()}`,
108
+ });
109
+ headers.set('Set-Cookie', setCookieHeader('airdraft_oauth_state', state, [
110
+ 'HttpOnly',
111
+ 'SameSite=Lax',
112
+ 'Path=/',
113
+ 'Max-Age=600',
114
+ ...(isSecure(req) ? ['Secure'] : []),
115
+ ]));
116
+ return new Response(null, { status: 302, headers });
117
+ }
118
+ /** GET /auth/github/callback */
119
+ async function githubCallbackHandler(req) {
120
+ const url = new URL(req.url);
121
+ const code = url.searchParams.get('code');
122
+ const state = url.searchParams.get('state');
123
+ // CSRF check
124
+ const savedState = parseCookie(req.headers.get('cookie'), 'airdraft_oauth_state');
125
+ if (!code || !state || state !== savedState) {
126
+ return new Response('OAuth state mismatch', { status: 400 });
127
+ }
128
+ // Exchange code for token
129
+ const callbackUrl = opts.callbackUrl ?? `${url.origin}${basePath}/auth/github/callback`;
130
+ const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
131
+ method: 'POST',
132
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({
134
+ client_id: opts.clientId,
135
+ client_secret: opts.clientSecret,
136
+ code,
137
+ redirect_uri: callbackUrl,
138
+ }),
139
+ });
140
+ if (!tokenRes.ok)
141
+ return new Response('Failed to exchange OAuth code', { status: 502 });
142
+ const { access_token } = (await tokenRes.json());
143
+ if (!access_token)
144
+ return new Response('No access token in GitHub response', { status: 502 });
145
+ // Fetch user
146
+ const ghUser = await getGitHubUser(access_token);
147
+ if (!ghUser)
148
+ return new Response('Failed to fetch GitHub user', { status: 502 });
149
+ const email = ghUser.email ?? (await getGitHubEmail(access_token)) ?? `${ghUser.login}@users.noreply.github.com`;
150
+ // Check allowlists
151
+ if (opts.allowedUsers?.length && !opts.allowedUsers.includes(ghUser.login)) {
152
+ return new Response('GitHub user not authorised', { status: 403 });
153
+ }
154
+ if (opts.allowedOrgs?.length) {
155
+ const orgsRes = await fetch('https://api.github.com/user/orgs', {
156
+ headers: { Authorization: `Bearer ${access_token}`, Accept: 'application/vnd.github+json' },
157
+ });
158
+ const orgs = orgsRes.ok ? (await orgsRes.json()) : [];
159
+ const memberOf = orgs.map((o) => o.login);
160
+ if (!opts.allowedOrgs.some((o) => memberOf.includes(o))) {
161
+ return new Response('GitHub org not authorised', { status: 403 });
162
+ }
163
+ }
164
+ const role = resolveRole(email);
165
+ const user = {
166
+ id: `github:${ghUser.id}`,
167
+ email,
168
+ name: ghUser.name ?? ghUser.login,
169
+ role,
170
+ };
171
+ const accessToken = signAccessToken(user, secret);
172
+ const refreshToken = signRefreshToken();
173
+ const secure = isSecure(req);
174
+ const headers = new Headers({
175
+ 'Content-Type': 'application/json',
176
+ Location: `${url.origin}${basePath.replace('/api/cms', '')}/cms`,
177
+ });
178
+ headers.append('Set-Cookie', setCookieHeader('airdraft_session', accessToken, [
179
+ 'HttpOnly',
180
+ 'SameSite=Strict',
181
+ 'Path=/',
182
+ `Max-Age=${ACCESS_TTL}`,
183
+ ...(secure ? ['Secure'] : []),
184
+ ]));
185
+ headers.append('Set-Cookie', setCookieHeader('airdraft_refresh', refreshToken, [
186
+ 'HttpOnly',
187
+ 'SameSite=Strict',
188
+ `Path=${basePath}/auth/refresh`,
189
+ `Max-Age=${REFRESH_TTL}`,
190
+ ...(secure ? ['Secure'] : []),
191
+ ]));
192
+ // Clear state cookie
193
+ headers.append('Set-Cookie', 'airdraft_oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0');
194
+ return new Response(null, { status: 302, headers });
195
+ }
196
+ /** POST /auth/logout */
197
+ async function logoutHandler(req) {
198
+ const cookie = req.headers.get('cookie');
199
+ const refreshToken = parseCookie(cookie, 'airdraft_refresh');
200
+ if (refreshToken)
201
+ refreshTokenBlocklist.add(refreshToken);
202
+ const headers = new Headers({ 'Content-Type': 'application/json' });
203
+ headers.append('Set-Cookie', `airdraft_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0`);
204
+ headers.append('Set-Cookie', `airdraft_refresh=; HttpOnly; SameSite=Strict; Path=${basePath}/auth/refresh; Max-Age=0`);
205
+ return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
206
+ }
207
+ /** GET /auth/me */
208
+ async function meHandler(req) {
209
+ const user = await verify(req);
210
+ if (!user)
211
+ return new Response(JSON.stringify({ error: 'Not authenticated' }), { status: 401 });
212
+ return new Response(JSON.stringify({ user: { id: user.id, email: user.email, name: user.name, role: user.role } }), { status: 200, headers: { 'Content-Type': 'application/json' } });
213
+ }
214
+ function handlers() {
215
+ return [
216
+ { path: '/auth/github', handler: githubInitHandler },
217
+ { path: '/auth/github/callback', handler: githubCallbackHandler },
218
+ { path: '/auth/logout', handler: logoutHandler },
219
+ { path: '/auth/me', handler: meHandler },
220
+ ];
221
+ }
222
+ return { verify, handlers };
223
+ }
224
+ //# sourceMappingURL=GitHubOAuthProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GitHubOAuthProvider.js","sourceRoot":"","sources":["../src/GitHubOAuthProvider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { Role } from '@airdraft/auth';
2
+ import type { AuthProvider } from './types.js';
3
+ export interface GoogleOAuthProviderOptions {
4
+ clientId: string;
5
+ clientSecret: string;
6
+ /**
7
+ * AIRDRAFT_JWT_SECRET — must be ≥ 32 chars (C7).
8
+ * Defaults to `process.env.AIRDRAFT_JWT_SECRET`.
9
+ */
10
+ secret?: string;
11
+ /** Email-keyed role overrides (C3). */
12
+ roles?: Record<string, Role>;
13
+ /** Only these verified email addresses may access the CMS. */
14
+ allowedEmails?: string[];
15
+ /** Only emails with these domains may access (e.g. `['mycompany.com']`). */
16
+ allowedDomains?: string[];
17
+ /**
18
+ * Default role for Google users not in `roles`.
19
+ * Defaults to 'editor'.
20
+ */
21
+ defaultRole?: Role;
22
+ /** Base path for the CMS API (default: '/api/cms'). */
23
+ basePath?: string;
24
+ /** OAuth callback URL (default: `{origin}{basePath}/auth/google/callback`). */
25
+ callbackUrl?: string;
26
+ }
27
+ /**
28
+ * Google OAuth2 provider for Airdraft CMS.
29
+ *
30
+ * Flow:
31
+ * 1. Browser hits `GET /auth/google` → redirected to Google OAuth
32
+ * 2. Google redirects back to `GET /auth/google/callback?code=...&state=...`
33
+ * 3. Server exchanges code for token, fetches Google user info, issues JWT
34
+ *
35
+ * CSRF protection: `state` param is a random nonce stored in a session cookie.
36
+ *
37
+ * ```ts
38
+ * withAuth({
39
+ * provider: GoogleOAuthProvider({
40
+ * clientId: process.env.GOOGLE_CLIENT_ID!,
41
+ * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
42
+ * secret: process.env.AIRDRAFT_JWT_SECRET!,
43
+ * allowedDomains: ['mycompany.com'],
44
+ * })
45
+ * })
46
+ * ```
47
+ */
48
+ export declare function GoogleOAuthProvider(opts: GoogleOAuthProviderOptions): AuthProvider;
49
+ //# sourceMappingURL=GoogleOAuthProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GoogleOAuthProvider.d.ts","sourceRoot":"","sources":["../src/GoogleOAuthProvider.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAY,IAAI,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,YAAY,EAAmB,MAAM,YAAY,CAAA;AA0D/D,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAC5B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB;;;OAGG;IACH,WAAW,CAAC,EAAE,IAAI,CAAA;IAClB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B,GAAG,YAAY,CAmLlF"}
@@ -0,0 +1,205 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { signAccessToken, signRefreshToken, verifyAccessToken, } from '@airdraft/auth';
3
+ import { refreshTokenBlocklist } from './blocklist.js';
4
+ async function getGoogleUserInfo(accessToken) {
5
+ const res = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
6
+ headers: { Authorization: `Bearer ${accessToken}` },
7
+ });
8
+ return res.ok ? (await res.json()) : null;
9
+ }
10
+ // ---------------------------------------------------------------------------
11
+ // Cookie helpers
12
+ // ---------------------------------------------------------------------------
13
+ function parseCookie(header, name) {
14
+ if (!header)
15
+ return null;
16
+ for (const part of header.split(';')) {
17
+ const [key, ...rest] = part.trim().split('=');
18
+ if (key?.trim() === name)
19
+ return decodeURIComponent(rest.join('='));
20
+ }
21
+ return null;
22
+ }
23
+ function isSecure(req) {
24
+ try {
25
+ return new URL(req.url).protocol === 'https:';
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function setCookieHeader(name, value, attrs) {
32
+ return [`${name}=${encodeURIComponent(value)}`, ...attrs].join('; ');
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // GoogleOAuthProvider (C12)
36
+ // ---------------------------------------------------------------------------
37
+ /**
38
+ * Google OAuth2 provider for Airdraft CMS.
39
+ *
40
+ * Flow:
41
+ * 1. Browser hits `GET /auth/google` → redirected to Google OAuth
42
+ * 2. Google redirects back to `GET /auth/google/callback?code=...&state=...`
43
+ * 3. Server exchanges code for token, fetches Google user info, issues JWT
44
+ *
45
+ * CSRF protection: `state` param is a random nonce stored in a session cookie.
46
+ *
47
+ * ```ts
48
+ * withAuth({
49
+ * provider: GoogleOAuthProvider({
50
+ * clientId: process.env.GOOGLE_CLIENT_ID!,
51
+ * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
52
+ * secret: process.env.AIRDRAFT_JWT_SECRET!,
53
+ * allowedDomains: ['mycompany.com'],
54
+ * })
55
+ * })
56
+ * ```
57
+ */
58
+ export function GoogleOAuthProvider(opts) {
59
+ const secret = opts.secret ?? process.env['AIRDRAFT_JWT_SECRET'] ?? '';
60
+ if (!secret || secret.length < 32) {
61
+ throw new Error('[airdraft] AIRDRAFT_JWT_SECRET must be at least 32 characters. ' +
62
+ 'Generate one with: npx airdraft generate-secret');
63
+ }
64
+ const configRoles = opts.roles ?? {};
65
+ const basePath = opts.basePath ?? '/api/cms';
66
+ const defaultRole = opts.defaultRole ?? 'editor';
67
+ const ACCESS_TTL = 15 * 60;
68
+ const REFRESH_TTL = 7 * 24 * 60 * 60;
69
+ function resolveRole(email) {
70
+ return configRoles[email.toLowerCase()] ?? defaultRole;
71
+ }
72
+ async function verify(req) {
73
+ const cookie = req.headers.get('cookie');
74
+ const token = parseCookie(cookie, 'airdraft_session');
75
+ if (!token)
76
+ return null;
77
+ return verifyAccessToken(token, secret);
78
+ }
79
+ /** GET /auth/google — redirect to Google OAuth */
80
+ async function googleInitHandler(req) {
81
+ const callbackUrl = opts.callbackUrl ?? `${new URL(req.url).origin}${basePath}/auth/google/callback`;
82
+ const state = randomBytes(16).toString('hex');
83
+ const params = new URLSearchParams({
84
+ client_id: opts.clientId,
85
+ redirect_uri: callbackUrl,
86
+ response_type: 'code',
87
+ scope: 'openid email profile',
88
+ access_type: 'offline',
89
+ prompt: 'consent',
90
+ state,
91
+ });
92
+ const headers = new Headers({
93
+ Location: `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`,
94
+ });
95
+ headers.set('Set-Cookie', setCookieHeader('airdraft_oauth_state', state, [
96
+ 'HttpOnly',
97
+ 'SameSite=Lax',
98
+ 'Path=/',
99
+ 'Max-Age=600',
100
+ ...(isSecure(req) ? ['Secure'] : []),
101
+ ]));
102
+ return new Response(null, { status: 302, headers });
103
+ }
104
+ /** GET /auth/google/callback */
105
+ async function googleCallbackHandler(req) {
106
+ const url = new URL(req.url);
107
+ const code = url.searchParams.get('code');
108
+ const state = url.searchParams.get('state');
109
+ // CSRF check
110
+ const savedState = parseCookie(req.headers.get('cookie'), 'airdraft_oauth_state');
111
+ if (!code || !state || state !== savedState) {
112
+ return new Response('OAuth state mismatch', { status: 400 });
113
+ }
114
+ const callbackUrl = opts.callbackUrl ?? `${url.origin}${basePath}/auth/google/callback`;
115
+ // Exchange code for token
116
+ const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
119
+ body: new URLSearchParams({
120
+ code,
121
+ client_id: opts.clientId,
122
+ client_secret: opts.clientSecret,
123
+ redirect_uri: callbackUrl,
124
+ grant_type: 'authorization_code',
125
+ }).toString(),
126
+ });
127
+ if (!tokenRes.ok)
128
+ return new Response('Failed to exchange OAuth code', { status: 502 });
129
+ const tokens = (await tokenRes.json());
130
+ // Fetch user info
131
+ const googleUser = await getGoogleUserInfo(tokens.access_token);
132
+ if (!googleUser)
133
+ return new Response('Failed to fetch Google user info', { status: 502 });
134
+ if (!googleUser.email_verified)
135
+ return new Response('Google email not verified', { status: 403 });
136
+ const email = googleUser.email.toLowerCase();
137
+ // Allowlist checks
138
+ if (opts.allowedEmails?.length && !opts.allowedEmails.map((e) => e.toLowerCase()).includes(email)) {
139
+ return new Response('Email not authorised', { status: 403 });
140
+ }
141
+ if (opts.allowedDomains?.length) {
142
+ const domain = email.split('@')[1];
143
+ if (!domain || !opts.allowedDomains.includes(domain)) {
144
+ return new Response('Email domain not authorised', { status: 403 });
145
+ }
146
+ }
147
+ const role = resolveRole(email);
148
+ const user = {
149
+ id: `google:${googleUser.sub}`,
150
+ email,
151
+ name: googleUser.name,
152
+ role,
153
+ };
154
+ const accessToken = signAccessToken(user, secret);
155
+ const refreshToken = signRefreshToken();
156
+ const secure = isSecure(req);
157
+ const headers = new Headers({
158
+ Location: `${url.origin}${basePath.replace('/api/cms', '')}/cms`,
159
+ });
160
+ headers.append('Set-Cookie', setCookieHeader('airdraft_session', accessToken, [
161
+ 'HttpOnly',
162
+ 'SameSite=Strict',
163
+ 'Path=/',
164
+ `Max-Age=${ACCESS_TTL}`,
165
+ ...(secure ? ['Secure'] : []),
166
+ ]));
167
+ headers.append('Set-Cookie', setCookieHeader('airdraft_refresh', refreshToken, [
168
+ 'HttpOnly',
169
+ 'SameSite=Strict',
170
+ `Path=${basePath}/auth/refresh`,
171
+ `Max-Age=${REFRESH_TTL}`,
172
+ ...(secure ? ['Secure'] : []),
173
+ ]));
174
+ headers.append('Set-Cookie', 'airdraft_oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0');
175
+ return new Response(null, { status: 302, headers });
176
+ }
177
+ /** POST /auth/logout */
178
+ async function logoutHandler(req) {
179
+ const cookie = req.headers.get('cookie');
180
+ const refreshToken = parseCookie(cookie, 'airdraft_refresh');
181
+ if (refreshToken)
182
+ refreshTokenBlocklist.add(refreshToken);
183
+ const headers = new Headers({ 'Content-Type': 'application/json' });
184
+ headers.append('Set-Cookie', `airdraft_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0`);
185
+ headers.append('Set-Cookie', `airdraft_refresh=; HttpOnly; SameSite=Strict; Path=${basePath}/auth/refresh; Max-Age=0`);
186
+ return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
187
+ }
188
+ /** GET /auth/me */
189
+ async function meHandler(req) {
190
+ const user = await verify(req);
191
+ if (!user)
192
+ return new Response(JSON.stringify({ error: 'Not authenticated' }), { status: 401 });
193
+ return new Response(JSON.stringify({ user: { id: user.id, email: user.email, name: user.name, role: user.role } }), { status: 200, headers: { 'Content-Type': 'application/json' } });
194
+ }
195
+ function handlers() {
196
+ return [
197
+ { path: '/auth/google', handler: googleInitHandler },
198
+ { path: '/auth/google/callback', handler: googleCallbackHandler },
199
+ { path: '/auth/logout', handler: logoutHandler },
200
+ { path: '/auth/me', handler: meHandler },
201
+ ];
202
+ }
203
+ return { verify, handlers };
204
+ }
205
+ //# sourceMappingURL=GoogleOAuthProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GoogleOAuthProvider.js","sourceRoot":"","sources":["../src/GoogleOAuthProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAqBtD,KAAK,UAAU,iBAAiB,CAAC,WAAmB;IAClD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,+CAA+C,EAAE;QACvE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE;KACpD,CAAC,CAAA;IACF,OAAO,GAAG,CAAC,EAAE,CAAC,CAAC,CAAE,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAoB,CAAC,CAAC,CAAC,IAAI,CAAA;AAC/D,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E,SAAS,WAAW,CAAC,MAAqB,EAAE,IAAY;IACtD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACxB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC7C,IAAI,GAAG,EAAE,IAAI,EAAE,KAAK,IAAI;YAAE,OAAO,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,QAAQ,CAAC,GAAY;IAC5B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,IAAY,EAAE,KAAa,EAAE,KAAe;IACnE,OAAO,CAAC,GAAG,IAAI,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACtE,CAAC;AA+BD,8EAA8E;AAC9E,6BAA6B;AAC7B,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAgC;IAClE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,IAAI,EAAE,CAAA;IACtE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CACb,iEAAiE;YAC/D,iDAAiD,CACpD,CAAA;IACH,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAA;IACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,UAAU,CAAA;IAC5C,MAAM,WAAW,GAAS,IAAI,CAAC,WAAW,IAAI,QAAQ,CAAA;IACtD,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,CAAA;IAC1B,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAA;IAEpC,SAAS,WAAW,CAAC,KAAa;QAChC,OAAQ,WAAW,CAAC,KAAK,CAAC,WAAW,EAAE,CAAsB,IAAI,WAAW,CAAA;IAC9E,CAAC;IAED,KAAK,UAAU,MAAM,CAAC,GAAY;QAChC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACxC,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;QACrD,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAA;QACvB,OAAO,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACzC,CAAC;IAED,kDAAkD;IAClD,KAAK,UAAU,iBAAiB,CAAC,GAAY;QAC3C,MAAM,WAAW,GACf,IAAI,CAAC,WAAW,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,QAAQ,uBAAuB,CAAA;QAClF,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QAC7C,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;YACjC,SAAS,EAAE,IAAI,CAAC,QAAQ;YACxB,YAAY,EAAE,WAAW;YACzB,aAAa,EAAE,MAAM;YACrB,KAAK,EAAE,sBAAsB;YAC7B,WAAW,EAAE,SAAS;YACtB,MAAM,EAAE,SAAS;YACjB,KAAK;SACN,CAAC,CAAA;QACF,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC;YAC1B,QAAQ,EAAE,gDAAgD,MAAM,CAAC,QAAQ,EAAE,EAAE;SAC9E,CAAC,CAAA;QACF,OAAO,CAAC,GAAG,CACT,YAAY,EACZ,eAAe,CAAC,sBAAsB,EAAE,KAAK,EAAE;YAC7C,UAAU;YACV,cAAc;YACd,QAAQ;YACR,aAAa;YACb,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SACrC,CAAC,CACH,CAAA;QACD,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;IACrD,CAAC;IAED,gCAAgC;IAChC,KAAK,UAAU,qBAAqB,CAAC,GAAY;QAC/C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC5B,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACzC,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAE3C,aAAa;QACb,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,sBAAsB,CAAC,CAAA;QACjF,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,KAAK,KAAK,UAAU,EAAE,CAAC;YAC5C,OAAO,IAAI,QAAQ,CAAC,sBAAsB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QAC9D,CAAC;QAED,MAAM,WAAW,GACf,IAAI,CAAC,WAAW,IAAI,GAAG,GAAG,CAAC,MAAM,GAAG,QAAQ,uBAAuB,CAAA;QAErE,0BAA0B;QAC1B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,qCAAqC,EAAE;YAClE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;YAChE,IAAI,EAAE,IAAI,eAAe,CAAC;gBACxB,IAAI;gBACJ,SAAS,EAAE,IAAI,CAAC,QAAQ;gBACxB,aAAa,EAAE,IAAI,CAAC,YAAY;gBAChC,YAAY,EAAE,WAAW;gBACzB,UAAU,EAAE,oBAAoB;aACjC,CAAC,CAAC,QAAQ,EAAE;SACd,CAAC,CAAA;QACF,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,IAAI,QAAQ,CAAC,+BAA+B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACvF,MAAM,MAAM,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAwB,CAAA;QAE7D,kBAAkB;QAClB,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;QAC/D,IAAI,CAAC,UAAU;YAAE,OAAO,IAAI,QAAQ,CAAC,kCAAkC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACzF,IAAI,CAAC,UAAU,CAAC,cAAc;YAAE,OAAO,IAAI,QAAQ,CAAC,2BAA2B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QAEjG,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,WAAW,EAAE,CAAA;QAE5C,mBAAmB;QACnB,IAAI,IAAI,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAClG,OAAO,IAAI,QAAQ,CAAC,sBAAsB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QAC9D,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;YAClC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBACrD,OAAO,IAAI,QAAQ,CAAC,6BAA6B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;YACrE,CAAC;QACH,CAAC;QAED,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;QAC/B,MAAM,IAAI,GAAa;YACrB,EAAE,EAAE,UAAU,UAAU,CAAC,GAAG,EAAE;YAC9B,KAAK;YACL,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,IAAI;SACL,CAAA;QACD,MAAM,WAAW,GAAG,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACjD,MAAM,YAAY,GAAG,gBAAgB,EAAE,CAAA;QACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;QAE5B,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC;YAC1B,QAAQ,EAAE,GAAG,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,MAAM;SACjE,CAAC,CAAA;QACF,OAAO,CAAC,MAAM,CACZ,YAAY,EACZ,eAAe,CAAC,kBAAkB,EAAE,WAAW,EAAE;YAC/C,UAAU;YACV,iBAAiB;YACjB,QAAQ;YACR,WAAW,UAAU,EAAE;YACvB,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9B,CAAC,CACH,CAAA;QACD,OAAO,CAAC,MAAM,CACZ,YAAY,EACZ,eAAe,CAAC,kBAAkB,EAAE,YAAY,EAAE;YAChD,UAAU;YACV,iBAAiB;YACjB,QAAQ,QAAQ,eAAe;YAC/B,WAAW,WAAW,EAAE;YACxB,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9B,CAAC,CACH,CAAA;QACD,OAAO,CAAC,MAAM,CACZ,YAAY,EACZ,kEAAkE,CACnE,CAAA;QACD,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;IACrD,CAAC;IAED,wBAAwB;IACxB,KAAK,UAAU,aAAa,CAAC,GAAY;QACvC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACxC,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;QAC5D,IAAI,YAAY;YAAE,qBAAqB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;QACzD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAA;QACnE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,iEAAiE,CAAC,CAAA;QAC/F,OAAO,CAAC,MAAM,CACZ,YAAY,EACZ,sDAAsD,QAAQ,0BAA0B,CACzF,CAAA;QACD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;IAC7E,CAAC;IAED,mBAAmB;IACnB,KAAK,UAAU,SAAS,CAAC,GAAY;QACnC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,CAAA;QAC9B,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QAC/F,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,EAC9F,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,CACjE,CAAA;IACH,CAAC;IAED,SAAS,QAAQ;QACf,OAAO;YACL,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,iBAAiB,EAAE;YACpD,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,qBAAqB,EAAE;YACjE,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa,EAAE;YAChD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE;SACzC,CAAA;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AAC7B,CAAC"}
@@ -0,0 +1,74 @@
1
+ import type { Role } from '@airdraft/auth';
2
+ export interface UserRecord {
3
+ id: string;
4
+ email: string;
5
+ name?: string;
6
+ role: Role;
7
+ /** `salt:hash` both hex-encoded. */
8
+ passwordHash: string;
9
+ createdAt: number;
10
+ }
11
+ export interface InviteRecord {
12
+ token: string;
13
+ email: string;
14
+ role: Role;
15
+ /** Unix epoch seconds. */
16
+ expiresAt: number;
17
+ }
18
+ /**
19
+ * Persistent user store backed by `.airdraft/users.json`.
20
+ *
21
+ * - Passwords are hashed with `scrypt` (N=16384, r=8, p=1) — OWASP compliant.
22
+ * - On first write, auto-creates `.airdraft/.gitignore` to prevent accidental commits.
23
+ * - Emits a console.warn on serverless runtimes (no persistent filesystem).
24
+ *
25
+ * ```ts
26
+ * const store = UserStore.json() // uses .airdraft/users.json relative to cwd
27
+ * await store.create('alice@example.com', 'hunter2', 'admin', 'Alice')
28
+ * ```
29
+ */
30
+ export declare class UserStore {
31
+ private readonly filePath;
32
+ private data;
33
+ constructor(filePath: string);
34
+ /** Creates a UserStore backed by `.airdraft/users.json` (or custom path). */
35
+ static json(path?: string): UserStore;
36
+ private load;
37
+ private save;
38
+ private warnIfServerless;
39
+ findByEmail(email: string): Promise<UserRecord | null>;
40
+ list(): Promise<UserRecord[]>;
41
+ /**
42
+ * Creates a new user with a scrypt-hashed password.
43
+ * Throws if a user with the same email already exists.
44
+ */
45
+ create(email: string, password: string, role: Role, name?: string): Promise<Omit<UserRecord, 'passwordHash'>>;
46
+ /**
47
+ * Verifies email + password and returns the user record (without hash) on success.
48
+ * Returns null if the user doesn't exist or the password is wrong.
49
+ */
50
+ verifyPassword(email: string, password: string): Promise<Omit<UserRecord, 'passwordHash'> | null>;
51
+ /**
52
+ * Updates the role of an existing user. Config-level roles take precedence
53
+ * over the store at runtime (C3), but this persists the base role.
54
+ */
55
+ updateRole(email: string, role: Role): Promise<Omit<UserRecord, 'passwordHash'> | null>;
56
+ /** Removes a user. Returns true if the user existed. */
57
+ remove(email: string): Promise<boolean>;
58
+ /**
59
+ * Creates an admin-generated invite token for the given email + role.
60
+ * The invite link is: `{basePath}/auth/invite/{token}` — displayed to the admin.
61
+ * Expiry: 48 hours.
62
+ */
63
+ createInvite(email: string, role: Role): Promise<InviteRecord>;
64
+ /** Returns all stored invites (including expired ones). */
65
+ listInvites(): Promise<InviteRecord[]>;
66
+ /** Removes an invite by token. No-op if the token doesn't exist. */
67
+ revokeInvite(token: string): Promise<void>;
68
+ /**
69
+ * Accepts an invite: sets the user's password and creates the account.
70
+ * Returns the new user record, or throws if the token is invalid/expired.
71
+ */
72
+ acceptInvite(token: string, password: string, name?: string): Promise<Omit<UserRecord, 'passwordHash'>>;
73
+ }
74
+ //# sourceMappingURL=UserStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserStore.d.ts","sourceRoot":"","sources":["../src/UserStore.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAM1C,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,IAAI,CAAA;IACV,oCAAoC;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,IAAI,CAAA;IACV,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAA;CAClB;AAiDD;;;;;;;;;;;GAWG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,IAAI,CAA6B;gBAE7B,QAAQ,EAAE,MAAM;IAK5B,6EAA6E;IAC7E,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS;YASvB,IAAI;YAeJ,IAAI;IAelB,OAAO,CAAC,gBAAgB;IAclB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAKtD,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAKnC;;;OAGG;IACG,MAAM,CACV,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IAqB5C;;;OAGG;IACG,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,GAAG,IAAI,CAAC;IAYnD;;;OAGG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,GAAG,IAAI,CAAC;IAU7F,wDAAwD;IAClD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa7C;;;;OAIG;IACG,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC;IAgBpE,2DAA2D;IACrD,WAAW,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAK5C,oEAAoE;IAC9D,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMhD;;;OAGG;IACG,YAAY,CAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;CAY7C"}