@kamod-ch/otok-auth 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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/dist/adapters/index.d.ts +2 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +2 -0
  6. package/dist/adapters/index.js.map +1 -0
  7. package/dist/adapters/kysely.d.ts +12 -0
  8. package/dist/adapters/kysely.d.ts.map +1 -0
  9. package/dist/adapters/kysely.js +52 -0
  10. package/dist/adapters/kysely.js.map +1 -0
  11. package/dist/adapters/memory.d.ts +26 -0
  12. package/dist/adapters/memory.d.ts.map +1 -0
  13. package/dist/adapters/memory.js +72 -0
  14. package/dist/adapters/memory.js.map +1 -0
  15. package/dist/context.d.ts +36 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +68 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/crypto.d.ts +5 -0
  20. package/dist/crypto.d.ts.map +1 -0
  21. package/dist/crypto.js +33 -0
  22. package/dist/crypto.js.map +1 -0
  23. package/dist/csrf.d.ts +19 -0
  24. package/dist/csrf.d.ts.map +1 -0
  25. package/dist/csrf.js +53 -0
  26. package/dist/csrf.js.map +1 -0
  27. package/dist/errors.d.ts +13 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +29 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/index.d.ts +16 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +15 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/loader.d.ts +24 -0
  36. package/dist/loader.d.ts.map +1 -0
  37. package/dist/loader.js +30 -0
  38. package/dist/loader.js.map +1 -0
  39. package/dist/middleware/api-guard.d.ts +3 -0
  40. package/dist/middleware/api-guard.d.ts.map +1 -0
  41. package/dist/middleware/api-guard.js +4 -0
  42. package/dist/middleware/api-guard.js.map +1 -0
  43. package/dist/middleware/csrf.d.ts +4 -0
  44. package/dist/middleware/csrf.d.ts.map +1 -0
  45. package/dist/middleware/csrf.js +22 -0
  46. package/dist/middleware/csrf.js.map +1 -0
  47. package/dist/middleware/index.d.ts +5 -0
  48. package/dist/middleware/index.d.ts.map +1 -0
  49. package/dist/middleware/index.js +5 -0
  50. package/dist/middleware/index.js.map +1 -0
  51. package/dist/middleware/rbac.d.ts +26 -0
  52. package/dist/middleware/rbac.d.ts.map +1 -0
  53. package/dist/middleware/rbac.js +63 -0
  54. package/dist/middleware/rbac.js.map +1 -0
  55. package/dist/middleware/require-auth.d.ts +11 -0
  56. package/dist/middleware/require-auth.d.ts.map +1 -0
  57. package/dist/middleware/require-auth.js +20 -0
  58. package/dist/middleware/require-auth.js.map +1 -0
  59. package/dist/password-webcrypto.d.ts +11 -0
  60. package/dist/password-webcrypto.d.ts.map +1 -0
  61. package/dist/password-webcrypto.js +65 -0
  62. package/dist/password-webcrypto.js.map +1 -0
  63. package/dist/password.d.ts +3 -0
  64. package/dist/password.d.ts.map +1 -0
  65. package/dist/password.js +8 -0
  66. package/dist/password.js.map +1 -0
  67. package/dist/plugin.d.ts +19 -0
  68. package/dist/plugin.d.ts.map +1 -0
  69. package/dist/plugin.js +132 -0
  70. package/dist/plugin.js.map +1 -0
  71. package/dist/redirect.d.ts +8 -0
  72. package/dist/redirect.d.ts.map +1 -0
  73. package/dist/redirect.js +21 -0
  74. package/dist/redirect.js.map +1 -0
  75. package/dist/registry.d.ts +22 -0
  76. package/dist/registry.d.ts.map +1 -0
  77. package/dist/registry.js +28 -0
  78. package/dist/registry.js.map +1 -0
  79. package/dist/session/index.d.ts +3 -0
  80. package/dist/session/index.d.ts.map +1 -0
  81. package/dist/session/index.js +2 -0
  82. package/dist/session/index.js.map +1 -0
  83. package/dist/session/manager.d.ts +3 -0
  84. package/dist/session/manager.d.ts.map +1 -0
  85. package/dist/session/manager.js +120 -0
  86. package/dist/session/manager.js.map +1 -0
  87. package/dist/session/types.d.ts +41 -0
  88. package/dist/session/types.d.ts.map +1 -0
  89. package/dist/session/types.js +2 -0
  90. package/dist/session/types.js.map +1 -0
  91. package/dist/types.d.ts +43 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +2 -0
  94. package/dist/types.js.map +1 -0
  95. package/package.json +110 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kamod
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 ADDED
@@ -0,0 +1,230 @@
1
+ # @kamod-ch/otok-auth
2
+
3
+ Cookie sessions, CSRF, and route middleware helpers for [Otok](https://github.com/kamod-ch/otok) apps.
4
+
5
+ Ships as an **Otok plugin** (`auth()`) and as **composition helpers** for manual wiring.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @kamod-ch/otok-auth hono @kamod-ch/otok
11
+ ```
12
+
13
+ ## Plugin (recommended)
14
+
15
+ ```ts
16
+ import { defineConfig } from "@kamod-ch/otok";
17
+ import auth from "@kamod-ch/otok-auth";
18
+ import { createMemorySessionAdapter } from "@kamod-ch/otok-auth/adapters/memory";
19
+
20
+ export default defineConfig({
21
+ plugins: [
22
+ auth({
23
+ secret: process.env.AUTH_SECRET!,
24
+ session: { cookieName: "otok_session" },
25
+ adapter: createMemorySessionAdapter({ resolveUser: ({ session }) => users.get(session.userId) ?? null }),
26
+ redirectAllowlist: ["/", "/dashboard"],
27
+ getRole: (user) => user.role,
28
+ }),
29
+ ],
30
+ });
31
+ ```
32
+
33
+ ## Loaders
34
+
35
+ ```ts
36
+ import { defineLoader } from "@kamod-ch/otok-auth/loader";
37
+
38
+ export const loader = defineLoader(async ({ auth }) => {
39
+ const user = await auth.requireUser();
40
+ return { user };
41
+ });
42
+ ```
43
+
44
+ ## Server APIs
45
+
46
+ ```ts
47
+ import { getSession, requireUser, requireRole } from "@kamod-ch/otok-auth/registry";
48
+ ```
49
+
50
+ ## Composition (manual wiring)
51
+
52
+ This package remains **composition-friendly**. Database schema and user lookup stay in your app via a `SessionAdapter`.
53
+
54
+
55
+ ```ts
56
+ import { createSessionManager, type SessionAdapter } from "@kamod-ch/otok-auth/session";
57
+
58
+ type User = { id: string; email: string };
59
+
60
+ const adapter: SessionAdapter<User> = {
61
+ async createRecord(input) { /* persist tokenHash + userId */ },
62
+ async revokeRecord(tokenHash) { /* mark revoked */ },
63
+ async resolveUser(tokenHash) { /* return user or null */ },
64
+ };
65
+
66
+ const sessions = createSessionManager<User>(
67
+ {
68
+ sessionCookie: "app_session",
69
+ csrfCookie: "app_csrf",
70
+ maxAgeSeconds: 60 * 60 * 24 * 14,
71
+ secure: process.env.NODE_ENV === "production",
72
+ },
73
+ adapter,
74
+ );
75
+
76
+ export const { createSession, revokeSession, getSessionUser } = sessions;
77
+ ```
78
+
79
+ ## Route middleware
80
+
81
+ ```ts
82
+ // src/app/routes/_middleware.ts
83
+ import { createRequireAuthMiddleware } from "@kamod-ch/otok-auth/middleware";
84
+ import { getSessionUser } from "../features/auth/session.js";
85
+
86
+ export default createRequireAuthMiddleware({
87
+ getUser: getSessionUser,
88
+ publicPaths: ["/login"],
89
+ onAuthenticated: (c, user) => {
90
+ c.set("user", user);
91
+ },
92
+ });
93
+ ```
94
+
95
+ ## Tenant and role middleware
96
+
97
+ Compose auth with tenant context and optional RBAC:
98
+
99
+ ```ts
100
+ import {
101
+ composeMiddleware,
102
+ createRequireAuthMiddleware,
103
+ createRequireRoleMiddleware,
104
+ createTenantMiddleware,
105
+ } from "@kamod-ch/otok-auth/middleware";
106
+
107
+ export default composeMiddleware(
108
+ createRequireAuthMiddleware({ getUser: getSessionUser, publicPaths: ["/login"] }),
109
+ createTenantMiddleware({
110
+ getTenantId: (user) => user.organizationId,
111
+ contextKey: "organizationId",
112
+ also: { role: (user) => user.role },
113
+ }),
114
+ createRequireRoleMiddleware({
115
+ getRole: (user) => user.role,
116
+ allowed: ["owner", "admin"],
117
+ }),
118
+ );
119
+ ```
120
+
121
+ ## CSRF
122
+
123
+ ```ts
124
+ import { createCsrfMiddleware } from "@kamod-ch/otok-auth/middleware";
125
+ import { ensureCsrfCookie, CSRF_FIELD } from "@kamod-ch/otok-auth/csrf";
126
+
127
+ // Option A: dedicated middleware
128
+ export default createCsrfMiddleware({ cookieName: "app_csrf" });
129
+
130
+ // Option B: hidden field in forms
131
+ <input type="hidden" name={CSRF_FIELD} value={ensureCsrfCookie(c)} />
132
+ ```
133
+
134
+ ## API guard (`configure()`)
135
+
136
+ Route `_middleware.ts` does **not** run for Hono routes registered via `createOtokApp({ configure })`. Use `createApiGuard`:
137
+
138
+ ```ts
139
+ import { createApiGuard } from "@kamod-ch/otok-auth/middleware";
140
+
141
+ const requireApiUser = createApiGuard(getSessionUser);
142
+
143
+ configure: (app) => {
144
+ app.get("/api/me", async (c) => {
145
+ const user = await requireApiUser(c);
146
+ if (!user) return c.json({ error: { code: "unauthorized" } }, 401);
147
+ return c.json(user);
148
+ });
149
+ };
150
+ ```
151
+
152
+ ## Password helpers
153
+
154
+ ```ts
155
+ import { hashPassword, verifyPassword } from "@kamod-ch/otok-auth/password";
156
+ ```
157
+
158
+ Requires peer dependency `@node-rs/argon2` (Node / `nodejs_compat` Workers).
159
+
160
+ ### Edge / Workers (Web Crypto)
161
+
162
+ When native Argon2 is unavailable, use PBKDF2 via Web Crypto:
163
+
164
+ ```ts
165
+ import {
166
+ hashPasswordWebCrypto,
167
+ verifyPasswordWebCrypto,
168
+ } from "@kamod-ch/otok-auth/password/webcrypto";
169
+
170
+ const hash = await hashPasswordWebCrypto("secret");
171
+ await verifyPasswordWebCrypto(hash, "secret");
172
+ ```
173
+
174
+ Prefer Argon2 on Node. Do not mix hash formats without a migration path.
175
+
176
+ ## Exports
177
+
178
+ | Subpath | Purpose |
179
+ |---------|---------|
180
+ | `@kamod-ch/otok-auth` | Re-exports |
181
+ | `@kamod-ch/otok-auth/session` | Session manager + types |
182
+ | `@kamod-ch/otok-auth/middleware` | Auth, CSRF, tenant context, RBAC, `composeMiddleware` |
183
+ | `@kamod-ch/otok-auth/csrf` | CSRF cookie/field helpers |
184
+ | `@kamod-ch/otok-auth/password` | Argon2 hash/verify |
185
+ | `@kamod-ch/otok-auth/password/webcrypto` | Edge-safe PBKDF2 hash/verify |
186
+ | `@kamod-ch/otok-auth/adapters/memory` | In-memory/file-backed session adapter |
187
+ | `@kamod-ch/otok-auth/adapters/kysely` | Kysely session CRUD adapter |
188
+
189
+ ## Memory session adapter
190
+
191
+ For dev, tests, or file-backed prototypes:
192
+
193
+ ```ts
194
+ import { createMemorySessionAdapter } from "@kamod-ch/otok-auth/adapters/memory";
195
+
196
+ const adapter = createMemorySessionAdapter<User>({
197
+ resolveUser: ({ session }) => users.get(session.userId) ?? null,
198
+ persistence: {
199
+ load: () => readSessionsFromDisk(),
200
+ save: (sessions) => writeSessionsToDisk(sessions),
201
+ },
202
+ });
203
+ ```
204
+
205
+ `resolveUser` stays app-specific. The adapter handles session CRUD, expiry, revoke, and touch.
206
+
207
+ ## Kysely session adapter
208
+
209
+ For Postgres/SQLite apps using Kysely:
210
+
211
+ ```ts
212
+ import { createKyselySessionAdapter } from "@kamod-ch/otok-auth/adapters/kysely";
213
+
214
+ const adapter = createKyselySessionAdapter<User>({
215
+ db,
216
+ table: "appSession",
217
+ resolveUser: async (tokenHash) => {
218
+ return db
219
+ .selectFrom("appSession")
220
+ .innerJoin("appUser", "appUser.id", "appSession.userId")
221
+ .select(["appUser.id", "appUser.email"])
222
+ .where("appSession.tokenHash", "=", tokenHash)
223
+ .where("appSession.revokedAt", "is", null)
224
+ .where("appSession.expiresAt", ">", new Date())
225
+ .executeTakeFirst();
226
+ },
227
+ });
228
+ ```
229
+
230
+ A reference SQL migration is exported as `SESSION_TABLE_MIGRATION_SQL`.
@@ -0,0 +1,2 @@
1
+ export { createMemorySessionAdapter, type MemorySessionAdapterOptions, type MemorySessionPersistence, type MemorySessionRecord, } from "./memory.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { createMemorySessionAdapter, } from "./memory.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,GAI3B,MAAM,aAAa,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Kysely } from "kysely";
2
+ import type { SessionAdapter } from "../session/types.js";
3
+ export declare const SESSION_TABLE_MIGRATION_SQL = "-- Reference migration for @kamod-ch/otok-auth session adapters\nCREATE TABLE IF NOT EXISTS app_session (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n user_id UUID NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL UNIQUE,\n user_agent TEXT,\n ip_address TEXT,\n expires_at TIMESTAMPTZ NOT NULL,\n revoked_at TIMESTAMPTZ,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n last_seen_at TIMESTAMPTZ\n);\n\nCREATE INDEX IF NOT EXISTS app_session_token_hash_idx ON app_session (token_hash);\nCREATE INDEX IF NOT EXISTS app_session_user_id_idx ON app_session (user_id);\nCREATE INDEX IF NOT EXISTS app_session_expires_at_idx ON app_session (expires_at);\n";
4
+ export interface KyselySessionAdapterOptions<TUser> {
5
+ db: Kysely<any>;
6
+ /** Kysely table name, e.g. "appSession". */
7
+ table: string;
8
+ /** App-specific lookup: joins, roles, tenant context, etc. */
9
+ resolveUser: (tokenHash: string) => Promise<TUser | null> | TUser | null;
10
+ }
11
+ export declare function createKyselySessionAdapter<TUser>(options: KyselySessionAdapterOptions<TUser>): SessionAdapter<TUser>;
12
+ //# sourceMappingURL=kysely.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kysely.d.ts","sourceRoot":"","sources":["../../src/adapters/kysely.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAA4B,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEpF,eAAO,MAAM,2BAA2B,2rBAgBvC,CAAC;AAEF,MAAM,WAAW,2BAA2B,CAAC,KAAK;IAChD,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAChB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC;CAC1E;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAC9C,OAAO,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAC1C,cAAc,CAAC,KAAK,CAAC,CAkCvB"}
@@ -0,0 +1,52 @@
1
+ export const SESSION_TABLE_MIGRATION_SQL = `-- Reference migration for @kamod-ch/otok-auth session adapters
2
+ CREATE TABLE IF NOT EXISTS app_session (
3
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4
+ user_id UUID NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
5
+ token_hash TEXT NOT NULL UNIQUE,
6
+ user_agent TEXT,
7
+ ip_address TEXT,
8
+ expires_at TIMESTAMPTZ NOT NULL,
9
+ revoked_at TIMESTAMPTZ,
10
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
11
+ last_seen_at TIMESTAMPTZ
12
+ );
13
+
14
+ CREATE INDEX IF NOT EXISTS app_session_token_hash_idx ON app_session (token_hash);
15
+ CREATE INDEX IF NOT EXISTS app_session_user_id_idx ON app_session (user_id);
16
+ CREATE INDEX IF NOT EXISTS app_session_expires_at_idx ON app_session (expires_at);
17
+ `;
18
+ export function createKyselySessionAdapter(options) {
19
+ const { db, table, resolveUser } = options;
20
+ return {
21
+ async createRecord(input) {
22
+ await db
23
+ .insertInto(table)
24
+ .values({
25
+ userId: input.userId,
26
+ tokenHash: input.tokenHash,
27
+ expiresAt: input.expiresAt,
28
+ userAgent: input.userAgent,
29
+ ipAddress: input.ipAddress,
30
+ })
31
+ .execute();
32
+ },
33
+ async revokeRecord(tokenHash) {
34
+ await db
35
+ .updateTable(table)
36
+ .set({ revokedAt: new Date() })
37
+ .where("tokenHash", "=", tokenHash)
38
+ .execute();
39
+ },
40
+ async resolveUser(tokenHash) {
41
+ return resolveUser(tokenHash);
42
+ },
43
+ async touchRecord(tokenHash) {
44
+ await db
45
+ .updateTable(table)
46
+ .set({ lastSeenAt: new Date() })
47
+ .where("tokenHash", "=", tokenHash)
48
+ .execute();
49
+ },
50
+ };
51
+ }
52
+ //# sourceMappingURL=kysely.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kysely.js","sourceRoot":"","sources":["../../src/adapters/kysely.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,2BAA2B,GAAG;;;;;;;;;;;;;;;;CAgB1C,CAAC;AAUF,MAAM,UAAU,0BAA0B,CACxC,OAA2C;IAE3C,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;IAE3C,OAAO;QACL,KAAK,CAAC,YAAY,CAAC,KAA+B;YAChD,MAAM,EAAE;iBACL,UAAU,CAAC,KAAK,CAAC;iBACjB,MAAM,CAAC;gBACN,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,SAAS,EAAE,KAAK,CAAC,SAAS;aAC3B,CAAC;iBACD,OAAO,EAAE,CAAC;QACf,CAAC;QACD,KAAK,CAAC,YAAY,CAAC,SAAS;YAC1B,MAAM,EAAE;iBACL,WAAW,CAAC,KAAK,CAAC;iBAClB,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;iBAC9B,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,SAAS,CAAC;iBAClC,OAAO,EAAE,CAAC;QACf,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,SAAS;YACzB,OAAO,WAAW,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,SAAS;YACzB,MAAM,EAAE;iBACL,WAAW,CAAC,KAAK,CAAC;iBAClB,GAAG,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;iBAC/B,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,SAAS,CAAC;iBAClC,OAAO,EAAE,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { SessionAdapter } from "../session/types.js";
2
+ export interface MemorySessionRecord {
3
+ tokenHash: string;
4
+ userId: string;
5
+ createdAt: Date;
6
+ expiresAt: Date;
7
+ userAgent: string | null;
8
+ ipAddress: string | null;
9
+ revokedAt: Date | null;
10
+ lastSeenAt: Date | null;
11
+ }
12
+ export interface MemorySessionPersistence {
13
+ load: () => MemorySessionRecord[] | Promise<MemorySessionRecord[]>;
14
+ save: (sessions: MemorySessionRecord[]) => void | Promise<void>;
15
+ }
16
+ export interface MemorySessionAdapterOptions<TUser> {
17
+ resolveUser: (input: {
18
+ tokenHash: string;
19
+ session: MemorySessionRecord;
20
+ }) => Promise<TUser | null> | TUser | null;
21
+ persistence?: MemorySessionPersistence;
22
+ /** Used when no persistence hook is configured (mainly tests). */
23
+ initialSessions?: MemorySessionRecord[];
24
+ }
25
+ export declare function createMemorySessionAdapter<TUser>(options: MemorySessionAdapterOptions<TUser>): SessionAdapter<TUser>;
26
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA4B,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEpF,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,mBAAmB,EAAE,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;IACnE,IAAI,EAAE,CAAC,QAAQ,EAAE,mBAAmB,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,2BAA2B,CAAC,KAAK;IAChD,WAAW,EAAE,CAAC,KAAK,EAAE;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,mBAAmB,CAAC;KAC9B,KAAK,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC;IAC3C,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,kEAAkE;IAClE,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACzC;AAMD,wBAAgB,0BAA0B,CAAC,KAAK,EAC9C,OAAO,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAC1C,cAAc,CAAC,KAAK,CAAC,CAiEvB"}
@@ -0,0 +1,72 @@
1
+ function isActive(session, now = Date.now()) {
2
+ return !session.revokedAt && session.expiresAt.getTime() > now;
3
+ }
4
+ export function createMemorySessionAdapter(options) {
5
+ let sessions = [];
6
+ let loaded = false;
7
+ async function ensureLoaded() {
8
+ if (loaded)
9
+ return;
10
+ sessions = options.persistence
11
+ ? [...(await options.persistence.load())]
12
+ : [...(options.initialSessions ?? [])];
13
+ loaded = true;
14
+ }
15
+ async function persist() {
16
+ if (options.persistence) {
17
+ await options.persistence.save(sessions);
18
+ }
19
+ }
20
+ return {
21
+ async createRecord(input) {
22
+ await ensureLoaded();
23
+ sessions = sessions.filter((session) => session.tokenHash !== input.tokenHash);
24
+ sessions.push({
25
+ tokenHash: input.tokenHash,
26
+ userId: input.userId,
27
+ createdAt: new Date(),
28
+ expiresAt: input.expiresAt,
29
+ userAgent: input.userAgent,
30
+ ipAddress: input.ipAddress,
31
+ revokedAt: null,
32
+ lastSeenAt: null,
33
+ });
34
+ await persist();
35
+ },
36
+ async revokeRecord(tokenHash) {
37
+ await ensureLoaded();
38
+ const session = sessions.find((entry) => entry.tokenHash === tokenHash);
39
+ if (session)
40
+ session.revokedAt = new Date();
41
+ await persist();
42
+ },
43
+ async resolveUser(tokenHash) {
44
+ await ensureLoaded();
45
+ const session = sessions.find((entry) => entry.tokenHash === tokenHash && isActive(entry));
46
+ if (!session)
47
+ return null;
48
+ return options.resolveUser({ tokenHash, session });
49
+ },
50
+ async touchRecord(tokenHash) {
51
+ await ensureLoaded();
52
+ const session = sessions.find((entry) => entry.tokenHash === tokenHash);
53
+ if (!session)
54
+ return;
55
+ session.lastSeenAt = new Date();
56
+ await persist();
57
+ },
58
+ async resolveRecord(tokenHash) {
59
+ await ensureLoaded();
60
+ const session = sessions.find((entry) => entry.tokenHash === tokenHash && isActive(entry));
61
+ if (!session)
62
+ return null;
63
+ return {
64
+ tokenHash: session.tokenHash,
65
+ userId: session.userId,
66
+ createdAt: session.createdAt,
67
+ expiresAt: session.expiresAt,
68
+ };
69
+ },
70
+ };
71
+ }
72
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { Context } from "hono";
2
+ import type { SessionManager } from "./session/types.js";
3
+ import type { AuthSession, AuthUser } from "./types.js";
4
+ export interface AuthHelpersOptions<TUser extends AuthUser> {
5
+ sessions: SessionManager<TUser>;
6
+ contextKey?: string;
7
+ loginPath?: string;
8
+ redirectAllowlist?: readonly string[];
9
+ getRole?: (user: TUser) => string;
10
+ }
11
+ export interface AuthHelpers<TUser extends AuthUser = AuthUser> {
12
+ getSession(c: Context): Promise<TUser | null>;
13
+ requireUser(c: Context): Promise<TUser>;
14
+ requireRole(c: Context, role: string): Promise<TUser>;
15
+ safeRedirect(raw: string | null | undefined): string | null;
16
+ createSession(c: Context, userId: string): Promise<void>;
17
+ revokeSession(c: Context): Promise<void>;
18
+ rotateSession(c: Context): Promise<void>;
19
+ }
20
+ export declare function createAuthHelpers<TUser extends AuthUser>(options: AuthHelpersOptions<TUser>): AuthHelpers<TUser>;
21
+ /** Build typed session metadata from a resolved user (optional helper). */
22
+ export declare function toAuthSession<TUser extends AuthUser>(user: TUser, getUserId: (user: TUser) => string, expiresAt: Date): AuthSession<TUser>;
23
+ /** Read user from Hono context (set by auth middleware or helpers). */
24
+ export declare function readContextUser<TUser extends AuthUser = AuthUser>(c: Context, contextKey?: string): TUser | undefined;
25
+ /** Helpers bound to a single request context (for loaders/actions). */
26
+ export interface AuthBoundHelpers<TUser extends AuthUser = AuthUser> {
27
+ getSession(): Promise<TUser | null>;
28
+ requireUser(): Promise<TUser>;
29
+ requireRole(role: string): Promise<TUser>;
30
+ safeRedirect(raw: string | null | undefined): string | null;
31
+ createSession(userId: string): Promise<void>;
32
+ revokeSession(): Promise<void>;
33
+ rotateSession(): Promise<void>;
34
+ }
35
+ export declare function authFromOtokContext<TUser extends AuthUser = AuthUser>(hono: Context, helpers: AuthHelpers<TUser>): AuthBoundHelpers<TUser>;
36
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAIpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,WAAW,kBAAkB,CAAC,KAAK,SAAS,QAAQ;IACxD,QAAQ,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,CAAC;CACnC;AAED,MAAM,WAAW,WAAW,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IAC5D,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9C,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IACxC,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IACtD,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5D,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,QAAQ,EACtD,OAAO,EAAE,kBAAkB,CAAC,KAAK,CAAC,GACjC,WAAW,CAAC,KAAK,CAAC,CAwCpB;AAED,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,KAAK,SAAS,QAAQ,EAClD,IAAI,EAAE,KAAK,EACX,SAAS,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,EAClC,SAAS,EAAE,IAAI,GACd,WAAW,CAAC,KAAK,CAAC,CAOpB;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ,EAC/D,CAAC,EAAE,OAAO,EACV,UAAU,SAAS,GAClB,KAAK,GAAG,SAAS,CAEnB;AAED,uEAAuE;AACvE,MAAM,WAAW,gBAAgB,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IACjE,UAAU,IAAI,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACpC,WAAW,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1C,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5D,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED,wBAAgB,mBAAmB,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ,EACnE,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,GAC1B,gBAAgB,CAAC,KAAK,CAAC,CAUzB"}
@@ -0,0 +1,68 @@
1
+ import { redirect } from "@kamod-ch/otok/server";
2
+ import { AuthError } from "./errors.js";
3
+ import { safeRedirectPath } from "./redirect.js";
4
+ export function createAuthHelpers(options) {
5
+ const contextKey = options.contextKey ?? "user";
6
+ const loginPath = options.loginPath ?? "/login";
7
+ const redirectAllowlist = options.redirectAllowlist ?? ["/"];
8
+ async function getSession(c) {
9
+ const cached = c.get(contextKey);
10
+ if (cached)
11
+ return cached;
12
+ const user = await options.sessions.getSessionUser(c);
13
+ if (user)
14
+ c.set(contextKey, user);
15
+ return user;
16
+ }
17
+ async function requireUser(c) {
18
+ const user = await getSession(c);
19
+ if (!user)
20
+ redirect(loginPath, 303);
21
+ return user;
22
+ }
23
+ async function requireRole(c, role) {
24
+ const user = await requireUser(c);
25
+ if (!options.getRole) {
26
+ throw new AuthError("forbidden", "Role checks are not configured");
27
+ }
28
+ const userRole = options.getRole(user);
29
+ if (userRole !== role) {
30
+ throw new AuthError("forbidden", "Insufficient permissions");
31
+ }
32
+ return user;
33
+ }
34
+ return {
35
+ getSession,
36
+ requireUser,
37
+ requireRole,
38
+ safeRedirect: (raw) => safeRedirectPath(raw, redirectAllowlist),
39
+ createSession: (c, userId) => options.sessions.createSession(c, userId),
40
+ revokeSession: (c) => options.sessions.revokeSession(c),
41
+ rotateSession: (c) => options.sessions.rotateSession(c),
42
+ };
43
+ }
44
+ /** Build typed session metadata from a resolved user (optional helper). */
45
+ export function toAuthSession(user, getUserId, expiresAt) {
46
+ return {
47
+ user,
48
+ userId: getUserId(user),
49
+ issuedAt: new Date(),
50
+ expiresAt,
51
+ };
52
+ }
53
+ /** Read user from Hono context (set by auth middleware or helpers). */
54
+ export function readContextUser(c, contextKey = "user") {
55
+ return c.get(contextKey);
56
+ }
57
+ export function authFromOtokContext(hono, helpers) {
58
+ return {
59
+ getSession: () => helpers.getSession(hono),
60
+ requireUser: () => helpers.requireUser(hono),
61
+ requireRole: (role) => helpers.requireRole(hono, role),
62
+ safeRedirect: helpers.safeRedirect,
63
+ createSession: (userId) => helpers.createSession(hono, userId),
64
+ revokeSession: () => helpers.revokeSession(hono),
65
+ rotateSession: () => helpers.rotateSession(hono),
66
+ };
67
+ }
68
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare function sha256(value: string): string;
2
+ /** Edge-compatible SHA-256 via Web Crypto API. */
3
+ export declare function sha256Async(value: string): Promise<string>;
4
+ export declare function randomToken(bytes?: number): string;
5
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAWA,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK5C;AAED,kDAAkD;AAClD,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMhE;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAK,GAAG,MAAM,CAO9C"}
package/dist/crypto.js ADDED
@@ -0,0 +1,33 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ function bytesToBase64Url(bytes) {
3
+ if (typeof Buffer !== "undefined") {
4
+ return Buffer.from(bytes).toString("base64url");
5
+ }
6
+ let binary = "";
7
+ for (const byte of bytes)
8
+ binary += String.fromCharCode(byte);
9
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
10
+ }
11
+ export function sha256(value) {
12
+ if (typeof createHash === "function") {
13
+ return createHash("sha256").update(value).digest("hex");
14
+ }
15
+ throw new Error("otok-auth: sha256 requires Node crypto or use sha256Async for Web Crypto");
16
+ }
17
+ /** Edge-compatible SHA-256 via Web Crypto API. */
18
+ export async function sha256Async(value) {
19
+ if (typeof globalThis.crypto?.subtle?.digest === "function") {
20
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
21
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
22
+ }
23
+ return sha256(value);
24
+ }
25
+ export function randomToken(bytes = 32) {
26
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
27
+ const arr = new Uint8Array(bytes);
28
+ crypto.getRandomValues(arr);
29
+ return bytesToBase64Url(arr);
30
+ }
31
+ return randomBytes(bytes).toString("base64url");
32
+ }
33
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,SAAS,gBAAgB,CAAC,KAAiB;IACzC,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAClD,CAAC;IACD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;QACrC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;AAC9F,CAAC;AAED,kDAAkD;AAClD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAa;IAC7C,IAAI,OAAO,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,UAAU,EAAE,CAAC;QAC5D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtF,OAAO,CAAC,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAK,GAAG,EAAE;IACpC,IAAI,OAAO,UAAU,CAAC,MAAM,EAAE,eAAe,KAAK,UAAU,EAAE,CAAC;QAC7D,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QAC5B,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAClD,CAAC"}
package/dist/csrf.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { Context } from "hono";
2
+ export declare const CSRF_FIELD = "_csrf";
3
+ export declare const DEFAULT_CSRF_COOKIE = "otok_csrf";
4
+ export interface CsrfOptions {
5
+ cookieName?: string;
6
+ secure?: boolean | ((c: Context) => boolean);
7
+ sameSite?: "Strict" | "Lax" | "None";
8
+ path?: string;
9
+ maxAge?: number;
10
+ }
11
+ export declare function createCsrfToken(): string;
12
+ /** Ensure a CSRF cookie exists on safe requests (GET/HEAD). */
13
+ export declare function ensureCsrfCookie(c: Context, options?: CsrfOptions): string;
14
+ export declare function verifyCsrf(c: Context, formToken?: string | null, options?: Pick<CsrfOptions, "cookieName">): boolean;
15
+ /** Reject mutating form posts when the cookie and form field do not match. */
16
+ export declare function assertCsrf(c: Context, formData: FormData | undefined, options?: Pick<CsrfOptions, "cookieName">): void;
17
+ /** Hidden input snippet for progressive-enhancement forms. */
18
+ export declare function csrfHiddenInput(token: string): string;
19
+ //# sourceMappingURL=csrf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAKpC,eAAO,MAAM,UAAU,UAAU,CAAC;AAClC,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAQD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,+DAA+D;AAC/D,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CAa9E;AAED,wBAAgB,UAAU,CACxB,CAAC,EAAE,OAAO,EACV,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EACzB,OAAO,GAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CAAM,GAC5C,OAAO,CAOT;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CACxB,CAAC,EAAE,OAAO,EACV,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,OAAO,GAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CAAM,GAC5C,IAAI,CAKN;AAED,8DAA8D;AAC9D,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAErD"}