@toa.io/extensions.exposition 1.0.0-alpha.253 → 1.0.0-alpha.256
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 +11 -0
- package/components/identity.bans/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/manifest.toa.yaml +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.credentials/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/manifest.toa.yaml +4 -4
- package/components/identity.federation/operations/authenticate.js +2 -1
- package/components/identity.federation/operations/authenticate.js.map +1 -1
- package/components/identity.federation/operations/decode.js +2 -1
- package/components/identity.federation/operations/decode.js.map +1 -1
- package/components/identity.federation/operations/delete.d.ts +2 -2
- package/components/identity.federation/operations/delete.js +3 -3
- package/components/identity.federation/operations/delete.js.map +1 -1
- package/components/identity.federation/operations/incept.js +2 -1
- package/components/identity.federation/operations/incept.js.map +1 -1
- package/components/identity.federation/operations/lib/Ctx.d.ts +2 -0
- package/components/identity.federation/operations/lib/decode.js +4 -27
- package/components/identity.federation/operations/lib/decode.js.map +1 -1
- package/components/identity.federation/operations/lib/discovery.d.ts +4 -3
- package/components/identity.federation/operations/lib/discovery.js +21 -31
- package/components/identity.federation/operations/lib/discovery.js.map +1 -1
- package/components/identity.federation/operations/lib/exchange.js +7 -6
- package/components/identity.federation/operations/lib/exchange.js.map +1 -1
- package/components/identity.federation/operations/lib/jose.d.ts +41 -0
- package/components/identity.federation/operations/lib/jose.js +27 -0
- package/components/identity.federation/operations/lib/jose.js.map +1 -0
- package/components/identity.federation/operations/list.js +2 -1
- package/components/identity.federation/operations/list.js.map +1 -1
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/types/context.d.ts +2 -0
- package/components/identity.federation/source/authenticate.ts +2 -1
- package/components/identity.federation/source/decode.ts +2 -1
- package/components/identity.federation/source/delete.ts +4 -4
- package/components/identity.federation/source/incept.ts +2 -1
- package/components/identity.federation/source/lib/Ctx.ts +2 -0
- package/components/identity.federation/source/lib/decode.ts +4 -5
- package/components/identity.federation/source/lib/discovery.test.ts +45 -0
- package/components/identity.federation/source/lib/discovery.ts +27 -8
- package/components/identity.federation/source/lib/exchange.ts +7 -7
- package/components/identity.federation/source/lib/jose.d.ts +1 -0
- package/components/identity.federation/source/lib/jose.js +5 -0
- package/components/identity.federation/source/list.test.ts +2 -1
- package/components/identity.federation/source/list.ts +2 -1
- package/components/identity.federation/source/types/context.ts +3 -0
- package/components/identity.federation/tsconfig.json +1 -4
- package/components/identity.keys/manifest.toa.yaml +1 -0
- package/components/identity.keys/operations/create.js +2 -2
- package/components/identity.keys/operations/create.js.map +1 -1
- package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.keys/source/create.ts +2 -2
- package/components/identity.otp/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/manifest.toa.yaml +21 -4
- package/components/identity.tokens/operations/decrypt.d.ts +4 -2
- package/components/identity.tokens/operations/decrypt.js +60 -12
- package/components/identity.tokens/operations/decrypt.js.map +1 -1
- package/components/identity.tokens/operations/encrypt.js +14 -13
- package/components/identity.tokens/operations/encrypt.js.map +1 -1
- package/components/identity.tokens/operations/lib/index.d.ts +1 -0
- package/components/identity.tokens/operations/lib/index.js +3 -0
- package/components/identity.tokens/operations/lib/index.js.map +1 -1
- package/components/identity.tokens/operations/lib/jose.d.ts +41 -0
- package/components/identity.tokens/operations/lib/jose.js +27 -0
- package/components/identity.tokens/operations/lib/jose.js.map +1 -0
- package/components/identity.tokens/operations/lib/key.d.ts +1 -0
- package/components/identity.tokens/operations/lib/key.js +11 -0
- package/components/identity.tokens/operations/lib/key.js.map +1 -0
- package/components/identity.tokens/operations/lib/types.d.ts +10 -1
- package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/source/authenticate.test.ts +4 -3
- package/components/identity.tokens/source/decrypt.test.ts +69 -8
- package/components/identity.tokens/source/decrypt.ts +72 -14
- package/components/identity.tokens/source/encrypt.test.ts +30 -3
- package/components/identity.tokens/source/encrypt.ts +18 -16
- package/components/identity.tokens/source/lib/index.ts +1 -0
- package/components/identity.tokens/source/lib/jose.d.ts +1 -0
- package/components/identity.tokens/source/lib/jose.js +5 -0
- package/components/identity.tokens/source/lib/key.ts +8 -0
- package/components/identity.tokens/source/lib/types.ts +12 -1
- package/{components/context.toa.yaml → context.toa.yaml} +6 -2
- package/documentation/authorities.md +1 -1
- package/documentation/components.md +70 -30
- package/documentation/identity.md +12 -14
- package/features/access.feature +1 -1
- package/features/cache.feature +5 -5
- package/features/cors.feature +4 -4
- package/features/credentials.feature +3 -3
- package/features/identity.basic.feature +1 -1
- package/features/identity.federation.feature +4 -5
- package/features/identity.roles.feature +1 -1
- package/features/identity.tokens.feature +3 -3
- package/features/steps/Gateway.ts +11 -3
- package/package.json +18 -21
- package/source/HTTP/formats/yaml.test.ts +9 -0
- package/source/HTTP/formats/yaml.ts +2 -1
- package/source/HTTP/messages.ts +1 -1
- package/source/directives/io/lib/throttle/Interval.ts +10 -1
- package/transpiled/HTTP/formats/yaml.js +2 -1
- package/transpiled/HTTP/formats/yaml.js.map +1 -1
- package/transpiled/HTTP/messages.js +24 -4
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Interval.d.ts +1 -0
- package/transpiled/directives/io/lib/throttle/Interval.js +9 -1
- package/transpiled/directives/io/lib/throttle/Interval.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
|
@@ -3,6 +3,7 @@ import type { Call, Observation, Query, telemetry, Transition } from '@toa.io/ty
|
|
|
3
3
|
import type { Entity } from './entity';
|
|
4
4
|
import type { Configuration } from './configuration';
|
|
5
5
|
export interface Context {
|
|
6
|
+
fetch: Fetch;
|
|
6
7
|
local: {
|
|
7
8
|
observe: Observation<Entity>;
|
|
8
9
|
enumerate: Observation<Entity[], never, Entity>;
|
|
@@ -21,6 +22,7 @@ export interface Context {
|
|
|
21
22
|
logs: telemetry.Logs;
|
|
22
23
|
configuration: Configuration;
|
|
23
24
|
}
|
|
25
|
+
export type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
24
26
|
export interface TransitInput {
|
|
25
27
|
readonly authority: string;
|
|
26
28
|
readonly iss: string;
|
|
@@ -10,7 +10,8 @@ export async function effect ({ scheme, authority, credentials }: Input, context
|
|
|
10
10
|
|
|
11
11
|
const ctx: Ctx = {
|
|
12
12
|
trust: context.configuration.trust,
|
|
13
|
-
logs: context.logs
|
|
13
|
+
logs: context.logs,
|
|
14
|
+
fetch: context.fetch
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const claims = scheme === 'bearer'
|
|
@@ -5,6 +5,7 @@ import type { Context } from './types'
|
|
|
5
5
|
export async function effect (token: string, context: Context): Promise<JWTPayload | Error> {
|
|
6
6
|
return await decode(token, {
|
|
7
7
|
trust: context.configuration.trust,
|
|
8
|
-
logs: context.logs
|
|
8
|
+
logs: context.logs,
|
|
9
|
+
fetch: context.fetch
|
|
9
10
|
})
|
|
10
11
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { Context } from './types'
|
|
2
2
|
|
|
3
|
-
export async function effect ({ authority, identity,
|
|
4
|
-
const object = await context.local.observe({ query: { id } })
|
|
3
|
+
export async function effect ({ authority, identity, credential }: Input, context: Context): Promise<void | null> {
|
|
4
|
+
const object = await context.local.observe({ query: { id: credential } })
|
|
5
5
|
|
|
6
6
|
if (object === null || object.authority !== authority || (object.identity ?? object.id) !== identity)
|
|
7
7
|
return null
|
|
8
8
|
|
|
9
|
-
return await context.local.terminate({ query: { id } })
|
|
9
|
+
return await context.local.terminate({ query: { id: credential } })
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
interface Input {
|
|
13
13
|
authority: string
|
|
14
14
|
identity: string
|
|
15
|
-
|
|
15
|
+
credential: string
|
|
16
16
|
}
|
|
@@ -6,7 +6,8 @@ import type { Context, TransitInput, Scheme } from './types'
|
|
|
6
6
|
export async function effect (input: Input, context: Context): Promise<Output | Error> {
|
|
7
7
|
const ctx: Ctx = {
|
|
8
8
|
trust: context.configuration.trust,
|
|
9
|
-
logs: context.logs
|
|
9
|
+
logs: context.logs,
|
|
10
|
+
fetch: context.fetch
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const claims = input.scheme === 'bearer'
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { load } from './jose'
|
|
2
2
|
import { createRemoteJWKSet } from './discovery'
|
|
3
3
|
import { ERR_TRUST, ERR_ISS, ERR_SUB } from './errors'
|
|
4
4
|
import type { Ctx } from './Ctx'
|
|
5
5
|
import type { Payload } from './Payload'
|
|
6
6
|
|
|
7
|
-
const jwks: Record<string, Awaited<ReturnType<typeof createRemoteJWKSet>>> = {}
|
|
8
|
-
|
|
9
7
|
export async function decode (token: string, ctx: Ctx): Promise<Payload | Error> {
|
|
8
|
+
const jose = await load()
|
|
10
9
|
const { iss, sub } = jose.decodeJwt(token)
|
|
11
10
|
|
|
12
11
|
if (typeof iss !== 'string')
|
|
@@ -20,9 +19,9 @@ export async function decode (token: string, ctx: Ctx): Promise<Payload | Error>
|
|
|
20
19
|
if (trusted === undefined)
|
|
21
20
|
return ERR_TRUST
|
|
22
21
|
|
|
23
|
-
jwks
|
|
22
|
+
const jwks = await createRemoteJWKSet(iss, ctx.fetch)
|
|
24
23
|
|
|
25
|
-
const { payload } = await jose.jwtVerify(token, jwks
|
|
24
|
+
const { payload } = await jose.jwtVerify(token, jwks, { audience: trusted.aud })
|
|
26
25
|
|
|
27
26
|
return payload as Payload
|
|
28
27
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createRemoteJWKSet, discover } from './discovery'
|
|
2
|
+
import { load } from './jose'
|
|
3
|
+
import type { Fetch } from '../types/context'
|
|
4
|
+
|
|
5
|
+
it('discovers an issuer using the component fetch', async () => {
|
|
6
|
+
const iss = `https://${Math.random().toString(36).slice(2)}.example.com`
|
|
7
|
+
const configuration = { issuer: iss, jwks_uri: `${iss}/jwks` }
|
|
8
|
+
|
|
9
|
+
const fetch = jest.fn(async (_input: string | URL | Request) =>
|
|
10
|
+
Response.json(configuration)) as jest.MockedFunction<Fetch>
|
|
11
|
+
|
|
12
|
+
await expect(discover(iss, fetch)).resolves.toMatchObject(configuration)
|
|
13
|
+
expect(fetch).toHaveBeenCalledWith(`${iss}/.well-known/openid-configuration`)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('loads and caches remote keys using the component fetch', async () => {
|
|
17
|
+
const jose = await load()
|
|
18
|
+
const iss = `https://${Math.random().toString(36).slice(2)}.example.com`
|
|
19
|
+
const jwksUri = `${iss}/jwks`
|
|
20
|
+
const { privateKey, publicKey } = await jose.generateKeyPair('ES256')
|
|
21
|
+
const jwk = await jose.exportJWK(publicKey)
|
|
22
|
+
|
|
23
|
+
const fetch = jest.fn(async (input: string | URL | Request) => {
|
|
24
|
+
const url = input instanceof Request ? input.url : input.toString()
|
|
25
|
+
|
|
26
|
+
if (url.endsWith('/.well-known/openid-configuration'))
|
|
27
|
+
return Response.json({ issuer: iss, jwks_uri: jwksUri })
|
|
28
|
+
|
|
29
|
+
return Response.json({ keys: [{ ...jwk, alg: 'ES256', kid: 'key0', use: 'sig' }] })
|
|
30
|
+
}) as jest.MockedFunction<Fetch>
|
|
31
|
+
|
|
32
|
+
const token = await new jose.SignJWT({ sub: 'subject' })
|
|
33
|
+
.setProtectedHeader({ alg: 'ES256', kid: 'key0' })
|
|
34
|
+
.setIssuer(iss)
|
|
35
|
+
.sign(privateKey)
|
|
36
|
+
|
|
37
|
+
const first = await createRemoteJWKSet(iss, fetch)
|
|
38
|
+
const second = await createRemoteJWKSet(iss, fetch)
|
|
39
|
+
|
|
40
|
+
expect(second).toBe(first)
|
|
41
|
+
await expect(jose.jwtVerify(token, first, { issuer: iss })).resolves.toMatchObject({
|
|
42
|
+
payload: { sub: 'subject' }
|
|
43
|
+
})
|
|
44
|
+
expect(fetch).toHaveBeenCalledTimes(2)
|
|
45
|
+
})
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { load } from './jose'
|
|
2
|
+
import type * as jose from 'jose'
|
|
2
3
|
import type { Configuration } from './Configuration'
|
|
4
|
+
import type { Fetch } from '../types/context'
|
|
3
5
|
|
|
4
6
|
const cache = new Map<string, Configuration>()
|
|
7
|
+
const resolvers = new WeakMap<Fetch, Map<string, Promise<jose.RemoteJWKSet>>>()
|
|
5
8
|
|
|
6
|
-
export async function discover (iss: string): Promise<Configuration> {
|
|
9
|
+
export async function discover (iss: string, fetch: Fetch): Promise<Configuration> {
|
|
7
10
|
if (!cache.has(iss)) {
|
|
8
|
-
const configuration = await fetchConfiguration(iss)
|
|
11
|
+
const configuration = await fetchConfiguration(iss, fetch)
|
|
9
12
|
|
|
10
13
|
cache.set(iss, configuration)
|
|
11
14
|
}
|
|
@@ -13,7 +16,7 @@ export async function discover (iss: string): Promise<Configuration> {
|
|
|
13
16
|
return cache.get(iss)!
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
async function fetchConfiguration (iss: string): Promise<Configuration> {
|
|
19
|
+
async function fetchConfiguration (iss: string, fetch: Fetch): Promise<Configuration> {
|
|
17
20
|
const response = await fetch(`${iss}/.well-known/openid-configuration`)
|
|
18
21
|
|
|
19
22
|
if (!response.ok)
|
|
@@ -22,9 +25,25 @@ async function fetchConfiguration (iss: string): Promise<Configuration> {
|
|
|
22
25
|
return await response.json() as Configuration
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export async function createRemoteJWKSet (iss: string): Promise<
|
|
26
|
-
|
|
27
|
-
const url = new URL(configuration.jwks_uri)
|
|
28
|
+
export async function createRemoteJWKSet (iss: string, fetch: Fetch): Promise<jose.RemoteJWKSet> {
|
|
29
|
+
let entries = resolvers.get(fetch)
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
if (entries === undefined) {
|
|
32
|
+
entries = new Map()
|
|
33
|
+
resolvers.set(fetch, entries)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!entries.has(iss))
|
|
37
|
+
entries.set(iss, create(iss, fetch))
|
|
38
|
+
|
|
39
|
+
return await entries.get(iss)!
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function create (iss: string, fetch: Fetch): Promise<jose.RemoteJWKSet> {
|
|
43
|
+
const jose = await load()
|
|
44
|
+
const configuration = await discover(iss, fetch)
|
|
45
|
+
|
|
46
|
+
return jose.createRemoteJWKSet(new URL(configuration.jwks_uri), {
|
|
47
|
+
[jose.customFetch]: fetch
|
|
48
|
+
})
|
|
30
49
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { load } from './jose'
|
|
2
2
|
import { createRemoteJWKSet, discover } from './discovery'
|
|
3
3
|
import * as errors from './errors'
|
|
4
4
|
import type { Trust } from '../types'
|
|
5
5
|
import type { Ctx } from './Ctx'
|
|
6
6
|
import type { Payload } from './Payload'
|
|
7
7
|
|
|
8
|
-
const jwks: Record<string, Awaited<ReturnType<typeof createRemoteJWKSet>>> = {}
|
|
9
|
-
|
|
10
8
|
export async function exchange (credentials: string, ctx: Ctx): Promise<Payload | Error> {
|
|
9
|
+
const jose = await load()
|
|
11
10
|
const properties = decode(credentials)
|
|
12
11
|
|
|
13
12
|
if (properties instanceof Error)
|
|
@@ -23,7 +22,7 @@ export async function exchange (credentials: string, ctx: Ctx): Promise<Payload
|
|
|
23
22
|
if (trusted.aud === undefined || (trusted.secret === undefined && trusted.signature === undefined))
|
|
24
23
|
return errors.ERR_CODE_NOT_ENABLED
|
|
25
24
|
|
|
26
|
-
const configuration = await discover(iss)
|
|
25
|
+
const configuration = await discover(iss, ctx.fetch)
|
|
27
26
|
|
|
28
27
|
if (configuration.token_endpoint === undefined)
|
|
29
28
|
return errors.ERR_CONFIG
|
|
@@ -47,7 +46,7 @@ export async function exchange (credentials: string, ctx: Ctx): Promise<Payload
|
|
|
47
46
|
code
|
|
48
47
|
})
|
|
49
48
|
|
|
50
|
-
const response = await fetch(configuration.token_endpoint, {
|
|
49
|
+
const response = await ctx.fetch(configuration.token_endpoint, {
|
|
51
50
|
method: 'POST',
|
|
52
51
|
headers: {
|
|
53
52
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
@@ -66,9 +65,9 @@ export async function exchange (credentials: string, ctx: Ctx): Promise<Payload
|
|
|
66
65
|
if (tokens.id_token === undefined)
|
|
67
66
|
return errors.ERR_NO_TOKEN
|
|
68
67
|
|
|
69
|
-
jwks
|
|
68
|
+
const jwks = await createRemoteJWKSet(iss, ctx.fetch)
|
|
70
69
|
|
|
71
|
-
const { payload } = await jose.jwtVerify(tokens.id_token, jwks
|
|
70
|
+
const { payload } = await jose.jwtVerify(tokens.id_token, jwks, {
|
|
72
71
|
audience: trusted.aud,
|
|
73
72
|
issuer: iss
|
|
74
73
|
})
|
|
@@ -92,6 +91,7 @@ function decode (credentials: string): Properties | Error {
|
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
async function sign (trust: Trust): Promise<string> {
|
|
94
|
+
const jose = await load()
|
|
95
95
|
const signature = trust.signature!
|
|
96
96
|
const aud = Array.isArray(trust.aud) ? trust.aud[0] : trust.aud!
|
|
97
97
|
const now = Math.floor(Date.now() / 1000)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function load (): Promise<typeof import('jose')>
|
|
@@ -4,6 +4,7 @@ import type { Entity } from './entity'
|
|
|
4
4
|
import type { Configuration } from './configuration'
|
|
5
5
|
|
|
6
6
|
export interface Context {
|
|
7
|
+
fetch: Fetch
|
|
7
8
|
local: {
|
|
8
9
|
observe: Observation<Entity>
|
|
9
10
|
enumerate: Observation<Entity[], never, Entity>
|
|
@@ -23,6 +24,8 @@ export interface Context {
|
|
|
23
24
|
configuration: Configuration
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
export type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>
|
|
28
|
+
|
|
26
29
|
export interface TransitInput {
|
|
27
30
|
readonly authority: string
|
|
28
31
|
readonly iss: string
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Transition = void 0;
|
|
4
|
-
const
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
class Transition {
|
|
6
6
|
async execute(input, object) {
|
|
7
|
-
object.key =
|
|
7
|
+
object.key = (0, node_crypto_1.randomBytes)(32).toString('base64url');
|
|
8
8
|
object.identity = input.identity;
|
|
9
9
|
object.label = input.label;
|
|
10
10
|
if (input.expires !== undefined)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.js","sourceRoot":"","sources":["../source/create.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"create.js","sourceRoot":"","sources":["../source/create.ts"],"names":[],"mappings":";;;AAAA,6CAAyC;AAGzC,MAAa,UAAU;IACd,KAAK,CAAC,OAAO,CAAE,KAAY,EAAE,MAAW;QAC7C,MAAM,CAAC,GAAG,GAAG,IAAA,yBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;QAClD,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAA;QAChC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAA;QAE1B,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;YAC7B,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;QAEhC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAA;IAChE,CAAC;CACF;AAXD,gCAWC"}
|