@estiva-app/identity 0.1.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.
- package/CHANGELOG.md +57 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/client.d.ts +124 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +398 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/shell.d.ts +195 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +166 -0
- package/dist/shell.js.map +1 -0
- package/package.json +53 -0
- package/src/client.ts +512 -0
- package/src/index.ts +81 -0
- package/src/shell.ts +280 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @estiva-app/identity
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-08-27
|
|
4
|
+
|
|
5
|
+
First release. SHA-4, extracted during REW-2.
|
|
6
|
+
|
|
7
|
+
**No wire behaviour.** This package publishes no events. It obtains and holds a
|
|
8
|
+
token; `@estiva-app/protocol` is what turns intent into bytes.
|
|
9
|
+
|
|
10
|
+
The PKCE flow, token storage and validity, silent renewal, sign-out, the returnTo
|
|
11
|
+
and shell-reason vocabulary, and the auth-shell state machine. Extracted from
|
|
12
|
+
Peek, which was ahead of Ship: Ship had no shell-reason vocabulary at all, so
|
|
13
|
+
this is an upgrade for Ship rather than a port of it.
|
|
14
|
+
|
|
15
|
+
### What is deliberately not here
|
|
16
|
+
|
|
17
|
+
- **Any app's data layer.** Peek's `useEstivaIdAuth.ts` — the
|
|
18
|
+
`ConvexProviderWithAuth` adapter — stays in Peek. If it were here, Ship and
|
|
19
|
+
every future app would inherit a Convex dependency to work around. Ship needing
|
|
20
|
+
no adapter at all is the test that the line held, and CI enforces it.
|
|
21
|
+
- **The shell's rendering.** A full-page surface each app will want its own of.
|
|
22
|
+
Shared is the state machine and the reason vocabulary, which is where the
|
|
23
|
+
expensive mistakes live — and keeping components out also keeps this package
|
|
24
|
+
clear of `@estiva-app/ui`, whose entry would trigger ADR 0002 §2's rule about
|
|
25
|
+
folding `ui` back into the foundation repo.
|
|
26
|
+
|
|
27
|
+
### Five behaviours that are each an incident, not a nicety
|
|
28
|
+
|
|
29
|
+
Every one is covered by a test that travelled with the code:
|
|
30
|
+
|
|
31
|
+
- **`clearSession` must not touch a sign-in in flight (PEEK-167).** The verifier,
|
|
32
|
+
state and returnTo belong to an authorization request currently in the air.
|
|
33
|
+
Clearing them on somebody else's schedule destroys a sign-in that is still
|
|
34
|
+
happening, and the failure surfaces as an `exchange_failed` for a code that was
|
|
35
|
+
never presented to `/token` — so the identity service has no record of it.
|
|
36
|
+
- **The silent guard is marked synchronously with those writes**, not next to the
|
|
37
|
+
navigation. `codeChallenge` awaits a digest in between, and that window is long
|
|
38
|
+
enough for a second boot to decide to probe as well.
|
|
39
|
+
- **`token_rejected` never reads as "session expired".** Telling somebody their
|
|
40
|
+
session expired when their token was refused for a configuration fault sends
|
|
41
|
+
them to re-authenticate against a wall.
|
|
42
|
+
- **Refresh tokens are single-use and a replay is treated as theft.** The stored
|
|
43
|
+
token is cleared when the grant is *refused*, and deliberately not when the
|
|
44
|
+
network is merely unreachable — a flaky connection must not sign somebody out.
|
|
45
|
+
- **Clearing the local token is not signing out (PEEK-122).** The
|
|
46
|
+
`estiva_id_session` cookie on the identity origin survives it, so the next
|
|
47
|
+
silent probe puts the person straight back in. Real sign-out navigates to
|
|
48
|
+
`/logout`.
|
|
49
|
+
|
|
50
|
+
### A prediction this package falsified
|
|
51
|
+
|
|
52
|
+
The expectation on record, from having just done `@estiva-app/protocol`, was that
|
|
53
|
+
identity could not follow ADR 0002 §4a as cleanly — that being browser-shaped
|
|
54
|
+
throughout, it would need `lib: dom` and a looser rule.
|
|
55
|
+
|
|
56
|
+
It did not need either. Details in the README; the short version is that a browser
|
|
57
|
+
API an app must inject is also a browser API a test can observe.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Estiva
|
|
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,99 @@
|
|
|
1
|
+
# @estiva-app/identity
|
|
2
|
+
|
|
3
|
+
Signing in with Estiva ID: authorization code + PKCE, token storage, silent
|
|
4
|
+
renewal, sign-out, and the auth-shell state machine.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @estiva-app/identity
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
No runtime dependencies. CI enforces that.
|
|
11
|
+
|
|
12
|
+
## The seam this package exists for
|
|
13
|
+
|
|
14
|
+
**In:** everything that decides *whether somebody is signed in* and *what to say
|
|
15
|
+
when they are not*.
|
|
16
|
+
|
|
17
|
+
**Not in: any app's data layer.** `peek-app/src/auth/useEstivaIdAuth.ts` is the
|
|
18
|
+
adapter `ConvexProviderWithAuth` reads, and it stays in Peek. If it were here,
|
|
19
|
+
Ship and every future app would inherit a Convex dependency to work around —
|
|
20
|
+
which is precisely the failure SHA-4's sequencing exists to avoid.
|
|
21
|
+
|
|
22
|
+
**Ship needing no adapter at all is the test that the line held.** It hands the
|
|
23
|
+
token straight to its relay client.
|
|
24
|
+
|
|
25
|
+
**Not in: the shell's rendering.** A full-page surface each app wants its own of.
|
|
26
|
+
Shared is the state machine and the reason vocabulary. That also keeps this
|
|
27
|
+
package clear of `@estiva-app/ui` — ADR 0002 §2 names identity depending on `ui`
|
|
28
|
+
as the trigger that folds `ui` back into the foundation repo, and that should be
|
|
29
|
+
a decision somebody makes rather than one a sign-in screen makes by accident.
|
|
30
|
+
|
|
31
|
+
## Every difference between the consumers is a parameter
|
|
32
|
+
|
|
33
|
+
The package is a factory, not a module of free functions, because Peek and Ship
|
|
34
|
+
genuinely differ on every one of these — and hardcoding any would have made the
|
|
35
|
+
package come out Peek-shaped, which is the whole risk SHA-4 names.
|
|
36
|
+
|
|
37
|
+
| injected | Peek | Ship |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `clientId` | `estiva-peek` | `estiva-ship` |
|
|
40
|
+
| `redirectUri` | a fixed `/auth/callback` | its current pathname — Ship routes on `location.hash` |
|
|
41
|
+
| `storage` | `sessionStorage`, which dies with the tab | `localStorage` |
|
|
42
|
+
| `keyPrefix` | `peek.estivaId` | `ship.estiva-id` |
|
|
43
|
+
| `navigate` | the one genuinely untestable act, so it is observable | |
|
|
44
|
+
|
|
45
|
+
**Do not unify `storage`.** NIP-RS read-state slots (CRO-4) must survive a restart
|
|
46
|
+
and belong in `localStorage`; an access token does not. Different lifetimes,
|
|
47
|
+
different homes.
|
|
48
|
+
|
|
49
|
+
An app instantiates once and re-exports its own names, so call sites do not churn:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const client = createEstivaId({
|
|
53
|
+
base: ESTIVA_ID_ORIGIN,
|
|
54
|
+
clientId: 'estiva-peek',
|
|
55
|
+
redirectUri: () => `${window.location.origin}/auth/callback`,
|
|
56
|
+
storage: () => (typeof window === 'undefined' ? null : window.sessionStorage),
|
|
57
|
+
keyPrefix: 'peek.estivaId',
|
|
58
|
+
navigate: (url) => window.location.assign(url),
|
|
59
|
+
})
|
|
60
|
+
export const { validToken, beginSignIn, completeSignIn, beginSignOut } = client
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## The prediction this package falsified
|
|
64
|
+
|
|
65
|
+
Before starting, the expectation on the ticket was that identity **could not**
|
|
66
|
+
follow ADR 0002 §4a as cleanly as `@estiva-app/protocol` did — that being
|
|
67
|
+
browser-shaped throughout, it would need `lib: dom`, and that the ADR would need
|
|
68
|
+
amending to say so.
|
|
69
|
+
|
|
70
|
+
That was wrong, and it is worth saying why rather than quietly not mentioning it:
|
|
71
|
+
|
|
72
|
+
- `crypto`, `btoa`, `TextEncoder`, `URL`, `URLSearchParams`, `fetch` and `console`
|
|
73
|
+
are declared inside the modules that use them and read inside function bodies.
|
|
74
|
+
- The DOM's `Storage` became a local `KeyValueStore` interface. `sessionStorage`,
|
|
75
|
+
`localStorage` and a plain object all satisfy it structurally, and the published
|
|
76
|
+
declarations name no ambient type.
|
|
77
|
+
- Navigation is a **required** `navigate` callback rather than a `location` reach.
|
|
78
|
+
|
|
79
|
+
So the package builds with `types: []` and no `lib: dom`, exactly as `protocol`
|
|
80
|
+
does, and a test can import it with every browser global deleted. CI asserts both.
|
|
81
|
+
|
|
82
|
+
The thing that made it possible was not cleverness. It was that **a browser API an
|
|
83
|
+
app must inject is also a browser API a test can observe** — the seams §4a forces
|
|
84
|
+
and the seams a test wants are the same seams. Worth remembering before assuming
|
|
85
|
+
the next browser-shaped package needs an exemption.
|
|
86
|
+
|
|
87
|
+
`@estiva-app/ui` is still the counter-example: it ships `.d.ts` files naming
|
|
88
|
+
`HTMLButtonElement`, and gets away with it only because both its consumers are
|
|
89
|
+
browser apps.
|
|
90
|
+
|
|
91
|
+
## Releasing
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git tag identity@0.1.1 && git push origin identity@0.1.1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The **first** publish is manual and once-ever, because a trusted publisher is
|
|
98
|
+
configured on a package that already exists — ADR 0002 §4c, §7. Every release
|
|
99
|
+
after it is a tag.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signing in with Estiva ID — authorization code + PKCE.
|
|
3
|
+
*
|
|
4
|
+
* The app does not hold a password or a key. It sends the person to the identity
|
|
5
|
+
* service and gets back a token whose `sub` is their Nostr pubkey.
|
|
6
|
+
*
|
|
7
|
+
* ## Why a redirect rather than a fetch
|
|
8
|
+
*
|
|
9
|
+
* `POST /token` is reachable cross-origin, but the *session* that authorises the
|
|
10
|
+
* redirect is a `SameSite=Lax` cookie on the identity origin. Lax cookies ride
|
|
11
|
+
* top-level navigations and nothing else, so the only way to prove who you are
|
|
12
|
+
* is to actually go there. That is deliberate on their side (PEEK-90): the origin
|
|
13
|
+
* holding every custodied key does not relax its cookie or send
|
|
14
|
+
* `Access-Control-Allow-Credentials`.
|
|
15
|
+
*
|
|
16
|
+
* **A hidden iframe does not work**, and the reason is the same property: `Lax`
|
|
17
|
+
* withholds the cookie from anything that is not a top-level navigation. The
|
|
18
|
+
* refresh-token grant exists because of this. Do not "simplify" it back.
|
|
19
|
+
*
|
|
20
|
+
* ## The bits that must match exactly
|
|
21
|
+
*
|
|
22
|
+
* `redirect_uri` is compared **exact-string** against the app's registered list —
|
|
23
|
+
* a trailing slash is a different URI and gets a 400 before anything else
|
|
24
|
+
* happens. An identity provider that prefix-matched would be an open redirector
|
|
25
|
+
* handing out authorization codes.
|
|
26
|
+
*
|
|
27
|
+
* The request body is **camelCase JSON** (`codeVerifier`, not `code_verifier`)
|
|
28
|
+
* while the response is snake_case (`access_token`). That is genuinely the
|
|
29
|
+
* contract, not a typo — see `docs/API.md` in estiva-id.
|
|
30
|
+
*
|
|
31
|
+
* ## Everything is injected, and each injection is a real difference
|
|
32
|
+
*
|
|
33
|
+
* This is a factory rather than a module of free functions because the two
|
|
34
|
+
* consumers genuinely differ, and hardcoding either would have made the package
|
|
35
|
+
* come out shaped like whichever app it was extracted from:
|
|
36
|
+
*
|
|
37
|
+
* - **`clientId`** — `estiva-peek` and `estiva-ship` are separate rows in
|
|
38
|
+
* `app_credentials` with separate `redirectUris` and separate kind ceilings.
|
|
39
|
+
* - **`redirectUri`** — Peek registers a fixed `/auth/callback`; Ship routes on
|
|
40
|
+
* `location.hash` and registers its current pathname. Neither is more correct.
|
|
41
|
+
* - **`storage`** — Peek keeps its token in `sessionStorage`, which dies with the
|
|
42
|
+
* tab and keeps a bearer token off a shared machine's disk. Ship uses
|
|
43
|
+
* `localStorage`. **Do not unify this**: NIP-RS slots (CRO-4) must survive a
|
|
44
|
+
* restart and belong in `localStorage`, while an access token does not.
|
|
45
|
+
* Different lifetimes, different homes.
|
|
46
|
+
* - **`keyPrefix`** — so two apps on one origin cannot read each other's keys.
|
|
47
|
+
* - **`navigate`** — the one genuinely untestable act. Injected so a test can
|
|
48
|
+
* observe where the flow *would* go.
|
|
49
|
+
*/
|
|
50
|
+
import { type KeyValueStore, type ShellReason } from './shell.js';
|
|
51
|
+
/** Just enough of `fetch` to redeem a code. A parameter as well as a global. */
|
|
52
|
+
export type FetchLike = (url: string, init: {
|
|
53
|
+
method: string;
|
|
54
|
+
headers: Record<string, string>;
|
|
55
|
+
body: string;
|
|
56
|
+
}) => Promise<{
|
|
57
|
+
ok: boolean;
|
|
58
|
+
status: number;
|
|
59
|
+
json(): Promise<unknown>;
|
|
60
|
+
}>;
|
|
61
|
+
export interface EstivaIdConfig {
|
|
62
|
+
/** Origin of the identity service, e.g. `https://id.estiva.app`. */
|
|
63
|
+
base: string;
|
|
64
|
+
/** This app's `client_id` row in `app_credentials`. Never defaulted. */
|
|
65
|
+
clientId: string;
|
|
66
|
+
/**
|
|
67
|
+
* Where Estiva ID sends people back to. Must match a registered entry
|
|
68
|
+
* **exactly** — a trailing slash is a refused sign-in.
|
|
69
|
+
*/
|
|
70
|
+
redirectUri: () => string;
|
|
71
|
+
/** See the note on storage above. Returns null outside a browser. */
|
|
72
|
+
storage: () => KeyValueStore | null;
|
|
73
|
+
/** Namespaces this app's keys, e.g. `peek.estivaId`. */
|
|
74
|
+
keyPrefix: string;
|
|
75
|
+
/** Defaults to the runtime's `location.assign`-equivalent via `navigate`. */
|
|
76
|
+
navigate?: (url: string) => void;
|
|
77
|
+
now?: () => number;
|
|
78
|
+
fetch?: FetchLike;
|
|
79
|
+
}
|
|
80
|
+
export interface StoredToken {
|
|
81
|
+
accessToken: string;
|
|
82
|
+
/** Epoch ms. Derived from `expires_in` at receipt, not sent by the server. */
|
|
83
|
+
expiresAt: number;
|
|
84
|
+
pubkey: string;
|
|
85
|
+
/**
|
|
86
|
+
* The credential that renews the one above without a redirect (PEEK-108).
|
|
87
|
+
*
|
|
88
|
+
* Absent when Estiva ID issued none — a deployment predating the grant. The
|
|
89
|
+
* app then behaves exactly as it did before, which is what lets the two sides
|
|
90
|
+
* ship independently.
|
|
91
|
+
*
|
|
92
|
+
* **Single-use.** Estiva ID rotates it on every redemption and treats a replay
|
|
93
|
+
* as evidence of theft, revoking the whole chain. So it is replaced, never
|
|
94
|
+
* kept alongside its successor.
|
|
95
|
+
*/
|
|
96
|
+
refreshToken?: string;
|
|
97
|
+
}
|
|
98
|
+
export declare class SignInError extends Error {
|
|
99
|
+
}
|
|
100
|
+
export interface EstivaIdClient {
|
|
101
|
+
redirectUri: () => string;
|
|
102
|
+
codeChallenge: (verifier: string) => Promise<string>;
|
|
103
|
+
beginSignIn: (returnTo: string, options?: {
|
|
104
|
+
silent?: boolean;
|
|
105
|
+
}) => Promise<void>;
|
|
106
|
+
completeSignIn: (search: string) => Promise<{
|
|
107
|
+
token: StoredToken;
|
|
108
|
+
returnTo: string;
|
|
109
|
+
}>;
|
|
110
|
+
refreshAccessToken: () => Promise<StoredToken | null>;
|
|
111
|
+
storedToken: () => StoredToken | null;
|
|
112
|
+
validToken: (now?: number) => StoredToken | null;
|
|
113
|
+
hasSession: () => boolean;
|
|
114
|
+
currentAccessToken: () => string | undefined;
|
|
115
|
+
beginSignOut: () => void;
|
|
116
|
+
clearSession: () => void;
|
|
117
|
+
hasPendingSignIn: () => boolean;
|
|
118
|
+
clearPendingSignIn: () => void;
|
|
119
|
+
takeReturnTo: () => string | null;
|
|
120
|
+
stashShellReason: (reason: ShellReason) => void;
|
|
121
|
+
takeShellReason: () => ShellReason | null;
|
|
122
|
+
}
|
|
123
|
+
export declare function createEstivaId(config: EstivaIdConfig): EstivaIdClient;
|
|
124
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAyB,MAAM,YAAY,CAAA;AAuBxF,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAAA;AAIvE,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,WAAW,EAAE,MAAM,MAAM,CAAA;IACzB,qEAAqE;IACrE,OAAO,EAAE,MAAM,aAAa,GAAG,IAAI,CAAA;IACnC,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,SAAS,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,qBAAa,WAAY,SAAQ,KAAK;CAAG;AASzC,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,MAAM,CAAA;IACzB,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACpD,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChF,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACrF,kBAAkB,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IACrD,WAAW,EAAE,MAAM,WAAW,GAAG,IAAI,CAAA;IACrC,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,WAAW,GAAG,IAAI,CAAA;IAChD,UAAU,EAAE,MAAM,OAAO,CAAA;IACzB,kBAAkB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC5C,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,gBAAgB,EAAE,MAAM,OAAO,CAAA;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAA;IAC9B,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACjC,gBAAgB,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;IAC/C,eAAe,EAAE,MAAM,WAAW,GAAG,IAAI,CAAA;CAC1C;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CA4WrE"}
|