@azlib/identity 0.2.1 → 0.3.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/README.md +119 -54
- package/dist/cookie-header-DdTShpuc.d.cts +83 -0
- package/dist/cookie-header-DdTShpuc.d.cts.map +1 -0
- package/dist/cookie-header-DdTShpuc.d.mts +83 -0
- package/dist/cookie-header-DdTShpuc.d.mts.map +1 -0
- package/dist/cookie-header-DptIxRmW.mjs +76 -0
- package/dist/cookie-header-DptIxRmW.mjs.map +1 -0
- package/dist/cookie-header-Dqd5k5BU.cjs +115 -0
- package/dist/{errors-CBbiRO2n.d.cts → errors-0Zh7EG06.d.cts} +4 -2
- package/dist/errors-0Zh7EG06.d.cts.map +1 -0
- package/dist/{errors-_OcodtdV.d.mts → errors-DVDEUk9-.d.mts} +4 -2
- package/dist/errors-DVDEUk9-.d.mts.map +1 -0
- package/dist/express.d.cts +3 -3
- package/dist/express.d.cts.map +1 -1
- package/dist/express.d.mts +3 -3
- package/dist/express.d.mts.map +1 -1
- package/dist/express.mjs.map +1 -1
- package/dist/{identity-Bz9RDOvT.mjs → identity-D0qyca_c.mjs} +37 -3
- package/dist/identity-D0qyca_c.mjs.map +1 -0
- package/dist/{identity-4eP45YIP.cjs → identity-Yz1BVkrM.cjs} +46 -0
- package/dist/{identity-router-D1xJ5H7y.d.cts → identity-router-BiratpJR.d.mts} +3 -3
- package/dist/identity-router-BiratpJR.d.mts.map +1 -0
- package/dist/{identity-router-tj3-teBI.d.mts → identity-router-D4oqYI60.d.cts} +3 -3
- package/dist/identity-router-D4oqYI60.d.cts.map +1 -0
- package/dist/{identity-service---OTann1.d.cts → identity-service-DE0Ffguz.d.cts} +3 -3
- package/dist/{identity-service---OTann1.d.cts.map → identity-service-DE0Ffguz.d.cts.map} +1 -1
- package/dist/{identity-service-WpOKb0MK.d.mts → identity-service-JtsoA6lv.d.mts} +3 -3
- package/dist/{identity-service-WpOKb0MK.d.mts.map → identity-service-JtsoA6lv.d.mts.map} +1 -1
- package/dist/identity-store-BRRahxcS.d.cts.map +1 -1
- package/dist/identity-store-BRRahxcS.d.mts.map +1 -1
- package/dist/index-CsYqHPvK.d.cts +54 -0
- package/dist/index-CsYqHPvK.d.cts.map +1 -0
- package/dist/index-LhSdoWEP.d.mts +54 -0
- package/dist/index-LhSdoWEP.d.mts.map +1 -0
- package/dist/index.cjs +11 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +3 -2
- package/dist/logger-CcCHJVVe.mjs.map +1 -1
- package/dist/nestjs.d.cts +3 -3
- package/dist/nestjs.d.cts.map +1 -1
- package/dist/nestjs.d.mts +3 -3
- package/dist/nestjs.d.mts.map +1 -1
- package/dist/nestjs.mjs.map +1 -1
- package/dist/next.cjs +280 -0
- package/dist/next.d.cts +58 -0
- package/dist/next.d.cts.map +1 -0
- package/dist/next.d.mts +58 -0
- package/dist/next.d.mts.map +1 -0
- package/dist/next.mjs +269 -0
- package/dist/next.mjs.map +1 -0
- package/dist/node.cjs +18 -19
- package/dist/node.d.cts +5 -4
- package/dist/node.d.cts.map +1 -1
- package/dist/node.d.mts +5 -4
- package/dist/node.d.mts.map +1 -1
- package/dist/node.mjs +10 -20
- package/dist/node.mjs.map +1 -1
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +7 -7
- package/schema/model.ts +110 -22
- package/dist/errors-CBbiRO2n.d.cts.map +0 -1
- package/dist/errors-_OcodtdV.d.mts.map +0 -1
- package/dist/identity-Bz9RDOvT.mjs.map +0 -1
- package/dist/identity-router-D1xJ5H7y.d.cts.map +0 -1
- package/dist/identity-router-tj3-teBI.d.mts.map +0 -1
- package/dist/index-CpufYgyn.d.cts +0 -30
- package/dist/index-CpufYgyn.d.cts.map +0 -1
- package/dist/index-CpufYgyn.d.mts +0 -30
- package/dist/index-CpufYgyn.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -9,13 +9,14 @@ you can plug the package into your own database engine and transport layer.
|
|
|
9
9
|
|
|
10
10
|
## Entry points
|
|
11
11
|
|
|
12
|
-
| Import
|
|
13
|
-
|
|
|
14
|
-
| `@azlib/identity`
|
|
15
|
-
| `@azlib/identity/node`
|
|
16
|
-
| `@azlib/identity/express`
|
|
17
|
-
| `@azlib/identity/nestjs`
|
|
18
|
-
| `@azlib/identity/
|
|
12
|
+
| Import | Purpose |
|
|
13
|
+
| -------------------------- | ----------------------------------------------------------------------- |
|
|
14
|
+
| `@azlib/identity` | Framework-agnostic types, config, errors, authorization, store contract, Edge-safe JWT verify and cookie presence helpers |
|
|
15
|
+
| `@azlib/identity/node` | Node runtime services (password hashing, JWT, register/login/refresh) |
|
|
16
|
+
| `@azlib/identity/express` | Express authentication and authorization middleware |
|
|
17
|
+
| `@azlib/identity/nestjs` | NestJS module, guards, and decorators |
|
|
18
|
+
| `@azlib/identity/next` | Next.js / Fetch HttpOnly cookie sessions, `getIdentity`, Route Handlers |
|
|
19
|
+
| `@azlib/identity/schema/*` | Predefined SQL and Prisma schema artifacts |
|
|
19
20
|
|
|
20
21
|
## Quick start (Express)
|
|
21
22
|
|
|
@@ -33,7 +34,10 @@ you can plug the package into your own database engine and transport layer.
|
|
|
33
34
|
```ts
|
|
34
35
|
import express from "express";
|
|
35
36
|
import { createIdentityService } from "@azlib/identity/node";
|
|
36
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
createIdentityRouter,
|
|
39
|
+
identityErrorHandler,
|
|
40
|
+
} from "@azlib/identity/express";
|
|
37
41
|
|
|
38
42
|
const service = createIdentityService(
|
|
39
43
|
{ accessTokenSecret: process.env.ACCESS_TOKEN_SECRET! },
|
|
@@ -48,15 +52,15 @@ you can plug the package into your own database engine and transport layer.
|
|
|
48
52
|
|
|
49
53
|
That's it. The following endpoints are ready immediately:
|
|
50
54
|
|
|
51
|
-
| Method | Path
|
|
52
|
-
|
|
53
|
-
| `POST` | `/auth/register`
|
|
54
|
-
| `POST` | `/auth/login`
|
|
55
|
-
| `POST` | `/auth/refresh`
|
|
56
|
-
| `POST` | `/auth/logout`
|
|
57
|
-
| `GET` | `/auth/me`
|
|
58
|
-
| `GET` | `/auth/:provider`
|
|
59
|
-
| `GET` | `/auth/:provider/callback` | Handle an OAuth callback
|
|
55
|
+
| Method | Path | Description |
|
|
56
|
+
| ------ | -------------------------- | ------------------------------------------ |
|
|
57
|
+
| `POST` | `/auth/register` | Create a new account |
|
|
58
|
+
| `POST` | `/auth/login` | Email + password login |
|
|
59
|
+
| `POST` | `/auth/refresh` | Rotate the refresh token |
|
|
60
|
+
| `POST` | `/auth/logout` | Revoke the session |
|
|
61
|
+
| `GET` | `/auth/me` | Return the authenticated principal |
|
|
62
|
+
| `GET` | `/auth/:provider` | Start an OAuth flow _(if configured)_ |
|
|
63
|
+
| `GET` | `/auth/:provider/callback` | Handle an OAuth callback _(if configured)_ |
|
|
60
64
|
|
|
61
65
|
### Cookie-mode refresh tokens
|
|
62
66
|
|
|
@@ -85,7 +89,7 @@ const service = createIdentityService(
|
|
|
85
89
|
oauth: {
|
|
86
90
|
providers: [
|
|
87
91
|
createGoogleOAuthProvider({
|
|
88
|
-
clientId:
|
|
92
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
89
93
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
90
94
|
}),
|
|
91
95
|
],
|
|
@@ -127,7 +131,10 @@ Authorization is **deny-by-default**: a principal is only allowed when an explic
|
|
|
127
131
|
through an assigned role; both resolve into the principal's `permissions`.
|
|
128
132
|
|
|
129
133
|
```ts
|
|
130
|
-
import {
|
|
134
|
+
import {
|
|
135
|
+
requirePermission,
|
|
136
|
+
requireAuthorization,
|
|
137
|
+
} from "@azlib/identity/express";
|
|
131
138
|
|
|
132
139
|
// Require a single permission.
|
|
133
140
|
app.get(
|
|
@@ -145,7 +152,8 @@ app.delete(
|
|
|
145
152
|
identity,
|
|
146
153
|
{
|
|
147
154
|
permission: "documents:delete",
|
|
148
|
-
policy: ({ principal, resource }) =>
|
|
155
|
+
policy: ({ principal, resource }) =>
|
|
156
|
+
resource.ownerId === principal.userId,
|
|
149
157
|
},
|
|
150
158
|
(req) => loadDocument(req.params.id),
|
|
151
159
|
),
|
|
@@ -156,7 +164,10 @@ app.delete(
|
|
|
156
164
|
Grant roles and permissions through the store contract:
|
|
157
165
|
|
|
158
166
|
```ts
|
|
159
|
-
await store.grantRolePermission({
|
|
167
|
+
await store.grantRolePermission({
|
|
168
|
+
roleId: "editor",
|
|
169
|
+
permission: "documents:write",
|
|
170
|
+
});
|
|
160
171
|
await store.assignRole({ userId, roleId: "editor" });
|
|
161
172
|
await store.grantDirectPermission({ userId, permission: "reports:read" });
|
|
162
173
|
```
|
|
@@ -194,7 +205,7 @@ IdentityModule.forRoot({
|
|
|
194
205
|
prefix: "v1/auth",
|
|
195
206
|
cookie: { name: "rt", secure: true },
|
|
196
207
|
},
|
|
197
|
-
})
|
|
208
|
+
});
|
|
198
209
|
```
|
|
199
210
|
|
|
200
211
|
### Guards and decorators
|
|
@@ -221,6 +232,49 @@ A runnable NestJS app lives in [`examples/nest-basic`](./examples/nest-basic). T
|
|
|
221
232
|
`IdentityService` core powers both the Express and NestJS adapters, so behavior is
|
|
222
233
|
identical across frameworks.
|
|
223
234
|
|
|
235
|
+
## Next.js (App Router)
|
|
236
|
+
|
|
237
|
+
Cookie-based sessions for Server Components, Server Actions, and Route Handlers.
|
|
238
|
+
Both the access JWT and the rotating refresh token are stored as `HttpOnly` cookies
|
|
239
|
+
(`azlib_at` / `azlib_rt` by default). Roles and tenant data stay out of the JWT —
|
|
240
|
+
hydrate those in your app after `getIdentity()`.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { cookies } from "next/headers";
|
|
244
|
+
import { createIdentityService } from "@azlib/identity/node";
|
|
245
|
+
import {
|
|
246
|
+
applyAuthCookies,
|
|
247
|
+
clearAuthCookies,
|
|
248
|
+
getIdentity,
|
|
249
|
+
hasIdentityCookies,
|
|
250
|
+
} from "@azlib/identity/next";
|
|
251
|
+
|
|
252
|
+
const identity = createIdentityService(config, store);
|
|
253
|
+
|
|
254
|
+
// Server Action — login
|
|
255
|
+
const result = await identity.login({ email, password });
|
|
256
|
+
if (!("kind" in result)) {
|
|
257
|
+
applyAuthCookies(await cookies(), result.tokens);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Server Component / Server Action — current principal
|
|
261
|
+
const principal = await getIdentity(identity, await cookies());
|
|
262
|
+
|
|
263
|
+
// proxy.ts (Edge) — presence only, no database
|
|
264
|
+
if (pathname.startsWith("/admin") && !hasIdentityCookies(request.headers)) {
|
|
265
|
+
return NextResponse.redirect(loginUrl);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Optional Route Handlers
|
|
269
|
+
import { createIdentityHandlers } from "@azlib/identity/next";
|
|
270
|
+
export const { login: POST } = createIdentityHandlers(identity);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`getIdentity` authenticates a valid access cookie, or silently refreshes when the
|
|
274
|
+
access token is expired. Refresh rotates the refresh token, so call it where cookies
|
|
275
|
+
can be written (Server Actions and Route Handlers). `verifyAccessToken` from
|
|
276
|
+
`@azlib/identity` is Edge-safe if you need to inspect JWT claims without Node.
|
|
277
|
+
|
|
224
278
|
## Account lockout
|
|
225
279
|
|
|
226
280
|
Protect against brute-force credential attacks by automatically locking accounts after too many consecutive failed logins.
|
|
@@ -230,8 +284,8 @@ createIdentityService(
|
|
|
230
284
|
{
|
|
231
285
|
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
|
|
232
286
|
lockout: {
|
|
233
|
-
maxFailedAttempts: 5,
|
|
234
|
-
durationSeconds:
|
|
287
|
+
maxFailedAttempts: 5, // 0 = disabled (default: 10)
|
|
288
|
+
durationSeconds: 900, // 0 = permanent lock (default: 900 = 15 min)
|
|
235
289
|
},
|
|
236
290
|
},
|
|
237
291
|
store,
|
|
@@ -268,13 +322,13 @@ const identity = createIdentityService(
|
|
|
268
322
|
oauth: {
|
|
269
323
|
providers: [
|
|
270
324
|
createGoogleOAuthProvider({
|
|
271
|
-
clientId:
|
|
325
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
272
326
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
273
327
|
}),
|
|
274
328
|
createMicrosoftOAuthProvider({
|
|
275
|
-
clientId:
|
|
329
|
+
clientId: process.env.MS_CLIENT_ID!,
|
|
276
330
|
clientSecret: process.env.MS_CLIENT_SECRET!,
|
|
277
|
-
tenantId:
|
|
331
|
+
tenantId: "common", // or a specific tenant UUID
|
|
278
332
|
}),
|
|
279
333
|
],
|
|
280
334
|
},
|
|
@@ -283,15 +337,18 @@ const identity = createIdentityService(
|
|
|
283
337
|
);
|
|
284
338
|
|
|
285
339
|
// identity.oauth is an OAuthService when providers are configured.
|
|
286
|
-
const { url, state } = identity.oauth!.buildAuthorizationUrl(
|
|
340
|
+
const { url, state } = identity.oauth!.buildAuthorizationUrl(
|
|
341
|
+
"google",
|
|
342
|
+
REDIRECT_URI,
|
|
343
|
+
);
|
|
287
344
|
// …redirect the user to `url`, persist `state` in a signed cookie/session
|
|
288
345
|
|
|
289
346
|
// In the callback handler:
|
|
290
347
|
const result = await identity.oauth!.handleCallback("google", {
|
|
291
|
-
code:
|
|
292
|
-
state:
|
|
348
|
+
code: req.query.code as string,
|
|
349
|
+
state: req.query.state as string,
|
|
293
350
|
expectedState: req.session.oauthState,
|
|
294
|
-
redirectUri:
|
|
351
|
+
redirectUri: REDIRECT_URI,
|
|
295
352
|
});
|
|
296
353
|
// result.tokens.accessToken is now ready
|
|
297
354
|
```
|
|
@@ -303,13 +360,18 @@ import { oauthAuthorize, oauthCallback } from "@azlib/identity/express";
|
|
|
303
360
|
|
|
304
361
|
const options = {
|
|
305
362
|
redirectUri: `${BASE_URL}/auth/google/callback`,
|
|
306
|
-
getState:
|
|
307
|
-
setState:
|
|
308
|
-
|
|
363
|
+
getState: (req) => req.session?.oauthState ?? null,
|
|
364
|
+
setState: (req, _res, state) => {
|
|
365
|
+
req.session!.oauthState = state;
|
|
366
|
+
},
|
|
367
|
+
onSuccess: (_req, res, result) => res.json(result),
|
|
309
368
|
};
|
|
310
369
|
|
|
311
|
-
app.get("/auth/google",
|
|
312
|
-
app.get(
|
|
370
|
+
app.get("/auth/google", oauthAuthorize(identity.oauth!, "google", options));
|
|
371
|
+
app.get(
|
|
372
|
+
"/auth/google/callback",
|
|
373
|
+
oauthCallback(identity.oauth!, "google", options),
|
|
374
|
+
);
|
|
313
375
|
```
|
|
314
376
|
|
|
315
377
|
### Callback flow
|
|
@@ -325,8 +387,6 @@ Your `IdentityStore` should implement the optional `findUserByOAuthId`, `createO
|
|
|
325
387
|
`listOAuthLinks` methods for full OAuth support. The shipped schema artifacts include the
|
|
326
388
|
`identity_oauth_linked_accounts` table.
|
|
327
389
|
|
|
328
|
-
|
|
329
|
-
|
|
330
390
|
The package never talks to a database directly — you provide an `IdentityStore`. Two
|
|
331
391
|
adoption paths:
|
|
332
392
|
|
|
@@ -381,16 +441,18 @@ createIdentityService(
|
|
|
381
441
|
|
|
382
442
|
### Core Exports
|
|
383
443
|
|
|
384
|
-
| Entry Point / Import
|
|
385
|
-
|
|
|
386
|
-
| `@azlib/identity`
|
|
387
|
-
| `@azlib/identity`
|
|
388
|
-
| `@azlib/identity/node`
|
|
389
|
-
| `@azlib/identity/node`
|
|
390
|
-
| `@azlib/identity/express` | `createIdentityRouter`
|
|
391
|
-
| `@azlib/identity/express` | `requireAuth`
|
|
392
|
-
| `@azlib/identity/express` | `requirePermission`
|
|
393
|
-
| `@azlib/identity/
|
|
444
|
+
| Entry Point / Import | Export | Type | Description |
|
|
445
|
+
| ------------------------- | --------------------------- | ------------- | ------------------------------------------------------------------ |
|
|
446
|
+
| `@azlib/identity` | `evaluateAuthorization` | Function | Evaluates custom permission requirements against a user principal. |
|
|
447
|
+
| `@azlib/identity` | `identitySchemaModel` | Object | Relation structures and constraints definitions. |
|
|
448
|
+
| `@azlib/identity/node` | `createIdentityService` | Function | Instantiates the core user authentication / registration service. |
|
|
449
|
+
| `@azlib/identity/node` | `createGoogleOAuthProvider` | Function | Factory for Google OAuth provider link configurations. |
|
|
450
|
+
| `@azlib/identity/express` | `createIdentityRouter` | Function | Mountable Express router containing all login/register endpoints. |
|
|
451
|
+
| `@azlib/identity/express` | `requireAuth` | Middleware | Express middleware checking request Bearer token authorization. |
|
|
452
|
+
| `@azlib/identity/express` | `requirePermission` | Middleware | Express middleware restricting actions to specific permissions. |
|
|
453
|
+
| `@azlib/identity/next` | `getIdentity` | Function | Resolve the principal from access/refresh cookies, with silent refresh. |
|
|
454
|
+
| `@azlib/identity/next` | `hasIdentityCookies` | Function | Edge-safe check that identity cookies are present. |
|
|
455
|
+
| `@azlib/identity/nestjs` | `IdentityModule` | NestJS Module | Core NestJS DI registration boundary. |
|
|
394
456
|
|
|
395
457
|
### Core Types
|
|
396
458
|
|
|
@@ -408,23 +470,26 @@ createIdentityService(
|
|
|
408
470
|
import { createIdentityService } from "@azlib/identity/node";
|
|
409
471
|
import { createIdentityRouter } from "@azlib/identity/express";
|
|
410
472
|
|
|
411
|
-
const service = createIdentityService(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
},
|
|
473
|
+
const service = createIdentityService(
|
|
474
|
+
{
|
|
475
|
+
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
|
|
476
|
+
lockout: { maxFailedAttempts: 5, durationSeconds: 900 },
|
|
477
|
+
},
|
|
478
|
+
myDatabaseStore,
|
|
479
|
+
);
|
|
415
480
|
|
|
416
481
|
app.use("/auth", createIdentityRouter(service));
|
|
417
482
|
```
|
|
418
483
|
|
|
419
484
|
### Behavioral Gotchas
|
|
485
|
+
|
|
420
486
|
- **Deny-by-Default**: Principals have no permissions by default. Access must be explicitly granted via roles or direct grants.
|
|
421
487
|
- **Scrypt Password Hashing**: Hashing is computational and synchronous. Avoid calling login in high-throughput loops without caching/throttling.
|
|
422
488
|
- **Refresh Token Rotation**: Refresh tokens are single-use. Re-using an old token immediately revokes all sessions associated with that user.
|
|
423
489
|
|
|
424
490
|
## Status
|
|
425
491
|
|
|
426
|
-
This package is under active implementation.
|
|
427
|
-
[`specs/009-identity-package`](../../specs/009-identity-package) for the plan and tasks.
|
|
492
|
+
This package is under active implementation.
|
|
428
493
|
|
|
429
494
|
## License
|
|
430
495
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
//#region core/cookie-header.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cookie names and Cookie-header parsing for identity sessions.
|
|
4
|
+
*
|
|
5
|
+
* Edge-safe: no Node APIs. Use {@link hasIdentityCookies} in Next.js `proxy.ts` /
|
|
6
|
+
* middleware to gate routes without hitting the database.
|
|
7
|
+
*/
|
|
8
|
+
/** Default HttpOnly cookie that holds the short-lived access JWT. */
|
|
9
|
+
declare const DEFAULT_ACCESS_COOKIE = "azlib_at";
|
|
10
|
+
/** Default HttpOnly cookie that holds the rotating refresh token. */
|
|
11
|
+
declare const DEFAULT_REFRESH_COOKIE = "azlib_rt";
|
|
12
|
+
/** Cookie attribute set shared by access and refresh cookies. */
|
|
13
|
+
interface IdentityCookieAttributes {
|
|
14
|
+
/** Mark cookies as `HttpOnly`. Default: `true`. */
|
|
15
|
+
httpOnly?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Mark cookies as `Secure`. Defaults to `true` when `NODE_ENV` is `"production"`,
|
|
18
|
+
* `false` otherwise.
|
|
19
|
+
*/
|
|
20
|
+
secure?: boolean;
|
|
21
|
+
/** `SameSite` policy. Default: `"lax"`. */
|
|
22
|
+
sameSite?: "strict" | "lax" | "none";
|
|
23
|
+
/** Cookie path. Default: `"/"`. */
|
|
24
|
+
path?: string;
|
|
25
|
+
/** Cookie domain. Omit to use the current host. */
|
|
26
|
+
domain?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Names and attributes for the access + refresh cookie pair. */
|
|
29
|
+
interface IdentityCookieOptions extends IdentityCookieAttributes {
|
|
30
|
+
/** Access-token cookie name. Default: {@link DEFAULT_ACCESS_COOKIE}. */
|
|
31
|
+
accessCookie?: string;
|
|
32
|
+
/** Refresh-token cookie name. Default: {@link DEFAULT_REFRESH_COOKIE}. */
|
|
33
|
+
refreshCookie?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Resolved cookie names. */
|
|
36
|
+
interface IdentityCookieNames {
|
|
37
|
+
access: string;
|
|
38
|
+
refresh: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A Next.js `cookies().get` / Request-cookies compatible reader.
|
|
42
|
+
*/
|
|
43
|
+
interface CookieReader {
|
|
44
|
+
get(name: string): {
|
|
45
|
+
value: string;
|
|
46
|
+
} | undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A Next.js `cookies().set` / `cookies().delete` compatible writer.
|
|
50
|
+
* `maxAge` is in **seconds** (Web / Next.js convention, not Express milliseconds).
|
|
51
|
+
*/
|
|
52
|
+
interface CookieWriter {
|
|
53
|
+
set(name: string, value: string, options?: {
|
|
54
|
+
httpOnly?: boolean;
|
|
55
|
+
secure?: boolean;
|
|
56
|
+
sameSite?: "strict" | "lax" | "none";
|
|
57
|
+
path?: string;
|
|
58
|
+
domain?: string;
|
|
59
|
+
maxAge?: number;
|
|
60
|
+
expires?: Date;
|
|
61
|
+
}): unknown;
|
|
62
|
+
delete(name: string): unknown;
|
|
63
|
+
}
|
|
64
|
+
type CookieStore = CookieReader & CookieWriter;
|
|
65
|
+
/** Anything {@link hasIdentityCookies} / {@link readCookieValue} can inspect. */
|
|
66
|
+
type CookieSource = CookieReader | Headers | string | null | undefined;
|
|
67
|
+
declare function resolveCookieNames(options?: IdentityCookieOptions): IdentityCookieNames;
|
|
68
|
+
declare function resolveCookieAttributes(options?: IdentityCookieOptions): Required<Pick<IdentityCookieAttributes, "httpOnly" | "sameSite" | "path">> & IdentityCookieAttributes;
|
|
69
|
+
/**
|
|
70
|
+
* Parses a raw `Cookie` header into a name → value map.
|
|
71
|
+
* Values are `decodeURIComponent`'d when possible.
|
|
72
|
+
*/
|
|
73
|
+
declare function parseCookieHeader(header: string | null | undefined): Map<string, string>;
|
|
74
|
+
/** Reads a single cookie value from a header string, `Headers`, or cookie reader. */
|
|
75
|
+
declare function readCookieValue(source: CookieSource, name: string): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Returns true when the request carries an identity access and/or refresh cookie.
|
|
78
|
+
* Intended for Edge/proxy gates: presence only — do not treat this as authentication.
|
|
79
|
+
*/
|
|
80
|
+
declare function hasIdentityCookies(source: CookieSource, options?: IdentityCookieOptions): boolean;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { DEFAULT_ACCESS_COOKIE as a, IdentityCookieNames as c, parseCookieHeader as d, readCookieValue as f, CookieWriter as i, IdentityCookieOptions as l, resolveCookieNames as m, CookieSource as n, DEFAULT_REFRESH_COOKIE as o, resolveCookieAttributes as p, CookieStore as r, IdentityCookieAttributes as s, CookieReader as t, hasIdentityCookies as u };
|
|
83
|
+
//# sourceMappingURL=cookie-header-DdTShpuc.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie-header-DdTShpuc.d.cts","names":[],"sources":["../core/cookie-header.ts"],"mappings":";;;;;;;;cAQa;;cAGA;;UAGI;;EAEf;;;;;EAKA;;EAEA;;EAEA;;EAEA;;;UAIe,8BAA8B;;EAE7C;;EAEA;;;UAIe;EACf;EACA;;;;;UAMe;EACf,IAAI;IAAiB;;;;;;;UAON;EACf,IACE,cACA,eACA;IACE;IACA;IACA;IACA;IACA;IACA;IACA,UAAU;;EAGd,OAAO;;KAGG,cAAc,eAAe;;KAG7B,eACR,eACA;iBAKY,mBACd,UAAS,wBACR;iBAOa,wBACd,UAAS,wBACR,SAAS,KAAK,+DACf;;;;;iBAcc,kBACd,oCACC;;iBA8Ba,gBACd,QAAQ,cACR;;;;;iBAqBc,mBACd,QAAQ,cACR,UAAS"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
//#region core/cookie-header.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cookie names and Cookie-header parsing for identity sessions.
|
|
4
|
+
*
|
|
5
|
+
* Edge-safe: no Node APIs. Use {@link hasIdentityCookies} in Next.js `proxy.ts` /
|
|
6
|
+
* middleware to gate routes without hitting the database.
|
|
7
|
+
*/
|
|
8
|
+
/** Default HttpOnly cookie that holds the short-lived access JWT. */
|
|
9
|
+
declare const DEFAULT_ACCESS_COOKIE = "azlib_at";
|
|
10
|
+
/** Default HttpOnly cookie that holds the rotating refresh token. */
|
|
11
|
+
declare const DEFAULT_REFRESH_COOKIE = "azlib_rt";
|
|
12
|
+
/** Cookie attribute set shared by access and refresh cookies. */
|
|
13
|
+
interface IdentityCookieAttributes {
|
|
14
|
+
/** Mark cookies as `HttpOnly`. Default: `true`. */
|
|
15
|
+
httpOnly?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Mark cookies as `Secure`. Defaults to `true` when `NODE_ENV` is `"production"`,
|
|
18
|
+
* `false` otherwise.
|
|
19
|
+
*/
|
|
20
|
+
secure?: boolean;
|
|
21
|
+
/** `SameSite` policy. Default: `"lax"`. */
|
|
22
|
+
sameSite?: "strict" | "lax" | "none";
|
|
23
|
+
/** Cookie path. Default: `"/"`. */
|
|
24
|
+
path?: string;
|
|
25
|
+
/** Cookie domain. Omit to use the current host. */
|
|
26
|
+
domain?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Names and attributes for the access + refresh cookie pair. */
|
|
29
|
+
interface IdentityCookieOptions extends IdentityCookieAttributes {
|
|
30
|
+
/** Access-token cookie name. Default: {@link DEFAULT_ACCESS_COOKIE}. */
|
|
31
|
+
accessCookie?: string;
|
|
32
|
+
/** Refresh-token cookie name. Default: {@link DEFAULT_REFRESH_COOKIE}. */
|
|
33
|
+
refreshCookie?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Resolved cookie names. */
|
|
36
|
+
interface IdentityCookieNames {
|
|
37
|
+
access: string;
|
|
38
|
+
refresh: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A Next.js `cookies().get` / Request-cookies compatible reader.
|
|
42
|
+
*/
|
|
43
|
+
interface CookieReader {
|
|
44
|
+
get(name: string): {
|
|
45
|
+
value: string;
|
|
46
|
+
} | undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A Next.js `cookies().set` / `cookies().delete` compatible writer.
|
|
50
|
+
* `maxAge` is in **seconds** (Web / Next.js convention, not Express milliseconds).
|
|
51
|
+
*/
|
|
52
|
+
interface CookieWriter {
|
|
53
|
+
set(name: string, value: string, options?: {
|
|
54
|
+
httpOnly?: boolean;
|
|
55
|
+
secure?: boolean;
|
|
56
|
+
sameSite?: "strict" | "lax" | "none";
|
|
57
|
+
path?: string;
|
|
58
|
+
domain?: string;
|
|
59
|
+
maxAge?: number;
|
|
60
|
+
expires?: Date;
|
|
61
|
+
}): unknown;
|
|
62
|
+
delete(name: string): unknown;
|
|
63
|
+
}
|
|
64
|
+
type CookieStore = CookieReader & CookieWriter;
|
|
65
|
+
/** Anything {@link hasIdentityCookies} / {@link readCookieValue} can inspect. */
|
|
66
|
+
type CookieSource = CookieReader | Headers | string | null | undefined;
|
|
67
|
+
declare function resolveCookieNames(options?: IdentityCookieOptions): IdentityCookieNames;
|
|
68
|
+
declare function resolveCookieAttributes(options?: IdentityCookieOptions): Required<Pick<IdentityCookieAttributes, "httpOnly" | "sameSite" | "path">> & IdentityCookieAttributes;
|
|
69
|
+
/**
|
|
70
|
+
* Parses a raw `Cookie` header into a name → value map.
|
|
71
|
+
* Values are `decodeURIComponent`'d when possible.
|
|
72
|
+
*/
|
|
73
|
+
declare function parseCookieHeader(header: string | null | undefined): Map<string, string>;
|
|
74
|
+
/** Reads a single cookie value from a header string, `Headers`, or cookie reader. */
|
|
75
|
+
declare function readCookieValue(source: CookieSource, name: string): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Returns true when the request carries an identity access and/or refresh cookie.
|
|
78
|
+
* Intended for Edge/proxy gates: presence only — do not treat this as authentication.
|
|
79
|
+
*/
|
|
80
|
+
declare function hasIdentityCookies(source: CookieSource, options?: IdentityCookieOptions): boolean;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { DEFAULT_ACCESS_COOKIE as a, IdentityCookieNames as c, parseCookieHeader as d, readCookieValue as f, CookieWriter as i, IdentityCookieOptions as l, resolveCookieNames as m, CookieSource as n, DEFAULT_REFRESH_COOKIE as o, resolveCookieAttributes as p, CookieStore as r, IdentityCookieAttributes as s, CookieReader as t, hasIdentityCookies as u };
|
|
83
|
+
//# sourceMappingURL=cookie-header-DdTShpuc.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie-header-DdTShpuc.d.mts","names":[],"sources":["../core/cookie-header.ts"],"mappings":";;;;;;;;cAQa;;cAGA;;UAGI;;EAEf;;;;;EAKA;;EAEA;;EAEA;;EAEA;;;UAIe,8BAA8B;;EAE7C;;EAEA;;;UAIe;EACf;EACA;;;;;UAMe;EACf,IAAI;IAAiB;;;;;;;UAON;EACf,IACE,cACA,eACA;IACE;IACA;IACA;IACA;IACA;IACA;IACA,UAAU;;EAGd,OAAO;;KAGG,cAAc,eAAe;;KAG7B,eACR,eACA;iBAKY,mBACd,UAAS,wBACR;iBAOa,wBACd,UAAS,wBACR,SAAS,KAAK,+DACf;;;;;iBAcc,kBACd,oCACC;;iBA8Ba,gBACd,QAAQ,cACR;;;;;iBAqBc,mBACd,QAAQ,cACR,UAAS"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region core/cookie-header.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cookie names and Cookie-header parsing for identity sessions.
|
|
4
|
+
*
|
|
5
|
+
* Edge-safe: no Node APIs. Use {@link hasIdentityCookies} in Next.js `proxy.ts` /
|
|
6
|
+
* middleware to gate routes without hitting the database.
|
|
7
|
+
*/
|
|
8
|
+
/** Default HttpOnly cookie that holds the short-lived access JWT. */
|
|
9
|
+
const DEFAULT_ACCESS_COOKIE = "azlib_at";
|
|
10
|
+
/** Default HttpOnly cookie that holds the rotating refresh token. */
|
|
11
|
+
const DEFAULT_REFRESH_COOKIE = "azlib_rt";
|
|
12
|
+
function resolveCookieNames(options = {}) {
|
|
13
|
+
return {
|
|
14
|
+
access: options.accessCookie ?? "azlib_at",
|
|
15
|
+
refresh: options.refreshCookie ?? "azlib_rt"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function resolveCookieAttributes(options = {}) {
|
|
19
|
+
return {
|
|
20
|
+
httpOnly: options.httpOnly ?? true,
|
|
21
|
+
secure: options.secure ?? process.env.NODE_ENV === "production",
|
|
22
|
+
sameSite: options.sameSite ?? "lax",
|
|
23
|
+
path: options.path ?? "/",
|
|
24
|
+
domain: options.domain
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parses a raw `Cookie` header into a name → value map.
|
|
29
|
+
* Values are `decodeURIComponent`'d when possible.
|
|
30
|
+
*/
|
|
31
|
+
function parseCookieHeader(header) {
|
|
32
|
+
const result = /* @__PURE__ */ new Map();
|
|
33
|
+
if (!header) return result;
|
|
34
|
+
for (const part of header.split(";")) {
|
|
35
|
+
const eq = part.indexOf("=");
|
|
36
|
+
if (eq === -1) continue;
|
|
37
|
+
const key = part.slice(0, eq).trim();
|
|
38
|
+
if (!key) continue;
|
|
39
|
+
const raw = part.slice(eq + 1).trim();
|
|
40
|
+
if (result.has(key)) continue;
|
|
41
|
+
try {
|
|
42
|
+
result.set(key, decodeURIComponent(raw));
|
|
43
|
+
} catch {
|
|
44
|
+
result.set(key, raw);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function isCookieReader(source) {
|
|
50
|
+
return typeof source === "object" && source !== null && "get" in source && typeof source.get === "function" && !(source instanceof Headers);
|
|
51
|
+
}
|
|
52
|
+
/** Reads a single cookie value from a header string, `Headers`, or cookie reader. */
|
|
53
|
+
function readCookieValue(source, name) {
|
|
54
|
+
if (source == null) return void 0;
|
|
55
|
+
if (typeof source === "string") {
|
|
56
|
+
const value = parseCookieHeader(source).get(name);
|
|
57
|
+
return value && value.length > 0 ? value : void 0;
|
|
58
|
+
}
|
|
59
|
+
if (source instanceof Headers) return readCookieValue(source.get("cookie"), name);
|
|
60
|
+
if (isCookieReader(source)) {
|
|
61
|
+
const value = source.get(name)?.value?.trim();
|
|
62
|
+
return value && value.length > 0 ? value : void 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns true when the request carries an identity access and/or refresh cookie.
|
|
67
|
+
* Intended for Edge/proxy gates: presence only — do not treat this as authentication.
|
|
68
|
+
*/
|
|
69
|
+
function hasIdentityCookies(source, options = {}) {
|
|
70
|
+
const names = resolveCookieNames(options);
|
|
71
|
+
return readCookieValue(source, names.access) !== void 0 || readCookieValue(source, names.refresh) !== void 0;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { readCookieValue as a, parseCookieHeader as i, DEFAULT_REFRESH_COOKIE as n, resolveCookieAttributes as o, hasIdentityCookies as r, resolveCookieNames as s, DEFAULT_ACCESS_COOKIE as t };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=cookie-header-DptIxRmW.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie-header-DptIxRmW.mjs","names":[],"sources":["../core/cookie-header.ts"],"sourcesContent":["/**\n * Cookie names and Cookie-header parsing for identity sessions.\n *\n * Edge-safe: no Node APIs. Use {@link hasIdentityCookies} in Next.js `proxy.ts` /\n * middleware to gate routes without hitting the database.\n */\n\n/** Default HttpOnly cookie that holds the short-lived access JWT. */\nexport const DEFAULT_ACCESS_COOKIE = \"azlib_at\";\n\n/** Default HttpOnly cookie that holds the rotating refresh token. */\nexport const DEFAULT_REFRESH_COOKIE = \"azlib_rt\";\n\n/** Cookie attribute set shared by access and refresh cookies. */\nexport interface IdentityCookieAttributes {\n /** Mark cookies as `HttpOnly`. Default: `true`. */\n httpOnly?: boolean;\n /**\n * Mark cookies as `Secure`. Defaults to `true` when `NODE_ENV` is `\"production\"`,\n * `false` otherwise.\n */\n secure?: boolean;\n /** `SameSite` policy. Default: `\"lax\"`. */\n sameSite?: \"strict\" | \"lax\" | \"none\";\n /** Cookie path. Default: `\"/\"`. */\n path?: string;\n /** Cookie domain. Omit to use the current host. */\n domain?: string;\n}\n\n/** Names and attributes for the access + refresh cookie pair. */\nexport interface IdentityCookieOptions extends IdentityCookieAttributes {\n /** Access-token cookie name. Default: {@link DEFAULT_ACCESS_COOKIE}. */\n accessCookie?: string;\n /** Refresh-token cookie name. Default: {@link DEFAULT_REFRESH_COOKIE}. */\n refreshCookie?: string;\n}\n\n/** Resolved cookie names. */\nexport interface IdentityCookieNames {\n access: string;\n refresh: string;\n}\n\n/**\n * A Next.js `cookies().get` / Request-cookies compatible reader.\n */\nexport interface CookieReader {\n get(name: string): { value: string } | undefined;\n}\n\n/**\n * A Next.js `cookies().set` / `cookies().delete` compatible writer.\n * `maxAge` is in **seconds** (Web / Next.js convention, not Express milliseconds).\n */\nexport interface CookieWriter {\n set(\n name: string,\n value: string,\n options?: {\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: \"strict\" | \"lax\" | \"none\";\n path?: string;\n domain?: string;\n maxAge?: number;\n expires?: Date;\n },\n ): unknown;\n delete(name: string): unknown;\n}\n\nexport type CookieStore = CookieReader & CookieWriter;\n\n/** Anything {@link hasIdentityCookies} / {@link readCookieValue} can inspect. */\nexport type CookieSource =\n | CookieReader\n | Headers\n | string\n | null\n | undefined;\n\nexport function resolveCookieNames(\n options: IdentityCookieOptions = {},\n): IdentityCookieNames {\n return {\n access: options.accessCookie ?? DEFAULT_ACCESS_COOKIE,\n refresh: options.refreshCookie ?? DEFAULT_REFRESH_COOKIE,\n };\n}\n\nexport function resolveCookieAttributes(\n options: IdentityCookieOptions = {},\n): Required<Pick<IdentityCookieAttributes, \"httpOnly\" | \"sameSite\" | \"path\">> &\n IdentityCookieAttributes {\n return {\n httpOnly: options.httpOnly ?? true,\n secure: options.secure ?? process.env.NODE_ENV === \"production\",\n sameSite: options.sameSite ?? \"lax\",\n path: options.path ?? \"/\",\n domain: options.domain,\n };\n}\n\n/**\n * Parses a raw `Cookie` header into a name → value map.\n * Values are `decodeURIComponent`'d when possible.\n */\nexport function parseCookieHeader(\n header: string | null | undefined,\n): Map<string, string> {\n const result = new Map<string, string>();\n if (!header) return result;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim();\n if (!key) continue;\n const raw = part.slice(eq + 1).trim();\n if (result.has(key)) continue;\n try {\n result.set(key, decodeURIComponent(raw));\n } catch {\n result.set(key, raw);\n }\n }\n return result;\n}\n\nfunction isCookieReader(source: CookieSource): source is CookieReader {\n return (\n typeof source === \"object\" &&\n source !== null &&\n \"get\" in source &&\n typeof source.get === \"function\" &&\n !(source instanceof Headers)\n );\n}\n\n/** Reads a single cookie value from a header string, `Headers`, or cookie reader. */\nexport function readCookieValue(\n source: CookieSource,\n name: string,\n): string | undefined {\n if (source == null) return undefined;\n if (typeof source === \"string\") {\n const value = parseCookieHeader(source).get(name);\n return value && value.length > 0 ? value : undefined;\n }\n if (source instanceof Headers) {\n return readCookieValue(source.get(\"cookie\"), name);\n }\n if (isCookieReader(source)) {\n const value = source.get(name)?.value?.trim();\n return value && value.length > 0 ? value : undefined;\n }\n return undefined;\n}\n\n/**\n * Returns true when the request carries an identity access and/or refresh cookie.\n * Intended for Edge/proxy gates: presence only — do not treat this as authentication.\n */\nexport function hasIdentityCookies(\n source: CookieSource,\n options: IdentityCookieOptions = {},\n): boolean {\n const names = resolveCookieNames(options);\n return (\n readCookieValue(source, names.access) !== undefined ||\n readCookieValue(source, names.refresh) !== undefined\n );\n}\n"],"mappings":";;;;;;;;AAQA,MAAa,wBAAwB;;AAGrC,MAAa,yBAAyB;AAuEtC,SAAgB,mBACd,UAAiC,CAAC,GACb;CACrB,OAAO;EACL,QAAQ,QAAQ,gBAAA;EAChB,SAAS,QAAQ,iBAAA;CACnB;AACF;AAEA,SAAgB,wBACd,UAAiC,CAAC,GAET;CACzB,OAAO;EACL,UAAU,QAAQ,YAAY;EAC9B,QAAQ,QAAQ,UAAU,QAAQ,IAAI,aAAa;EACnD,UAAU,QAAQ,YAAY;EAC9B,MAAM,QAAQ,QAAQ;EACtB,QAAQ,QAAQ;CAClB;AACF;;;;;AAMA,SAAgB,kBACd,QACqB;CACrB,MAAM,yBAAS,IAAI,IAAoB;CACvC,IAAI,CAAC,QAAQ,OAAO;CACpB,KAAK,MAAM,QAAQ,OAAO,MAAM,GAAG,GAAG;EACpC,MAAM,KAAK,KAAK,QAAQ,GAAG;EAC3B,IAAI,OAAO,IAAI;EACf,MAAM,MAAM,KAAK,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK;EACnC,IAAI,CAAC,KAAK;EACV,MAAM,MAAM,KAAK,MAAM,KAAK,CAAC,CAAC,CAAC,KAAK;EACpC,IAAI,OAAO,IAAI,GAAG,GAAG;EACrB,IAAI;GACF,OAAO,IAAI,KAAK,mBAAmB,GAAG,CAAC;EACzC,QAAQ;GACN,OAAO,IAAI,KAAK,GAAG;EACrB;CACF;CACA,OAAO;AACT;AAEA,SAAS,eAAe,QAA8C;CACpE,OACE,OAAO,WAAW,YAClB,WAAW,QACX,SAAS,UACT,OAAO,OAAO,QAAQ,cACtB,EAAE,kBAAkB;AAExB;;AAGA,SAAgB,gBACd,QACA,MACoB;CACpB,IAAI,UAAU,MAAM,OAAO,KAAA;CAC3B,IAAI,OAAO,WAAW,UAAU;EAC9B,MAAM,QAAQ,kBAAkB,MAAM,CAAC,CAAC,IAAI,IAAI;EAChD,OAAO,SAAS,MAAM,SAAS,IAAI,QAAQ,KAAA;CAC7C;CACA,IAAI,kBAAkB,SACpB,OAAO,gBAAgB,OAAO,IAAI,QAAQ,GAAG,IAAI;CAEnD,IAAI,eAAe,MAAM,GAAG;EAC1B,MAAM,QAAQ,OAAO,IAAI,IAAI,CAAC,EAAE,OAAO,KAAK;EAC5C,OAAO,SAAS,MAAM,SAAS,IAAI,QAAQ,KAAA;CAC7C;AAEF;;;;;AAMA,SAAgB,mBACd,QACA,UAAiC,CAAC,GACzB;CACT,MAAM,QAAQ,mBAAmB,OAAO;CACxC,OACE,gBAAgB,QAAQ,MAAM,MAAM,MAAM,KAAA,KAC1C,gBAAgB,QAAQ,MAAM,OAAO,MAAM,KAAA;AAE/C"}
|