@better-forge/auth 0.0.0-stage → 0.1.1

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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/dist/apiTokenRoutes.d.ts +8 -0
  4. package/dist/apiTokenRoutes.d.ts.map +1 -0
  5. package/dist/apiTokenRoutes.js +67 -0
  6. package/dist/apiTokenRoutes.js.map +1 -0
  7. package/dist/apiTokens.d.ts +25 -0
  8. package/dist/apiTokens.d.ts.map +1 -0
  9. package/dist/apiTokens.js +50 -0
  10. package/dist/apiTokens.js.map +1 -0
  11. package/dist/authRoutes.d.ts +37 -0
  12. package/dist/authRoutes.d.ts.map +1 -0
  13. package/dist/authRoutes.js +403 -0
  14. package/dist/authRoutes.js.map +1 -0
  15. package/dist/context.d.ts +98 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +2 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/createAuth.d.ts +146 -0
  20. package/dist/createAuth.d.ts.map +1 -0
  21. package/dist/createAuth.js +52 -0
  22. package/dist/createAuth.js.map +1 -0
  23. package/dist/identityConfirmation.d.ts +13 -0
  24. package/dist/identityConfirmation.d.ts.map +1 -0
  25. package/dist/identityConfirmation.js +37 -0
  26. package/dist/identityConfirmation.js.map +1 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +14 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/middleware.d.ts +41 -0
  32. package/dist/middleware.d.ts.map +1 -0
  33. package/dist/middleware.js +100 -0
  34. package/dist/middleware.js.map +1 -0
  35. package/dist/migrations.d.ts +4 -0
  36. package/dist/migrations.d.ts.map +1 -0
  37. package/dist/migrations.js +77 -0
  38. package/dist/migrations.js.map +1 -0
  39. package/dist/module.d.ts +8 -0
  40. package/dist/module.d.ts.map +1 -0
  41. package/dist/module.js +5 -0
  42. package/dist/module.js.map +1 -0
  43. package/dist/oidc.d.ts +85 -0
  44. package/dist/oidc.d.ts.map +1 -0
  45. package/dist/oidc.js +213 -0
  46. package/dist/oidc.js.map +1 -0
  47. package/dist/oidcConfig.d.ts +39 -0
  48. package/dist/oidcConfig.d.ts.map +1 -0
  49. package/dist/oidcConfig.js +72 -0
  50. package/dist/oidcConfig.js.map +1 -0
  51. package/dist/oidcSettings.d.ts +15 -0
  52. package/dist/oidcSettings.d.ts.map +1 -0
  53. package/dist/oidcSettings.js +51 -0
  54. package/dist/oidcSettings.js.map +1 -0
  55. package/dist/oidcSettingsRoutes.d.ts +19 -0
  56. package/dist/oidcSettingsRoutes.d.ts.map +1 -0
  57. package/dist/oidcSettingsRoutes.js +133 -0
  58. package/dist/oidcSettingsRoutes.js.map +1 -0
  59. package/dist/roles.d.ts +25 -0
  60. package/dist/roles.d.ts.map +1 -0
  61. package/dist/roles.js +44 -0
  62. package/dist/roles.js.map +1 -0
  63. package/dist/routeScope.d.ts +34 -0
  64. package/dist/routeScope.d.ts.map +1 -0
  65. package/dist/routeScope.js +32 -0
  66. package/dist/routeScope.js.map +1 -0
  67. package/dist/sessions.d.ts +46 -0
  68. package/dist/sessions.d.ts.map +1 -0
  69. package/dist/sessions.js +159 -0
  70. package/dist/sessions.js.map +1 -0
  71. package/dist/setupRoutes.d.ts +37 -0
  72. package/dist/setupRoutes.d.ts.map +1 -0
  73. package/dist/setupRoutes.js +70 -0
  74. package/dist/setupRoutes.js.map +1 -0
  75. package/dist/signInAudit.d.ts +19 -0
  76. package/dist/signInAudit.d.ts.map +1 -0
  77. package/dist/signInAudit.js +16 -0
  78. package/dist/signInAudit.js.map +1 -0
  79. package/dist/tables.d.ts +716 -0
  80. package/dist/tables.d.ts.map +1 -0
  81. package/dist/tables.js +56 -0
  82. package/dist/tables.js.map +1 -0
  83. package/dist/tokenCatalogue.d.ts +21 -0
  84. package/dist/tokenCatalogue.d.ts.map +1 -0
  85. package/dist/tokenCatalogue.js +27 -0
  86. package/dist/tokenCatalogue.js.map +1 -0
  87. package/dist/totp.d.ts +12 -0
  88. package/dist/totp.d.ts.map +1 -0
  89. package/dist/totp.js +93 -0
  90. package/dist/totp.js.map +1 -0
  91. package/dist/twoFactor.d.ts +10 -0
  92. package/dist/twoFactor.d.ts.map +1 -0
  93. package/dist/twoFactor.js +43 -0
  94. package/dist/twoFactor.js.map +1 -0
  95. package/dist/userRoutes.d.ts +13 -0
  96. package/dist/userRoutes.d.ts.map +1 -0
  97. package/dist/userRoutes.js +135 -0
  98. package/dist/userRoutes.js.map +1 -0
  99. package/package.json +66 -4
  100. package/src/apiTokenRoutes.ts +82 -0
  101. package/src/apiTokens.ts +62 -0
  102. package/src/authRoutes.ts +452 -0
  103. package/src/context.ts +85 -0
  104. package/src/createAuth.ts +58 -0
  105. package/src/identityConfirmation.ts +50 -0
  106. package/src/index.ts +17 -0
  107. package/src/middleware.ts +123 -0
  108. package/src/migrations.ts +77 -0
  109. package/src/module.ts +6 -0
  110. package/src/oidc.ts +291 -0
  111. package/src/oidcConfig.ts +96 -0
  112. package/src/oidcSettings.ts +60 -0
  113. package/src/oidcSettingsRoutes.ts +165 -0
  114. package/src/roles.ts +60 -0
  115. package/src/routeScope.ts +50 -0
  116. package/src/sessions.ts +209 -0
  117. package/src/setupRoutes.ts +100 -0
  118. package/src/signInAudit.ts +43 -0
  119. package/src/tables.ts +59 -0
  120. package/src/tokenCatalogue.ts +42 -0
  121. package/src/totp.ts +96 -0
  122. package/src/twoFactor.ts +49 -0
  123. package/src/userRoutes.ts +144 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BetterStatusPage contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # @better-forge/auth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Users, sessions, 2FA, SSO/OIDC, roles, API tokens and sign-in routes.
