@hatk/hatk 0.0.1-alpha.84 → 0.0.1-alpha.86
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/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +2 -0
- package/dist/cli.js +5 -1
- package/dist/config.d.ts +47 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -1
- package/dist/database/db.d.ts +11 -0
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +356 -160
- package/dist/database/dialect.js +2 -2
- package/dist/database/fts.d.ts.map +1 -1
- package/dist/database/fts.js +6 -2
- package/dist/database/index.d.ts +1 -1
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/index.js +1 -1
- package/dist/database/schema.d.ts +2 -0
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +46 -4
- package/dist/dev-entry.d.ts.map +1 -1
- package/dist/dev-entry.js +2 -2
- package/dist/feeds.d.ts +14 -0
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +24 -13
- package/dist/hooks.d.ts +17 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +26 -3
- package/dist/hydrate.d.ts +42 -0
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +18 -5
- package/dist/indexer.d.ts +4 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +81 -64
- package/dist/lex-types.d.ts +2 -0
- package/dist/lex-types.d.ts.map +1 -1
- package/dist/main.js +26 -2
- package/dist/oauth/db.d.ts +9 -0
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +12 -0
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +16 -4
- package/dist/private-collections.d.ts +7 -2
- package/dist/private-collections.d.ts.map +1 -1
- package/dist/private-collections.js +9 -4
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +167 -15
- package/dist/spaces/blob.d.ts +51 -0
- package/dist/spaces/blob.d.ts.map +1 -0
- package/dist/spaces/blob.js +111 -0
- package/dist/spaces/credential.d.ts +62 -0
- package/dist/spaces/credential.d.ts.map +1 -0
- package/dist/spaces/credential.js +184 -0
- package/dist/spaces/engine.d.ts +97 -0
- package/dist/spaces/engine.d.ts.map +1 -0
- package/dist/spaces/engine.js +552 -0
- package/dist/spaces/guard.d.ts +41 -0
- package/dist/spaces/guard.d.ts.map +1 -0
- package/dist/spaces/guard.js +79 -0
- package/dist/spaces/identity.d.ts +37 -0
- package/dist/spaces/identity.d.ts.map +1 -0
- package/dist/spaces/identity.js +88 -0
- package/dist/spaces/index.d.ts +55 -0
- package/dist/spaces/index.d.ts.map +1 -0
- package/dist/spaces/index.js +133 -0
- package/dist/spaces/notify.d.ts +51 -0
- package/dist/spaces/notify.d.ts.map +1 -0
- package/dist/spaces/notify.js +121 -0
- package/dist/spaces/records.d.ts +37 -0
- package/dist/spaces/records.d.ts.map +1 -0
- package/dist/spaces/records.js +63 -0
- package/dist/spaces/store.d.ts +51 -0
- package/dist/spaces/store.d.ts.map +1 -0
- package/dist/spaces/store.js +99 -0
- package/dist/spaces/uri.d.ts +60 -0
- package/dist/spaces/uri.d.ts.map +1 -0
- package/dist/spaces/uri.js +97 -0
- package/dist/spaces/verify.d.ts +39 -0
- package/dist/spaces/verify.d.ts.map +1 -0
- package/dist/spaces/verify.js +87 -0
- package/dist/spaces/viewer.d.ts +51 -0
- package/dist/spaces/viewer.d.ts.map +1 -0
- package/dist/spaces/viewer.js +167 -0
- package/dist/spaces/visibility.d.ts +57 -0
- package/dist/spaces/visibility.d.ts.map +1 -0
- package/dist/spaces/visibility.js +81 -0
- package/dist/templates/feed.tpl +4 -0
- package/dist/templates/xrpc.tpl +13 -1
- package/dist/test.d.ts +2 -0
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +8 -5
- package/dist/xrpc.d.ts +19 -3
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +62 -32
- package/package.json +8 -4
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting into a space to read it.
|
|
3
|
+
*
|
|
4
|
+
* hatk is not a member of anything. It has no DID that any community has
|
|
5
|
+
* admitted, and asking every authority to admit an indexer would make indexing
|
|
6
|
+
* a per-community act of trust before a single record could be read.
|
|
7
|
+
*
|
|
8
|
+
* So it reads as somebody who is already inside: a viewer whose OAuth session
|
|
9
|
+
* it already holds, whose PDS will mint a delegation token for the space, and
|
|
10
|
+
* whom the authority already lets in. The exchange is the same one the
|
|
11
|
+
* community's own web app makes from the browser —
|
|
12
|
+
*
|
|
13
|
+
* the reader's PDS signs a delegation token (60s, single-use, addressed to
|
|
14
|
+
* the authority's space host)
|
|
15
|
+
* the authority checks it, checks the reader against the space's policy, and
|
|
16
|
+
* returns a credential bound to the key that signed our DPoP proof (2h)
|
|
17
|
+
*
|
|
18
|
+
* — and the credential is then presented to every writer's host in the space.
|
|
19
|
+
* It carries no audience, which is why it must be bound to a key rather than
|
|
20
|
+
* held as a bearer token: a host handed a bearer credential to serve its own
|
|
21
|
+
* repo could replay it against every other host in the space.
|
|
22
|
+
*
|
|
23
|
+
* The consequence to be honest about: a space is only readable while somebody
|
|
24
|
+
* who can read it has a live session here. That is the same bound the data has
|
|
25
|
+
* anyway — nobody outside the space's audience was ever entitled to this — but
|
|
26
|
+
* it does mean indexing lapses when the last member logs out.
|
|
27
|
+
*/
|
|
28
|
+
import { computeJwkThumbprint, generateKeyPair } from "../oauth/crypto.js";
|
|
29
|
+
import { createDpopProof } from "../oauth/dpop.js";
|
|
30
|
+
import { pdsXrpc } from "../pds-proxy.js";
|
|
31
|
+
import { emit } from "../logger.js";
|
|
32
|
+
import { spaceHostEndpoint } from "./identity.js";
|
|
33
|
+
import { parseSpaceRef } from "./uri.js";
|
|
34
|
+
/** Re-mint this long before the credential's own expiry rather than racing it. */
|
|
35
|
+
const RENEW_LEAD_MS = 5 * 60 * 1000;
|
|
36
|
+
/**
|
|
37
|
+
* The key credentials are bound to, generated once per process and never
|
|
38
|
+
* persisted. It is registered nowhere, so a restart costs one round trip to
|
|
39
|
+
* mint fresh credentials and nothing else.
|
|
40
|
+
*/
|
|
41
|
+
let bindingKey = null;
|
|
42
|
+
function getBindingKey() {
|
|
43
|
+
bindingKey ??= generateKeyPair();
|
|
44
|
+
return bindingKey;
|
|
45
|
+
}
|
|
46
|
+
export class SpaceCredentialError extends Error {
|
|
47
|
+
status;
|
|
48
|
+
code;
|
|
49
|
+
constructor(status, code, message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.status = status;
|
|
52
|
+
this.code = code;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** A refusal about who is asking, rather than about the space or the network. */
|
|
56
|
+
export function isNotAuthorized(err) {
|
|
57
|
+
return err instanceof SpaceCredentialError && (err.code === 'UserNotAuthorized' || err.code === 'NotAuthorized');
|
|
58
|
+
}
|
|
59
|
+
export function isSpaceGone(err) {
|
|
60
|
+
return err instanceof SpaceCredentialError && (err.code === 'SpaceDeleted' || err.code === 'SpaceNotFound');
|
|
61
|
+
}
|
|
62
|
+
function expiryOf(jwt) {
|
|
63
|
+
try {
|
|
64
|
+
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString());
|
|
65
|
+
return typeof payload.exp === 'number' ? payload.exp * 1000 : Date.now() + 60 * 60 * 1000;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// An unreadable expiry is not a reason to refuse a credential the authority
|
|
69
|
+
// just issued; treat it as short-lived and re-mint sooner.
|
|
70
|
+
return Date.now() + 10 * 60 * 1000;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Exchange a delegation token for a credential at the authority.
|
|
75
|
+
*
|
|
76
|
+
* The proof sent here carries no `ath`: a delegation token is an authorization
|
|
77
|
+
* grant rather than an access token, so there is nothing to hash yet, and a
|
|
78
|
+
* server that checks will refuse a proof that includes one. `dpopJkt` is sent
|
|
79
|
+
* alongside because implementations differ on which they read — the reference
|
|
80
|
+
* reads the proof, others read the field — and they agree here by construction.
|
|
81
|
+
*/
|
|
82
|
+
async function exchange(authorityHost, space, delegationToken) {
|
|
83
|
+
const { privateJwk, publicJwk } = await getBindingKey();
|
|
84
|
+
const url = `${authorityHost}/xrpc/com.atproto.space.getSpaceCredential`;
|
|
85
|
+
const proof = await createDpopProof(privateJwk, publicJwk, 'POST', url);
|
|
86
|
+
const dpopJkt = await computeJwkThumbprint(publicJwk);
|
|
87
|
+
const res = await fetch(url, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
Accept: 'application/json',
|
|
92
|
+
Authorization: `Bearer ${delegationToken}`,
|
|
93
|
+
DPoP: proof,
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({ space, dpopJkt }),
|
|
96
|
+
});
|
|
97
|
+
const body = (await res.json().catch(() => ({})));
|
|
98
|
+
if (!res.ok || !body.credential) {
|
|
99
|
+
throw new SpaceCredentialError(res.status, body.error ?? (res.status >= 500 ? 'UpstreamFailure' : 'InvalidRequest'), body.message ?? `getSpaceCredential refused (${res.status})`);
|
|
100
|
+
}
|
|
101
|
+
return body.credential;
|
|
102
|
+
}
|
|
103
|
+
/** Mint a credential for `space` using `readerDid`'s stored session. */
|
|
104
|
+
export async function mintSpaceCredential(oauthConfig, space, readerDid) {
|
|
105
|
+
const ref = parseSpaceRef(space);
|
|
106
|
+
if (!ref)
|
|
107
|
+
throw new SpaceCredentialError(400, 'InvalidRequest', `Not a space ref: ${space}`);
|
|
108
|
+
// The reader's own PDS signs this, under the whole-space read grant their
|
|
109
|
+
// session carries. A session without that scope is refused here, before the
|
|
110
|
+
// authority is ever contacted.
|
|
111
|
+
let token;
|
|
112
|
+
try {
|
|
113
|
+
const out = await pdsXrpc(oauthConfig, { did: readerDid }, 'com.atproto.space.getDelegationToken', {
|
|
114
|
+
params: { space },
|
|
115
|
+
});
|
|
116
|
+
if (typeof out.token !== 'string')
|
|
117
|
+
throw new Error('PDS returned no delegation token');
|
|
118
|
+
token = out.token;
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
throw new SpaceCredentialError(err?.status ?? 502, 'DelegationFailed', err?.message ?? 'delegation failed');
|
|
122
|
+
}
|
|
123
|
+
const authorityHost = await spaceHostEndpoint(ref.authority);
|
|
124
|
+
const credential = await exchange(authorityHost, space, token);
|
|
125
|
+
const { privateJwk, publicJwk } = await getBindingKey();
|
|
126
|
+
const credentialFetch = async (input, init) => {
|
|
127
|
+
const url = typeof input === 'string' ? input : input.toString();
|
|
128
|
+
const method = (init?.method ?? 'GET').toUpperCase();
|
|
129
|
+
const proof = await createDpopProof(privateJwk, publicJwk, method, url, credential);
|
|
130
|
+
const headers = new Headers(init?.headers);
|
|
131
|
+
headers.set('Authorization', `DPoP ${credential}`);
|
|
132
|
+
headers.set('DPoP', proof);
|
|
133
|
+
return fetch(url, { ...init, method, headers });
|
|
134
|
+
};
|
|
135
|
+
return { space, readerDid, expiresAt: expiryOf(credential), fetch: credentialFetch };
|
|
136
|
+
}
|
|
137
|
+
// --- Cache ---
|
|
138
|
+
const cache = new Map();
|
|
139
|
+
function fresh(cred) {
|
|
140
|
+
return !!cred && cred.expiresAt - RENEW_LEAD_MS > Date.now();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A credential for `space`, minted with whichever candidate session works.
|
|
144
|
+
*
|
|
145
|
+
* Candidates are tried in order and the first that succeeds is cached. A
|
|
146
|
+
* refusal about the reader — they were ejected, or never belonged — moves on to
|
|
147
|
+
* the next candidate; a refusal about the space itself stops the whole attempt,
|
|
148
|
+
* since no other reader would fare better.
|
|
149
|
+
*/
|
|
150
|
+
export async function getSpaceCredential(oauthConfig, space, candidates, opts = {}) {
|
|
151
|
+
if (!opts.refresh) {
|
|
152
|
+
const cached = cache.get(space);
|
|
153
|
+
if (fresh(cached))
|
|
154
|
+
return cached;
|
|
155
|
+
}
|
|
156
|
+
let lastError;
|
|
157
|
+
for (const readerDid of candidates) {
|
|
158
|
+
try {
|
|
159
|
+
const credential = await mintSpaceCredential(oauthConfig, space, readerDid);
|
|
160
|
+
cache.set(space, credential);
|
|
161
|
+
return credential;
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
if (isSpaceGone(err))
|
|
165
|
+
throw err;
|
|
166
|
+
lastError = err;
|
|
167
|
+
emit('spaces', 'credential_refused', {
|
|
168
|
+
space,
|
|
169
|
+
reader_did: readerDid,
|
|
170
|
+
error: err instanceof Error ? err.message : String(err),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
cache.delete(space);
|
|
175
|
+
throw lastError ?? new SpaceCredentialError(403, 'NoReader', `No session on this instance can read ${space}`);
|
|
176
|
+
}
|
|
177
|
+
export function forgetSpaceCredential(space) {
|
|
178
|
+
cache.delete(space);
|
|
179
|
+
}
|
|
180
|
+
/** For tests: drop every cached credential and the process binding key. */
|
|
181
|
+
export function resetSpaceCredentials() {
|
|
182
|
+
cache.clear();
|
|
183
|
+
bindingKey = null;
|
|
184
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping the index level with a permissioned space.
|
|
3
|
+
*
|
|
4
|
+
* A space never reaches a firehose. There is no stream to tail and no relay
|
|
5
|
+
* that has seen it, so the only way to know a space has moved is to ask the
|
|
6
|
+
* authority who has written into it and then ask each of those writers' hosts
|
|
7
|
+
* what changed. Two loops do that:
|
|
8
|
+
*
|
|
9
|
+
* reconcile the authority's `listRepos` gives the writer set with each
|
|
10
|
+
* writer's current rev. Anything whose rev has moved is synced;
|
|
11
|
+
* anything the authority no longer names is dropped.
|
|
12
|
+
* sync one writer's repo, forward from the rev we last read, through
|
|
13
|
+
* `listRepoOps` — or wholesale through `listRecords` when there
|
|
14
|
+
* is no rev to start from, or when the op log has been compacted
|
|
15
|
+
* past it.
|
|
16
|
+
*
|
|
17
|
+
* Write notices exist too and are far cheaper, but they are best-effort by
|
|
18
|
+
* design: the reference host forwards them once from an in-memory queue and
|
|
19
|
+
* logs the failure. So reconcile is the thing that makes sync correct, and a
|
|
20
|
+
* notice only makes it prompt.
|
|
21
|
+
*
|
|
22
|
+
* What is deliberately not done here is verification. The alpha signs a commit
|
|
23
|
+
* over the repo's LtHash state, and a syncer can check it. hatk does not check
|
|
24
|
+
* the equivalent on the firehose path either — it reads records out of relayed
|
|
25
|
+
* CAR blocks and trusts them — so checking here would claim a guarantee the
|
|
26
|
+
* rest of the index does not make. Adding it means depending on `@atproto/space`
|
|
27
|
+
* for LtHash and the commit verifier, and is worth doing to both paths at once
|
|
28
|
+
* rather than to this one alone.
|
|
29
|
+
*/
|
|
30
|
+
import type { OAuthConfig } from '../config.ts';
|
|
31
|
+
import { type SpaceCredential } from './credential.ts';
|
|
32
|
+
import { type SpaceWatch } from './store.ts';
|
|
33
|
+
export interface SpaceEngineOptions {
|
|
34
|
+
oauth: OAuthConfig;
|
|
35
|
+
/** Space type NSIDs this instance will follow. Anything else is refused. */
|
|
36
|
+
types: Set<string>;
|
|
37
|
+
/** Collections hatk indexes at all; a space collection outside this is skipped. */
|
|
38
|
+
collections: Set<string>;
|
|
39
|
+
/**
|
|
40
|
+
* This instance's service identifier — `did:web:...#atproto_space_syncer`.
|
|
41
|
+
* Absent when the instance receives no notices, in which case the sweep is
|
|
42
|
+
* the only thing that notices a write.
|
|
43
|
+
*/
|
|
44
|
+
serviceId?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function configureSpaceEngine(opts: SpaceEngineOptions): void;
|
|
47
|
+
/**
|
|
48
|
+
* The collections a space of this type holds, narrowed to the ones hatk has a
|
|
49
|
+
* table for.
|
|
50
|
+
*
|
|
51
|
+
* A space type's lexicon declares its collections, which is what makes a space
|
|
52
|
+
* readable without the app being told what is in it — but a space may name
|
|
53
|
+
* collections this instance does not index, and those are simply not its
|
|
54
|
+
* business.
|
|
55
|
+
*/
|
|
56
|
+
export declare function collectionsForSpaceType(spaceType: string): string[];
|
|
57
|
+
/** Bring one writer's repo in a space up to date, however that has to happen. */
|
|
58
|
+
export declare function syncSpaceRepo(watch: SpaceWatch, writer: string, credential: SpaceCredential): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Compare the authority's writer set against what the index holds, and close
|
|
61
|
+
* the difference.
|
|
62
|
+
*
|
|
63
|
+
* The writer set is what the authority has been told, not what is true: a
|
|
64
|
+
* repo's own host is the source of truth, and after a community migrates hosts
|
|
65
|
+
* the set is empty until each writer writes again. So a writer the authority
|
|
66
|
+
* stops naming has their rows dropped, but the rev comparison is only ever a
|
|
67
|
+
* reason to read — never a reason to believe a repo has not moved.
|
|
68
|
+
*/
|
|
69
|
+
export declare function reconcileSpace(watch: SpaceWatch): Promise<void>;
|
|
70
|
+
/** Follow a space from now on, and read it once. */
|
|
71
|
+
export declare function watchSpace(space: string): Promise<SpaceWatch>;
|
|
72
|
+
/** Stop following a space and drop everything indexed from it. */
|
|
73
|
+
export declare function unwatchSpace(space: string): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Act on a write notice: read the one repo it names.
|
|
76
|
+
*
|
|
77
|
+
* Only for a space already followed. A notice about anything else is not an
|
|
78
|
+
* instruction to start following it — that decision belongs to config or to the
|
|
79
|
+
* app, never to whoever can reach this endpoint.
|
|
80
|
+
*
|
|
81
|
+
* The notice's `rev` is deliberately not trusted as the new position: it is
|
|
82
|
+
* read from the repo's own host, which is the source of truth, by the ordinary
|
|
83
|
+
* sync path. The notice only says "look again".
|
|
84
|
+
*/
|
|
85
|
+
export declare function handleWriteNotice(notice: {
|
|
86
|
+
space: string;
|
|
87
|
+
repo: string;
|
|
88
|
+
}): Promise<boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* Reconcile every followed space.
|
|
91
|
+
*
|
|
92
|
+
* Failures are per space and recorded rather than thrown: one community's host
|
|
93
|
+
* being unreachable is not a reason to stop reading the others, and a space
|
|
94
|
+
* whose last reader logged out should go quiet rather than take the sweep down.
|
|
95
|
+
*/
|
|
96
|
+
export declare function reconcileAll(): Promise<void>;
|
|
97
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/spaces/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAO/C,OAAO,EAA0D,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAE9G,OAAO,EAUL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAA;AAMnB,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,WAAW,CAAA;IAClB,4EAA4E;IAC5E,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAClB,mFAAmF;IACnF,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAaD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAEnE;AAOD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAQnE;AAkUD,iFAAiF;AACjF,wBAAsB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBjH;AAkED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCrE;AAID,oDAAoD;AACpD,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAUnE;AAED,kEAAkE;AAClE,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/D;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAKjG;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAclD"}
|