@schlessera/brain-ui-server 0.4.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 (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/agent/backend.d.ts +30 -0
  4. package/dist/agent/backend.d.ts.map +1 -0
  5. package/dist/agent/backend.js +226 -0
  6. package/dist/agent/backend.js.map +1 -0
  7. package/dist/app.d.ts +27 -0
  8. package/dist/app.d.ts.map +1 -0
  9. package/dist/app.js +116 -0
  10. package/dist/app.js.map +1 -0
  11. package/dist/brain/client.d.ts +43 -0
  12. package/dist/brain/client.d.ts.map +1 -0
  13. package/dist/brain/client.js +154 -0
  14. package/dist/brain/client.js.map +1 -0
  15. package/dist/brain/types.d.ts +41 -0
  16. package/dist/brain/types.d.ts.map +1 -0
  17. package/dist/brain/types.js +2 -0
  18. package/dist/brain/types.js.map +1 -0
  19. package/dist/cron/scheduler.d.ts +9 -0
  20. package/dist/cron/scheduler.d.ts.map +1 -0
  21. package/dist/cron/scheduler.js +58 -0
  22. package/dist/cron/scheduler.js.map +1 -0
  23. package/dist/db/client.d.ts +10 -0
  24. package/dist/db/client.d.ts.map +1 -0
  25. package/dist/db/client.js +73 -0
  26. package/dist/db/client.js.map +1 -0
  27. package/dist/files/walker.d.ts +63 -0
  28. package/dist/files/walker.d.ts.map +1 -0
  29. package/dist/files/walker.js +416 -0
  30. package/dist/files/walker.js.map +1 -0
  31. package/dist/index.d.ts +18 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +25 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/middleware/auth.d.ts +59 -0
  36. package/dist/middleware/auth.d.ts.map +1 -0
  37. package/dist/middleware/auth.js +246 -0
  38. package/dist/middleware/auth.js.map +1 -0
  39. package/dist/middleware/passkeys.d.ts +36 -0
  40. package/dist/middleware/passkeys.d.ts.map +1 -0
  41. package/dist/middleware/passkeys.js +427 -0
  42. package/dist/middleware/passkeys.js.map +1 -0
  43. package/dist/middleware/tailscale.d.ts +31 -0
  44. package/dist/middleware/tailscale.d.ts.map +1 -0
  45. package/dist/middleware/tailscale.js +64 -0
  46. package/dist/middleware/tailscale.js.map +1 -0
  47. package/dist/render/template.d.ts +7 -0
  48. package/dist/render/template.d.ts.map +1 -0
  49. package/dist/render/template.js +112 -0
  50. package/dist/render/template.js.map +1 -0
  51. package/dist/routes/brain.d.ts +155 -0
  52. package/dist/routes/brain.d.ts.map +1 -0
  53. package/dist/routes/brain.js +250 -0
  54. package/dist/routes/brain.js.map +1 -0
  55. package/dist/routes/files.d.ts +89 -0
  56. package/dist/routes/files.d.ts.map +1 -0
  57. package/dist/routes/files.js +91 -0
  58. package/dist/routes/files.js.map +1 -0
  59. package/dist/routes/health.d.ts +37 -0
  60. package/dist/routes/health.d.ts.map +1 -0
  61. package/dist/routes/health.js +27 -0
  62. package/dist/routes/health.js.map +1 -0
  63. package/dist/routes/providers.d.ts +33 -0
  64. package/dist/routes/providers.d.ts.map +1 -0
  65. package/dist/routes/providers.js +9 -0
  66. package/dist/routes/providers.js.map +1 -0
  67. package/dist/routes/render.d.ts +25 -0
  68. package/dist/routes/render.d.ts.map +1 -0
  69. package/dist/routes/render.js +47 -0
  70. package/dist/routes/render.js.map +1 -0
  71. package/dist/routes/sessions.d.ts +79 -0
  72. package/dist/routes/sessions.d.ts.map +1 -0
  73. package/dist/routes/sessions.js +37 -0
  74. package/dist/routes/sessions.js.map +1 -0
  75. package/dist/routes/voice.d.ts +92 -0
  76. package/dist/routes/voice.d.ts.map +1 -0
  77. package/dist/routes/voice.js +64 -0
  78. package/dist/routes/voice.js.map +1 -0
  79. package/dist/voice/deepgram-token.d.ts +5 -0
  80. package/dist/voice/deepgram-token.d.ts.map +1 -0
  81. package/dist/voice/deepgram-token.js +63 -0
  82. package/dist/voice/deepgram-token.js.map +1 -0
  83. package/dist/voice/keyterm-builder.d.ts +18 -0
  84. package/dist/voice/keyterm-builder.d.ts.map +1 -0
  85. package/dist/voice/keyterm-builder.js +391 -0
  86. package/dist/voice/keyterm-builder.js.map +1 -0
  87. package/dist/voice/speech-providers.d.ts +25 -0
  88. package/dist/voice/speech-providers.d.ts.map +1 -0
  89. package/dist/voice/speech-providers.js +104 -0
  90. package/dist/voice/speech-providers.js.map +1 -0
  91. package/dist/ws/attachments.d.ts +10 -0
  92. package/dist/ws/attachments.d.ts.map +1 -0
  93. package/dist/ws/attachments.js +63 -0
  94. package/dist/ws/attachments.js.map +1 -0
  95. package/dist/ws/bridge.d.ts +7 -0
  96. package/dist/ws/bridge.d.ts.map +1 -0
  97. package/dist/ws/bridge.js +86 -0
  98. package/dist/ws/bridge.js.map +1 -0
  99. package/dist/ws/clients.d.ts +20 -0
  100. package/dist/ws/clients.d.ts.map +1 -0
  101. package/dist/ws/clients.js +48 -0
  102. package/dist/ws/clients.js.map +1 -0
  103. package/dist/ws/connection.d.ts +8 -0
  104. package/dist/ws/connection.d.ts.map +1 -0
  105. package/dist/ws/connection.js +116 -0
  106. package/dist/ws/connection.js.map +1 -0
  107. package/dist/ws/dispatch.d.ts +5 -0
  108. package/dist/ws/dispatch.d.ts.map +1 -0
  109. package/dist/ws/dispatch.js +139 -0
  110. package/dist/ws/dispatch.js.map +1 -0
  111. package/dist/ws/frames.d.ts +17 -0
  112. package/dist/ws/frames.d.ts.map +1 -0
  113. package/dist/ws/frames.js +37 -0
  114. package/dist/ws/frames.js.map +1 -0
  115. package/dist/ws/handler.d.ts +34 -0
  116. package/dist/ws/handler.d.ts.map +1 -0
  117. package/dist/ws/handler.js +33 -0
  118. package/dist/ws/handler.js.map +1 -0
  119. package/dist/ws/history.d.ts +17 -0
  120. package/dist/ws/history.d.ts.map +1 -0
  121. package/dist/ws/history.js +45 -0
  122. package/dist/ws/history.js.map +1 -0
  123. package/dist/ws/host.d.ts +40 -0
  124. package/dist/ws/host.d.ts.map +1 -0
  125. package/dist/ws/host.js +49 -0
  126. package/dist/ws/host.js.map +1 -0
  127. package/dist/ws/routing.d.ts +12 -0
  128. package/dist/ws/routing.d.ts.map +1 -0
  129. package/dist/ws/routing.js +31 -0
  130. package/dist/ws/routing.js.map +1 -0
  131. package/dist/ws/run-session.d.ts +17 -0
  132. package/dist/ws/run-session.d.ts.map +1 -0
  133. package/dist/ws/run-session.js +160 -0
  134. package/dist/ws/run-session.js.map +1 -0
  135. package/dist/ws/session-catalog.d.ts +25 -0
  136. package/dist/ws/session-catalog.d.ts.map +1 -0
  137. package/dist/ws/session-catalog.js +45 -0
  138. package/dist/ws/session-catalog.js.map +1 -0
  139. package/dist/ws/shrink.d.ts +34 -0
  140. package/dist/ws/shrink.d.ts.map +1 -0
  141. package/dist/ws/shrink.js +91 -0
  142. package/dist/ws/shrink.js.map +1 -0
  143. package/dist/ws/turns.d.ts +77 -0
  144. package/dist/ws/turns.d.ts.map +1 -0
  145. package/dist/ws/turns.js +89 -0
  146. package/dist/ws/turns.js.map +1 -0
  147. package/migrations/001_initial.sql +29 -0
  148. package/migrations/002_session_provider.sql +1 -0
  149. package/migrations/003_session_backend.sql +5 -0
  150. package/migrations/004_drop_gemini_backend.sql +20 -0
  151. package/migrations/005_passkey_credentials.sql +18 -0
  152. package/package.json +72 -0
  153. package/src/agent/backend.ts +308 -0
  154. package/src/app.ts +159 -0
  155. package/src/brain/client.ts +186 -0
  156. package/src/brain/types.ts +44 -0
  157. package/src/cron/scheduler.ts +80 -0
  158. package/src/db/client.ts +85 -0
  159. package/src/files/walker.ts +418 -0
  160. package/src/index.ts +39 -0
  161. package/src/middleware/auth.ts +321 -0
  162. package/src/middleware/passkeys.ts +510 -0
  163. package/src/middleware/tailscale.ts +81 -0
  164. package/src/render/template.ts +124 -0
  165. package/src/routes/brain.ts +305 -0
  166. package/src/routes/files.ts +99 -0
  167. package/src/routes/health.ts +29 -0
  168. package/src/routes/providers.ts +9 -0
  169. package/src/routes/render.ts +68 -0
  170. package/src/routes/sessions.ts +49 -0
  171. package/src/routes/voice.ts +80 -0
  172. package/src/voice/deepgram-token.ts +80 -0
  173. package/src/voice/keyterm-builder.ts +426 -0
  174. package/src/voice/speech-providers.ts +111 -0
  175. package/src/ws/attachments.ts +86 -0
  176. package/src/ws/bridge.ts +139 -0
  177. package/src/ws/clients.ts +57 -0
  178. package/src/ws/connection.ts +132 -0
  179. package/src/ws/dispatch.ts +162 -0
  180. package/src/ws/frames.ts +42 -0
  181. package/src/ws/handler.ts +50 -0
  182. package/src/ws/history.ts +51 -0
  183. package/src/ws/host.ts +69 -0
  184. package/src/ws/routing.ts +43 -0
  185. package/src/ws/run-session.ts +192 -0
  186. package/src/ws/session-catalog.ts +97 -0
  187. package/src/ws/shrink.ts +95 -0
  188. package/src/ws/turns.ts +151 -0
@@ -0,0 +1,246 @@
1
+ import { Hono } from "hono";
2
+ import { getSignedCookie, setSignedCookie, deleteCookie } from "hono/cookie";
3
+ import { isTailscaleAllowed, clientIp } from "./tailscale.js";
4
+ const COOKIE_NAME = "brain_ui_session";
5
+ const SESSION_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days
6
+ export const LOGIN_RATE_LIMIT = 5; // attempts per window, per client IP
7
+ const LOGIN_RATE_WINDOW_MS = 60_000; // per minute
8
+ // A global cap in addition to the per-IP one: the per-IP key is derived from
9
+ // X-Forwarded-For, which a client behind a trusted proxy can rotate to mint a
10
+ // fresh bucket per request. The global cap bounds brute force across rotated
11
+ // IPs. It stays well above a human's needs and, combined with the argon2id
12
+ // verify cost, makes online guessing infeasible without being a lockout an
13
+ // attacker could weaponize to deny the owner access.
14
+ export const GLOBAL_LOGIN_RATE_LIMIT = 20; // attempts per window, all IPs combined
15
+ const DEFAULT_PROXY_HEADER = "x-forwarded-user";
16
+ export function resolveAuthMode() {
17
+ const explicit = process.env.AUTH_MODE?.trim().toLowerCase();
18
+ if (explicit === "password" ||
19
+ explicit === "tailscale" ||
20
+ explicit === "proxy" ||
21
+ explicit === "none") {
22
+ return explicit;
23
+ }
24
+ if (explicit) {
25
+ console.warn(`[auth] Unknown AUTH_MODE="${explicit}"; auto-detecting instead.`);
26
+ }
27
+ if (process.env.BRAIN_UI_PASSWORD_HASH)
28
+ return "password";
29
+ return "tailscale";
30
+ }
31
+ /**
32
+ * Validate the auth configuration at startup. Throws (refusing to boot) on an
33
+ * unsafe or unusable configuration.
34
+ */
35
+ export function assertAuthConfig(mode) {
36
+ const host = process.env.HOST ?? "";
37
+ const loopback = host === "127.0.0.1" || host === "::1" || host === "localhost";
38
+ if (mode === "password") {
39
+ if (!process.env.BRAIN_UI_PASSWORD_HASH) {
40
+ throw new Error("AUTH_MODE=password requires BRAIN_UI_PASSWORD_HASH. Generate one with:\n" +
41
+ " bun -e 'console.log(await Bun.password.hash(process.argv[1]))' 'your-password'");
42
+ }
43
+ if (!process.env.COOKIE_SECRET) {
44
+ throw new Error("AUTH_MODE=password requires COOKIE_SECRET (a long random string used to " +
45
+ "sign the session cookie). Generate one with:\n openssl rand -hex 32");
46
+ }
47
+ }
48
+ // Deliberately NOT gated on NODE_ENV: a forgotten env var must never be the
49
+ // difference between "auth required" and "agent with file-write access
50
+ // exposed to the network".
51
+ if (mode === "none" && !loopback) {
52
+ if (process.env.BRAIN_UI_DANGEROUSLY_DISABLE_AUTH === "1") {
53
+ console.warn("[auth] AUTH_MODE=none on a non-loopback host, allowed by " +
54
+ "BRAIN_UI_DANGEROUSLY_DISABLE_AUTH=1 — every network peer has full " +
55
+ "agent access. Do not run this on anything but a trusted network.");
56
+ }
57
+ else {
58
+ throw new Error("AUTH_MODE=none refuses to start unless HOST is loopback " +
59
+ `(got HOST="${host}"). Running with no auth would expose an agent with ` +
60
+ "write access to your files to the whole network. Set AUTH_MODE=password " +
61
+ "(recommended), tailscale, or proxy — or bind HOST to 127.0.0.1. " +
62
+ "To intentionally run unauthenticated on a trusted network, set " +
63
+ "BRAIN_UI_DANGEROUSLY_DISABLE_AUTH=1.");
64
+ }
65
+ }
66
+ if (mode === "proxy") {
67
+ if (process.env.TRUST_PROXY !== "1") {
68
+ throw new Error("AUTH_MODE=proxy requires TRUST_PROXY=1 — the proxy-auth header is only " +
69
+ "trustworthy when a fronting proxy is guaranteed to set it and strip " +
70
+ "any client-supplied copy. Set TRUST_PROXY=1 once that holds.");
71
+ }
72
+ console.log(`[auth] mode: proxy (trusting header "${proxyHeaderName()}"; ensure your ` +
73
+ "upstream proxy sets it and strips any client-supplied copy)");
74
+ return;
75
+ }
76
+ console.log(`[auth] mode: ${mode}`);
77
+ }
78
+ /** Middleware guarding /api/* according to the resolved mode. */
79
+ export function authGuard(mode) {
80
+ switch (mode) {
81
+ case "tailscale":
82
+ return async (c, next) => {
83
+ if (isTailscaleAllowed(c, process.env.TRUST_PROXY === "1")) {
84
+ await next();
85
+ }
86
+ else {
87
+ return c.json({ error: "VPN access required" }, 403);
88
+ }
89
+ };
90
+ case "proxy":
91
+ return async (c, next) => {
92
+ if (hasProxyAuth(c)) {
93
+ await next();
94
+ }
95
+ else {
96
+ return c.json({ error: "Authentication required", authRequired: true }, 401);
97
+ }
98
+ };
99
+ case "password":
100
+ return async (c, next) => {
101
+ if (await hasValidSession(c)) {
102
+ await next();
103
+ }
104
+ else {
105
+ return c.json({ error: "Authentication required", authRequired: true }, 401);
106
+ }
107
+ };
108
+ case "none":
109
+ return async (_c, next) => {
110
+ await next();
111
+ };
112
+ }
113
+ }
114
+ /**
115
+ * Authorize a WebSocket upgrade. The upgrade handler must call this itself —
116
+ * browsers can't set headers on the WS handshake, so the session cookie (sent
117
+ * automatically same-origin) is the credential, and no header-modifying
118
+ * middleware may sit on the WS route (immutable-header errors). Mirrors
119
+ * {@link authGuard} without emitting a response.
120
+ */
121
+ export async function isWsAuthorized(c, mode) {
122
+ switch (mode) {
123
+ case "none":
124
+ return true;
125
+ case "tailscale":
126
+ return isTailscaleAllowed(c, process.env.TRUST_PROXY === "1");
127
+ case "proxy":
128
+ return hasProxyAuth(c);
129
+ case "password":
130
+ return hasValidSession(c);
131
+ }
132
+ }
133
+ // --- password mode helpers ---
134
+ /**
135
+ * Mint the session cookie. The single place a session comes into existence —
136
+ * both password login and passkey login (middleware/passkeys.ts) call this, so
137
+ * authGuard / isWsAuthorized / TTL semantics stay identical across methods.
138
+ */
139
+ export async function issueSessionCookie(c) {
140
+ const secret = process.env.COOKIE_SECRET ?? "";
141
+ await setSignedCookie(c, COOKIE_NAME, String(Date.now()), secret, {
142
+ httpOnly: true,
143
+ sameSite: "Strict",
144
+ // Always Secure — every real deployment serves over HTTPS, and we do not
145
+ // want a missing NODE_ENV to silently drop the flag and expose the cookie
146
+ // over plaintext. (Local password testing must use https/localhost.)
147
+ secure: true,
148
+ path: "/",
149
+ maxAge: SESSION_TTL_SECONDS,
150
+ });
151
+ }
152
+ async function hasValidSession(c) {
153
+ const secret = process.env.COOKIE_SECRET ?? "";
154
+ if (!secret)
155
+ return false;
156
+ try {
157
+ const value = await getSignedCookie(c, secret, COOKIE_NAME);
158
+ if (typeof value !== "string" || value.length === 0)
159
+ return false;
160
+ // The cookie value is the issue timestamp. Validate its age server-side:
161
+ // Max-Age is client-discardable, so the signature alone does not bound a
162
+ // session's lifetime — this does.
163
+ const issuedAt = Number(value);
164
+ if (!Number.isFinite(issuedAt))
165
+ return false;
166
+ const ageSeconds = (Date.now() - issuedAt) / 1000;
167
+ return ageSeconds >= 0 && ageSeconds < SESSION_TTL_SECONDS;
168
+ }
169
+ catch {
170
+ return false;
171
+ }
172
+ }
173
+ // --- proxy mode helpers ---
174
+ function proxyHeaderName() {
175
+ return (process.env.PROXY_AUTH_HEADER || DEFAULT_PROXY_HEADER).toLowerCase();
176
+ }
177
+ function hasProxyAuth(c) {
178
+ // The proxy-auth header is only meaningful when a trusted proxy fronts the app
179
+ // and TRUST_PROXY says so; otherwise a client could set it directly. Gate on
180
+ // TRUST_PROXY, consistent with tailscale-mode XFF trust. (assertAuthConfig
181
+ // already refuses to boot proxy mode without it — this is belt-and-braces.)
182
+ if (process.env.TRUST_PROXY !== "1")
183
+ return false;
184
+ const user = c.req.header(proxyHeaderName());
185
+ return !!user && user.trim().length > 0;
186
+ }
187
+ const loginBuckets = new Map();
188
+ /** Test-only: clear all rate-limit buckets (they are module-global state). */
189
+ export function resetLoginRateLimiter() {
190
+ loginBuckets.clear();
191
+ }
192
+ export function consumeLoginToken(key, limit) {
193
+ const now = Date.now();
194
+ let bucket = loginBuckets.get(key);
195
+ if (!bucket || now >= bucket.resetAt) {
196
+ bucket = { tokens: limit, resetAt: now + LOGIN_RATE_WINDOW_MS };
197
+ loginBuckets.set(key, bucket);
198
+ }
199
+ if (bucket.tokens <= 0)
200
+ return false;
201
+ bucket.tokens--;
202
+ return true;
203
+ }
204
+ /** Login/logout routes. Only functional in `password` mode. */
205
+ export function authRoutes(mode, deps = {}) {
206
+ const app = new Hono();
207
+ app.post("/auth/login", async (c) => {
208
+ if (mode !== "password") {
209
+ return c.json({ error: "Password login is not enabled" }, 400);
210
+ }
211
+ const key = clientIp(c, process.env.TRUST_PROXY === "1") || "unknown";
212
+ const perIpOk = consumeLoginToken(`ip:${key}`, LOGIN_RATE_LIMIT);
213
+ const globalOk = consumeLoginToken("global", GLOBAL_LOGIN_RATE_LIMIT);
214
+ if (!perIpOk || !globalOk) {
215
+ return c.json({ error: "Too many attempts. Try again in a minute." }, 429);
216
+ }
217
+ if (deps.passwordDisabled?.(c)) {
218
+ return c.json({ error: "Password login is disabled — sign in with a passkey." }, 403);
219
+ }
220
+ const hash = process.env.BRAIN_UI_PASSWORD_HASH ?? "";
221
+ const secret = process.env.COOKIE_SECRET ?? "";
222
+ let body;
223
+ try {
224
+ body = await c.req.json();
225
+ }
226
+ catch {
227
+ return c.json({ error: "Invalid request body" }, 400);
228
+ }
229
+ const password = typeof body.password === "string" ? body.password : "";
230
+ if (!password || !hash || !secret) {
231
+ return c.json({ error: "Invalid credentials" }, 401);
232
+ }
233
+ const ok = await Bun.password.verify(password, hash).catch(() => false);
234
+ if (!ok) {
235
+ return c.json({ error: "Invalid credentials" }, 401);
236
+ }
237
+ await issueSessionCookie(c);
238
+ return c.json({ ok: true });
239
+ });
240
+ app.post("/auth/logout", (c) => {
241
+ deleteCookie(c, COOKIE_NAME, { path: "/" });
242
+ return c.json({ ok: true });
243
+ });
244
+ return app;
245
+ }
246
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { Hono } from "hono";
2
+ import type { Context } from "hono";
3
+ import { verifyAuthenticationResponse as realVerifyAuthentication, verifyRegistrationResponse as realVerifyRegistration } from "@simplewebauthn/server";
4
+ import { type AuthMode } from "./auth.js";
5
+ /**
6
+ * Refuse to boot on a WebAuthn user handle the browser would reject. Called
7
+ * from createApp(): an oversized handle otherwise produces registration
8
+ * options that look fine server-side and fail silently in every browser.
9
+ */
10
+ export declare function assertPasskeyConfig(): void;
11
+ /** Injectable seams for tests; production callers pass nothing. */
12
+ export interface PasskeyDeps {
13
+ verifyRegistrationResponse?: typeof realVerifyRegistration;
14
+ verifyAuthenticationResponse?: typeof realVerifyAuthentication;
15
+ now?: () => number;
16
+ }
17
+ /**
18
+ * Once a passkey exists for the request's RP, password login is disabled —
19
+ * the password becomes registration-bootstrap and break-glass recovery only
20
+ * (set BRAIN_UI_ALLOW_PASSWORD=1 to re-enable it, e.g. after losing every
21
+ * authenticator). Scoped per RP so e.g. localhost dev keeps password login
22
+ * until it has its own credential. Host-derived rpID is fine here: a forged
23
+ * Host can at most re-enable password auth, which still requires the password.
24
+ */
25
+ export declare function passwordLoginDisabled(c: Context): boolean;
26
+ /**
27
+ * Unauthenticated passkey routes. Mount BEFORE the auth guard, next to
28
+ * authRoutes.
29
+ */
30
+ export declare function passkeyPublicRoutes(mode: AuthMode, deps?: PasskeyDeps): Hono;
31
+ /**
32
+ * Session-gated passkey routes. Mount AFTER the auth guard — gating comes from
33
+ * mount position, not per-route checks.
34
+ */
35
+ export declare function passkeyManagementRoutes(mode: AuthMode, deps?: PasskeyDeps): Hono;
36
+ //# sourceMappingURL=passkeys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkeys.d.ts","sourceRoot":"","sources":["../../src/middleware/passkeys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAGL,4BAA4B,IAAI,wBAAwB,EACxD,0BAA0B,IAAI,sBAAsB,EACrD,MAAM,wBAAwB,CAAC;AAMhC,OAAO,EACL,KAAK,QAAQ,EAKd,MAAM,WAAW,CAAC;AAkDnB;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAQ1C;AAQD,mEAAmE;AACnE,MAAM,WAAW,WAAW;IAC1B,0BAA0B,CAAC,EAAE,OAAO,sBAAsB,CAAC;IAC3D,4BAA4B,CAAC,EAAE,OAAO,wBAAwB,CAAC;IAC/D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAkKD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAMzD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAE,WAAgB,GAAG,IAAI,CAqGhF;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAE,WAAgB,GAAG,IAAI,CA6HpF"}