4
+
5
+ Status: skeleton. Depends on: `@better-forge/db`, `@better-forge/audit`.
@@ -0,0 +1,8 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AuthOptions } from './context.js';
3
+ import type { ApiTokens } from './apiTokens.js';
4
+ import type { IdentityConfirmation } from './identityConfirmation.js';
5
+ export declare function createApiTokenRoutes(ctx: AuthOptions, tokens: ApiTokens, identityConfirmation: IdentityConfirmation): {
6
+ apiTokenRoutes: (app: FastifyInstance) => Promise<void>;
7
+ };
8
+ //# sourceMappingURL=apiTokenRoutes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiTokenRoutes.d.ts","sourceRoot":"","sources":["../src/apiTokenRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAI9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAWrE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,oBAAoB;0BAM/E,eAAe;EA0DnD"}
@@ -0,0 +1,67 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { apiTokens } from './tables.js';
3
+ import { requestIdentity } from './middleware.js';
4
+ const MAX_NAME_LENGTH = 80;
5
+ const MAX_EXPIRY_DAYS = 3650;
6
+ /** A token that nobody asked to live forever should not. */
7
+ const DEFAULT_EXPIRY_DAYS = 90;
8
+ const DAY_MS = 24 * 60 * 60 * 1000;
9
+ export function createApiTokenRoutes(ctx, tokens, identityConfirmation) {
10
+ const { db, audit } = ctx;
11
+ const hooks = ctx.hooks ?? {};
12
+ const { generateApiToken, parseScopes } = tokens;
13
+ const { confirmIdentity } = identityConfirmation;
14
+ async function apiTokenRoutes(app) {
15
+ function publicToken(row) {
16
+ const { tokenHash: _tokenHash, scopes, ...rest } = row;
17
+ return { ...rest, scopes: parseScopes(scopes) };
18
+ }
19
+ app.get('/', async () => (await db.select().from(apiTokens)).map(publicToken));
20
+ // `expiresInDays` left out means the default; `null` is the explicit "never".
21
+ app.post('/', {
22
+ config: { rateLimit: ctx.rateLimits.sensitiveAction },
23
+ }, async (req, reply) => {
24
+ const { name, scopes: requested, expiresInDays } = req.body ?? {};
25
+ if (typeof name !== 'string' || !name.trim() || name.trim().length > MAX_NAME_LENGTH) {
26
+ return reply.code(400).send({ error: `Name is required (up to ${MAX_NAME_LENGTH} characters)` });
27
+ }
28
+ const scopes = ctx.tokens.normalize(requested);
29
+ if (!scopes)
30
+ return reply.code(400).send({ error: 'scopes must be a list with at least one known permission' });
31
+ if (expiresInDays !== undefined && expiresInDays !== null
32
+ && (!Number.isInteger(expiresInDays) || expiresInDays < 1 || expiresInDays > MAX_EXPIRY_DAYS)) {
33
+ return reply.code(400).send({ error: `expiresInDays must be a whole number from 1 to ${MAX_EXPIRY_DAYS}, or null for no expiry` });
34
+ }
35
+ const identity = requestIdentity(req);
36
+ if (!await confirmIdentity(identity, req.body?.currentPassword, reply))
37
+ return reply;
38
+ const now = Date.now();
39
+ const generated = generateApiToken();
40
+ const days = expiresInDays === undefined ? DEFAULT_EXPIRY_DAYS : expiresInDays;
41
+ const row = (await db.insert(apiTokens).values({
42
+ name: name.trim(),
43
+ tokenHash: generated.hash,
44
+ prefix: generated.prefix,
45
+ scopes: JSON.stringify(scopes),
46
+ createdAt: now,
47
+ expiresAt: typeof days === 'number' ? now + days * DAY_MS : null,
48
+ }).returning())[0];
49
+ await audit.write(audit.actor(identity), 'create', 'api_token', row.id, row.name, audit.snapshot({ name: row.name, permissions: scopes.join(', '), prefix: row.prefix, expiresAt: row.expiresAt }));
50
+ // The only time the plaintext token leaves the server.
51
+ return { ...publicToken(row), token: generated.token };
52
+ });
53
+ app.delete('/:id', async (req, reply) => {
54
+ const id = Number(req.params.id);
55
+ const existing = Number.isSafeInteger(id) ? (await db.select().from(apiTokens).where(eq(apiTokens.id, id)))[0] : undefined;
56
+ if (!existing)
57
+ return reply.code(404).send({ error: 'Not found' });
58
+ await db.delete(apiTokens).where(eq(apiTokens.id, id));
59
+ // A live event stream opened with the token must not outlast it.
60
+ hooks.onSessionsRevoked?.({ sessionId: `api-token:${id}` });
61
+ await audit.write(audit.actor(requestIdentity(req)), 'delete', 'api_token', id, existing.name, audit.snapshot({ name: existing.name, permissions: parseScopes(existing.scopes).join(', '), prefix: existing.prefix }));
62
+ return reply.code(204).send();
63
+ });
64
+ }
65
+ return { apiTokenRoutes };
66
+ }
67
+ //# sourceMappingURL=apiTokenRoutes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiTokenRoutes.js","sourceRoot":"","sources":["../src/apiTokenRoutes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { AuthOptions } from './context.js';
2
+ export declare function createApiTokens(ctx: AuthOptions): {
3
+ parseScopes: (stored: string) => string[];
4
+ isApiToken: (value: string) => boolean;
5
+ hashApiToken: (token: string) => string;
6
+ generateApiToken: () => {
7
+ token: string;
8
+ hash: string;
9
+ prefix: string;
10
+ };
11
+ findUsableApiToken: (token: string) => Promise<{
12
+ id: number;
13
+ name: string;
14
+ tokenHash: string;
15
+ prefix: string;
16
+ createdAt: number;
17
+ expiresAt: number | null;
18
+ lastUsedAt: number | null;
19
+ scopes: string;
20
+ } | null>;
21
+ consumeApiTokenBudget: (tokenId: number, now?: number) => boolean;
22
+ resetApiTokenBudgets: () => void;
23
+ };
24
+ export type ApiTokens = ReturnType<typeof createApiTokens>;
25
+ //# sourceMappingURL=apiTokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiTokens.d.ts","sourceRoot":"","sources":["../src/apiTokens.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW;;wBAUnB,MAAM,KAAG,OAAO;0BAId,MAAM,KAAG,MAAM;4BAKf;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;gCAMnC,MAAM;;;;;;;;;;qCAcP,MAAM,mBAAqB,OAAO;gCAUzC,IAAI;EAKtC;AAED,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAA"}
@@ -0,0 +1,50 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { eq } from 'drizzle-orm';
3
+ import { apiTokens } from './tables.js';
4
+ export function createApiTokens(ctx) {
5
+ const { db, apiTokenPrefix: API_TOKEN_PREFIX } = ctx;
6
+ const parseScopes = ctx.tokens.parse;
7
+ const LAST_USED_RESOLUTION_MS = 60_000;
8
+ const RATE_LIMIT_PER_MINUTE = 300;
9
+ const RATE_WINDOW_MS = 60_000;
10
+ function isApiToken(value) {
11
+ return value.startsWith(API_TOKEN_PREFIX);
12
+ }
13
+ function hashApiToken(token) {
14
+ return createHash('sha256').update(token).digest('hex');
15
+ }
16
+ /** 256 random bits, so a plain SHA-256 is enough to store them. The plaintext is shown once and never kept. */
17
+ function generateApiToken() {
18
+ const token = `${API_TOKEN_PREFIX}${randomBytes(32).toString('base64url')}`;
19
+ return { token, hash: hashApiToken(token), prefix: token.slice(0, API_TOKEN_PREFIX.length + 6) };
20
+ }
21
+ /** The token row when the token may be used right now: it exists and has not expired. */
22
+ async function findUsableApiToken(token) {
23
+ const row = (await db.select().from(apiTokens).where(eq(apiTokens.tokenHash, hashApiToken(token))))[0];
24
+ if (!row)
25
+ return null;
26
+ const now = Date.now();
27
+ if (row.expiresAt !== null && row.expiresAt <= now)
28
+ return null;
29
+ if (row.lastUsedAt === null || now - row.lastUsedAt >= LAST_USED_RESOLUTION_MS) {
30
+ await db.update(apiTokens).set({ lastUsedAt: now }).where(eq(apiTokens.id, row.id));
31
+ }
32
+ return row;
33
+ }
34
+ const windows = new Map();
35
+ /** Fixed-window limit per token, so one runaway script cannot starve the API. Returns false once the budget is spent. */
36
+ function consumeApiTokenBudget(tokenId, now = Date.now()) {
37
+ const current = windows.get(tokenId);
38
+ if (!current || now - current.startedAt >= RATE_WINDOW_MS) {
39
+ windows.set(tokenId, { startedAt: now, count: 1 });
40
+ return true;
41
+ }
42
+ current.count += 1;
43
+ return current.count <= RATE_LIMIT_PER_MINUTE;
44
+ }
45
+ function resetApiTokenBudgets() {
46
+ windows.clear();
47
+ }
48
+ return { parseScopes, isApiToken, hashApiToken, generateApiToken, findUsableApiToken, consumeApiTokenBudget, resetApiTokenBudgets };
49
+ }
50
+ //# sourceMappingURL=apiTokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiTokens.js","sourceRoot":"","sources":["../src/apiTokens.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import type { FastifyInstance, FastifyReply } from 'fastify';
2
+ import { type OidcConfig } from './oidcConfig.js';
3
+ import type { AuthOptions } from './context.js';
4
+ import type { Sessions } from './sessions.js';
5
+ import type { Middleware } from './middleware.js';
6
+ import type { TwoFactor } from './twoFactor.js';
7
+ import type { IdentityConfirmation } from './identityConfirmation.js';
8
+ import type { SignInAudit } from './signInAudit.js';
9
+ import type { OidcSettings } from './oidcSettings.js';
10
+ import { type Oidc, type OidcFlowState } from './oidc.js';
11
+ interface OidcFlowClaims extends OidcFlowState {
12
+ /**
13
+ * 'oidc-flow' signs a user in; 'oidc-test' only reports the outcome to the administrator who started it;
14
+ * 'oidc-confirm' re-authenticates the user of an existing SSO session before a sensitive action.
15
+ */
16
+ purpose?: 'oidc-flow' | 'oidc-test' | 'oidc-confirm';
17
+ adminUserId?: number;
18
+ /** 'oidc-confirm': the session to mark as confirmed and its user. The callback has no session cookie (SameSite=Strict). */
19
+ sessionId?: string;
20
+ userId?: number;
21
+ /** 'oidc-flow' started from a screen that named a `returnTo` target: the application's `signInRedirect` hook decides where it ends. */
22
+ returnTo?: string | undefined;
23
+ }
24
+ export declare function createSignInRoutes(ctx: AuthOptions, parts: {
25
+ sessions: Sessions;
26
+ middleware: Middleware;
27
+ twoFactor: TwoFactor;
28
+ identityConfirmation: IdentityConfirmation;
29
+ signInAudit: SignInAudit;
30
+ oidcSettings: OidcSettings;
31
+ oidc: Oidc;
32
+ }): {
33
+ startOidcFlow: (app: FastifyInstance, reply: FastifyReply, cfg: OidcConfig, extra: Pick<OidcFlowClaims, "purpose" | "adminUserId" | "sessionId" | "userId" | "returnTo">) => Promise<never>;
34
+ authRoutes: (app: FastifyInstance) => Promise<void>;
35
+ };
36
+ export {};
37
+ //# sourceMappingURL=authRoutes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authRoutes.d.ts","sourceRoot":"","sources":["../src/authRoutes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAO5D,OAAO,EAAwB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACvE,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,cAAc,CAAA;AAC9D,OAAO,KAAK,EAA4B,QAAQ,EAAE,MAAM,eAAe,CAAA;AACvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AACrE,OAAO,KAAK,EAAwB,WAAW,EAAgB,MAAM,kBAAkB,CAAA;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAqC,KAAK,IAAI,EAA0C,KAAK,aAAa,EAAE,MAAM,WAAW,CAAA;AAEpI,UAAU,cAAe,SAAQ,aAAa;IAC5C;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,cAAc,CAAA;IACpD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2HAA2H;IAC3H,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uIAAuI;IACvI,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;IACpB,oBAAoB,EAAE,oBAAoB,CAAA;IAC1C,WAAW,EAAE,WAAW,CAAA;IACxB,YAAY,EAAE,YAAY,CAAA;IAC1B,IAAI,EAAE,IAAI,CAAA;CACX;yBA6BQ,eAAe,SAAS,YAAY,OAAO,UAAU,SACnD,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;sBAoE/D,eAAe;EAmT/C"}