@sentropic/auth-hono 0.2.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.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/contracts.d.ts +64 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +85 -0
- package/dist/contracts.js.map +1 -0
- package/dist/credential-route-handlers.d.ts +13 -0
- package/dist/credential-route-handlers.d.ts.map +1 -0
- package/dist/credential-route-handlers.js +102 -0
- package/dist/credential-route-handlers.js.map +1 -0
- package/dist/email-verification.d.ts +41 -0
- package/dist/email-verification.d.ts.map +1 -0
- package/dist/email-verification.js +65 -0
- package/dist/email-verification.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/magic-link.d.ts +40 -0
- package/dist/magic-link.d.ts.map +1 -0
- package/dist/magic-link.js +94 -0
- package/dist/magic-link.js.map +1 -0
- package/dist/middleware.d.ts +25 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +104 -0
- package/dist/middleware.js.map +1 -0
- package/dist/ports.d.ts +279 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/route-handlers.d.ts +19 -0
- package/dist/route-handlers.d.ts.map +1 -0
- package/dist/route-handlers.js +106 -0
- package/dist/route-handlers.js.map +1 -0
- package/dist/router.d.ts +59 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +65 -0
- package/dist/router.js.map +1 -0
- package/dist/session-route-handlers.d.ts +9 -0
- package/dist/session-route-handlers.d.ts.map +1 -0
- package/dist/session-route-handlers.js +74 -0
- package/dist/session-route-handlers.js.map +1 -0
- package/dist/session.d.ts +33 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +123 -0
- package/dist/session.js.map +1 -0
- package/dist/webauthn-authentication-route-handlers.d.ts +26 -0
- package/dist/webauthn-authentication-route-handlers.d.ts.map +1 -0
- package/dist/webauthn-authentication-route-handlers.js +97 -0
- package/dist/webauthn-authentication-route-handlers.js.map +1 -0
- package/dist/webauthn-authentication.d.ts +39 -0
- package/dist/webauthn-authentication.d.ts.map +1 -0
- package/dist/webauthn-authentication.js +110 -0
- package/dist/webauthn-authentication.js.map +1 -0
- package/dist/webauthn-registration-route-handlers.d.ts +47 -0
- package/dist/webauthn-registration-route-handlers.d.ts.map +1 -0
- package/dist/webauthn-registration-route-handlers.js +111 -0
- package/dist/webauthn-registration-route-handlers.js.map +1 -0
- package/dist/webauthn-registration.d.ts +47 -0
- package/dist/webauthn-registration.d.ts.map +1 -0
- package/dist/webauthn-registration.js +103 -0
- package/dist/webauthn-registration.js.map +1 -0
- package/package.json +98 -0
- package/src/contracts.ts +99 -0
- package/src/credential-route-handlers.ts +178 -0
- package/src/email-verification.ts +127 -0
- package/src/index.ts +14 -0
- package/src/magic-link.ts +167 -0
- package/src/middleware.ts +178 -0
- package/src/ports.ts +289 -0
- package/src/route-handlers.ts +182 -0
- package/src/router.ts +149 -0
- package/src/session-route-handlers.ts +128 -0
- package/src/session.ts +201 -0
- package/src/webauthn-authentication-route-handlers.ts +200 -0
- package/src/webauthn-authentication.ts +211 -0
- package/src/webauthn-registration-route-handlers.ts +248 -0
- package/src/webauthn-registration.ts +204 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Fabien Antoine
|
|
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,80 @@
|
|
|
1
|
+
# @sentropic/auth-hono
|
|
2
|
+
|
|
3
|
+
Reusable Hono authentication route factories, contracts, and server-side auth helpers for Sentropic-compatible apps.
|
|
4
|
+
|
|
5
|
+
## Boundary
|
|
6
|
+
|
|
7
|
+
`@sentropic/auth-hono` owns backend auth route composition and reusable ceremony logic for:
|
|
8
|
+
|
|
9
|
+
- email code verification;
|
|
10
|
+
- magic-link verification;
|
|
11
|
+
- passkey registration;
|
|
12
|
+
- passkey authentication;
|
|
13
|
+
- session refresh/logout;
|
|
14
|
+
- credential list/rename/revoke;
|
|
15
|
+
- Hono auth middleware factories.
|
|
16
|
+
|
|
17
|
+
Application-owned adapters provide storage, email delivery, audit logging, cookies, token secrets, and workspace/account policy.
|
|
18
|
+
|
|
19
|
+
## Auth UI Alignment
|
|
20
|
+
|
|
21
|
+
The package route contract is aligned with the `@sentropic/auth-ui` `AuthUiTransport` shape from BR-39a. Backend implementations must preserve those request/result boundaries while allowing host apps to mount routes under their own prefix, such as Sentropic `/auth/*` or `spa-transpose-cv` `/admin/auth/*`.
|
|
22
|
+
|
|
23
|
+
## Quick start (per-route mounting)
|
|
24
|
+
|
|
25
|
+
Each route handler is independent; the host app composes only the ones it wants.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import {
|
|
29
|
+
createAuthEmailRouteHandlers,
|
|
30
|
+
createAuthWebAuthnRegistrationRouteHandlers,
|
|
31
|
+
createAuthWebAuthnAuthenticationRouteHandlers,
|
|
32
|
+
} from '@sentropic/auth-hono';
|
|
33
|
+
import { Hono } from 'hono';
|
|
34
|
+
|
|
35
|
+
const router = new Hono();
|
|
36
|
+
|
|
37
|
+
const emailHandlers = createAuthEmailRouteHandlers({ service: hostEmailService });
|
|
38
|
+
router.post('/email/verify-request', emailHandlers.requestEmailCode!);
|
|
39
|
+
router.post('/email/verify-code', emailHandlers.verifyEmailCode!);
|
|
40
|
+
|
|
41
|
+
const registerHandlers = createAuthWebAuthnRegistrationRouteHandlers({
|
|
42
|
+
prepareRegistrationOptions: hostPrepare,
|
|
43
|
+
resolveRegistrationUser: hostResolveUser,
|
|
44
|
+
service: hostRegistrationService,
|
|
45
|
+
// Optional: own the success response (session creation, cookie, rich body)
|
|
46
|
+
finalizeRegistration: hostFinalizeRegistration,
|
|
47
|
+
});
|
|
48
|
+
router.post('/register/options', registerHandlers.createPasskeyRegistrationOptions!);
|
|
49
|
+
router.post('/register/verify', registerHandlers.verifyPasskeyRegistration!);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Hooks (since 0.2.x)
|
|
53
|
+
|
|
54
|
+
WebAuthn route handlers expose two host-owned extension points so the package can stay storage- and policy-agnostic while still letting hosts express their flow:
|
|
55
|
+
|
|
56
|
+
- **Error short-circuit** — `prepareRegistrationOptions` / `resolveRegistrationUser` / `resolveAuthenticationOptions` may return either their normal success value **or** an `AuthHonoRouteHandlerError` (`{ error: { status, code, message } }`). The handler maps it directly to the HTTP response, so the host can refuse early (e.g. unverified email → 403) without throwing.
|
|
57
|
+
- **Finalize hook** — `finalizeRegistration` / `finalizeAuthentication` are optional callbacks invoked after a successful credential verification with `{ credentialId, userId, request }` and the Hono `Context`. They return the final `Response`, giving the host full control over session creation, cookies, and the response body. Without a hook the package returns the default structured `{ credentialId, success, userId }` body.
|
|
58
|
+
|
|
59
|
+
## Response contract (structured)
|
|
60
|
+
|
|
61
|
+
All package handlers emit structured responses to keep contracts predictable across hosts:
|
|
62
|
+
|
|
63
|
+
- success bodies carry domain-specific fields (e.g. `{ delivery: 'email', expiresAt, success: true }`, `{ success: true, verificationToken }`, `{ sessionToken, refreshToken, expiresAt, success: true }` when the host's `finalize` builds the session response);
|
|
64
|
+
- error bodies are always `{ error: { code, message } }` plus an HTTP status. Hosts that need a host-specific shape (legacy flat errors, additional metadata) can wrap a handler or use the prepare/finalize hooks to project a different payload.
|
|
65
|
+
|
|
66
|
+
## Mounting recipes
|
|
67
|
+
|
|
68
|
+
- **Sentropic-style app** with a Drizzle/Postgres backend, WebAuthn-only login, and workspace-scoped sessions: implement `AuthHonoCredentialPort`, an `AuthHonoSessionService` adapter (or the package's `createAuthSessionService` when the full `AuthHonoPorts` bundle is wired), an `AuthHonoWebAuthnRegistrationService`/`AuthHonoWebAuthnAuthenticationService` adapter, and provide `prepareRegistrationOptions`/`resolveRegistrationUser` for first-admin + account-status policy and `finalizeRegistration`/`finalizeAuthentication` for session creation and cookie issuance. Sentropic's `api/src/services/auth/*-adapter.ts` modules show this end-to-end.
|
|
69
|
+
- **DB-less admin flow** (e.g. `spa-transpose-cv` mounting at `/admin/auth/*`): use a file- or memory-backed implementation of `AuthHonoCredentialPort` and the relevant ports, skip workspace bootstrap entirely, and either rely on the default verify response or supply a minimal `finalizeAuthentication` that issues a host-managed session cookie. The package never touches workspace state, so no DB schema is required.
|
|
70
|
+
|
|
71
|
+
## First Publish
|
|
72
|
+
|
|
73
|
+
This is a brand-new public package. First publish requires the one-shot bootstrap flow from `rules/workflow.md`: trigger `ci.yml` with `bootstrap_publish_target=auth-hono`, handle any npm token or 2FA requirement with the npm owner, then attach the npm OIDC trusted publisher for `rhanka/sentropic` workflow `ci.yml`. Steady-state publishes should use trusted publishing and skip if the version already exists.
|
|
74
|
+
|
|
75
|
+
## Versioning
|
|
76
|
+
|
|
77
|
+
This branch ships `0.2.1`:
|
|
78
|
+
|
|
79
|
+
- `0.2.0` adds `AuthHonoRouteHandlerError` short-circuit on WebAuthn prepare/resolve hooks and the `finalizeRegistration`/`finalizeAuthentication` post-verify hooks. Additive; existing handler signatures stay valid.
|
|
80
|
+
- `0.2.1` patches `extractChallenge` (both WebAuthn handlers) to handle `credential.response === null` defensively (returns 400 `invalid_credential` instead of throwing 500).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export declare const AUTH_HONO_AUTH_UI_METHODS: readonly ["requestEmailCode", "verifyEmailCode", "requestMagicLink", "verifyMagicLink", "createPasskeyRegistrationOptions", "verifyPasskeyRegistration", "createPasskeyAuthenticationOptions", "verifyPasskeyAuthentication", "refreshSession", "logout", "listCredentials", "renameCredential", "revokeCredential"];
|
|
2
|
+
export type AuthHonoAuthUiMethod = (typeof AUTH_HONO_AUTH_UI_METHODS)[number];
|
|
3
|
+
export type AuthHonoHttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
4
|
+
export interface AuthHonoRouteContract {
|
|
5
|
+
method: AuthHonoHttpMethod;
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const AUTH_HONO_ROUTE_MAP: {
|
|
9
|
+
readonly requestEmailCode: {
|
|
10
|
+
readonly method: "POST";
|
|
11
|
+
readonly path: "/email/verify-request";
|
|
12
|
+
};
|
|
13
|
+
readonly verifyEmailCode: {
|
|
14
|
+
readonly method: "POST";
|
|
15
|
+
readonly path: "/email/verify-code";
|
|
16
|
+
};
|
|
17
|
+
readonly requestMagicLink: {
|
|
18
|
+
readonly method: "POST";
|
|
19
|
+
readonly path: "/magic-link/request";
|
|
20
|
+
};
|
|
21
|
+
readonly verifyMagicLink: {
|
|
22
|
+
readonly method: "POST";
|
|
23
|
+
readonly path: "/magic-link/verify";
|
|
24
|
+
};
|
|
25
|
+
readonly createPasskeyRegistrationOptions: {
|
|
26
|
+
readonly method: "POST";
|
|
27
|
+
readonly path: "/register/options";
|
|
28
|
+
};
|
|
29
|
+
readonly verifyPasskeyRegistration: {
|
|
30
|
+
readonly method: "POST";
|
|
31
|
+
readonly path: "/register/verify";
|
|
32
|
+
};
|
|
33
|
+
readonly createPasskeyAuthenticationOptions: {
|
|
34
|
+
readonly method: "POST";
|
|
35
|
+
readonly path: "/login/options";
|
|
36
|
+
};
|
|
37
|
+
readonly verifyPasskeyAuthentication: {
|
|
38
|
+
readonly method: "POST";
|
|
39
|
+
readonly path: "/login/verify";
|
|
40
|
+
};
|
|
41
|
+
readonly refreshSession: {
|
|
42
|
+
readonly method: "POST";
|
|
43
|
+
readonly path: "/session/refresh";
|
|
44
|
+
};
|
|
45
|
+
readonly logout: {
|
|
46
|
+
readonly method: "DELETE";
|
|
47
|
+
readonly path: "/session";
|
|
48
|
+
};
|
|
49
|
+
readonly listCredentials: {
|
|
50
|
+
readonly method: "GET";
|
|
51
|
+
readonly path: "/credentials";
|
|
52
|
+
};
|
|
53
|
+
readonly renameCredential: {
|
|
54
|
+
readonly method: "PUT";
|
|
55
|
+
readonly path: "/credentials/:id";
|
|
56
|
+
};
|
|
57
|
+
readonly revokeCredential: {
|
|
58
|
+
readonly method: "DELETE";
|
|
59
|
+
readonly path: "/credentials/:id";
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export declare const AUTH_HONO_REQUIRED_PORTS: readonly ["users", "credentials", "challenges", "sessions", "emailVerification", "magicLinks", "emailDelivery", "cookies", "tokens", "auditLog", "clock", "random", "accountPolicy"];
|
|
63
|
+
export type AuthHonoRequiredPort = (typeof AUTH_HONO_REQUIRED_PORTS)[number];
|
|
64
|
+
//# sourceMappingURL=contracts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,yBAAyB,sTAc5B,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9E,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDwC,CAAC;AAEzE,eAAO,MAAM,wBAAwB,sLAcgB,CAAC;AAEtD,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const AUTH_HONO_AUTH_UI_METHODS = [
|
|
2
|
+
'requestEmailCode',
|
|
3
|
+
'verifyEmailCode',
|
|
4
|
+
'requestMagicLink',
|
|
5
|
+
'verifyMagicLink',
|
|
6
|
+
'createPasskeyRegistrationOptions',
|
|
7
|
+
'verifyPasskeyRegistration',
|
|
8
|
+
'createPasskeyAuthenticationOptions',
|
|
9
|
+
'verifyPasskeyAuthentication',
|
|
10
|
+
'refreshSession',
|
|
11
|
+
'logout',
|
|
12
|
+
'listCredentials',
|
|
13
|
+
'renameCredential',
|
|
14
|
+
'revokeCredential',
|
|
15
|
+
];
|
|
16
|
+
export const AUTH_HONO_ROUTE_MAP = {
|
|
17
|
+
requestEmailCode: {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
path: '/email/verify-request',
|
|
20
|
+
},
|
|
21
|
+
verifyEmailCode: {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
path: '/email/verify-code',
|
|
24
|
+
},
|
|
25
|
+
requestMagicLink: {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
path: '/magic-link/request',
|
|
28
|
+
},
|
|
29
|
+
verifyMagicLink: {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
path: '/magic-link/verify',
|
|
32
|
+
},
|
|
33
|
+
createPasskeyRegistrationOptions: {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
path: '/register/options',
|
|
36
|
+
},
|
|
37
|
+
verifyPasskeyRegistration: {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
path: '/register/verify',
|
|
40
|
+
},
|
|
41
|
+
createPasskeyAuthenticationOptions: {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
path: '/login/options',
|
|
44
|
+
},
|
|
45
|
+
verifyPasskeyAuthentication: {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
path: '/login/verify',
|
|
48
|
+
},
|
|
49
|
+
refreshSession: {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
path: '/session/refresh',
|
|
52
|
+
},
|
|
53
|
+
logout: {
|
|
54
|
+
method: 'DELETE',
|
|
55
|
+
path: '/session',
|
|
56
|
+
},
|
|
57
|
+
listCredentials: {
|
|
58
|
+
method: 'GET',
|
|
59
|
+
path: '/credentials',
|
|
60
|
+
},
|
|
61
|
+
renameCredential: {
|
|
62
|
+
method: 'PUT',
|
|
63
|
+
path: '/credentials/:id',
|
|
64
|
+
},
|
|
65
|
+
revokeCredential: {
|
|
66
|
+
method: 'DELETE',
|
|
67
|
+
path: '/credentials/:id',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
export const AUTH_HONO_REQUIRED_PORTS = [
|
|
71
|
+
'users',
|
|
72
|
+
'credentials',
|
|
73
|
+
'challenges',
|
|
74
|
+
'sessions',
|
|
75
|
+
'emailVerification',
|
|
76
|
+
'magicLinks',
|
|
77
|
+
'emailDelivery',
|
|
78
|
+
'cookies',
|
|
79
|
+
'tokens',
|
|
80
|
+
'auditLog',
|
|
81
|
+
'clock',
|
|
82
|
+
'random',
|
|
83
|
+
'accountPolicy',
|
|
84
|
+
];
|
|
85
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,kBAAkB;IAClB,iBAAiB;IACjB,kBAAkB;IAClB,iBAAiB;IACjB,kCAAkC;IAClC,2BAA2B;IAC3B,oCAAoC;IACpC,6BAA6B;IAC7B,gBAAgB;IAChB,QAAQ;IACR,iBAAiB;IACjB,kBAAkB;IAClB,kBAAkB;CACV,CAAC;AAWX,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,gBAAgB,EAAE;QAChB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,uBAAuB;KAC9B;IACD,eAAe,EAAE;QACf,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,oBAAoB;KAC3B;IACD,gBAAgB,EAAE;QAChB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,qBAAqB;KAC5B;IACD,eAAe,EAAE;QACf,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,oBAAoB;KAC3B;IACD,gCAAgC,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,mBAAmB;KAC1B;IACD,yBAAyB,EAAE;QACzB,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,kBAAkB;KACzB;IACD,kCAAkC,EAAE;QAClC,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,gBAAgB;KACvB;IACD,2BAA2B,EAAE;QAC3B,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,eAAe;KACtB;IACD,cAAc,EAAE;QACd,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,kBAAkB;KACzB;IACD,MAAM,EAAE;QACN,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,UAAU;KACjB;IACD,eAAe,EAAE;QACf,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,cAAc;KACrB;IACD,gBAAgB,EAAE;QAChB,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,kBAAkB;KACzB;IACD,gBAAgB,EAAE;QAChB,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,kBAAkB;KACzB;CACqE,CAAC;AAEzE,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,OAAO;IACP,aAAa;IACb,YAAY;IACZ,UAAU;IACV,mBAAmB;IACnB,YAAY;IACZ,eAAe;IACf,SAAS;IACT,QAAQ;IACR,UAAU;IACV,OAAO;IACP,QAAQ;IACR,eAAe;CACoC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { AuthHonoCredentialPort } from './ports.js';
|
|
3
|
+
import type { AuthHonoRouteHandlers } from './router.js';
|
|
4
|
+
export interface AuthHonoCredentialRouteSession {
|
|
5
|
+
userId: string;
|
|
6
|
+
}
|
|
7
|
+
export type AuthHonoCredentialSessionResolver = (c: Context) => Promise<AuthHonoCredentialRouteSession | null>;
|
|
8
|
+
export interface CreateAuthCredentialRouteHandlersOptions {
|
|
9
|
+
credentials: AuthHonoCredentialPort;
|
|
10
|
+
resolveSession: AuthHonoCredentialSessionResolver;
|
|
11
|
+
}
|
|
12
|
+
export declare const createAuthCredentialRouteHandlers: (options: CreateAuthCredentialRouteHandlersOptions) => AuthHonoRouteHandlers;
|
|
13
|
+
//# sourceMappingURL=credential-route-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-route-handlers.d.ts","sourceRoot":"","sources":["../src/credential-route-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,KAAK,EAAE,sBAAsB,EAA4B,MAAM,YAAY,CAAC;AACnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,iCAAiC,GAAG,CAC9C,CAAC,EAAE,OAAO,KACP,OAAO,CAAC,8BAA8B,GAAG,IAAI,CAAC,CAAC;AAEpD,MAAM,WAAW,wCAAwC;IACvD,WAAW,EAAE,sBAAsB,CAAC;IACpC,cAAc,EAAE,iCAAiC,CAAC;CACnD;AAMD,eAAO,MAAM,iCAAiC,YACnC,wCAAwC,KAChD,qBAiED,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const renameCredentialSchema = z.object({
|
|
3
|
+
deviceName: z.string().min(1).max(100),
|
|
4
|
+
});
|
|
5
|
+
export const createAuthCredentialRouteHandlers = (options) => ({
|
|
6
|
+
async listCredentials(c) {
|
|
7
|
+
const session = await options.resolveSession(c);
|
|
8
|
+
if (!session) {
|
|
9
|
+
return authenticationRequired(c);
|
|
10
|
+
}
|
|
11
|
+
const credentials = await options.credentials.listForUser(session.userId);
|
|
12
|
+
return c.json({ credentials: credentials.map(toCredentialResponse) });
|
|
13
|
+
},
|
|
14
|
+
async renameCredential(c) {
|
|
15
|
+
const session = await options.resolveSession(c);
|
|
16
|
+
if (!session) {
|
|
17
|
+
return authenticationRequired(c);
|
|
18
|
+
}
|
|
19
|
+
const parsed = await parseJson(c, renameCredentialSchema);
|
|
20
|
+
if (!parsed.ok) {
|
|
21
|
+
return invalidInput(c, parsed.error);
|
|
22
|
+
}
|
|
23
|
+
const checked = await requireOwnedCredential(c, options, session.userId);
|
|
24
|
+
if (!checked.ok) {
|
|
25
|
+
return checked.response;
|
|
26
|
+
}
|
|
27
|
+
const renamed = await options.credentials.rename(checked.credential.id, session.userId, parsed.value.deviceName);
|
|
28
|
+
if (!renamed) {
|
|
29
|
+
return credentialNotFound(c);
|
|
30
|
+
}
|
|
31
|
+
return c.json({ credential: toCredentialResponse(renamed), success: true });
|
|
32
|
+
},
|
|
33
|
+
async revokeCredential(c) {
|
|
34
|
+
const session = await options.resolveSession(c);
|
|
35
|
+
if (!session) {
|
|
36
|
+
return authenticationRequired(c);
|
|
37
|
+
}
|
|
38
|
+
const checked = await requireOwnedCredential(c, options, session.userId);
|
|
39
|
+
if (!checked.ok) {
|
|
40
|
+
return checked.response;
|
|
41
|
+
}
|
|
42
|
+
const revoked = await options.credentials.revoke(checked.credential.id, session.userId);
|
|
43
|
+
if (!revoked) {
|
|
44
|
+
return credentialNotFound(c);
|
|
45
|
+
}
|
|
46
|
+
return c.json({ success: true });
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const toCredentialResponse = (credential) => ({
|
|
50
|
+
backedUp: credential.backedUp,
|
|
51
|
+
counter: credential.counter,
|
|
52
|
+
createdAt: credential.createdAt.toISOString(),
|
|
53
|
+
credentialId: credential.credentialId,
|
|
54
|
+
deviceName: credential.name ?? 'Unnamed credential',
|
|
55
|
+
deviceType: credential.deviceType,
|
|
56
|
+
id: credential.id,
|
|
57
|
+
lastUsedAt: credential.lastUsedAt ? credential.lastUsedAt.toISOString() : null,
|
|
58
|
+
transports: credential.transports,
|
|
59
|
+
});
|
|
60
|
+
const requireOwnedCredential = async (c, options, userId) => {
|
|
61
|
+
const credentialId = c.req.param('id');
|
|
62
|
+
if (!credentialId) {
|
|
63
|
+
return { ok: false, response: credentialNotFound(c) };
|
|
64
|
+
}
|
|
65
|
+
const credential = await options.credentials.findById(credentialId);
|
|
66
|
+
if (!credential) {
|
|
67
|
+
return { ok: false, response: credentialNotFound(c) };
|
|
68
|
+
}
|
|
69
|
+
if (credential.userId !== userId) {
|
|
70
|
+
return { ok: false, response: forbidden(c) };
|
|
71
|
+
}
|
|
72
|
+
return { credential, ok: true };
|
|
73
|
+
};
|
|
74
|
+
const parseJson = async (c, schema) => {
|
|
75
|
+
const body = await c.req.json().catch(() => null);
|
|
76
|
+
const parsed = schema.safeParse(body);
|
|
77
|
+
if (!parsed.success) {
|
|
78
|
+
return { error: parsed.error, ok: false };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, value: parsed.data };
|
|
81
|
+
};
|
|
82
|
+
const authenticationRequired = (c) => c.json({
|
|
83
|
+
error: {
|
|
84
|
+
code: 'authentication_required',
|
|
85
|
+
message: 'Authentication is required to access credentials.',
|
|
86
|
+
},
|
|
87
|
+
}, 401);
|
|
88
|
+
const invalidInput = (c, error) => c.json({
|
|
89
|
+
error: {
|
|
90
|
+
code: 'invalid_input',
|
|
91
|
+
details: error.errors,
|
|
92
|
+
message: 'Invalid request data.',
|
|
93
|
+
},
|
|
94
|
+
}, 400);
|
|
95
|
+
const credentialNotFound = (c) => c.json({ error: { code: 'credential_not_found', message: 'Credential not found.' } }, 404);
|
|
96
|
+
const forbidden = (c) => c.json({
|
|
97
|
+
error: {
|
|
98
|
+
code: 'forbidden',
|
|
99
|
+
message: 'Credential does not belong to the authenticated user.',
|
|
100
|
+
},
|
|
101
|
+
}, 403);
|
|
102
|
+
//# sourceMappingURL=credential-route-handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-route-handlers.js","sourceRoot":"","sources":["../src/credential-route-handlers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AuthHonoPorts } from './ports.js';
|
|
2
|
+
export interface CreateAuthEmailVerificationServiceOptions {
|
|
3
|
+
codeLength?: number;
|
|
4
|
+
codeTtlSeconds?: number;
|
|
5
|
+
maxRequestsPerWindow?: number;
|
|
6
|
+
ports: AuthHonoPorts;
|
|
7
|
+
verificationTokenTtlSeconds?: number;
|
|
8
|
+
windowSeconds?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AuthHonoRequestEmailCodeInput {
|
|
11
|
+
email: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AuthHonoVerifyEmailCodeInput {
|
|
14
|
+
code: string;
|
|
15
|
+
email: string;
|
|
16
|
+
}
|
|
17
|
+
export type AuthHonoRequestEmailCodeResult = {
|
|
18
|
+
expiresAt: Date;
|
|
19
|
+
success: true;
|
|
20
|
+
} | {
|
|
21
|
+
error: AuthHonoServiceError;
|
|
22
|
+
success: false;
|
|
23
|
+
};
|
|
24
|
+
export type AuthHonoVerifyEmailCodeResult = {
|
|
25
|
+
valid: true;
|
|
26
|
+
verificationToken: string;
|
|
27
|
+
} | {
|
|
28
|
+
error: AuthHonoServiceError;
|
|
29
|
+
valid: false;
|
|
30
|
+
};
|
|
31
|
+
export interface AuthHonoServiceError {
|
|
32
|
+
code: string;
|
|
33
|
+
message: string;
|
|
34
|
+
status: number;
|
|
35
|
+
}
|
|
36
|
+
export interface AuthHonoEmailVerificationService {
|
|
37
|
+
requestEmailCode(input: AuthHonoRequestEmailCodeInput): Promise<AuthHonoRequestEmailCodeResult>;
|
|
38
|
+
verifyEmailCode(input: AuthHonoVerifyEmailCodeInput): Promise<AuthHonoVerifyEmailCodeResult>;
|
|
39
|
+
}
|
|
40
|
+
export declare const createAuthEmailVerificationService: (options: CreateAuthEmailVerificationServiceOptions) => AuthHonoEmailVerificationService;
|
|
41
|
+
//# sourceMappingURL=email-verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-verification.d.ts","sourceRoot":"","sources":["../src/email-verification.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,yCAAyC;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,8BAA8B,GACtC;IACE,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;CACf,GACD;IACE,KAAK,EAAE,oBAAoB,CAAC;IAC5B,OAAO,EAAE,KAAK,CAAC;CAChB,CAAC;AAEN,MAAM,MAAM,6BAA6B,GACrC;IACE,KAAK,EAAE,IAAI,CAAC;IACZ,iBAAiB,EAAE,MAAM,CAAC;CAC3B,GACD;IACE,KAAK,EAAE,oBAAoB,CAAC;IAC5B,KAAK,EAAE,KAAK,CAAC;CACd,CAAC;AAEN,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gCAAgC;IAC/C,gBAAgB,CAAC,KAAK,EAAE,6BAA6B,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAChG,eAAe,CAAC,KAAK,EAAE,4BAA4B,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAAC;CAC9F;AAED,eAAO,MAAM,kCAAkC,YACpC,yCAAyC,KACjD,gCAyEF,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const createAuthEmailVerificationService = (options) => {
|
|
2
|
+
const codeLength = options.codeLength ?? 6;
|
|
3
|
+
const codeTtlSeconds = options.codeTtlSeconds ?? 10 * 60;
|
|
4
|
+
const maxRequestsPerWindow = options.maxRequestsPerWindow ?? 3;
|
|
5
|
+
const verificationTokenTtlSeconds = options.verificationTokenTtlSeconds ?? 15 * 60;
|
|
6
|
+
const windowSeconds = options.windowSeconds ?? 10 * 60;
|
|
7
|
+
return {
|
|
8
|
+
async requestEmailCode(input) {
|
|
9
|
+
const email = options.ports.accountPolicy.normalizeEmail(input.email);
|
|
10
|
+
const now = options.ports.clock.now();
|
|
11
|
+
const windowStart = options.ports.clock.addSeconds(now, -windowSeconds);
|
|
12
|
+
const recentCount = await options.ports.emailVerification.countRecent(email, windowStart);
|
|
13
|
+
if (recentCount >= maxRequestsPerWindow) {
|
|
14
|
+
return {
|
|
15
|
+
error: {
|
|
16
|
+
code: 'rate_limited',
|
|
17
|
+
message: 'Too many verification code requests.',
|
|
18
|
+
status: 429,
|
|
19
|
+
},
|
|
20
|
+
success: false,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const code = options.ports.random.numericCode(codeLength);
|
|
24
|
+
const codeHash = await options.ports.tokens.hashSecret(code);
|
|
25
|
+
const expiresAt = options.ports.clock.addSeconds(now, codeTtlSeconds);
|
|
26
|
+
await options.ports.emailVerification.createCode({
|
|
27
|
+
email,
|
|
28
|
+
codeHash,
|
|
29
|
+
expiresAt,
|
|
30
|
+
now,
|
|
31
|
+
});
|
|
32
|
+
await options.ports.emailDelivery.sendVerificationCode({ email, code, expiresAt });
|
|
33
|
+
return {
|
|
34
|
+
expiresAt,
|
|
35
|
+
success: true,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
async verifyEmailCode(input) {
|
|
39
|
+
const email = options.ports.accountPolicy.normalizeEmail(input.email);
|
|
40
|
+
const codeHash = await options.ports.tokens.hashSecret(input.code);
|
|
41
|
+
const now = options.ports.clock.now();
|
|
42
|
+
const record = await options.ports.emailVerification.findLatestValidCode(email, codeHash, now);
|
|
43
|
+
if (!record || record.used || record.expiresAt <= now) {
|
|
44
|
+
return {
|
|
45
|
+
error: {
|
|
46
|
+
code: 'invalid_or_expired_code',
|
|
47
|
+
message: 'Verification code is invalid or expired.',
|
|
48
|
+
status: 400,
|
|
49
|
+
},
|
|
50
|
+
valid: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const verificationToken = await options.ports.tokens.signVerificationToken({
|
|
54
|
+
email,
|
|
55
|
+
expiresAt: options.ports.clock.addSeconds(now, verificationTokenTtlSeconds),
|
|
56
|
+
});
|
|
57
|
+
await options.ports.emailVerification.markUsedWithVerificationToken(record.id, verificationToken);
|
|
58
|
+
return {
|
|
59
|
+
valid: true,
|
|
60
|
+
verificationToken,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=email-verification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-verification.js","sourceRoot":"","sources":["../src/email-verification.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './contracts.js';
|
|
2
|
+
export * from './credential-route-handlers.js';
|
|
3
|
+
export * from './email-verification.js';
|
|
4
|
+
export * from './magic-link.js';
|
|
5
|
+
export * from './middleware.js';
|
|
6
|
+
export * from './ports.js';
|
|
7
|
+
export * from './route-handlers.js';
|
|
8
|
+
export * from './router.js';
|
|
9
|
+
export * from './session.js';
|
|
10
|
+
export * from './session-route-handlers.js';
|
|
11
|
+
export * from './webauthn-authentication.js';
|
|
12
|
+
export * from './webauthn-authentication-route-handlers.js';
|
|
13
|
+
export * from './webauthn-registration.js';
|
|
14
|
+
export * from './webauthn-registration-route-handlers.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2CAA2C,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './contracts.js';
|
|
2
|
+
export * from './credential-route-handlers.js';
|
|
3
|
+
export * from './email-verification.js';
|
|
4
|
+
export * from './magic-link.js';
|
|
5
|
+
export * from './middleware.js';
|
|
6
|
+
export * from './ports.js';
|
|
7
|
+
export * from './route-handlers.js';
|
|
8
|
+
export * from './router.js';
|
|
9
|
+
export * from './session.js';
|
|
10
|
+
export * from './session-route-handlers.js';
|
|
11
|
+
export * from './webauthn-authentication.js';
|
|
12
|
+
export * from './webauthn-authentication-route-handlers.js';
|
|
13
|
+
export * from './webauthn-registration.js';
|
|
14
|
+
export * from './webauthn-registration-route-handlers.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2CAA2C,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AuthHonoPorts, AuthHonoUserRecord } from './ports.js';
|
|
2
|
+
export interface CreateAuthMagicLinkServiceOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
ports: AuthHonoPorts;
|
|
5
|
+
tokenBytes?: number;
|
|
6
|
+
ttlSeconds?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AuthHonoRequestMagicLinkInput {
|
|
9
|
+
email: string;
|
|
10
|
+
userId?: string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface AuthHonoVerifyMagicLinkInput {
|
|
13
|
+
token: string;
|
|
14
|
+
}
|
|
15
|
+
export type AuthHonoRequestMagicLinkResult = {
|
|
16
|
+
expiresAt: Date;
|
|
17
|
+
success: true;
|
|
18
|
+
} | {
|
|
19
|
+
error: AuthHonoMagicLinkError;
|
|
20
|
+
success: false;
|
|
21
|
+
};
|
|
22
|
+
export type AuthHonoVerifyMagicLinkResult = {
|
|
23
|
+
email: string;
|
|
24
|
+
user: AuthHonoUserRecord;
|
|
25
|
+
valid: true;
|
|
26
|
+
} | {
|
|
27
|
+
error: AuthHonoMagicLinkError;
|
|
28
|
+
valid: false;
|
|
29
|
+
};
|
|
30
|
+
export interface AuthHonoMagicLinkError {
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
status: number;
|
|
34
|
+
}
|
|
35
|
+
export interface AuthHonoMagicLinkService {
|
|
36
|
+
requestMagicLink(input: AuthHonoRequestMagicLinkInput): Promise<AuthHonoRequestMagicLinkResult>;
|
|
37
|
+
verifyMagicLink(input: AuthHonoVerifyMagicLinkInput): Promise<AuthHonoVerifyMagicLinkResult>;
|
|
38
|
+
}
|
|
39
|
+
export declare const createAuthMagicLinkService: (options: CreateAuthMagicLinkServiceOptions) => AuthHonoMagicLinkService;
|
|
40
|
+
//# sourceMappingURL=magic-link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"magic-link.d.ts","sourceRoot":"","sources":["../src/magic-link.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,WAAW,iCAAiC;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,aAAa,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,8BAA8B,GACtC;IACE,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;CACf,GACD;IACE,KAAK,EAAE,sBAAsB,CAAC;IAC9B,OAAO,EAAE,KAAK,CAAC;CAChB,CAAC;AAEN,MAAM,MAAM,6BAA6B,GACrC;IACE,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,IAAI,CAAC;CACb,GACD;IACE,KAAK,EAAE,sBAAsB,CAAC;IAC9B,KAAK,EAAE,KAAK,CAAC;CACd,CAAC;AAEN,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,gBAAgB,CAAC,KAAK,EAAE,6BAA6B,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAChG,eAAe,CAAC,KAAK,EAAE,4BAA4B,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAAC;CAC9F;AAED,eAAO,MAAM,0BAA0B,YAC5B,iCAAiC,KACzC,wBAyDF,CAAC"}
|