@opendatalabs/vana-sdk
Version:
A TypeScript library for interacting with Vana Network smart contracts.
1 lines • 17.7 kB
Source Map (JSON)
{"version":3,"sources":["../../src/auth/oauth-client.ts"],"sourcesContent":["/**\n * OAuth 2.0 Authorization Code + PKCE client orchestration.\n *\n * @remarks\n * Drives the full authorize → callback → token-exchange → refresh dance on top\n * of the {@link TokenStore} and PKCE primitives that ship with this package.\n * Implements RFC 6749 §4.1 with the RFC 7636 PKCE extension (S256 only).\n *\n * @category Auth\n * @module auth/oauth-client\n */\n\nimport { computePkceChallenge, generatePkceVerifier } from \"./pkce\";\nimport {\n InMemoryTokenStore,\n type TokenRecord,\n type TokenStore,\n} from \"./token-store\";\n\n/**\n * Constructor options for {@link OAuthClient}.\n */\nexport interface OAuthClientConfig {\n /** Authorization endpoint, e.g. `https://account.vana.org/oauth/authorize`. */\n authorizationEndpoint: string;\n /** Token endpoint, e.g. `https://account.vana.org/oauth/token`. */\n tokenEndpoint: string;\n /** OAuth `client_id` (public; PKCE protects the flow). */\n clientId: string;\n /** Redirect URI registered with the authorization server. */\n redirectUri: string;\n /** Default scope; can be overridden per call. */\n scope?: string;\n /**\n * Where to persist access + refresh tokens and the in-flight code verifier\n * between `authorize` → `callback`. Defaults to a fresh\n * {@link InMemoryTokenStore}. Use IndexedDB/localStorage-backed\n * implementations for browser apps where the user navigates away during the\n * dance.\n */\n tokenStore?: TokenStore;\n /** Override the global `fetch` (e.g. for tests). Defaults to `globalThis.fetch`. */\n fetchImpl?: typeof fetch;\n /**\n * Override the random-state generator (mostly for tests). Must return a\n * URL-safe string of >= 16 bytes of entropy.\n */\n generateState?: () => string;\n}\n\n/**\n * Result of {@link OAuthClient.buildAuthorizationUrl}.\n */\nexport interface AuthorizationUrlResult {\n /** The full authorize URL to redirect / `window.open` to. */\n url: string;\n /** The `state` value the auth server will echo back; used for CSRF check. */\n state: string;\n}\n\n/** TTL for the in-flight verifier record (seconds). */\nconst VERIFIER_TTL_SECONDS = 600;\n\n/** RFC 6749 spec-compliant OAuth error payload shape. */\ninterface OAuthErrorBody {\n error?: string;\n error_description?: string;\n error_uri?: string;\n}\n\n/** Successful token-endpoint response shape (RFC 6749 §5.1). */\ninterface TokenEndpointResponse {\n access_token: string;\n token_type?: string;\n expires_in?: number;\n refresh_token?: string;\n scope?: string;\n}\n\n/**\n * Authorize-URL parameters the client owns. Callers may NOT supply these\n * via `extraParams` — otherwise PKCE/CSRF protection can be silently\n * bypassed (e.g. `extraParams: { state: \"x\" }` would store the verifier\n * under the generated state but send `x` on the wire, breaking the\n * callback CSRF check; `code_challenge_method` could downgrade S256).\n */\nconst RESERVED_AUTHORIZE_PARAMS = new Set([\n \"response_type\",\n \"client_id\",\n \"redirect_uri\",\n \"scope\",\n \"state\",\n \"code_challenge\",\n \"code_challenge_method\",\n]);\n\n/**\n * OAuth 2.0 Authorization Code + PKCE client.\n *\n * @remarks\n * Storage layout under the supplied {@link TokenStore} (all keys namespaced):\n * - `oauth:tokens:{clientId}` → access token record\n * - `oauth:refresh:{clientId}` → refresh token record (no expiry)\n * - `oauth:verifier:{state}` → in-flight PKCE verifier (10 min TTL)\n *\n * @category Auth\n */\nexport class OAuthClient {\n readonly #config: Required<\n Omit<\n OAuthClientConfig,\n \"scope\" | \"tokenStore\" | \"fetchImpl\" | \"generateState\"\n >\n > & {\n scope?: string;\n tokenStore: TokenStore;\n fetchImpl: typeof fetch;\n generateState: () => string;\n };\n\n public constructor(config: OAuthClientConfig) {\n const fetchImpl = config.fetchImpl ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new TypeError(\n \"OAuthClient requires a global `fetch` or an explicit `fetchImpl`\",\n );\n }\n\n this.#config = {\n authorizationEndpoint: config.authorizationEndpoint,\n tokenEndpoint: config.tokenEndpoint,\n clientId: config.clientId,\n redirectUri: config.redirectUri,\n scope: config.scope,\n tokenStore: config.tokenStore ?? new InMemoryTokenStore(),\n fetchImpl,\n generateState: config.generateState ?? defaultGenerateState,\n };\n }\n\n /** Build the authorize URL and persist the PKCE verifier keyed by `state`. */\n public async buildAuthorizationUrl(\n opts: {\n state?: string;\n scope?: string;\n extraParams?: Record<string, string>;\n } = {},\n ): Promise<AuthorizationUrlResult> {\n const state = opts.state ?? this.#config.generateState();\n const scope = opts.scope ?? this.#config.scope;\n\n const verifier = generatePkceVerifier();\n const challenge = await computePkceChallenge(verifier);\n\n await this.#config.tokenStore.set(this.#verifierKey(state), {\n token: verifier,\n expiresAt: Math.floor(Date.now() / 1000) + VERIFIER_TTL_SECONDS,\n });\n\n const params = new URLSearchParams();\n params.set(\"response_type\", \"code\");\n params.set(\"client_id\", this.#config.clientId);\n params.set(\"redirect_uri\", this.#config.redirectUri);\n if (scope !== undefined && scope.length > 0) {\n params.set(\"scope\", scope);\n }\n params.set(\"state\", state);\n params.set(\"code_challenge\", challenge);\n params.set(\"code_challenge_method\", \"S256\");\n if (opts.extraParams !== undefined) {\n for (const k of Object.keys(opts.extraParams)) {\n if (RESERVED_AUTHORIZE_PARAMS.has(k)) {\n throw new Error(\n `extraParams may not override the reserved OAuth/PKCE parameter \"${k}\"`,\n );\n }\n }\n for (const [k, v] of Object.entries(opts.extraParams)) {\n params.set(k, v);\n }\n }\n\n const sep = this.#config.authorizationEndpoint.includes(\"?\") ? \"&\" : \"?\";\n const url = `${this.#config.authorizationEndpoint}${sep}${params.toString()}`;\n\n return { url, state };\n }\n\n /**\n * Handle the redirect-callback URL. Validates `state`, retrieves the saved\n * verifier, exchanges the authorization code + verifier for tokens, and\n * persists them. Returns the access {@link TokenRecord}.\n */\n public async handleCallback(callbackUrl: string): Promise<TokenRecord> {\n const parsed = new URL(callbackUrl);\n const params = parsed.searchParams;\n\n const errorCode = params.get(\"error\");\n if (errorCode !== null) {\n throw new Error(\n formatOAuthError({\n error: errorCode,\n error_description: params.get(\"error_description\") ?? undefined,\n }),\n );\n }\n\n const code = params.get(\"code\");\n const state = params.get(\"state\");\n if (code === null || state === null) {\n throw new Error(\"OAuth callback is missing `code` or `state`\");\n }\n\n const verifierRecord = await this.#config.tokenStore.get(\n this.#verifierKey(state),\n );\n if (verifierRecord === null) {\n throw new Error(\n \"OAuth callback state does not match any in-flight verifier (possible CSRF or expired flow)\",\n );\n }\n\n const body = new URLSearchParams();\n body.set(\"grant_type\", \"authorization_code\");\n body.set(\"code\", code);\n body.set(\"redirect_uri\", this.#config.redirectUri);\n body.set(\"client_id\", this.#config.clientId);\n body.set(\"code_verifier\", verifierRecord.token);\n\n let tokens: TokenEndpointResponse;\n try {\n tokens = await this.#tokenRequest(body);\n } finally {\n // Always clear the one-shot verifier, even on a failed exchange.\n await this.#config.tokenStore.delete(this.#verifierKey(state));\n }\n\n return this.#persistTokens(tokens);\n }\n\n /**\n * Exchange a stored refresh token for a fresh access token. Throws if no\n * refresh token is available.\n */\n public async refresh(): Promise<TokenRecord> {\n const refreshRecord = await this.#config.tokenStore.get(this.#refreshKey());\n if (refreshRecord === null) {\n throw new Error(\"OAuth refresh failed: no refresh token stored\");\n }\n\n const body = new URLSearchParams();\n body.set(\"grant_type\", \"refresh_token\");\n body.set(\"refresh_token\", refreshRecord.token);\n body.set(\"client_id\", this.#config.clientId);\n\n const tokens = await this.#tokenRequest(body);\n return this.#persistTokens(tokens, refreshRecord.token);\n }\n\n /**\n * Get the current access token if valid (refreshing first if expired and a\n * refresh token is available). Returns `null` when no usable token exists.\n */\n public async getAccessToken(): Promise<string | null> {\n const stored = await this.#config.tokenStore.get(this.#accessKey());\n if (stored !== null) return stored.token;\n\n // Stored access token is missing or already evicted by the store's TTL.\n const refresh = await this.#config.tokenStore.get(this.#refreshKey());\n if (refresh === null) return null;\n\n try {\n const refreshed = await this.refresh();\n return refreshed.token;\n } catch {\n return null;\n }\n }\n\n /** Forget tokens (logout). Does NOT call any remote revocation endpoint. */\n public async signOut(): Promise<void> {\n await this.#config.tokenStore.delete(this.#accessKey());\n await this.#config.tokenStore.delete(this.#refreshKey());\n }\n\n #accessKey(): string {\n return `oauth:tokens:${this.#config.clientId}`;\n }\n\n #refreshKey(): string {\n return `oauth:refresh:${this.#config.clientId}`;\n }\n\n #verifierKey(state: string): string {\n return `oauth:verifier:${state}`;\n }\n\n async #tokenRequest(body: URLSearchParams): Promise<TokenEndpointResponse> {\n const response = await this.#config.fetchImpl(this.#config.tokenEndpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: body.toString(),\n });\n\n const text = await response.text();\n const parsed = parseJsonBody(text);\n\n if (!response.ok) {\n throw new Error(formatOAuthError(parsed ?? {}, response.status));\n }\n\n if (\n parsed === null ||\n typeof parsed !== \"object\" ||\n typeof (parsed as { access_token?: unknown }).access_token !== \"string\"\n ) {\n throw new Error(\n \"OAuth token endpoint returned a response without an `access_token` string\",\n );\n }\n\n return parsed as TokenEndpointResponse;\n }\n\n async #persistTokens(\n tokens: TokenEndpointResponse,\n previousRefreshToken?: string,\n ): Promise<TokenRecord> {\n const record: TokenRecord = { token: tokens.access_token };\n if (typeof tokens.expires_in === \"number\" && tokens.expires_in > 0) {\n record.expiresAt = Math.floor(Date.now() / 1000) + tokens.expires_in;\n }\n await this.#config.tokenStore.set(this.#accessKey(), record);\n\n const newRefresh = tokens.refresh_token ?? previousRefreshToken;\n if (newRefresh !== undefined) {\n await this.#config.tokenStore.set(this.#refreshKey(), {\n token: newRefresh,\n });\n }\n\n return record;\n }\n}\n\nfunction defaultGenerateState(): string {\n const bytes = new Uint8Array(24);\n crypto.getRandomValues(bytes);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] as number);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nfunction parseJsonBody(text: string): unknown {\n if (text.length === 0) return null;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return null;\n }\n}\n\nfunction formatOAuthError(body: OAuthErrorBody, status?: number): string {\n const parts: string[] = [\"OAuth token request failed\"];\n if (status !== undefined) parts.push(`(HTTP ${String(status)})`);\n if (body.error !== undefined && body.error.length > 0) {\n parts.push(`: ${body.error}`);\n if (\n body.error_description !== undefined &&\n body.error_description.length > 0\n ) {\n parts.push(`- ${body.error_description}`);\n }\n }\n return parts.join(\" \").replace(\" : \", \": \").replace(\" - \", \" - \");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA,kBAA2D;AAC3D,yBAIO;AA4CP,MAAM,uBAAuB;AAyB7B,MAAM,4BAA4B,oBAAI,IAAI;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAaM,MAAM,YAAY;AAAA,EACd;AAAA,EAYF,YAAY,QAA2B;AAC5C,UAAM,YAAY,OAAO,aAAa,WAAW;AACjD,QAAI,OAAO,cAAc,YAAY;AACnC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,SAAK,UAAU;AAAA,MACb,uBAAuB,OAAO;AAAA,MAC9B,eAAe,OAAO;AAAA,MACtB,UAAU,OAAO;AAAA,MACjB,aAAa,OAAO;AAAA,MACpB,OAAO,OAAO;AAAA,MACd,YAAY,OAAO,cAAc,IAAI,sCAAmB;AAAA,MACxD;AAAA,MACA,eAAe,OAAO,iBAAiB;AAAA,IACzC;AAAA,EACF;AAAA;AAAA,EAGA,MAAa,sBACX,OAII,CAAC,GAC4B;AACjC,UAAM,QAAQ,KAAK,SAAS,KAAK,QAAQ,cAAc;AACvD,UAAM,QAAQ,KAAK,SAAS,KAAK,QAAQ;AAEzC,UAAM,eAAW,kCAAqB;AACtC,UAAM,YAAY,UAAM,kCAAqB,QAAQ;AAErD,UAAM,KAAK,QAAQ,WAAW,IAAI,KAAK,aAAa,KAAK,GAAG;AAAA,MAC1D,OAAO;AAAA,MACP,WAAW,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI;AAAA,IAC7C,CAAC;AAED,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,iBAAiB,MAAM;AAClC,WAAO,IAAI,aAAa,KAAK,QAAQ,QAAQ;AAC7C,WAAO,IAAI,gBAAgB,KAAK,QAAQ,WAAW;AACnD,QAAI,UAAU,UAAa,MAAM,SAAS,GAAG;AAC3C,aAAO,IAAI,SAAS,KAAK;AAAA,IAC3B;AACA,WAAO,IAAI,SAAS,KAAK;AACzB,WAAO,IAAI,kBAAkB,SAAS;AACtC,WAAO,IAAI,yBAAyB,MAAM;AAC1C,QAAI,KAAK,gBAAgB,QAAW;AAClC,iBAAW,KAAK,OAAO,KAAK,KAAK,WAAW,GAAG;AAC7C,YAAI,0BAA0B,IAAI,CAAC,GAAG;AACpC,gBAAM,IAAI;AAAA,YACR,mEAAmE,CAAC;AAAA,UACtE;AAAA,QACF;AAAA,MACF;AACA,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,WAAW,GAAG;AACrD,eAAO,IAAI,GAAG,CAAC;AAAA,MACjB;AAAA,IACF;AAEA,UAAM,MAAM,KAAK,QAAQ,sBAAsB,SAAS,GAAG,IAAI,MAAM;AACrE,UAAM,MAAM,GAAG,KAAK,QAAQ,qBAAqB,GAAG,GAAG,GAAG,OAAO,SAAS,CAAC;AAE3E,WAAO,EAAE,KAAK,MAAM;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAa,eAAe,aAA2C;AACrE,UAAM,SAAS,IAAI,IAAI,WAAW;AAClC,UAAM,SAAS,OAAO;AAEtB,UAAM,YAAY,OAAO,IAAI,OAAO;AACpC,QAAI,cAAc,MAAM;AACtB,YAAM,IAAI;AAAA,QACR,iBAAiB;AAAA,UACf,OAAO;AAAA,UACP,mBAAmB,OAAO,IAAI,mBAAmB,KAAK;AAAA,QACxD,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,OAAO,OAAO,IAAI,MAAM;AAC9B,UAAM,QAAQ,OAAO,IAAI,OAAO;AAChC,QAAI,SAAS,QAAQ,UAAU,MAAM;AACnC,YAAM,IAAI,MAAM,6CAA6C;AAAA,IAC/D;AAEA,UAAM,iBAAiB,MAAM,KAAK,QAAQ,WAAW;AAAA,MACnD,KAAK,aAAa,KAAK;AAAA,IACzB;AACA,QAAI,mBAAmB,MAAM;AAC3B,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,IAAI,gBAAgB;AACjC,SAAK,IAAI,cAAc,oBAAoB;AAC3C,SAAK,IAAI,QAAQ,IAAI;AACrB,SAAK,IAAI,gBAAgB,KAAK,QAAQ,WAAW;AACjD,SAAK,IAAI,aAAa,KAAK,QAAQ,QAAQ;AAC3C,SAAK,IAAI,iBAAiB,eAAe,KAAK;AAE9C,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,KAAK,cAAc,IAAI;AAAA,IACxC,UAAE;AAEA,YAAM,KAAK,QAAQ,WAAW,OAAO,KAAK,aAAa,KAAK,CAAC;AAAA,IAC/D;AAEA,WAAO,KAAK,eAAe,MAAM;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAa,UAAgC;AAC3C,UAAM,gBAAgB,MAAM,KAAK,QAAQ,WAAW,IAAI,KAAK,YAAY,CAAC;AAC1E,QAAI,kBAAkB,MAAM;AAC1B,YAAM,IAAI,MAAM,+CAA+C;AAAA,IACjE;AAEA,UAAM,OAAO,IAAI,gBAAgB;AACjC,SAAK,IAAI,cAAc,eAAe;AACtC,SAAK,IAAI,iBAAiB,cAAc,KAAK;AAC7C,SAAK,IAAI,aAAa,KAAK,QAAQ,QAAQ;AAE3C,UAAM,SAAS,MAAM,KAAK,cAAc,IAAI;AAC5C,WAAO,KAAK,eAAe,QAAQ,cAAc,KAAK;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAa,iBAAyC;AACpD,UAAM,SAAS,MAAM,KAAK,QAAQ,WAAW,IAAI,KAAK,WAAW,CAAC;AAClE,QAAI,WAAW,KAAM,QAAO,OAAO;AAGnC,UAAM,UAAU,MAAM,KAAK,QAAQ,WAAW,IAAI,KAAK,YAAY,CAAC;AACpE,QAAI,YAAY,KAAM,QAAO;AAE7B,QAAI;AACF,YAAM,YAAY,MAAM,KAAK,QAAQ;AACrC,aAAO,UAAU;AAAA,IACnB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,MAAa,UAAyB;AACpC,UAAM,KAAK,QAAQ,WAAW,OAAO,KAAK,WAAW,CAAC;AACtD,UAAM,KAAK,QAAQ,WAAW,OAAO,KAAK,YAAY,CAAC;AAAA,EACzD;AAAA,EAEA,aAAqB;AACnB,WAAO,gBAAgB,KAAK,QAAQ,QAAQ;AAAA,EAC9C;AAAA,EAEA,cAAsB;AACpB,WAAO,iBAAiB,KAAK,QAAQ,QAAQ;AAAA,EAC/C;AAAA,EAEA,aAAa,OAAuB;AAClC,WAAO,kBAAkB,KAAK;AAAA,EAChC;AAAA,EAEA,MAAM,cAAc,MAAuD;AACzE,UAAM,WAAW,MAAM,KAAK,QAAQ,UAAU,KAAK,QAAQ,eAAe;AAAA,MACxE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,QAAQ;AAAA,MACV;AAAA,MACA,MAAM,KAAK,SAAS;AAAA,IACtB,CAAC;AAED,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,UAAM,SAAS,cAAc,IAAI;AAEjC,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,iBAAiB,UAAU,CAAC,GAAG,SAAS,MAAM,CAAC;AAAA,IACjE;AAEA,QACE,WAAW,QACX,OAAO,WAAW,YAClB,OAAQ,OAAsC,iBAAiB,UAC/D;AACA,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,eACJ,QACA,sBACsB;AACtB,UAAM,SAAsB,EAAE,OAAO,OAAO,aAAa;AACzD,QAAI,OAAO,OAAO,eAAe,YAAY,OAAO,aAAa,GAAG;AAClE,aAAO,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI,OAAO;AAAA,IAC5D;AACA,UAAM,KAAK,QAAQ,WAAW,IAAI,KAAK,WAAW,GAAG,MAAM;AAE3D,UAAM,aAAa,OAAO,iBAAiB;AAC3C,QAAI,eAAe,QAAW;AAC5B,YAAM,KAAK,QAAQ,WAAW,IAAI,KAAK,YAAY,GAAG;AAAA,QACpD,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,uBAA+B;AACtC,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAW;AAAA,EAClD;AACA,SAAO,KAAK,MAAM,EACf,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAEA,SAAS,cAAc,MAAuB;AAC5C,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,iBAAiB,MAAsB,QAAyB;AACvE,QAAM,QAAkB,CAAC,4BAA4B;AACrD,MAAI,WAAW,OAAW,OAAM,KAAK,SAAS,OAAO,MAAM,CAAC,GAAG;AAC/D,MAAI,KAAK,UAAU,UAAa,KAAK,MAAM,SAAS,GAAG;AACrD,UAAM,KAAK,KAAK,KAAK,KAAK,EAAE;AAC5B,QACE,KAAK,sBAAsB,UAC3B,KAAK,kBAAkB,SAAS,GAChC;AACA,YAAM,KAAK,KAAK,KAAK,iBAAiB,EAAE;AAAA,IAC1C;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG,EAAE,QAAQ,OAAO,IAAI,EAAE,QAAQ,OAAO,KAAK;AAClE;","names":[]}