From 6376274658ce8ef1dead5016a4340481b4ae84f7 Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Sun, 21 Jun 2026 17:57:01 +0100 Subject: [PATCH 1/6] Add integrations settings flow --- .../server/src/auth/ServerSecretStore.test.ts | 20 + apps/server/src/auth/ServerSecretStore.ts | 132 +++- apps/server/src/integrations.test.ts | 75 +++ apps/server/src/integrations.ts | 188 ++++++ apps/server/src/serverSettings.test.ts | 56 ++ apps/server/src/serverSettings.ts | 164 ++++- apps/server/src/ws.ts | 10 + apps/web/src/components/Icons.tsx | 34 +- .../settings/IntegrationsSettings.tsx | 582 ++++++++++++++++++ .../settings/SettingsSidebarNav.tsx | 3 + apps/web/src/routeTree.gen.ts | 21 + apps/web/src/routes/settings.integrations.tsx | 7 + .../after/integrations-settings-after.mp4 | Bin 0 -> 13756 bytes .../after/integrations-settings-after.png | Bin 0 -> 72955 bytes packages/client-runtime/src/rpc/client.ts | 1 + packages/client-runtime/src/state/server.ts | 6 + packages/contracts/src/rpc.ts | 17 +- packages/contracts/src/settings.test.ts | 32 +- packages/contracts/src/settings.ts | 117 ++++ packages/shared/src/serverSettings.test.ts | 45 ++ packages/shared/src/serverSettings.ts | 3 +- 21 files changed, 1486 insertions(+), 27 deletions(-) create mode 100644 apps/server/src/integrations.test.ts create mode 100644 apps/server/src/integrations.ts create mode 100644 apps/web/src/components/settings/IntegrationsSettings.tsx create mode 100644 apps/web/src/routes/settings.integrations.tsx create mode 100644 artifacts/pr/8c79c383/after/integrations-settings-after.mp4 create mode 100644 artifacts/pr/8c79c383/after/integrations-settings-after.png diff --git a/apps/server/src/auth/ServerSecretStore.test.ts b/apps/server/src/auth/ServerSecretStore.test.ts index d4411fb9f3b..0534bcf8043 100644 --- a/apps/server/src/auth/ServerSecretStore.test.ts +++ b/apps/server/src/auth/ServerSecretStore.test.ts @@ -8,6 +8,7 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as PlatformError from "effect/PlatformError"; +import * as NodePath from "node:path"; import * as ServerConfig from "../config.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts"; @@ -225,6 +226,25 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { }).pipe(Effect.provide(NodeServices.layer)), ); + it.effect("encrypts secret contents at rest", () => + Effect.gen(function* () { + const serverConfig = yield* ServerConfig.ServerConfig; + const fileSystem = yield* FileSystem.FileSystem; + const secretStore = yield* ServerSecretStore.ServerSecretStore; + const plaintext = new TextEncoder().encode("super-secret-token"); + + yield* secretStore.set("session-signing-key", plaintext); + + const persisted = yield* fileSystem.readFile( + NodePath.join(serverConfig.secretsDir, "session-signing-key.bin"), + ); + + assert.notDeepEqual(Array.from(persisted), Array.from(plaintext)); + const roundTrip = yield* secretStore.get("session-signing-key"); + assert.deepEqual(Array.from(Option.getOrThrow(roundTrip)), Array.from(plaintext)); + }).pipe(Effect.provide(makeServerSecretStoreLayer())), + ); + it.effect("propagates read failures other than missing-file errors", () => Effect.gen(function* () { const secretStore = yield* ServerSecretStore.ServerSecretStore; diff --git a/apps/server/src/auth/ServerSecretStore.ts b/apps/server/src/auth/ServerSecretStore.ts index 5e9890c1ea2..f1d76084c24 100644 --- a/apps/server/src/auth/ServerSecretStore.ts +++ b/apps/server/src/auth/ServerSecretStore.ts @@ -8,6 +8,7 @@ import * as Path from "effect/Path"; import * as Predicate from "effect/Predicate"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; +import * as NodeCrypto from "node:crypto"; import * as ServerConfig from "../config.ts"; @@ -16,6 +17,41 @@ const secretStoreErrorContext = { cause: Schema.Defect(), }; +const SECRET_STORE_KEY_FILENAME = ".secret-store-key"; +const SECRET_STORE_KEY_BYTES = 32; +const SECRET_STORE_IV_BYTES = 12; +const SECRET_STORE_AUTH_TAG_BYTES = 16; +const SECRET_STORE_MAGIC = Buffer.from("T3S1"); + +function isEncryptedSecret(bytes: Uint8Array): boolean { + return Buffer.from(bytes.subarray(0, SECRET_STORE_MAGIC.length)).equals(SECRET_STORE_MAGIC); +} + +function encryptSecretBytes(key: Uint8Array, plaintext: Uint8Array): Uint8Array { + const iv = NodeCrypto.randomBytes(SECRET_STORE_IV_BYTES); + const cipher = NodeCrypto.createCipheriv("aes-256-gcm", Buffer.from(key), iv); + const ciphertext = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]); + const authTag = cipher.getAuthTag(); + return Uint8Array.from(Buffer.concat([SECRET_STORE_MAGIC, iv, authTag, ciphertext])); +} + +function decryptSecretBytes(key: Uint8Array, payload: Uint8Array): Uint8Array { + if (!isEncryptedSecret(payload)) { + return Uint8Array.from(payload); + } + + const bytes = Buffer.from(payload); + const ivStart = SECRET_STORE_MAGIC.length; + const authTagStart = ivStart + SECRET_STORE_IV_BYTES; + const ciphertextStart = authTagStart + SECRET_STORE_AUTH_TAG_BYTES; + const iv = bytes.subarray(ivStart, authTagStart); + const authTag = bytes.subarray(authTagStart, ciphertextStart); + const ciphertext = bytes.subarray(ciphertextStart); + const decipher = NodeCrypto.createDecipheriv("aes-256-gcm", Buffer.from(key), iv); + decipher.setAuthTag(authTag); + return Uint8Array.from(Buffer.concat([decipher.update(ciphertext), decipher.final()])); +} + export class SecretStoreSecureError extends Schema.TaggedErrorClass()( "SecretStoreSecureError", { @@ -149,6 +185,84 @@ export class ServerSecretStore extends Context.Service< } >()("t3/auth/ServerSecretStore") {} +const loadOrCreateEncryptionKey = Effect.fn("ServerSecretStore.loadOrCreateEncryptionKey")( + function* (keyPath: string) { + const crypto = yield* Crypto.Crypto; + const fileSystem = yield* FileSystem.FileSystem; + + const readKey = () => + fileSystem.readFile(keyPath).pipe( + Effect.map((bytes) => Uint8Array.from(bytes)), + Effect.catch((cause) => + cause.reason._tag === "NotFound" + ? Effect.fail(cause) + : Effect.fail( + new SecretStoreReadError({ + resource: `secret store key ${keyPath}`, + cause, + }), + ), + ), + ); + + const createKey = () => + crypto.randomBytes(SECRET_STORE_KEY_BYTES).pipe( + Effect.mapError( + (cause) => + new SecretStoreRandomGenerationError({ + resource: `secret store key ${keyPath}`, + cause, + }), + ), + Effect.flatMap((generated) => + Effect.scoped( + Effect.gen(function* () { + const file = yield* fileSystem.open(keyPath, { + flag: "wx", + mode: 0o600, + }); + yield* file.writeAll(generated); + yield* file.sync; + yield* fileSystem.chmod(keyPath, 0o600); + return Uint8Array.from(generated); + }), + ).pipe( + Effect.catch((cause) => + cause.reason._tag === "AlreadyExists" + ? readKey() + : Effect.fail( + new SecretStorePersistError({ + resource: `secret store key ${keyPath}`, + cause, + }), + ), + ), + ), + ), + ); + + return yield* readKey().pipe( + Effect.flatMap((key) => + key.length === SECRET_STORE_KEY_BYTES + ? Effect.succeed(key) + : Effect.fail( + new SecretStoreSecureError({ + resource: `secret store key ${keyPath}`, + cause: new Error( + `Expected ${SECRET_STORE_KEY_BYTES} key bytes, received ${key.length}.`, + ), + }), + ), + ), + Effect.catchIf( + (cause): cause is PlatformError.PlatformError => + Predicate.isTagged(cause, "PlatformError") && cause.reason._tag === "NotFound", + () => createKey(), + ), + ); + }, +); + export const make = Effect.gen(function* () { const crypto = yield* Crypto.Crypto; const fileSystem = yield* FileSystem.FileSystem; @@ -167,10 +281,20 @@ export const make = Effect.gen(function* () { ); const resolveSecretPath = (name: string) => path.join(serverConfig.secretsDir, `${name}.bin`); + const encryptionKeyPath = path.join(serverConfig.secretsDir, SECRET_STORE_KEY_FILENAME); + const encryptionKey = yield* loadOrCreateEncryptionKey(encryptionKeyPath).pipe( + Effect.mapError( + (cause) => + new SecretStoreSecureError({ + resource: `secret store key ${encryptionKeyPath}`, + cause, + }), + ), + ); const get: ServerSecretStore["Service"]["get"] = (name) => fileSystem.readFile(resolveSecretPath(name)).pipe( - Effect.map((bytes) => Option.some(Uint8Array.from(bytes))), + Effect.map((bytes) => Option.some(decryptSecretBytes(encryptionKey, bytes))), Effect.catch((cause) => cause.reason._tag === "NotFound" ? Effect.succeed(Option.none()) @@ -186,6 +310,7 @@ export const make = Effect.gen(function* () { const set: ServerSecretStore["Service"]["set"] = (name, value) => { const secretPath = resolveSecretPath(name); + const encryptedValue = encryptSecretBytes(encryptionKey, value); return crypto.randomUUIDv4.pipe( Effect.mapError( (cause) => @@ -197,7 +322,7 @@ export const make = Effect.gen(function* () { Effect.flatMap((uuid) => { const tempPath = `${secretPath}.${uuid}.tmp`; return Effect.gen(function* () { - yield* fileSystem.writeFile(tempPath, value); + yield* fileSystem.writeFile(tempPath, encryptedValue); yield* fileSystem.chmod(tempPath, 0o600); yield* fileSystem.rename(tempPath, secretPath); yield* fileSystem.chmod(secretPath, 0o600); @@ -223,13 +348,14 @@ export const make = Effect.gen(function* () { const create: ServerSecretStore["Service"]["create"] = (name, value) => { const secretPath = resolveSecretPath(name); + const encryptedValue = encryptSecretBytes(encryptionKey, value); return Effect.scoped( Effect.gen(function* () { const file = yield* fileSystem.open(secretPath, { flag: "wx", mode: 0o600, }); - yield* file.writeAll(value); + yield* file.writeAll(encryptedValue); yield* file.sync; yield* fileSystem.chmod(secretPath, 0o600); }), diff --git a/apps/server/src/integrations.test.ts b/apps/server/src/integrations.test.ts new file mode 100644 index 00000000000..8ad07b9bb26 --- /dev/null +++ b/apps/server/src/integrations.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { HttpClient, HttpClientResponse } from "effect/unstable/http"; + +import { type IntegrationAccountTokenValidationInput } from "@t3tools/contracts"; + +import { testIntegrationToken } from "./integrations.ts"; + +function makeHttpClient(response: Response) { + return HttpClient.make((request) => + Effect.succeed(HttpClientResponse.fromWeb(request, response)), + ); +} + +function provideHttpClient( + input: T, + response: Response, +) { + return testIntegrationToken(input).pipe( + Effect.provideService(HttpClient.HttpClient, makeHttpClient(response)), + ); +} + +describe("testIntegrationToken", () => { + it.effect("verifies GitHub user tokens", () => + Effect.gen(function* () { + const result = yield* provideHttpClient( + { kind: "github", apiKey: "ghp_test" }, + Response.json({ login: "octocat", name: "Monalisa Octocat", email: null }, { status: 200 }), + ); + + expect(result.accountLabel).toBe("Monalisa Octocat"); + }), + ); + + it.effect("verifies GitLab user tokens", () => + Effect.gen(function* () { + const result = yield* provideHttpClient( + { kind: "gitlab", apiKey: "glpat_test" }, + Response.json({ username: "octocat", name: "Monalisa Octocat" }, { status: 200 }), + ); + + expect(result.accountLabel).toBe("Monalisa Octocat"); + }), + ); + + it.effect("verifies Jira tokens against the configured base URL", () => + Effect.gen(function* () { + const result = yield* provideHttpClient( + { + kind: "jira", + baseUrl: "https://jira.example.test", + apiKey: "jira_token", + }, + Response.json( + { displayName: "Jira User", emailAddress: "jira@example.test" }, + { status: 200 }, + ), + ); + + expect(result.accountLabel).toBe("Jira User"); + }), + ); + + it.effect("verifies Linear API keys", () => + Effect.gen(function* () { + const result = yield* provideHttpClient( + { kind: "linear", apiKey: "lin_test" }, + Response.json({ data: { viewer: { name: "Linear User", email: null } } }, { status: 200 }), + ); + + expect(result.accountLabel).toBe("Linear User"); + }), + ); +}); diff --git a/apps/server/src/integrations.ts b/apps/server/src/integrations.ts new file mode 100644 index 00000000000..0126aab2b75 --- /dev/null +++ b/apps/server/src/integrations.ts @@ -0,0 +1,188 @@ +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; + +import { + INTEGRATION_DISPLAY_NAMES, + IntegrationAccountTokenValidationError, + type IntegrationAccountTokenValidationInput, + type IntegrationAccountTokenValidationResult, + TrimmedNonEmptyString, +} from "@t3tools/contracts"; + +const GitHubUserResponse = Schema.Struct({ + login: TrimmedNonEmptyString, + name: Schema.NullOr(TrimmedNonEmptyString), +}); + +const GitLabUserResponse = Schema.Struct({ + name: Schema.NullOr(TrimmedNonEmptyString), + username: TrimmedNonEmptyString, +}); + +const JiraCurrentUserResponse = Schema.Struct({ + displayName: TrimmedNonEmptyString, + emailAddress: Schema.NullOr(TrimmedNonEmptyString), +}); + +const LinearGraphQLError = Schema.Struct({ + message: TrimmedNonEmptyString, +}); + +const LinearViewerResponse = Schema.Struct({ + data: Schema.Struct({ + viewer: Schema.NullOr( + Schema.Struct({ + name: Schema.NullOr(TrimmedNonEmptyString), + email: Schema.NullOr(TrimmedNonEmptyString), + }), + ), + }), + errors: Schema.optional(Schema.Array(LinearGraphQLError)), +}); + +function validationError( + input: IntegrationAccountTokenValidationInput, + detail: string, + cause?: unknown, +) { + return new IntegrationAccountTokenValidationError({ + kind: input.kind, + detail, + ...(cause === undefined ? {} : { cause }), + }); +} + +function makeUrl(baseUrl: string, path: string): string { + return new URL(path, baseUrl).toString(); +} + +type IntegrationValidator = ( + input: IntegrationAccountTokenValidationInput, + httpClient: HttpClient.HttpClient, +) => Effect.Effect; + +const INTEGRATION_VALIDATORS: Record< + IntegrationAccountTokenValidationInput["kind"], + IntegrationValidator +> = { + github: (input, httpClient) => { + const request = HttpClientRequest.get("https://api.github.com/user").pipe( + HttpClientRequest.bearerToken(input.apiKey), + HttpClientRequest.acceptJson, + HttpClientRequest.setHeader("user-agent", "t3-code"), + HttpClientRequest.setHeader("x-github-api-version", "2022-11-28"), + ); + + return httpClient.execute(request).pipe( + Effect.flatMap(HttpClientResponse.filterStatusOk), + Effect.flatMap(HttpClientResponse.schemaBodyJson(GitHubUserResponse)), + Effect.mapError((cause) => + validationError( + input, + `Could not verify the ${INTEGRATION_DISPLAY_NAMES[input.kind]} token.`, + cause, + ), + ), + Effect.map((response) => ({ + accountLabel: response.name ?? response.login, + })), + ); + }, + gitlab: (input, httpClient) => { + const baseUrl = input.baseUrl ?? "https://gitlab.com"; + const request = HttpClientRequest.get(makeUrl(baseUrl, "/api/v4/user")).pipe( + HttpClientRequest.setHeader("private-token", input.apiKey), + HttpClientRequest.acceptJson, + ); + + return httpClient.execute(request).pipe( + Effect.flatMap(HttpClientResponse.filterStatusOk), + Effect.flatMap(HttpClientResponse.schemaBodyJson(GitLabUserResponse)), + Effect.mapError((cause) => + validationError( + input, + `Could not verify the ${INTEGRATION_DISPLAY_NAMES[input.kind]} token.`, + cause, + ), + ), + Effect.map((response) => ({ + accountLabel: response.name ?? response.username, + })), + ); + }, + jira: (input, httpClient) => { + if (input.baseUrl === undefined || input.baseUrl.length === 0) { + return Effect.fail( + validationError(input, "Jira requires a base URL before testing the token."), + ); + } + + const request = HttpClientRequest.get(makeUrl(input.baseUrl, "/rest/api/3/myself")).pipe( + HttpClientRequest.bearerToken(input.apiKey), + HttpClientRequest.acceptJson, + ); + + return httpClient.execute(request).pipe( + Effect.flatMap(HttpClientResponse.filterStatusOk), + Effect.flatMap(HttpClientResponse.schemaBodyJson(JiraCurrentUserResponse)), + Effect.mapError((cause) => + validationError( + input, + `Could not verify the ${INTEGRATION_DISPLAY_NAMES[input.kind]} token.`, + cause, + ), + ), + Effect.map((response) => ({ + accountLabel: response.displayName, + })), + ); + }, + linear: (input, httpClient) => { + const request = HttpClientRequest.post("https://api.linear.app/graphql").pipe( + HttpClientRequest.bearerToken(input.apiKey), + HttpClientRequest.acceptJson, + HttpClientRequest.bodyJsonUnsafe({ + query: "query Me { viewer { name email } }", + }), + ); + + return httpClient.execute(request).pipe( + Effect.flatMap(HttpClientResponse.filterStatusOk), + Effect.flatMap(HttpClientResponse.schemaBodyJson(LinearViewerResponse)), + Effect.mapError((cause) => + validationError( + input, + `Could not verify the ${INTEGRATION_DISPLAY_NAMES[input.kind]} token.`, + cause, + ), + ), + Effect.flatMap((response) => { + if ((response.errors?.length ?? 0) > 0) { + return Effect.fail( + validationError( + input, + response.errors?.[0]?.message ?? "Linear API returned an error.", + ), + ); + } + return Effect.succeed({ + accountLabel: + response.data.viewer?.name ?? response.data.viewer?.email ?? "Linear account", + }); + }), + ); + }, +}; + +export const testIntegrationToken = ( + input: IntegrationAccountTokenValidationInput, +): Effect.Effect< + IntegrationAccountTokenValidationResult, + IntegrationAccountTokenValidationError, + HttpClient.HttpClient +> => + Effect.gen(function* () { + const httpClient = yield* HttpClient.HttpClient; + return yield* INTEGRATION_VALIDATORS[input.kind](input, httpClient); + }); diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index 504d99e18de..77d908f4605 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -1,6 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { DEFAULT_SERVER_SETTINGS, + IntegrationAccountId, ProviderDriverKind, ProviderInstanceId, ServerSettings, @@ -589,4 +590,59 @@ it.layer(NodeServices.layer)("server settings", (it) => { ); }).pipe(Effect.provide(makeServerSettingsLayer())), ); + + it.effect("stores integration API keys outside settings.json and redacts them for clients", () => + Effect.gen(function* () { + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; + const serverConfig = yield* ServerConfig.ServerConfig; + const fileSystem = yield* FileSystem.FileSystem; + + const next = yield* serverSettings.updateSettings({ + integrations: { + github: [ + { + id: IntegrationAccountId.make("github_personal"), + name: "Personal", + apiKey: "ghp_secret", + apiKeyRedacted: true, + }, + ], + gitlab: [], + jira: [], + linear: [], + }, + }); + + assert.deepEqual(next.integrations.github[0], { + id: IntegrationAccountId.make("github_personal"), + name: "Personal", + apiKey: "ghp_secret", + apiKeyRedacted: true, + }); + + const raw = yield* fileSystem.readFileString(serverConfig.settingsPath); + assert.notInclude(raw, "ghp_secret"); + // @effect-diagnostics-next-line preferSchemaOverJson:off + assert.deepEqual(JSON.parse(raw).integrations.github, [ + { + id: IntegrationAccountId.make("github_personal"), + name: "Personal", + apiKey: "", + apiKeyRedacted: true, + }, + ]); + + assert.deepEqual( + ServerSettingsModule.redactServerSettingsForClient(next).integrations.github, + [ + { + id: IntegrationAccountId.make("github_personal"), + name: "Personal", + apiKey: "", + apiKeyRedacted: true, + }, + ], + ); + }).pipe(Effect.provide(makeServerSettingsLayer())), + ); }); diff --git a/apps/server/src/serverSettings.ts b/apps/server/src/serverSettings.ts index 4119a72640f..67664f515d6 100644 --- a/apps/server/src/serverSettings.ts +++ b/apps/server/src/serverSettings.ts @@ -15,6 +15,8 @@ import { DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER, DEFAULT_MODEL_BY_PROVIDER, DEFAULT_SERVER_SETTINGS, + type IntegrationAccount, + type IntegrationKind, isProviderDriverKind, type ModelSelection, type ProviderInstanceConfig, @@ -79,6 +81,13 @@ function providerEnvironmentSecretName(input: { return `provider-env-${Buffer.from(input.instanceId, "utf8").toString("base64url")}-${Buffer.from(input.name, "utf8").toString("base64url")}`; } +function integrationAccountSecretName(input: { + readonly kind: IntegrationKind; + readonly accountId: string; +}): string { + return `integration-${input.kind}-${input.accountId}`; +} + function redactProviderEnvironmentVariable( variable: ProviderInstanceEnvironmentVariable, ): ProviderInstanceEnvironmentVariable { @@ -93,6 +102,18 @@ function redactProviderEnvironmentVariable( }; } +function redactIntegrationAccount(account: IntegrationAccount): IntegrationAccount { + if (!account.apiKeyRedacted) { + const { apiKeyRedacted: _omit, ...rest } = account; + return rest; + } + return { + ...account, + apiKey: "", + ...(account.apiKey.length > 0 || account.apiKeyRedacted ? { apiKeyRedacted: true } : {}), + }; +} + export function redactServerSettingsForClient(settings: ServerSettings): ServerSettings { const providerInstances = Object.fromEntries( Object.entries(settings.providerInstances).map(([instanceId, instance]) => [ @@ -105,7 +126,13 @@ export function redactServerSettingsForClient(settings: ServerSettings): ServerS : instance, ]), ); - return { ...settings, providerInstances }; + const integrations = { + github: settings.integrations.github.map(redactIntegrationAccount), + gitlab: settings.integrations.gitlab.map(redactIntegrationAccount), + jira: settings.integrations.jira.map(redactIntegrationAccount), + linear: settings.integrations.linear.map(redactIntegrationAccount), + } satisfies ServerSettings["integrations"]; + return { ...settings, providerInstances, integrations }; } export class ServerSettingsService extends Context.Service< @@ -363,6 +390,52 @@ const make = Effect.gen(function* () { }; }); + const materializeIntegrationAccountSecrets = ( + settings: ServerSettings, + ): Effect.Effect => + Effect.gen(function* () { + const integrations: Record = { + github: [], + gitlab: [], + jira: [], + linear: [], + }; + + for (const [kind, accounts] of Object.entries(settings.integrations) as Array< + [IntegrationKind, IntegrationAccount[]] + >) { + const hydratedAccounts: IntegrationAccount[] = []; + for (const account of accounts) { + if (!account.apiKeyRedacted) { + hydratedAccounts.push(account); + continue; + } + const secret = yield* secretStore + .get(integrationAccountSecretName({ kind, accountId: account.id })) + .pipe( + Effect.mapError( + (cause) => + new ServerSettingsError({ + settingsPath, + operation: "read-secret", + cause, + }), + ), + ); + hydratedAccounts.push({ + ...account, + apiKey: Option.isSome(secret) ? textDecoder.decode(secret.value) : "", + }); + } + integrations[kind] = hydratedAccounts; + } + + return { + ...settings, + integrations: integrations as ServerSettings["integrations"], + }; + }); + const persistProviderEnvironmentSecrets = ( current: ServerSettings, next: ServerSettings, @@ -464,6 +537,85 @@ const make = Effect.gen(function* () { }; }); + const persistIntegrationAccountSecrets = ( + current: ServerSettings, + next: ServerSettings, + ): Effect.Effect => + Effect.gen(function* () { + const integrations: Record = { + github: [], + gitlab: [], + jira: [], + linear: [], + }; + + const nextSecretKeys = new Set(); + for (const [kind, accounts] of Object.entries(next.integrations) as Array< + [IntegrationKind, IntegrationAccount[]] + >) { + const persistedAccounts: IntegrationAccount[] = []; + for (const account of accounts) { + const secretName = integrationAccountSecretName({ kind, accountId: account.id }); + if (!account.apiKeyRedacted) { + yield* secretStore.remove(secretName).pipe( + Effect.mapError( + (cause) => + new ServerSettingsError({ + settingsPath, + operation: "remove-secret", + cause, + }), + ), + ); + persistedAccounts.push(redactIntegrationAccount(account)); + continue; + } + + nextSecretKeys.add(secretName); + if (account.apiKey.length > 0) { + yield* secretStore.set(secretName, textEncoder.encode(account.apiKey)).pipe( + Effect.mapError( + (cause) => + new ServerSettingsError({ + settingsPath, + operation: "write-secret", + cause, + }), + ), + ); + persistedAccounts.push({ ...account, apiKey: "", apiKeyRedacted: true }); + } else { + persistedAccounts.push(redactIntegrationAccount(account)); + } + } + integrations[kind] = persistedAccounts; + } + + for (const [kind, accounts] of Object.entries(current.integrations) as Array< + [IntegrationKind, IntegrationAccount[]] + >) { + for (const account of accounts) { + const secretName = integrationAccountSecretName({ kind, accountId: account.id }); + if (nextSecretKeys.has(secretName)) continue; + yield* secretStore.remove(secretName).pipe( + Effect.mapError( + (cause) => + new ServerSettingsError({ + settingsPath, + operation: "remove-stale-secret", + cause, + }), + ), + ); + } + } + + return { + ...next, + integrations: integrations as ServerSettings["integrations"], + }; + }); + const writeSettingsAtomically = Effect.fnUntraced( function* (settings: ServerSettings) { const sparseSettingsJson = yield* encodeServerSettingsJson( @@ -561,6 +713,7 @@ const make = Effect.gen(function* () { ready: Deferred.await(startedDeferred), getSettings: getSettingsFromCache.pipe( Effect.flatMap(materializeProviderEnvironmentSecrets), + Effect.flatMap(materializeIntegrationAccountSecrets), Effect.map(resolveTextGenerationProvider), ), updateSettings: (patch) => @@ -570,21 +723,24 @@ const make = Effect.gen(function* () { const nextPersisted = yield* persistProviderEnvironmentSecrets( current, applyServerSettingsPatch(current, patch), - ); + ).pipe(Effect.flatMap((settings) => persistIntegrationAccountSecrets(current, settings))); const next = yield* normalizeServerSettings(nextPersisted); yield* writeSettingsAtomically(next); yield* Cache.set(settingsCache, cacheKey, next); yield* emitChange(next); const materialized = yield* materializeProviderEnvironmentSecrets(next); - return resolveTextGenerationProvider(materialized); + const materializedWithIntegrations = + yield* materializeIntegrationAccountSecrets(materialized); + return resolveTextGenerationProvider(materializedWithIntegrations); }), ), get streamChanges() { return Stream.fromPubSub(changesPubSub).pipe( Stream.mapEffect((settings) => materializeProviderEnvironmentSecrets(settings).pipe( + Effect.flatMap(materializeIntegrationAccountSecrets), Effect.catch((error: ServerSettingsError) => - Effect.logWarning("failed to materialize provider environment secrets", { + Effect.logWarning("failed to materialize server settings secrets", { operation: error.operation, providerInstanceId: error.providerInstanceId, environmentVariable: error.environmentVariable, diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 554a942d78a..094c807fbef 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -77,6 +77,7 @@ import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; import * as ProviderMaintenanceRunner from "./provider/providerMaintenanceRunner.ts"; import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; +import * as Integrations from "./integrations.ts"; import * as ServerSettings from "./serverSettings.ts"; import * as TerminalManager from "./terminal/Manager.ts"; import * as PreviewAutomationBroker from "./mcp/PreviewAutomationBroker.ts"; @@ -289,6 +290,7 @@ const RPC_REQUIRED_SCOPE = new Map([ [WS_METHODS.serverRemoveKeybinding, AuthOrchestrationOperateScope], [WS_METHODS.serverGetSettings, AuthOrchestrationReadScope], [WS_METHODS.serverUpdateSettings, AuthOrchestrationOperateScope], + [WS_METHODS.serverTestIntegrationToken, AuthOrchestrationOperateScope], [WS_METHODS.serverDiscoverSourceControl, AuthOrchestrationReadScope], [WS_METHODS.serverGetTraceDiagnostics, AuthOrchestrationReadScope], [WS_METHODS.serverGetProcessDiagnostics, AuthOrchestrationReadScope], @@ -1226,6 +1228,14 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => "rpc.aggregate": "server", }, ), + [WS_METHODS.serverTestIntegrationToken]: (input) => + observeRpcEffect( + WS_METHODS.serverTestIntegrationToken, + Integrations.testIntegrationToken(input), + { + "rpc.aggregate": "server", + }, + ), [WS_METHODS.serverDiscoverSourceControl]: (_input) => observeRpcEffect( WS_METHODS.serverDiscoverSourceControl, diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx index 8ea38c51958..0444b844bb1 100644 --- a/apps/web/src/components/Icons.tsx +++ b/apps/web/src/components/Icons.tsx @@ -15,6 +15,15 @@ export const GitHubIcon: Icon = (props) => ( ); +export const LinearIcon: Icon = (props) => ( + + + +); + export const GitIcon: Icon = (props) => ( { }; export const GitLabIcon: Icon = (props) => ( - - - - - + + + +); + +export const JiraIcon: Icon = (props) => ( + + ); diff --git a/apps/web/src/components/settings/IntegrationsSettings.tsx b/apps/web/src/components/settings/IntegrationsSettings.tsx new file mode 100644 index 00000000000..138e2548c5e --- /dev/null +++ b/apps/web/src/components/settings/IntegrationsSettings.tsx @@ -0,0 +1,582 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + INTEGRATION_DEFINITIONS, + INTEGRATION_DISPLAY_NAMES, + INTEGRATION_KINDS, + IntegrationAccountId, + type IntegrationAccount, + type IntegrationKind, +} from "@t3tools/contracts"; +import { CheckIcon, KeyRoundIcon, PencilIcon, PlusIcon, Trash2Icon } from "lucide-react"; + +import { usePrimaryEnvironment } from "~/state/environments"; +import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; +import { useAtomCommand } from "~/state/use-atom-command"; +import { serverEnvironment } from "~/state/server"; +import { toastManager } from "../ui/toast"; +import { GitHubIcon, GitLabIcon, JiraIcon, LinearIcon } from "../Icons"; +import { Button } from "../ui/button"; +import { + Dialog, + DialogDescription, + DialogFooter, + DialogHeader, + DialogPopup, + DialogTitle, +} from "../ui/dialog"; +import { Input } from "../ui/input"; +import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsLayout"; + +const INTEGRATION_ICON_BY_KIND: Record = { + github: GitHubIcon, + gitlab: GitLabIcon, + jira: JiraIcon, + linear: LinearIcon, +}; + +function slugifyAccountName(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") + .slice(0, 48); +} + +function nextAvailableAccountId( + kind: IntegrationKind, + name: string, + existingAccounts: readonly IntegrationAccount[], +): string { + const slug = slugifyAccountName(name); + if (slug.length === 0) return ""; + + const base = `${kind}_${slug}`; + if (!existingAccounts.some((account) => account.id === base)) { + return base; + } + + let suffix = 2; + while (existingAccounts.some((account) => account.id === `${base}_${suffix}`)) { + suffix += 1; + } + return `${base}_${suffix}`; +} + +function isDuplicateAccountName( + accounts: readonly IntegrationAccount[], + name: string, + excludeId?: string, +): boolean { + const normalized = name.trim().toLowerCase(); + return accounts.some( + (account) => account.id !== excludeId && account.name.trim().toLowerCase() === normalized, + ); +} + +interface AccountDialogState { + readonly kind: IntegrationKind; + readonly account: IntegrationAccount | null; +} + +function AccountDialog({ + state, + existingAccounts, + onSave, + onCancel, +}: { + state: AccountDialogState; + existingAccounts: readonly IntegrationAccount[]; + onSave: (account: IntegrationAccount) => void; + onCancel: () => void; +}) { + const environmentId = usePrimaryEnvironment()?.environmentId ?? null; + const testIntegrationToken = useAtomCommand(serverEnvironment.testIntegrationToken, { + reportFailure: false, + }); + const definition = INTEGRATION_DEFINITIONS[state.kind]; + const requiresBaseUrl = definition.baseUrlRequired === true; + const steps = requiresBaseUrl ? ["Name", "Site", "Token", "Review"] : ["Name", "Token", "Review"]; + + const [step, setStep] = useState(0); + const [name, setName] = useState(state.account?.name ?? ""); + const [baseUrl, setBaseUrl] = useState(state.account?.baseUrl ?? ""); + const [apiKey, setApiKey] = useState(""); + const [error, setError] = useState(null); + const [isTesting, setIsTesting] = useState(false); + + const isEdit = state.account !== null; + const preserveSavedToken = + isEdit && state.account?.apiKeyRedacted === true && apiKey.trim().length === 0; + const preserveSavedBaseUrl = + isEdit && (baseUrl.trim().length === 0 || baseUrl.trim() === (state.account?.baseUrl ?? "")); + + useEffect(() => { + setStep(0); + setName(state.account?.name ?? ""); + setBaseUrl(state.account?.baseUrl ?? ""); + setApiKey(""); + setError(null); + setIsTesting(false); + }, [state.account, state.kind]); + + const validateName = useCallback(() => { + const trimmedName = name.trim(); + if (trimmedName.length === 0) { + setError("Account name is required."); + return null; + } + if (isDuplicateAccountName(existingAccounts, trimmedName, state.account?.id)) { + setError(`An account named "${trimmedName}" already exists.`); + return null; + } + return trimmedName; + }, [existingAccounts, name, state.account?.id]); + + const validateBaseUrl = useCallback(() => { + if (!requiresBaseUrl) return null; + const trimmedBaseUrl = baseUrl.trim(); + if (trimmedBaseUrl.length === 0) { + if (state.account?.baseUrl !== undefined) { + return state.account.baseUrl; + } + setError(`${definition.baseUrlLabel ?? "Base URL"} is required.`); + return null; + } + try { + return new URL(trimmedBaseUrl).toString(); + } catch { + setError(`${definition.baseUrlLabel ?? "Base URL"} must be a valid URL.`); + return null; + } + }, [baseUrl, definition.baseUrlLabel, requiresBaseUrl]); + + const handleNext = useCallback(() => { + setError(null); + if (step === 0) { + if (validateName() !== null) { + setStep(1); + } + return; + } + if (requiresBaseUrl && step === 1) { + if (validateBaseUrl() !== null) { + setStep(2); + } + return; + } + if ((requiresBaseUrl && step === 2) || (!requiresBaseUrl && step === 1)) { + if (apiKey.trim().length === 0 && !preserveSavedToken) { + setError(`${definition.tokenLabel} is required.`); + return; + } + setStep(requiresBaseUrl ? 3 : 2); + } + }, [ + apiKey, + definition.tokenLabel, + preserveSavedToken, + requiresBaseUrl, + step, + validateBaseUrl, + validateName, + ]); + + const handleSave = useCallback(async () => { + const trimmedName = validateName(); + if (trimmedName === null) { + setStep(0); + return; + } + + const normalizedBaseUrl = validateBaseUrl(); + if (requiresBaseUrl && normalizedBaseUrl === null) { + setStep(1); + return; + } + + const trimmedKey = apiKey.trim(); + if (!preserveSavedToken && trimmedKey.length === 0) { + setError(`${definition.tokenLabel} is required.`); + setStep(requiresBaseUrl ? 2 : 1); + return; + } + + if (environmentId === null) { + setError("Unable to validate integration tokens without an active environment."); + return; + } + + try { + setIsTesting(true); + if (!preserveSavedToken) { + const result = await testIntegrationToken({ + environmentId, + input: { + kind: state.kind, + ...(normalizedBaseUrl !== null ? { baseUrl: normalizedBaseUrl } : {}), + apiKey: trimmedKey, + }, + }); + + if (result._tag !== "Success") { + throw new Error("Could not verify the token."); + } + + toastManager.add({ + type: "success", + title: `${INTEGRATION_DISPLAY_NAMES[state.kind]} token verified`, + description: `Connected to ${result.value.accountLabel}.`, + }); + } + + const existing = state.account; + const nextId = + existing?.id ?? + IntegrationAccountId.make( + nextAvailableAccountId(state.kind, trimmedName, existingAccounts), + ); + onSave({ + id: nextId, + name: trimmedName, + ...(normalizedBaseUrl !== null + ? { baseUrl: normalizedBaseUrl } + : preserveSavedBaseUrl && existing?.baseUrl !== undefined + ? { baseUrl: existing.baseUrl } + : {}), + apiKey: trimmedKey, + ...(trimmedKey.length > 0 || preserveSavedToken ? { apiKeyRedacted: true } : {}), + }); + } catch (cause) { + const message = cause instanceof Error ? cause.message : "Could not verify the token."; + setError(message); + setStep(requiresBaseUrl ? 2 : 1); + toastManager.add({ + type: "error", + title: `${INTEGRATION_DISPLAY_NAMES[state.kind]} token check failed`, + description: message, + }); + } finally { + setIsTesting(false); + } + }, [ + apiKey, + definition.tokenLabel, + environmentId, + existingAccounts, + onSave, + preserveSavedBaseUrl, + preserveSavedToken, + requiresBaseUrl, + state.account, + state.kind, + validateBaseUrl, + validateName, + ]); + + const submitLabel = preserveSavedToken + ? "Save changes" + : isEdit + ? "Test token & save" + : "Test token & add account"; + + const currentStepLabel = steps[step] ?? steps[steps.length - 1] ?? "Review"; + + return ( + !open && onCancel()}> + + + {`${isEdit ? "Edit" : "Add"} ${INTEGRATION_DISPLAY_NAMES[state.kind]} account`} + {definition.accountHint} +
+ {steps.map((label, index) => { + const active = index === step; + const complete = index < step; + return ( + + ); + })} +
+ + +
+ {step === 0 ? ( + + ) : null} + + {requiresBaseUrl && step === 1 ? ( + + ) : null} + + {(requiresBaseUrl ? step === 2 : step === 1) ? ( + + ) : null} + + {(requiresBaseUrl ? step === 3 : step === 2) ? ( +
+

+ {preserveSavedToken + ? "We’ll keep the existing encrypted key and save these changes." + : "We’ll test this token from the server before saving the account."} +

+
+
+
Integration
+
+ {INTEGRATION_DISPLAY_NAMES[state.kind]} +
+
+
+
Account
+
+ {name.trim() || "Untitled account"} +
+
+ {requiresBaseUrl ? ( +
+
Base URL
+
{baseUrl.trim() || "Not set"}
+
+ ) : null} +
+
+ ) : null} + + {error ?

{error}

: null} +
+ + +
+ + {step > 0 ? ( + + ) : null} +
+ {currentStepLabel === "Review" ? ( + + ) : ( + + )} +
+ + + ); +} + +export function IntegrationsSettingsPanel() { + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); + const [activeDialog, setActiveDialog] = useState(null); + + const sections = useMemo( + () => + INTEGRATION_KINDS.map((kind) => ({ + kind, + accounts: settings.integrations[kind] ?? [], + })), + [settings.integrations], + ); + + const handleSaveAccount = useCallback( + (kind: IntegrationKind, account: IntegrationAccount) => { + const nextAccounts = (() => { + const currentAccounts: readonly IntegrationAccount[] = settings.integrations[kind] ?? []; + const existingIndex = currentAccounts.findIndex( + (candidate: IntegrationAccount) => candidate.id === account.id, + ); + if (existingIndex === -1) { + return [...currentAccounts, account]; + } + return currentAccounts.map((candidate: IntegrationAccount) => + candidate.id === account.id ? account : candidate, + ); + })(); + + void updateSettings({ + integrations: { + ...settings.integrations, + [kind]: nextAccounts, + }, + }); + setActiveDialog(null); + }, + [settings.integrations, updateSettings], + ); + + const handleDeleteAccount = useCallback( + (kind: IntegrationKind, accountId: string) => { + const currentAccounts: readonly IntegrationAccount[] = settings.integrations[kind] ?? []; + void updateSettings({ + integrations: { + ...settings.integrations, + [kind]: currentAccounts.filter((account: IntegrationAccount) => account.id !== accountId), + }, + }); + }, + [settings.integrations, updateSettings], + ); + + return ( + +
+

Integrations

+
+ + {sections.map(({ kind, accounts }) => { + const Icon = INTEGRATION_ICON_BY_KIND[kind]; + const definition = INTEGRATION_DEFINITIONS[kind]; + return ( + } + headerAction={ + + } + > + {accounts.length === 0 ? ( +
+ No {INTEGRATION_DISPLAY_NAMES[kind]} accounts yet. +
+ ) : ( +
+ {accounts.map((account) => ( + + + +
+ } + > +
+ + + {INTEGRATION_DISPLAY_NAMES[kind]} account + +
+ + ))} + + )} +
+ ); + })} + + {activeDialog ? ( + handleSaveAccount(activeDialog.kind, account)} + onCancel={() => setActiveDialog(null)} + /> + ) : null} +
+ ); +} diff --git a/apps/web/src/components/settings/SettingsSidebarNav.tsx b/apps/web/src/components/settings/SettingsSidebarNav.tsx index 6774b6f333f..c6f51d74d9d 100644 --- a/apps/web/src/components/settings/SettingsSidebarNav.tsx +++ b/apps/web/src/components/settings/SettingsSidebarNav.tsx @@ -6,6 +6,7 @@ import { GitBranchIcon, KeyboardIcon, Link2Icon, + PuzzleIcon, Settings2Icon, } from "lucide-react"; import { useCanGoBack, useNavigate } from "@tanstack/react-router"; @@ -26,6 +27,7 @@ export type SettingsSectionPath = | "/settings/general" | "/settings/keybindings" | "/settings/providers" + | "/settings/integrations" | "/settings/source-control" | "/settings/connections" | "/settings/archived"; @@ -40,6 +42,7 @@ export const SETTINGS_NAV_ITEMS: ReadonlyArray<{ { label: "Providers", to: "/settings/providers", icon: BotIcon }, { label: "Source Control", to: "/settings/source-control", icon: GitBranchIcon }, { label: "Connections", to: "/settings/connections", icon: Link2Icon }, + { label: "Integrations", to: "/settings/integrations", icon: PuzzleIcon }, { label: "Archive", to: "/settings/archived", icon: ArchiveIcon }, ]; diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 3a9140e278c..4969a31fea0 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -16,6 +16,7 @@ import { Route as ChatIndexRouteImport } from './routes/_chat.index' import { Route as SettingsSourceControlRouteImport } from './routes/settings.source-control' import { Route as SettingsProvidersRouteImport } from './routes/settings.providers' import { Route as SettingsKeybindingsRouteImport } from './routes/settings.keybindings' +import { Route as SettingsIntegrationsRouteImport } from './routes/settings.integrations' import { Route as SettingsGeneralRouteImport } from './routes/settings.general' import { Route as SettingsDiagnosticsRouteImport } from './routes/settings.diagnostics' import { Route as SettingsConnectionsRouteImport } from './routes/settings.connections' @@ -57,6 +58,11 @@ const SettingsKeybindingsRoute = SettingsKeybindingsRouteImport.update({ path: '/keybindings', getParentRoute: () => SettingsRoute, } as any) +const SettingsIntegrationsRoute = SettingsIntegrationsRouteImport.update({ + id: '/integrations', + path: '/integrations', + getParentRoute: () => SettingsRoute, +} as any) const SettingsGeneralRoute = SettingsGeneralRouteImport.update({ id: '/general', path: '/general', @@ -97,6 +103,7 @@ export interface FileRoutesByFullPath { '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute '/settings/general': typeof SettingsGeneralRoute + '/settings/integrations': typeof SettingsIntegrationsRoute '/settings/keybindings': typeof SettingsKeybindingsRoute '/settings/providers': typeof SettingsProvidersRoute '/settings/source-control': typeof SettingsSourceControlRoute @@ -110,6 +117,7 @@ export interface FileRoutesByTo { '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute '/settings/general': typeof SettingsGeneralRoute + '/settings/integrations': typeof SettingsIntegrationsRoute '/settings/keybindings': typeof SettingsKeybindingsRoute '/settings/providers': typeof SettingsProvidersRoute '/settings/source-control': typeof SettingsSourceControlRoute @@ -126,6 +134,7 @@ export interface FileRoutesById { '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute '/settings/general': typeof SettingsGeneralRoute + '/settings/integrations': typeof SettingsIntegrationsRoute '/settings/keybindings': typeof SettingsKeybindingsRoute '/settings/providers': typeof SettingsProvidersRoute '/settings/source-control': typeof SettingsSourceControlRoute @@ -143,6 +152,7 @@ export interface FileRouteTypes { | '/settings/connections' | '/settings/diagnostics' | '/settings/general' + | '/settings/integrations' | '/settings/keybindings' | '/settings/providers' | '/settings/source-control' @@ -156,6 +166,7 @@ export interface FileRouteTypes { | '/settings/connections' | '/settings/diagnostics' | '/settings/general' + | '/settings/integrations' | '/settings/keybindings' | '/settings/providers' | '/settings/source-control' @@ -171,6 +182,7 @@ export interface FileRouteTypes { | '/settings/connections' | '/settings/diagnostics' | '/settings/general' + | '/settings/integrations' | '/settings/keybindings' | '/settings/providers' | '/settings/source-control' @@ -236,6 +248,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsKeybindingsRouteImport parentRoute: typeof SettingsRoute } + '/settings/integrations': { + id: '/settings/integrations' + path: '/integrations' + fullPath: '/settings/integrations' + preLoaderRoute: typeof SettingsIntegrationsRouteImport + parentRoute: typeof SettingsRoute + } '/settings/general': { id: '/settings/general' path: '/general' @@ -300,6 +319,7 @@ interface SettingsRouteChildren { SettingsConnectionsRoute: typeof SettingsConnectionsRoute SettingsDiagnosticsRoute: typeof SettingsDiagnosticsRoute SettingsGeneralRoute: typeof SettingsGeneralRoute + SettingsIntegrationsRoute: typeof SettingsIntegrationsRoute SettingsKeybindingsRoute: typeof SettingsKeybindingsRoute SettingsProvidersRoute: typeof SettingsProvidersRoute SettingsSourceControlRoute: typeof SettingsSourceControlRoute @@ -310,6 +330,7 @@ const SettingsRouteChildren: SettingsRouteChildren = { SettingsConnectionsRoute: SettingsConnectionsRoute, SettingsDiagnosticsRoute: SettingsDiagnosticsRoute, SettingsGeneralRoute: SettingsGeneralRoute, + SettingsIntegrationsRoute: SettingsIntegrationsRoute, SettingsKeybindingsRoute: SettingsKeybindingsRoute, SettingsProvidersRoute: SettingsProvidersRoute, SettingsSourceControlRoute: SettingsSourceControlRoute, diff --git a/apps/web/src/routes/settings.integrations.tsx b/apps/web/src/routes/settings.integrations.tsx new file mode 100644 index 00000000000..641a3036c6f --- /dev/null +++ b/apps/web/src/routes/settings.integrations.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { IntegrationsSettingsPanel } from "../components/settings/IntegrationsSettings"; + +export const Route = createFileRoute("/settings/integrations")({ + component: IntegrationsSettingsPanel, +}); diff --git a/artifacts/pr/8c79c383/after/integrations-settings-after.mp4 b/artifacts/pr/8c79c383/after/integrations-settings-after.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..33403918f9bb304908dc4124ee3ffbc0e8ead70b GIT binary patch literal 13756 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�Ln&U|` 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 SYM|l*I$0&n%T&ed_x=w;K;5kX literal 0 HcmV?d00001 diff --git a/packages/client-runtime/src/rpc/client.ts b/packages/client-runtime/src/rpc/client.ts index 92892431e45..a222528082e 100644 --- a/packages/client-runtime/src/rpc/client.ts +++ b/packages/client-runtime/src/rpc/client.ts @@ -61,6 +61,7 @@ export type EnvironmentStreamRpcTag = | EnvironmentStreamCommandRpcTag; export type EnvironmentUnaryRpcTag = Exclude; + const isRpcClientError = Schema.is(RpcClientError.RpcClientError); export type EnvironmentRpcInput = Parameters>[0]; diff --git a/packages/client-runtime/src/state/server.ts b/packages/client-runtime/src/state/server.ts index eb784183793..94b2c96b434 100644 --- a/packages/client-runtime/src/state/server.ts +++ b/packages/client-runtime/src/state/server.ts @@ -186,6 +186,12 @@ export function createServerEnvironmentAtoms( scheduler: configScheduler, concurrency: configConcurrency, }), + testIntegrationToken: createEnvironmentRpcCommand(runtime, { + label: "environment-data:server:test-integration-token", + tag: WS_METHODS.serverTestIntegrationToken, + scheduler: configScheduler, + concurrency: configConcurrency, + }), signalProcess: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:signal-process", tag: WS_METHODS.serverSignalProcess, diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index a2a8e9106aa..c0402a0baa5 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -130,7 +130,14 @@ import { ServerUpsertKeybindingInput, ServerUpsertKeybindingResult, } from "./server.ts"; -import { ServerSettings, ServerSettingsError, ServerSettingsPatch } from "./settings.ts"; +import { + IntegrationAccountTokenValidationError, + IntegrationAccountTokenValidationInput, + IntegrationAccountTokenValidationResult, + ServerSettings, + ServerSettingsError, + ServerSettingsPatch, +} from "./settings.ts"; import { SourceControlCloneRepositoryInput, SourceControlCloneRepositoryResult, @@ -207,6 +214,7 @@ export const WS_METHODS = { serverRemoveKeybinding: "server.removeKeybinding", serverGetSettings: "server.getSettings", serverUpdateSettings: "server.updateSettings", + serverTestIntegrationToken: "server.testIntegrationToken", serverDiscoverSourceControl: "server.discoverSourceControl", serverGetTraceDiagnostics: "server.getTraceDiagnostics", serverGetProcessDiagnostics: "server.getProcessDiagnostics", @@ -283,6 +291,12 @@ export const WsServerUpdateSettingsRpc = Rpc.make(WS_METHODS.serverUpdateSetting error: Schema.Union([ServerSettingsError, EnvironmentAuthorizationError]), }); +export const WsServerTestIntegrationTokenRpc = Rpc.make(WS_METHODS.serverTestIntegrationToken, { + payload: IntegrationAccountTokenValidationInput, + success: IntegrationAccountTokenValidationResult, + error: Schema.Union([IntegrationAccountTokenValidationError, EnvironmentAuthorizationError]), +}); + export const WsServerDiscoverSourceControlRpc = Rpc.make(WS_METHODS.serverDiscoverSourceControl, { payload: Schema.Struct({}), success: SourceControlDiscoveryResult, @@ -687,6 +701,7 @@ export const WsRpcGroup = RpcGroup.make( WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, WsServerUpdateSettingsRpc, + WsServerTestIntegrationTokenRpc, WsServerDiscoverSourceControlRpc, WsServerGetTraceDiagnosticsRpc, WsServerGetProcessDiagnosticsRpc, diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index aba97cbe205..7e173072a6e 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -2,7 +2,12 @@ import { describe, expect, it } from "vite-plus/test"; import * as Schema from "effect/Schema"; import { ProviderInstanceId } from "./providerInstance.ts"; -import { DEFAULT_SERVER_SETTINGS, ServerSettings, ServerSettingsPatch } from "./settings.ts"; +import { + DEFAULT_SERVER_SETTINGS, + INTEGRATION_DISPLAY_NAMES, + ServerSettings, + ServerSettingsPatch, +} from "./settings.ts"; const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings); const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch); @@ -64,6 +69,19 @@ describe("ServerSettings.providerInstances (slice-2 invariant)", () => { }); }); +describe("ServerSettings.integrations", () => { + it("defaults to empty account arrays for GitHub and Linear", () => { + expect(DEFAULT_SERVER_SETTINGS.integrations).toEqual({ + github: [], + gitlab: [], + jira: [], + linear: [], + }); + expect(INTEGRATION_DISPLAY_NAMES.github).toBe("GitHub"); + expect(INTEGRATION_DISPLAY_NAMES.linear).toBe("Linear"); + }); +}); + describe("ServerSettings worktree defaults", () => { it("defaults start-from-origin off for legacy configs", () => { expect(decodeServerSettings({}).newWorktreesStartFromOrigin).toBe(false); @@ -106,6 +124,18 @@ describe("ServerSettingsPatch.providerInstances", () => { }); }); +describe("ServerSettingsPatch.integrations", () => { + it("treats integrations as an optional whole-map replacement", () => { + const patch = decodeServerSettingsPatch({}); + expect(patch.integrations).toBeUndefined(); + + const replacement = decodeServerSettingsPatch({ + integrations: { github: [], gitlab: [], jira: [], linear: [] }, + }); + expect(replacement.integrations).toEqual({ github: [], gitlab: [], jira: [], linear: [] }); + }); +}); + describe("ServerSettingsPatch string normalization", () => { it("trims string settings while decoding patches", () => { const patch = decodeServerSettingsPatch({ diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 7ba267b1e72..00a66cba438 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -39,6 +39,119 @@ export const SidebarThreadPreviewCount = Schema.Int.check( export type SidebarThreadPreviewCount = typeof SidebarThreadPreviewCount.Type; export const DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT: SidebarThreadPreviewCount = 6; +// ── Integrations ──────────────────────────────────────────────────── + +export const IntegrationKind = Schema.Literals(["github", "gitlab", "jira", "linear"]); +export type IntegrationKind = typeof IntegrationKind.Type; + +export const INTEGRATION_KINDS = [ + "github", + "gitlab", + "jira", + "linear", +] as const satisfies readonly IntegrationKind[]; + +export const INTEGRATION_DISPLAY_NAMES: Record = { + github: "GitHub", + gitlab: "GitLab", + jira: "Jira", + linear: "Linear", +}; + +export interface IntegrationDefinition { + readonly accountPlaceholder: string; + readonly accountHint: string; + readonly tokenLabel: string; + readonly baseUrlLabel?: string | undefined; + readonly baseUrlPlaceholder?: string | undefined; + readonly baseUrlRequired?: boolean | undefined; +} + +export const INTEGRATION_DEFINITIONS: Record = { + github: { + accountPlaceholder: "Personal", + accountHint: "Connect a GitHub personal access token.", + tokenLabel: "personal access token", + }, + gitlab: { + accountPlaceholder: "Work", + accountHint: "Connect a GitLab personal access token.", + tokenLabel: "personal access token", + }, + jira: { + accountPlaceholder: "Issue tracking", + accountHint: "Connect a Jira API token for issue tracking.", + tokenLabel: "API token", + baseUrlLabel: "Jira URL", + baseUrlPlaceholder: "https://your-domain.atlassian.net", + baseUrlRequired: true, + }, + linear: { + accountPlaceholder: "Design", + accountHint: "Connect a Linear API token.", + tokenLabel: "API token", + }, +}; + +const INTEGRATION_ACCOUNT_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/; +const integrationAccountIdSchema = TrimmedNonEmptyString.check( + Schema.isMaxLength(64), + Schema.isPattern(INTEGRATION_ACCOUNT_ID_PATTERN), +); + +export const IntegrationAccountId = integrationAccountIdSchema.pipe( + Schema.brand("IntegrationAccountId"), +); +export type IntegrationAccountId = typeof IntegrationAccountId.Type; + +export const IntegrationAccount = Schema.Struct({ + id: IntegrationAccountId, + name: TrimmedNonEmptyString, + baseUrl: Schema.optionalKey(TrimmedString), + apiKey: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed(""))), + apiKeyRedacted: Schema.optionalKey(Schema.Boolean), +}); +export type IntegrationAccount = typeof IntegrationAccount.Type; + +export const IntegrationsSettings = Schema.Struct({ + github: Schema.Array(IntegrationAccount).pipe(Schema.withDecodingDefault(Effect.succeed([]))), + gitlab: Schema.Array(IntegrationAccount).pipe(Schema.withDecodingDefault(Effect.succeed([]))), + jira: Schema.Array(IntegrationAccount).pipe(Schema.withDecodingDefault(Effect.succeed([]))), + linear: Schema.Array(IntegrationAccount).pipe(Schema.withDecodingDefault(Effect.succeed([]))), +}); +export type IntegrationsSettings = typeof IntegrationsSettings.Type; + +export const DEFAULT_INTEGRATIONS_SETTINGS: IntegrationsSettings = Schema.decodeSync( + IntegrationsSettings, +)({}); + +export const IntegrationAccountTokenValidationInput = Schema.Struct({ + kind: IntegrationKind, + baseUrl: Schema.optionalKey(TrimmedString), + apiKey: TrimmedNonEmptyString, +}); +export type IntegrationAccountTokenValidationInput = + typeof IntegrationAccountTokenValidationInput.Type; + +export const IntegrationAccountTokenValidationResult = Schema.Struct({ + accountLabel: TrimmedNonEmptyString, +}); +export type IntegrationAccountTokenValidationResult = + typeof IntegrationAccountTokenValidationResult.Type; + +export class IntegrationAccountTokenValidationError extends Schema.TaggedErrorClass()( + "IntegrationAccountTokenValidationError", + { + kind: IntegrationKind, + detail: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Integration token validation failed for ${INTEGRATION_DISPLAY_NAMES[this.kind]}: ${this.detail}`; + } +} + export const ClientSettingsSchema = Schema.Struct({ autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), @@ -408,6 +521,9 @@ export const ServerSettings = Schema.Struct({ providerInstances: Schema.Record(ProviderInstanceId, ProviderInstanceConfig).pipe( Schema.withDecodingDefault(Effect.succeed({})), ), + integrations: IntegrationsSettings.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_INTEGRATIONS_SETTINGS)), + ), observability: ObservabilitySettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), }); export type ServerSettings = typeof ServerSettings.Type; @@ -530,6 +646,7 @@ export const ServerSettingsPatch = Schema.Struct({ // patches risk leaving driver-specific config in a half-merged state. // The web UI sends a fully-formed map every time it edits this field. providerInstances: Schema.optionalKey(Schema.Record(ProviderInstanceId, ProviderInstanceConfig)), + integrations: Schema.optionalKey(IntegrationsSettings), }); export type ServerSettingsPatch = typeof ServerSettingsPatch.Type; diff --git a/packages/shared/src/serverSettings.test.ts b/packages/shared/src/serverSettings.test.ts index 5bec7d386b6..2a07cf27ffb 100644 --- a/packages/shared/src/serverSettings.test.ts +++ b/packages/shared/src/serverSettings.test.ts @@ -1,5 +1,6 @@ import { DEFAULT_SERVER_SETTINGS, + IntegrationAccountId, ProviderDriverKind, ProviderInstanceId, } from "@t3tools/contracts"; @@ -194,4 +195,48 @@ describe("serverSettings helpers", () => { config: { homePath: "~/.codex" }, }); }); + + it("replaces integration account arrays instead of merging them by index", () => { + const current = { + ...DEFAULT_SERVER_SETTINGS, + integrations: { + github: [ + { + id: IntegrationAccountId.make("github_personal"), + name: "Personal", + apiKey: "secret", + apiKeyRedacted: true, + }, + ], + gitlab: [], + jira: [], + linear: [], + }, + }; + + expect( + applyServerSettingsPatch(current, { + integrations: { + github: [ + { + id: IntegrationAccountId.make("github_work"), + name: "Work", + apiKey: "", + apiKeyRedacted: true, + }, + ], + gitlab: [], + jira: [], + linear: [], + }, + }).integrations.github, + ).toEqual([ + { + id: IntegrationAccountId.make("github_work"), + name: "Work", + apiKey: "", + apiKeyRedacted: true, + }, + ]); + }); }); diff --git a/packages/shared/src/serverSettings.ts b/packages/shared/src/serverSettings.ts index 1bbf466f60b..465bb9e232b 100644 --- a/packages/shared/src/serverSettings.ts +++ b/packages/shared/src/serverSettings.ts @@ -76,13 +76,14 @@ export function applyServerSettingsPatch( patch: ServerSettingsPatch, ): ServerSettings { const selectionPatch = patch.textGenerationModelSelection; - const { automaticGitFetchInterval, ...patchForMerge } = patch; + const { automaticGitFetchInterval, integrations, ...patchForMerge } = patch; const next = deepMerge(current, patchForMerge); const nextWithReplacements = { ...next, ...(patch.providerInstances !== undefined ? { providerInstances: patch.providerInstances } : {}), + ...(integrations !== undefined ? { integrations } : {}), ...(automaticGitFetchInterval !== undefined ? { automaticGitFetchInterval } : {}), }; if (!selectionPatch) { From c7ab73118301f417af1467776fbe46569fdbabb5 Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Sun, 21 Jun 2026 18:06:43 +0100 Subject: [PATCH 2/6] Replace integrations video with wizard screenshot --- .../after/integrations-settings-after.mp4 | Bin 13756 -> 0 bytes .../after/integrations-wizard-after.png | Bin 0 -> 59447 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 artifacts/pr/8c79c383/after/integrations-settings-after.mp4 create mode 100644 artifacts/pr/8c79c383/after/integrations-wizard-after.png diff --git a/artifacts/pr/8c79c383/after/integrations-settings-after.mp4 b/artifacts/pr/8c79c383/after/integrations-settings-after.mp4 deleted file mode 100644 index 33403918f9bb304908dc4124ee3ffbc0e8ead70b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13756 zcmeHuXH*o;((saV5G4u{6afi>X77F7!^z9t z4FVyA>_dE&!q-#QzBXcR9ui`Bdas+iyDtO+@ptoev?ahJ`(@s|yXXHP(WkFO2!tDg zXZXSMmG+kgp7Kju`TK=`0|@c*dAvQXUGV*m-Y#F|6YcZSwyt*jJOp}fPFL;m$RS@hN85b@zWyk@w%__VY(4GvdG^bGKaG#4E7y0NfZogd ziYvZ>hh8>#_@m(4-d;Z%cz6ikAMjIqAI0<7xc@}!#PjS=V!!MQs@-4tudQ$GpC^ac z5?)6d_}=}xUkIpFE~<%fsk!5)?eLBC%b5iLJcZuF+RI~~|9d@j#oN>S7fwFBWHka+ z2tSyC(DUJ8sj|Qgq6zh3Vq#))0thkxONCJUB?oQXT|KYbxccJ<+yCJUznia}KOS;< zdwzxcv>%f8@gKzzZ^`>grFXOcs`!2SKHmkrg?(KR?|nKUglnHp@O5a&A#ZzI?{6rc z`mNyQ_*^A`5Ru@U`&}M*GsPF8j}XWy27G06v$fv8J1J5U;j;$KTEF>V8RkMZCBA)8u06*cmv^AC7vB-v?fb2{=!*VS-3{Ih-9PoI& zG?F-j`>1695%EOYuxyS&vte6YIFE{cFO46%bn1)WyCC9I55jZfP9#1_FBbinh5p2J zQ$SL0wCTBi$A|@5H!4&KF5KNfK$Et-t-L)hEHFXi1q=!G$qpe^A2g6DD5Yzm%Vqtu zwF38?Z2dumPI}oe`~v^lvv+T;iD}Zc2(n9k9^XqnH6lGjX$bEMpTnBNm5-U|y+E8!x3tqGc;7q1#di2PtW*2B78PBl)px zhmpW|-CuYNgeZ6E`xaV`%zFn_FOEy3ZJhK&qqd(M6`XEq=cKS*bAgABnv}m5HAUL1 zlUAEV(M{dR2u-Dp^R|cZIMs1)CreIVi6+h)dQ=jh0D**(E81OkU?U=gfywQlI8Z-4 z({nxc9&(l4@Q!KH$qxt8i4(v_uufnc>h*Mk?d=V_b0y`33-5KRA-gBGALkfykr67V z(F`^VrC-J`SBw_5PsRY)efaba#2PW5Iku6bIACZP9j19mp))Iody;L;cDCK)hr zKh*aF_oxL;-96~h(UL|6@(JfodoC%(9N)uv#x{o@pUgSh?UqL&XHNahIZ@pY9nNU8 z311FQ!=xx)GhPp^(s%@Mj--gx)tKTgcL|^7Hx$%I?lv&1mNzZ^2S-*jsuhF!c0mP5 zT2(oI^#9!FUwgv;_F~0wJ^zu=Uzrz{c8XfxX8RvZAk3;UyjOZK{gHN}^E=%GCaiL_ zwfcQW8jtlr<@sAV>G~=k-GHzcF2_pMk3N=iAZ{oQ4S{nwneA04P3J+g;@Yp1}Q><)Xd>Gnv%s13(6D_aQnID+)fzP%B1f|Fg1HshAx zO?kIdEDv)kCKfacKU%#4GY9EX+;HrCDKXe9-p| za!$u#=F*A+pV7v%*Jg2hUyb^kY5!+uML)dKzdOZ`D>sROUcMRRRx>wq?M6YRg4!WZ ztHIO7q)97!AF10|LZ}IQSen{Sk4(cWpH_hR%MbM3NOzhEXh|@8^F=y#tZfWb+Fu1cOt#+`m zaCBV5MT`ur?ml~Pkg{D`PV6=!hj`IGfXPT_B!50a8v@Qri!P{IY|7B3$%wDT+!OLW zaN31vUX&{veee90qH=F^AkNBlI*ziPaq*?ZS(Nhqg6)CK@x$u_C8Kb_@12qLdnB zd*St*MRt-6P6q5INiSP?TNFWO#Wdtbhr2v3@gvpGW8~*G0I$7 zl`5x;nsG}Va(nGdy9QgdwNVT=ElSw6jh;Qj_P?1HZdiS)oXSNtoq-JFCM6^Fk%0J! zy;T>iQRK}bs8Eel^Ac;GJSdh>^JNFtGbS9;a}CR#Eb=O8NORk-@}&~lF>h50r{lq= zZ?;qCFx)@;@T$w6>Pw2aaPv|zMwSrV7v3a)J-Fy~irTTzVfO~gXRaHkGr*SuXOV&S zh@F|L**6lI&u$W(U*7`mw3sMhd~JM$R_ zQ|H*BCw34QeUBaGuspKTXqHQZ?!lE>yzS0R=j;%r`k@Htv^rq^8;948-GpqrWTyra8H z8RwCje>Py+G~1-w6McyyzuWgthc>!%3qjs0qP7P-~1@ciEe1{WPhd>+3*vdtLwK-%yJZ5*z8U} zr%6JrAU>>A)dqIJv4*!!r#I$ic4vlJnoVcUH5XrBgrH+Kj@OBIi0i*mx+>VRr)4Dd zF+s>{Q{-KCwZ}%Lrnpzw9uFr)hp8@Me;sSK4Zl!fS+34s^as|1O`Dkq&vM|mWprC- zY$DXnk*w7D_lB^?)mNjXxwCGR?tKIs>O!N9^4eFl_r^rYt+dO16b>&aM4GmBnGG4k zLY*%&D4aYl0Klr%OF3Sr?45j?PM-r4`hC0UL=tbM1!Oe_WMg~uXM= zG;z<0F{Gn2Gq0AmKr+hcEO~t1<-0ky`%FlY6J7B-8gEntkPRxz^BXY)rKuGj99N$e zNFQ3O58}Do6kU2^TyH3~@P^I8aFo z>km|8F1#eUdcXoTiR}1Tq%I%M3{fUMebht8z3oly3$9PN>AQmt^EBOgvu~Oii|>sy zlZ2$TmxRtq$#VC4Hq#A>@{J;&b0~*StwpYDZ8XadJO@_`_xz$!%^9x;Jvq_XRjuy9 z*P%F8z*C`_@ooaS*ZMIEaaQUv^=;Rt^fjWRMcY~*guE|V6&)5`^F!enI_G(>`XRyn z#}i_xQRi0#heTxAM5RTpb4w14IBB=5*1bsZm1T!eqldko-e)cc%A~V>uJ?8IVpCL+ zi^G|*9oNe&U^&u_fMN=8HP0u)i!jdPGsyBSX8}Uzh3gIH$;~)_NhW`2>YD$abo`Jo z0O*DTL-$^Q9nSp(|BorndlPdu&JRW4RAoXaWwFC^&61FywBD}0yjj~NF;Ri6PNI1C zTaVq`o<4A?o*o=)Oov|dVe<)yOVH@fKtGn~B#cEau?OOwyj(j(OuY{O zII=2S-OMRMfvQ@{IxU1Xu2^72E`e-BNzT!jqc$rJRS=&-Up=g7jak`3YwYRy^N)*W zN;1zU8F)XpwTK_wwTA<#g;%?yJLl$SzjXCiK{x56Z;2JxZmS7%)LXX(L_MZoC5}^9 zid&spOv3RcGEPf+x^cv+oP5W+0*Nj?4BETYjdAl-8yYhwi#oFy1k$KnD zEcXQvhUz+2DjQEz5b5O2=%ZT!CuV_I0b)F+9nEQtsOXzn6ZsnS3M;13CL`47P!GHO zb8%^;?h8+f)rXJJs5y=tC~>Mktz(&w#RVCqY!gJIusmlhH_BQf7NB#xv9W{fXq~kY zMHyPfAgt+2#Jo&rd3{>o;K3H$+Y;%dA)MEPAdsC>{ZjHa+WkwJrslf;#PirO;ds>; zVrRxT^l;&phttfY08+h+6#l_hPV7m^8WwIc}`%dCyury8e)-Q)?7 zvX>^9KC{~!u3wl8CNq)urVLv==Ass|JaO|mUrf>vM={OyLmd9JdHP+wBcHGKG%~R# zSQ_lD;_l*#+%2E!-R#j!@jDQhy6KtP%~rUYR;kYK>YD}5ff`LjQE`;G%#Uo^jgxnldv*R;Lk$W%d&Fsf$0)k~M% z+=(n^@)VJ7Wp*#QHe9?c9*+W8$)Sg?%kWPPf(ToH&<#o zS37;^92NQ3yGs~}-|`SdMHHUUw&k@vLi<6x;J$Hv*#-_%ok?$NQ_fd#csvv7_ju&< zylSD(c?2y(tLA<3u9*BZguU|n#{gP0Kb$Rt@wBd8asL1*OoAZ=w9ec@8%51v73uOh z)>@F)pz*Waz(ecr?5koM_C~ve)b%!F$f+yjCHbz=BS#=6pLfYJs053^Q?pYd>S@jx z8GYr&)pH`Hk3s^KmoTBPTH=+OZql6N4qDfRj*CC9it(a{AI%`^h^QMSze|5SS(GTl ziZ0@P>?pl7DoM<8uh%w}zJ+e9r9?|PO{FaMvfPcLTqtCKKYW+<;nqUaDqjHX=>^%D z>YnG0j+Bj09rWY+H{0~`gymjY#i?JpozL`{tn3~Ow>_^w_?bpHZtg4stnwO znsyz@b9At=_W>A|m&}RvLMo0RuzlEqWU;XX7H!GD|c4mW^ ziHR25dW#|=+$`#HvL6YYh&!%Zux0=ngZQ>i4%U6O7Vph(4`x4`4ghz_J&=emQ_v8} zmGGwIsQ|xjFz&vG58V4l>Hf3bpAPVGa|U-0m*eoi9^0%+=@R27)|PxOm=F^HwJ8@h zJS}(oTrHcu=@^Yb{FDdg`dz_ZXAfe7txm~{Wp@vY$EvI6?2xzz1Sd(x`^C`O!rL@X z)T;tAHJwJdJFEbu7)$X&Vd-;W`K7M=+s$cx$t3hzo`M(aq#RS}wAu$m&HLy?&E z-MHbXY)my%cnWg>T~mo*ZXo>n``c5UhC?6ePbniGYMS08eRtO=mi_STxswX_Ok_rj zaTtvGax8LKMXeH0OoUQ2VJ3Udr?QDZyR1ubcbzZb=j|=%0CZzefJ;_uqx65n7T>6O zJ^LgM9|4Kv*Fp{{Vi^?2LDT!n3dsDctXKrC$dHKD>8mT1rsh|pKuCk}qlYVR6$lUy=$w(`;D2PhDNb_Ccg{^LWZBl~;W8&hsZO zH#29Z;usp|32RKJUp^KntI?yvg+H0p2(dFTpVOOJJfaPQYsikgy=}t4tzkVQAT_^4 z%w)28U(5*!ByIes+>uY7$`4&J?a|zIm2=z)&y>yNtV-r3ii#*Q@@AoYp{{(%?Ao-{ zUJBd*a`1v7+KrF;Y##6E>oDPmIEEWVP7)O?>Kj)o7o0fACJk@Ctqo7m&Q)3tvMD?k zEWIeSSk5`aPmsI*0h0d2;DN@)D7VxYR3WWgE2oDePBB6EZjb5`4WB;feoK%W zgqY!{KKY@;=!nnV8GWWf8!i=2=d{>{i# z0`4j94!)1(Q>+|EybN%}F?GZ??NMj-LPFvdA0sAKv5lRK=?lG=33 z>F%N%?Ey42$}t639v=00qymQaw$`(c2yW-1B!5y#_GwReu&ZO{_=ZR?ZR=+)6zRuF z-nREu*P%)sm|d)N+%DYa3yhKubPPFA)pwoB?&Fj0JO0Zy;OD04w@_MGk;+;e%|peO z*-HYU;aZ&I$rat3COmlDI(WdZwLJA8kCjGteao?D6bHh!CX%NH0z_X`ISff3)lqX6ZGZicUx?kMe1kVF~<}Gf>vqAP-TnVZBOYF+-aPZn<|7XO;;c zI9w5Jx`E-P=Wp2Si~|LRw1V{Kz#_|?df~~}x0#{}9=#A{ax883wXmN3M6d2_Du&5Q zfsL|~4wXIjTMD&C3?|%v-84^cej9~!&(;?Tdns8`xoH7k(qDpVMR~TaB zp;3kfF=5x?s}Cdzs@o!pebG`*5!lY+N7F8l_BP3D!!!Ee9xM<^CpEq^zIW*|j2W!;lW30dtHOMw$ctXh*uEKgV z>C@n$jfK6@M`dE=xJB|6HqvNm7KOB!(%QMUyhW-y3!%|dR?585fwN($d?$j?bC*-K zaz{?SpFUK%Ye`{=Afe1cc&YDC8Ps zFNCJ|E)!#Ar9~TCM?a$tEp zDi3|?#S8DUR!WY{Na|j zDFTD`2pVc7^LMP4z#bzpe|WU`^gM>_;;>a@L}@GW?S-4nR^ec@jaH*qrioCQUiuF8g7r{hXmKuG z^)?wi%)`lJUiFN|rnW(stFfEIp(lO#7jKx$2a5&ka$gia-{A&HU^OdyFE--a5@h|p z5)CM@huAmsLCb=3qAt!q#f{(hn%M0xfX4inEcQ-Y)g$p6}R2 zq1MUHr#4V;Y#rjO%{f^;LF_Xc%k?VLT9(&8fX}u=RJC4UZs?i57$Oj{j&8L0Yz%Fx zx=2pnzCt`CO?alg%Wph@OlP~?-qwP;u~l;T#NyR3V&C-^Pxaa>qjET%dg*jK*eWGm zt=s)5b7OSe$`Lwd<|B(eo0XoimWZ4-waRgfiu1A((T5{a`m)w@NiRa!<{Rex2fNvh zWUpV~d!-WAE*~n$jeqNeY73IBAc2ZVXHgPc(qtD{xaekn$kv5^K9u$hWf=%oWFX=# z^6we$G!D=_f;tRfbSF5zjr0d@RE~jV26WvR%G~4m%PpZz)>{}F`ynSRy2^^ z2?-CK6A>C(K4J0B_^kMrrJ=o3ITRp=y-emC&2v82favL`^lc|oGH8w2l@xOc<*p_X z(D$Uj?4ocnYaqtxC)|P8hhQe&EnbJcq|13|6$;;e$8_PgKNXHO+vjAAXiwn*f+y8{ z>+yS>OBOZAgvs{`w=T&c+0Ngx(qY4v50YLjc*D38v^%mPbDMyyyYp;NB^(*1a za?zArSAJgCEG#w*>UZv*nE2 zqg8atw`ddabI|#Qqp&cn;-eP6f6Q{vzx&J|##n0o-uL_P`;-cR^{7Y)%-Og9Z@a+q zKR961hdSU6-lwtEX{}y)LM2iEsrG~;sdW5d$exI^6A!DyEyl~~SxkfnilnLY6v)oV zrp2W^TZhMZ)J4cpz3Ch%d!!bN-k4G$SGQrcd~+5R=m33PEs6~cT?KB-09NPuC|q*4E}wt>p-WuGLiQwFtHsd#TNl_^~DLJzPI&F`hNziM9yv60XYM1AQK(0A52|TZt z(A!aht@hN$u#9X~n$sEuupJTV&>bQ57o}n`+#o(;3Bh7CatnRCB!)TAxY_I=f@!VO zQNF7rMCbB8hdC@LJ>Qk5pANA&Z*?-#+Q(k?T%gruj??^;ZSsT*T_$T_FH?h2 z^5l?*UL`}0BC>~Pn=tcqGJ?d4Y4(Ye$}#b{KsFFc_Cbb#!$4iB!#_~DoWJ_jon-5S z!8oPFQ-^4loYTGRSaz+)Dj$#^b)_!P6($~0e??ne_y{Ap8)#UZga7CK69U^O^3|>i zzZ=cZMurd9JKoLt%4$i2EAyzwAyuks#apydm`McU?=2a;_Q#!b@$cS-ed1VvB42cH zE@cC-Cocy37vUS+3N)pgi>*;*=`eT~A_KPS;Yd#WONQUG!p{^a77kt(ufJvq`0m}h zfACIwJzN%iO9W-LyK?43)BfE9B|1dWp7WQ|vvHw^KCvVP&eN ze%SSnrP;149I-W34|w0r)S!y*g>>%?L1s)IH3lQKjP&d){9xK}4GlL1NZ;7Wd)vxx z!DSPr3U7x&v;=|ZW4_psZl?3)fc|1#+cg|^=oJWMjnS?eUm3sHG5jUL&tct5!EGHt zG{R?0?0F>9(IE#xDg0}`k}unq(+F7+fy ziF~AbDXuHd*k$8}eD{d+5c}(nM`5g-dMgin--*EYUa2@HmkYOt#3`(7J!u-NdCfv8 z#hN-+p2xQKma8jfwXOh=KNgFhK4##o_&(LtRw}l*xk>-!6;WpRWhID4POs+iyB|e{ zsHCLN!T|ISEAVebS{&!=bWRuoi9%zxZzh0_ReGSd+6NWk#2rEJ$o)CueKT0wZwC8^ z;L*Hy7R)T7AEf@{mSA@B+Y}Xlnd!gT^!sLgLw`y!uqD2X>}`$V<=dfg%M|aEy8$2F zTu!mv=*v)Qh=d)_CqwOo(F!6YUia2Wv;=YxF8^!(&GDJRG;c5{?`Mo9+O1gu$ zkJre7OepaYBIl8~Kh5py?O6gWSCsUQ%-VYJ2MhKWIp8MtFj`&zKpvRyJ+WH;HLPmZ zv!<9;x@0+%^-}J3W*?&oY=;Yr_w+Ug}-FEBtAoed;RfPR%3+me(v{@kR zG$-1x4es16j&Kj5+!A&y;sXTjGZqX9woXNwH|VN)>`713edfsQqT*UyAu45>V-y$R z+_NNz^!J)q)$y~^BlCQqqb$Te7urnLqDUMVDllwXfg|{mb@#|3Nvtq}h`&cHR{=Fm zkUspHQ>BluuQNw%oQQ3KDa`tEA!sLk)@9H9)foS{F4tL?WpLq!PggAxq&Q>qIX~9B zp1L)~6>OG0gYtW`%EERIPTPB47EE(~h7vtndPM4a-B=N)5)GRXh{Fz<%H5}uH7>^4 z4Vj@u1B434fkO{sTk8;C&zlk@UR21ihq*}WnGWSR=7j9kC@}Lm7zD!p#S*N~_KQD( zMe-XN8l0_LxuyqZL>P{-p}ja@0hsE+A*Ya)E{Xn20Grwdoe?fqv+%)4FA1s-X7x4W zwl8%m76Ey&`)saYnm8NFiy+r*83#4TAFiL}3xU19_x42tuCXr9S`LZ<;%(u8!ZR?w zYT~1VQCPmu5DeB?8&Al=ao#iG{r+OH-{RKBX8#fSwe~_wV`=-3^ltjhRu3#TG}s`oRO(qXEoYkpCjA1?Cm^n*?@a^u80dr5@HNV+9js=sQ) z>_zVv82F+@Um<>Aw^&a7ef4M7F9phS?;3GPtQsWJ-?aErX3O}U1Bl7kMIqk_@r_$x z1^+r}#zI=MeH={Tx*RPR0!Y5^W1Wb$5NBU0*DpEzBNbt@%4kfF&9>EK5dsGyc z0Q!{}zZ{;XKpj?8^UyiW*K=77-VHd2C|6lGPvKzV0u~C8o!TcK8G;DCHI2Wi=3ku^ ze~(nxkYG%g?i1%WOrU<=*}-=gF(@2`<;_6e4pA!Yz6Q21#ZV)&d)b5Bq4@tdcuF}M z2T}sUoA;nkh1FJ|kO(3iLwsYIAMDgD$_=Bo<)%{tFMWmMw!)8LzFgBfeu>R|as+|; z0Jt8)R`1pdUtT}T(MW*!&^qshQQf@-&a~rA&EfpS9@rOQ@EcC zjHE{Au#Vqrz_EuwfZwg}SIQqYG!^Tbpy$9C7#h<3$Jb54*nCSI`N?li`=<~R{JYM7 zqkQN2!}9#bYW}yBKMDVP?%$%!e}(_1lD}2;&z%2N`+s0$KWp^A2KTcW{rmWT4etMw z!GUFRYC)AynjlR#AF*-B@i%YEHta>iCI*~vy3-u@*AT!|g&LxtJZofh5Y{$!U8)ea z%_gyC_s*0mh1nD^(lhf!T6HR}DKVf$-X5mXN{3bzl|vqdvb@_^+)kx^Sn>XjnPb{O z>Kos~k}Dt+K}_E{`Vu#VKCG|T;`M9V`om-R_Z`=-oP`xx!~}pMfQ4nWqpEpO5sLh@ z2v)Iz?AcqBC`1y_fFVAA;9Q>(KEVOcFin&_XvTp+3bGBsB(xNlb#OOO0IOJMzQk+k z%}M(=BL0oZgg`;N8MWq%;7$lRgq-yu{-X{;Qb+th)Q_SF#q_a1kiD}1$E93}9w^sP zD2=uq6V!pQlnL=EJaz})j~)Hj75qVIJn`(V+Jj@qAjjg}(MI5RzGrvz zv>_1R%MeI#7zDBlnu2E`5Kl=6WX=i#k^2OJu)C%p9x71A+Nx@+LLjZzTkpE6Lm*6$ zyXvY2-YE+NzqgmH>ke1OEHxB4TrbKGbx7SPm&ZD8YQSE0l8zt4(tDrcxyZ*fbynqO zCewqrZ!}axQq)ug`Ocq<*5vyo7;={NW+caLI+5Swg8UraVxzRPw+@sxhXVbl^K$bG zkOg~x+V|o+pI7A~z?W_QDA~aH5H$cOf-R~KlCO{rSak2-KVfar$Y&L^nT5dmz}DZZi5$c=rq!`X zZAOz%cl6)SeugruGVnn?R?q@xA53x-MH6Pn)KlktVK5jJ3az)fsFCP+Trl#ok;w4C zn#S5lf6Z5yH|KZ_8rR0qxXd97@7|IIf4NOuuJy9zT-jLRLzkYkBNl$JQ$!2Caa|;d z77akya8XfFTGs}NktaAsJdRDqju^(!Y8r;jpg74@$Ch7rA0~)RpSkRgvL+c{-ED7P zV*z`bt^cKgE>bI0xq5Q2@Nv0xi(F%LI5G=H=J3+C=vBY}fE%j-ttvP(lGcO?4?o<8 z!>#V?>Y4`5M{oypNt9F+b0znhYpr3HK1MA{d16c}gcso*8qJH}SPs2e{6=#uQo$k( zu6cZD0!~yRiAsg}D`JxOvBr;i@yC zhit496neQOra& zyC3pW2>AnLX4aZq_KbL1y zG#w8VOJ?I&@Dm#v#lc<1h**oPcByD6;dV7!>|ZgM|3k}o9ogXYHrWrbZ&e0rghHOL; zi^H|HxAKanl4nOH(WV6}IU49>?vsFgchvFC(;w@&n_}+3Wu8_KrQ!elT6Db7NE1)3 zJm8PF^4=_T!m^ZOUkaHhkTt5~&YdL!x{hvWK-jRyA{S;;Ra8p#%JEm1;wh~U%LgrqRGqBo- zpnK9{@qWUQM{GXpAc_uU@UpdYH;cRKa~0)GhcCm@8^JqtS86d!wR1dT(X`>+Jl0;4 zZxpE(g%{Jm_9*4ylnQZa`-`p;E>i&~GA#FCS%?HS;VZ8ET!7v?nPs$F@(SlOs``u` zdz<}{mbb0*0FBOavv2r$uC#NKl3U^Sm`gYi+Im#ev}{mA@QF% zzQxmeg-rH}-9CL9+j5HB)V#Lp*L&lki7SDxw!HCvSe7<+2Bo-V7P)78g?5x5(>1TH z`g`Vu%T^C7`S@0PMV|3RK#HW9$eU#=1OMxm7WSG^BlO=Bm0y^^d{GJI&Rk+M-)ezO zg+E^Y?Ip}Z_ZGgNsmCKT^Ey|=qc!wIiQ2<)3hzanZ&yz;MkKmKOk~3D#(C*g6fUd> zx>b}Xam1vs5Y?`55*zieEkzJ6qt>EoEGtUmOVn2OPk^C%d6&_hcgS&ZS|Xphw7IXO z$?Tfn4YQ?biE8b}nWfLZz~rh6cF$bzhI!3uHVxHeQglbc!qdbyU8PEN)tqu%uLK8{AbH@xy`gL)?76g zR5@ckwO&Mfk)9lNq6>uy)S_QZr$sJ@yjsGC{$-exA~&%(x)qt7(GqcF{#BBF712e} zLI^?A$-p=kB4unaZ2f5%4fS8e069s;{GXqIry%r~@qpj|=_>xGnYJ7DSD`}AL+W^l ziW|($xK6z^GKz%#gfyK7_3dZ0Ef?BEKGIC#_^k?Ba zq{L#nc9xo!-KtTP>Gq%vEdAqEyEk;GLI)iYMk=H=x{4~KZ^e{P-!+Cq@$npc*(vY3Is&A6FuoFQY8>^- zEE`AglCV9;Rn2oX*EINwS3KVc*^ye4|05TjE}0Iv9!*iSG?Kw)VFh$WB4WY=qw@}f zC6*;6?d|P|9*$n+A0}pTNF}&h34y7UJ{PBO`}UlTRNtGi{lLoICx6DEGsEUEwK14P zg$dror1)`ilLh8`)9Hok^(#Yd#}3VRF^2uFw6_)~bz#iXzzW;VVN- zKj+1^#LCFsRQw<7yD=AA=|2}KFA{@835YtI))?-irRQ>sv)5Vz#mdE$JmbYg9~YYR z&O~$4WVOP$kfORM_(s;a2{zlL@9QibS=A+R%tQ?5fEOuazglJ%iMvMaBOxr7<5e84xpv;CQ=J`=Cc*Tze(X2wO7*K!Z6~d#QNLpq;4SI7$>mcO(SD|ZOW(>X zf9olF<633}Y<_Td02C z-p1oo;)_Hc)_nVlulXR?S07nfG36mG>->B;r{9pl(tfDe9MR)j`KjI5F_3*f;Ri`} z+1V4PND`!CZ-2atj} z#9!P2<0!WA1WEby@yLVJRZEQ*^ZMMyG$V3&r4vd^oy5HKFF?b4Q||u*CL*V_({Pz} zl!$R9o0#riivvDn?I)e`$fC*P#^vq$c&7vGIWNuTJOA@(|%4#JjHgL8e+8V(}DjEz^CQHGF*ll4lm&J&L!P zVe2W4#*O`HlP`6){N^8U{EDyydceg2lcC(MS#9|pL(7(9!85=#g zgN)X}$AMH9#7Fytmx&uUK(^qv}BkPuVgUtj=nnb`oz~y{`H$A9I3*$S9);DPm~n zX)LYR{3X-z#v&y9<OlZr7pc96!~UH08ZhCRDy}Sc%CpE*3x?PE7DqB0S#* zNM;L#b7oxG)W?=`Tu|wvM1KF;3oycJ=E7?T9)#f{ZW18nnd|b z`SEmwg8b!_x=YCuEw@V!rJZCxll%F1!Z@Oq$FEKWrXs92m7nA4;nt((ep*#QvWVHw^0Io0=fBgtVf664 zq^iqXvA4aLNvC(hv9(4O=+F z+bEPmBjLZ$?w?ju#6>n3DswOY!)$3{!cXB0BfxjE8qJ);w(R{_h$^w)Q9`@aDW5+- zHZwD`uo$iNDbMUbOj%YO)pRwbACb+Uny;ecw1Cn0?S1*gUFFG3H|R`+JzX2QF3yghp4 zblnyr#jd|BPnq$cqLHaM$i_{%`ON6u`jUf-qpD#@nJ`|%L%!ni zPd=iqrqkL|2Tx5q9VQe-m4)yj%G)QdA9{GuPjOQ-6|ECK_YtUf7A-cGEj3+Lp`v(= z4T1@M3vp5M*jCz6EV+J_zxEtI*Q~_mmO9GA;rpvs)EmRAtE<4tKgw@pu1En;b3BSYt^5*4JaPn3iXH`JqyLe&^O zzH~kqnpyzxXND~^F`J2~64a)#oh0RW&gRz*)MxH_6-O8Cs)e?MqBE;j!`SEqq%0$Y zMN%hBft&Z%dx7HuhCnw(P%8nk7~viuDi=p#cIDtpb;Jt^8% zxt5H?*Jz$=a9DEr$l(Nqb}hLMbt^@wyee27(0$B8?-uk8FM_f5k+dR(;C1zty9l>% zi+Fq3i1gNriOnkKC|Bz8RKE@e#9&di;LvdiU#FeQcL^m)qd_#lP9U=&`;*IB4v(e- z-`R#E%{_tabhy1XB_i&@igus4JR8sSnXB*a?$*`Soo6f~b02Ag*xBaTEA2>Mw<%LX z4kJl|nZYBDT>NM2g^f+i(sJcBBSJ#S8PDCoJe$sYnCo7Tp+1i#O-KyIFhaq7#j;KN zQ4-f5Qu!~lFx->hN%yA~0CC#08kLeJci_WoQZP%88nOJk`%HA|G-^GVlU?QFm*qni zV_o*B2#wA);a0*;;&4-Z&znv&7DxK~+S*vH%w748_nP1ky(&0~J{MEHzWeGkkII`| zfq9t&(}2Fcy}rJFztgPK|1H^0THsS)Veyv&e0_Yw(3f9-LJ0u+1|Hd#6K8?-)<2-k{ki_>(0n zBr>wEdblTg>5&%nx@p(=%VplnhXe0kX*f-TWw*V_&Zl37fnx-WuusgprQUA9L8 zZ^^o+ebwD0d;!aA7OhD&S_xg>bBaQ|u=ld6)p{ZCzDEq_B?VPf@6p|mp{sw^$^e$n z>RDv+ZSVTy549)o*OUaFq>)hj9npm^<_W8ikfqMe9w|1?;Wwhcq7A%f{Jx#y$9XyGc>qlTW@_yj#JPda%EPeeo>e z9H~sGy78G8I2j_&UiBXGaW$zO{xe;vxGLvz#xb|tC*{{%<;jmeuiZO&W}3!z`MUb& z8fvc^;ssH{#R(tVo@Hy7JF6DfuF7s({M-6}?KNhAwp+CKLmwk#^DwyBerG#r!3BCo zq9NK^$pO=1vk_hSjguE9bYtxMlhA;aPx6k4!&A88hER-4r!#tQJEB(2ZJ+(zYRh5P zwG>u-NudoRGly7!mBGsVW$jMojXr>F)0Kb-*dsUJDugX(AtxbDzTkr)eTIr53e{&QW7z!K9!p(4 zR2fZgU3fNxcIZ~}$KWPnB>{d{uuHqeD&t(<#~A!;NHX|o{QgMQraaI7&*qvbyLS_F zb5f3jzXPfh10bK|=}nuCcWd>9NQ+k+>6b@#*w(yOQ2RQyJE-9KG{(>(sC{LMDIzWBi@E34qA>QUgDQFbPg+dakxS z$hOh6(8`sdkh1^Vhi`L7KiA^*sjTMkOAbPax_PN2yN`Iz{p2)exNCM5cGYgvOc>K z@~pi)Yb<>FCo-Y}Q3It=bOBIws`{q$mxpVs$rPY- zVQ+#T&^`st)cpFW70g-phL$Gy1g!y>o!HOI+?7Eee5{EV5Ee`{&<4)n$SN{9)nPur zvbF_6ckAE-7($nO@MigSdY0)eel8Wwl|%r4Sjkqh5;5XpW2MwL=stKCI!sDYv)nYD zHdJ5(10cc2jKN%7h71SEGHUFjsXu|nXf+mzSq?!OH9rHi0Z7nwK-0VGnQ6?j(w3+Um%InYLuoVhAUM~*K{0=-O*wH4-9rElU=~SP&D^>)Zl+wW>bna zs?v%9;>|zu+6z~XVu0$qL90%n)r^oZ-qN$~2IZW*f{F5Ax}o zD%+VYf{ze$h(U1@ZES+WdT#xP8w)8P7R@zSV=37ih*bmNyH1;m*jRyBtE=HXal>{; zO1e-bV-HCt3vNt?IxrDm2qxxFNu31Ed_82s!NG^!6;-MHzc8Ym{@l++&pcjUgV3X; zdRX){p(R@6?F^2iod&19lXLSkpKGNV;G+l;WIe3^J#Jo4S?SwJAWqCqXO4m7SHk?w zJ#3RtOH0cyUA+GA%K7tPu=+=ACJndFXDUcfj!l-_C+`lH{UP(~N=iybIxvP)xv?a_ z3|+vxWkxJL3Pt8DaZjw+4+$u_OSxf9T~(TbIQarT%SvL}x6dglDRz3_2@>_J$z^JN zEW}Wd`wR}E-GsE%F2}6Gqo94mv3e!mkcRK&iw~2Td7QF9Of=3$)Xw%^fvU)0xt|v( zo(Fewn+8rZZXug~^?CBlY(c;ECTpZ+&6X{0VI3rlAU%^K5w6N8a^U6~?sHO-$Y?bU z=k9O((^;O4Jzm6Szy|3 z<92$GOn}HBu?!)#-jJ8)Xdst{Ou{tb3n|dvV+$&2hTs) zm}8H4AgLK*DbAbw%Lz4ds1|3=KMdN8Eyd(_o0c7HnE>~406fVH1osK%<0i%P@JX~>D8-O*-27T zj=1kRP~p^O4uUX2iqKlxAyob~5Z@CyCr~?{8ZJ1)lxiyUF>j3LaoQ%2H6(fm^+Nl0 zwWrHw#){P}vXY8uG9BBk`l;OuICyaw7cF?7*;u9d#X?i%_`?@js9-hU<#w>~N`WlK%1$quZZD=F?$6bo*2 zw@VFrGRn(uSWuTS`tB8iw+<`NZ63Hs^C`ajj}4=Fx#Lu-jQDQ2;6SLD&hdBuVgbh? z!7QD};uO!=FF%DliyP*HT=8jR>+GCI*nA=yLaZ$C0nUQJG$+aAn~28s7pV$ zXSq%forY7^Znfq55e#vxVFH{WZ9*FK>2>4JV{0`JRUNrt6K;#VqbO#jcgK@Se)v(K74y z!KBU$*mGNbgvzZ1gj4jMahrx850X$T>{%1tfLw8vIm=@}Wx5vF-^Z+EG|}Wef^oX! z9j| zr5Q0$LT?nvJd{I|pP}TriPjp^<)LffW@SiJa1lxLM3I37$?Ev>sAaJWfhM*B`0l0g z;7vONW3<=@(i{5M5@KS^#L%yr0=0e3oJC`o7E{gXK%^D+0c_{65HsN&1Nid8iN5>S zVE><<_S!4s5@#urFQ9#U`^PT^xY#%v9g-T`8Kma@u18QnVb)c-d5ynzm1C%I;mtbm zGKNcM5h(R-D?SjTSY5fvNrWXNzAh`8zZB~l^gJ*G=*ab|L4G{9ue84W=l0&-5As|D zIR%%LTW{tTY6e3>JIWW${aY#%oWG?rb;aC<>k5}Yapm{7x+E*_+gP}N%L5e%CJvV3 zByRc$(Nm|NVEdVNS+V-*1?cYe_Q~!s%lwsiTWhTikVASWl+4y>WfH!gU3RAkk3WQ13!q<0gX0i%mpSjcSu#4J<&t{9`N4rq1$m%R z&a9NSHtp*5Pu~t=JXAsJm;5n@QLJHmcg?4u-B07+k=OWZ5C6;OoNZJMy+rpxjIsEV z%bolUMu^IVML_g^&cS0Xt$N`?BCb9}@#Sg>K?dZ_6%;v$jfHX}aIu9Um^RG%EK$V~ z+-i^cQG=H=WPbL;|2fT+0DkNd`+9j$O>$Y@5H|F)41QNeYAr{7i+A|PRu=HbVj>Un zW%kKSC%|<`VdJl@O$eHAKP1}|C0HpJK)yM-YQYBaM4ElbNo~;#uC}@ie7|Ug8_$wb2YAUD_XW*}vXAIox+Z@o5Rq0mk=u0x$9j+eje$1Uu zS(GV8=&HfQS5c?=9vlo6eGuaZ&TW{^4`)XN${S;Pzb^chkpVC8qC@3sD2dh{`n;{& z2lOD>^WaV$21uZt@f=y0;^0W0({s5AyqJ-A-=NbJhlcJ|KPM;@3x;KnrvD=a90awC z8yXM+0w7O)!(tqaSkB#%{9V&ti{G~UGK)W;>m!sBA2s3%q1QB_^3+Mfp zvS`()WsC*7guX}KWZMrEbM@%!E$M#Y5YzC^O}7w#sx?h|6upQY0bdI0cp{;VN111x*;;T&1&$A)j13~SwF-KC_7eIotA^W#rdXcM&|ZXoqzp7a8u&D9k9yQ{k^5&3@SY8_ zLl)11nk5yjI@D`f{@PH0T)K-4ivvpNDkzi)pyfg6A(#7q>sHp@7SZ=I-=H>{Lp^nS z4_`TDZ^WL!3kj}|^9`bZQFopwKn{8;trkX=sy}to;rqJDkE$w#y7GOn!xGB8pooWe zaVGXk6bLW66gwKm^8#;3UGWurYHFG``Zy58K|mUgwwc&(P#|PbDPlI2Sp!^y35YVwWg9h@oA|pD@JKgkLzX0CsP5kk@vQ zA2YyEEPPl%(C>?tatBG{h!}?_jV6x{?xlhJHr?AJvM{tR&IehFmXpl|neL1?B)v`W zY|RI?!zH<_si{3r+}z6cvAmZZeQkaFbbcANTh^tHOZe<{gM`(|yZ@d`HkDs2>*Ut4 zZ}$X)u4V3sfdaa?Z0&)MD5KomEh)~$RPPlG82E0B|DU~XCa~3`gJC9`H!o2=h~GR^ z-;S4k*zLkzC-x~Vt*XTOV|rOf;p$D?T0&?pyaFaAG%>Q zkl-U(yM~TFglwVtT!+#JwCC{wG_SRH#*nqj_W&<)Bejx>?4Rsn<0DI0`@NgknSW{e z>H91}Gdn(1EHhQVYK!?DHvxfs9ko>BVjeY2MJ_db7e4fxG4@)vu$D2Hhg&win4X|b z7t7uMXl)bEPV5|X(v(8p(H6S}6rPF^g=h2zVN~aNreu^C)xGtd9|yW3h3Rof^vA_- z_Y!Wnv^h%xmju#~ND(Y`+2DGEz_mW7Fqz~W=mf)UZ;MD{PNL$s-$|trxwWCaN0&bwS{A7OMVAccD|tvb;bSYY)U)c5g!B?LB@{PD zU?Gs7_m1uomx`qbYRCROk);2tEGcq1CdXU)9$|_;e0J{~W$VfRCDCM<0q~aTcaiYe zqS&WjKdSaH-uGt`zRH(v!N(2H8WX6Kc|bz+0fkp!54(DM$=P<1G_Bt>_oVat9-2tr zOMmWz$jGDJYOSNhi}xuV4KEO%?YwqR{Kmp~9-_Y^;dHb4%t4{G?%Lo(tcItu2;nfwa$d1UUwcS7 zHqL`ZtRDtI=a67&3OCI_#UX5a^|j;v3(g>yyx`=*jvtD$6Rm7F?AiJg?{OR-`_jD z$#M4D-x1r^5Rr#FMk0Hern+qhx80QU{SDVcY=(5_rl=tq>_=oYGZ!B>b^;Kw#MP@; z#l^*aJR$<+EW8m}3x3>w_jcA!?a2RvdfxV85e>Nc@}l03Q9jy=RQ3`-Yjm)E93mN} z=hDq=tQr;zTTVp5?YPy(iNi?=2K?uglytOid1@vSvK=ji&23}zH3tY02FYBaw4 zUOvWr+PBTg9(JOhpx;VzYGm*b;<19{>`&(pt>P$vNyg(m_{yiTuIcLyECq;y#m&U8NyFT_Skf4;!i&6k8xhES>;#*#0_t_>Wl> zGn6r(`|0AZ$18cgMxGKSzvb|Vn;gW1mEzy*^G$!G!#(Ss)Z9EApegY3+i3KZ4bP!6 zPk7n{ps9Cyb7YAhm1l{YZ${2ne@mHhSfYZ1+CTpRU8(3OSHH=MpzAlh_;$?}Kf$z1 zM4{iX)i1IF#H#Y3?j4tPn;u&`UvUnIMlKmmA~X?}h)GffsK)Hk7MAu0)<`(}FLfl| zH^tv8j>9j`Z;0-QfkXoU_R2f)4`;>EoF_LEl8Q{>BSTLyN)k4|Z+nk^cl}HY_X;(AiOWC~J93X2gtNen;dYXhWkC#<;)OwzoH=eB}uDd5tMMi7K z-l_jjnvB9kg%@2thB}(TJeeH~ZB_fQvF%XeW(^)&+g(3{-xhLdmsd=l@^H+;Hhv4# z=e4)-YE1nUO-u9PPUQ-P62T)%0(7f0M^d{L?!4HW3o@&^Ts2v`_zYndH6nAM`bXNd zRp}+ZFrxIDyYZjor}##qm_NzuU7vO-`?&@3GK@TQweExt&Rjz{*({SgH>}~H< zt(67?0r7tJXHJa^+J9iLVO!dF>De`hOr?(kJLn*e@$ zX=;;^gAt939iH|3qUj*4lt7H-v}I9jS5B(4l0{6VK&v6mc<#1o8Pp|}Tu&GaSiHwi zZz<>^1+Q?N7GLi3|_cuAHa7x#hl zrqM6mKhOg7!}$<-J-wYhU`PN&>;#H7Vt?CN?0E6#`wT=RMr0 zITA%_3qI(DhHZV+$-NHDA^5UuRk`h*`yZGV#zfTq3uQtU`OwLKE3qLX*B&qj+QlWM zD*tB)4f7}nr6{9&VqWgPDPy~d8z2V;0rwTWL8&}XB=cksw7{0eEGZ2t65sgHn(g)? z+NM#%q_3Vno#M)rcB}88v=KYZQxFBKwwz3Tu*&*!RpLTLmOCRS<<@Fy^rUC8S-sK>q@fp5y96*~0rcyDyx~At- zsmwiJIdrmN?Rg)q+hJ5cpJvfQRAT4<`2he)?tzGbH>3h3w9s5xjp+lFs-H}JAI4OI zEx^;NaQT7H5l3dZ&EE|F5=CgIK8l{Lw|KkURq54q?l1KRy)|zZ&2->7ERKwXp#fUB z@L!y{PQue8T(5-DAS;BHaTLw%y!7D<&MopUz@Hy4hfvEKqRCecD|sM!PdAcEO>p;~ z*~|X+GmwU0I~WW8VV?2Hlls8oEaYJ&(VRKg-$j<0HHOXrs?4wz5_gY?V@lup==A$^Y=kn?UYpV zJSbM`khmzB^7pTuP#;r1hc3#9$1 z{7+Th$65UPfw<;e>*>WgP|{n}gxe9k`pAZ#>%6qsKI=pY2tWa_b|Q(;UGiJ3fPO}~ zYE|H~Xv*l=)E)ApFXn&(%vtfCejWF7e!Y++LIu0ixn#1y;KjQ}0uYGyi{zpYdyA@_ z(!aYp6|ipyl|u7O4jhkB`*F_-fQ*SMR+(JUEH^)gyXY9q@gkCIq~w`*4EPpH4L#D-g_l< zMj{*~xF_c4fYwP4;!wwC@ekyu?OtS<{`sexopX!O*JxwX8~8y|^#t z72hDQBXM(%YdIBZpU+!?<}4~h<&$JboX2n#lDi(R!tXrUUK{ArNM0I7ldEEv=`mzpghAG=j7r6_ zJUXGucBD41!B$AfY1Xgl_r+vejjE2Ax%sM{mBlkGo89$eaxzu+yGv>lFgxD|L)7oN zQXI11F050#1eR0Ybg7{1l+n44RO4);MnjIE46@kN7#=8yban1bdYk=qfjXq7<`zL> z9>9r`z>WJpLLUmacRW2g$-qQHH`*yh&VU+9tb6ui^?SDPVY)bGDtTH6?DmgI{?-TE zSHjy5+xs-f>TIHj(gRM(#~_LYowV@d#w8|xrcN%y-=92d$%$Y1NBJkWGNJvOzptOY zid6BQH7Mkx$z~yvUxAdxoiN4s4ZGzPuYie4zAj&<+%!(_1QP?=qelXySc1;uPvfWHQH!0o7IHZq_L<$h z?z;M<>D+huQtVgp{kuvS<5|#`pBcKeTXoD;)KwX69yy!I$%iy%}PX^ zrUY{BB5#+YM_uFS;K#Z&JrX{A_G0kdcrp0)>jk(p;vh*IP!L9B4*IO^Ur(YOcc5jyNBxH&%gGwesW!^~mS0-ZU zOTZgR^hURmkbnEaw>@uHL%2L|a{Z7Xj{UO1A5NQAG4!o#z|4siTtXADn5<{*59=-v zVSQR!T9L~;J3jI)(X_mjJmG#olS}euUMc3!N@@0r+TeXIguQPi{cF=m@IEem)epB{ zeKugN<0sTappY6=`pF_uFH_NDG~d(cJX`15bvtO2+r(Pg_p7Ggj+ zMJ%1Rt)D)xJP-y)(9*Qnvknf}(Ip^jzv}pq?!eq`?r~#8V$f8*RZwQ_hX5X@H2o4M z2!#JMf9;cZw)(Z>>LQrg&U)>RI$Xhz*7&fZ(Svm#nrdqS|4RZJ0ue1hF1L|Rk+^Tw z)K&F}AXv>_u5fFm@q=pI_>$@%3;cIa2E704lIdk74yp1g((=%_a^-@oTI zT4S7fPshk+H)OL5SC$(%+e7iC4#YW$;?1qbyd312oQE&jHN6w99n&c(m3oj;8QW)d zS&FpQS6C_(^(i|==vr@Hl6a;+ilST*+A8v*6DIBo%V>T7JSQX=WgoOQG(8adRbr=0 zA{Wb2l8`K{{ zXK%`A1ujJoY4=CZoLMcg`t|MgnhR`qcD3TW6?gBSjX&LC7hgai{&`zNlu2i`7F`nd ziGwdBP(qHEuiw*nwi(H-=llZ}V>;4R)V*+jpU>qm?>Iyy{lCzigM0EvZb*7za{lLz zpM%dT&XyZ5ysMoO9xFB<{Ic=KhEmDObmw27&%wv>H}5Nw?WlnO)^}a1)^{p0F*mZK zL~mv=H~DT4@Jm6|`Y$a-&XFNoFOe38?b^D!xFH3s@7rnXMy`CrS_KSA8!hH61O`!2 zEXxr(dW85nCTH@2r^8XNpH5*OeVLfLcw`X&7lMd8=Lu{r?VQoax5K|pEDlyhf#Qeo ziBNDhAVvXsmD&$oHP-pi0XHsSed&rn)!oya64TuYS* zQ{9}3T{h1J1>mf|Ow$4{FnTFJy#$Vd;qL#~x54VdEO%tf=_xVBqlau(|JxicXSTZE zTi}a}&#o!a6{_uwtgf!VNk~NHZBQb*cOb#@7t`%u9Rx2Xn_IJGIO{s&JvagTyd3ai z&f$O+rzou8`|nOt>Gf8uym^cxCa=ZA9EF?PZ(yFu!Ij9LDWKW*e^(#D^6fB_x<3A6 zY8(157BDI)V9py;^ijljEuqyie-wk+xR6Roys7*bT9xPg>J&y3o?|=TWqfy?tpkPq zlhxiA%AJ;*|B7+?i0J>)+;!6aX4d@;>v?hclu>P;_PBt6FYF#30n?q7C$w}mfAc3m zTJKfe_Zi@b9=02=9IWfqaC2<$(+4E46O%21w+1BguP#Q)`r@P-CGP!mJ{MYpp@zX3h6j`&N%%O_s5_eiV# zrl1p~LXgOF`7{#|RN-tY{eGtgs$VS^$JP5f(>@`x`luk1!)I+cK{kVMAE3<*58SDt z_&e2yGq^?ik9eWkbqY#xMPM=TGDs;yIP_CwHgapT`u4&*c}==52JMNquHWxI`ZqS; zhtuu*&2bXruzm@|S!|6qfnTLp6^w3J$P6kYf}Zn1AgR+M!rKx(N&U!laIs%Lc^|!8 zY7z)OX5c=6g$Kand*Cy6b#`_FPAOzxvv^$ha@ehX0|1GWN4ZWq8{6MF_4sq@;Z*xu*8YKYEiPoHh zH@b2%dbykvLIl@?7bI#=Dh@8Y%FuOrUk4}J?OC1rvHv7^gp>hF0~*FT^SGmc>x|2( zn|PK6AdGCMf<@p+G_Qw$%+aZ@c#Ln&!gBS{`okjG<>-F|s)2nXP* zstxYhR{ix=V37lFPNzx`Qq#l`Qo42X$1<*geLNQsRsKx**oq9)u#vp1HG)zk3=A}t z<5!FEs?^IUP&+1naCc;1Kj#cH!@4jS@s3tRUs7CV+-+=8n610QgIv zGAM*rxuS*qcT7{ZRr$P2$(d0pz@t#$OPx&E^ImRiYh$KZg5ot9WS~wg5Mm~6j>IW@=lxb*+`F+_pOn6e&Upu`0vA`Fa_P#Zu@x zJRl>zmMCvPLd+=7hzc(cJrCz{yVHpO@9bt_d&&NUuuY!zO7Ao*7rl7ttv9LJmxn+9 z?0SIY+s#jg-LUu04Kb}a+;wSLbHSKDZGXm+Xy#c^cY`#{b5@uE($wOtzTT-y!+ldx zG2r)m%`{i^DWNp)iI-F~RZxNhtFy~FiQ)TzdE-F9UJiiAQF(cJfWYi*LEOupqI0?~ zevBr;#HSi%CR9+QZR+GJ0?8&IFzXn!k z3)qB*U0idc2V_7dMxfG7X?r}BT(H+SU;HN*#=7|H(QU_Yjg8`4!&@}m9`6V}QI`8p zIagp)OEfjwg*Em{1@pNrFW`2@aKR4osvupm;qN6*3=txkf*({*{-m8Oy*P$CG%WEQ z01}W@0_x4(CHB}*1>gbhkwg~i@W#{w!2stu@lznSLh+QBn{g0_muckh&!0c*>nd(g z@oqO{Ct4uSpRO6$BvDAfpoCl;0db$bZ!AyHhv>mU}q7dHHU|Hjw|&nI#}$#eOs>US?z^WbsK z1G}J2bZz)RX{(@j)pmFK0v;d5fN5S9a$LJW3ki7QNnUpviQ)2FDAnZl{HW&-0_0r{ z+{eyj8AncMQ#GJ;L?|jkj-8m4JGZroa|v2*q* zWWI$PJJ>WG+Z}8ZO7{mHaUY?FP5l18zPs-=d3JWXgAU?L{F}QTvEYR{JwXM0;F8~% zU%MFGf1|*XybCx39tKh#U3lSNgp2ogMT^C{nNS~O@sejz2lFUs?TQBOaVevcw4OR~cNaBQ6tDaeFmI!?4r-OYqLar>@ndKzBD({k;2hT(UHnFxP zwX65$m*4MiSL{KbZB+eZG<0&YT(f+SamDH~g^;FER09j9U7dhpUQs!%z8*SF(ZYwhkp z32cc@Vq5FZf0Uuo48NK4K4Pej6dSZZc}Ai(jN3%mqJsT6cckKLnh zIiEwN7i%^x1Y$3V0(?hLDD__NWnfP;l2fkwx?jpv*K6qMB^a)j7S$b181m+>p=TzW z`KI!{K^VcT2V&%vO9`MmZqD{l&+@WT($@|5nvY7H$7icd=})`lU!z+tR=FpmerCv- z%U`MWsD}2#s@<#+?J5^@``;F+tZ` z!|!);-v(qL0iz#kzGFoxA@EC&FZNDzJ`3r1E@2l zqp%0~Z!C$2PSC-*tfuR5mOn6sYE@3T~dJ&9%4y%81PRUEtXF(I( zN#|IZc=s=5ywQT3ayRrc%E}epDQ};@ zSmU>Uqb9g|r-TJMYQh7 zK%D1ND4QK6Vy?lnNN#}^1Uis$M$NY6>+3+AQcm0;uXvWhWmvP(LMnO~O*gs#Y>nE; zEk^h2L|xPESin_8Ul2f(By8NFmlkCriJORwb&_%RwJ!X@>av;Ql7I*Ku1CV((L3x@Nl<*nW#2jCpL47~I^>-%^>|2oXEX#Q7MJRLK?pOK@?! z?A4;4;y3^2F{IBD#Q05Uu_u{}b|$|sK_UFK3_F){Zke>%;`)ln8F zj~_Q5i*{C0jWfR18y+cYch9c(G>9GaN51Q^s^0GE@w73F*}`y?l(k@d`jtkp8}i~K z{Pm!q$UtTwRG{vw@p`!0VBzF9KOM+txDSi-`}k?JcCWj8*+e^a^Dch>dc(qHDb?ay zit_{yACIGUdLZUB`BV=SgY=l)kSMXUf&@fSajDh;Y^pk8Wx~*TeAPhQ=i5%IH=vKl z!_JndTfAV7(2|m2euFOi(mJ{DlHQ|&&9%b_x~nCnfV#D;V;wRtV(Y_229VC|gHW~0 zB>kh2u`U4Wt7_Bw|XS~r^ZlXoMo-qZeF4+8~S82=L_;{(NoG)j(J?V zIK$+NUv2{1wQ3&v1E&1hXVf3jybd8i?J%I}ifhqNt_o6 z%2;?IDz6S$s9QF;mswz1%9lC^!yK2u==12O8DgBhdkvdGxV9GqR%hbSDnnS}Yb}3k zXR1QF`bWx8$u)Vnw>n?mH@kD3IW zu5!4D*4E1#*rLhoZKH&dwV-saxT{6`>(`Kpm#<@$<$r+9O92Wey^3e+pn@^JS2%PA zJfR+xd)jJ(vJCk*8-RlyYNr^Fikz;*dE}gd^x+8#5)=3FmpGeiQk{{lt)KHTB=iJk zjI&Alv!M`R#?L4lo?Prj1=7mYt~G+49NX6YGLp@9;~aC3UgjoawKLAf{)nL~gi(#! zCcrVyj+;>a26#`}oa>`IEX83f0|i^#H8{&B!e=J6)%v`q4%Z@5@F% zJW6@lVlaS z^Z~VLujU#3+!0?Lh6yHLR{djOCF)0Q?3I}}d9B`EluFnWDFQGU^X~r7;;b~@45h8=&rS~kBzlQ}Zxq|FFX(en1zd z1we|Gc|$&u2-yS#V8D%z@KBv*S07|-MzSlpXTIDB9J3YU@R&0*hD43Y3xWp|2%L&L z09~1p<~E{8mFkP$(bU-v0}hb7v>>+SmxD>Ns83+LNVYH4>`4bOOP=;^2Pli@K~TM{ zSUUh8s;#XJH2$h|dDor&mT3$^3>;Pu#jywC+w=Z#Vv`}JEcdLZ6GfvOJ^*C!w!bXa zwH-J=MqziX#fvF1+}M3?5G8c*#6dDhE>8Hk~EGhmO!W4|p)OF>uT%QS`Sb zn%Ty0J1Om+zJ#W&>?B#I3j+57;D2Y$?UcXeR8_p~W$4x~Tq;oFRW)a7Ix{Y`dP(2N zHXG%#n$@HGVp409#Mrf)WpZp5KnadF{gP0y$*^Aozd){-_7Z&s(<6?!4s|}ZR zSPli#KeP`cOkDvST5G4rl+(lQ%!MT+h|9JDCL880Z_IOs#{VeZUgv{OUImTKC$)6OFnv08yCk{qQcT z;%WoC8=HYICuiz@7R^hB%{TeV+o2RDmkWw*9BrCxSv{n@xV~VD-7!6S&05=%2Nh{u z4nbhhWRa;&FLB1E88q$JqJV`>_qdk0>lSia4X9%z)Eti{aPGVa?_ zqob`u% zmw{@@iE8Wd~+&yqZ9dOzdb( zyU5bA(Z~!{t-Ci9v{2@cu>Z*Won`ESPMV!+Q2{R05$}LA7&2xZ3QjZ$sC(daS%y6; z3xIIejQ?`#x_p50`h&6;J4hOQMPuJNBY9--354ZU;Lrmv21T+zyma7nJ8GJUMWR6P zE)_1=fwo_Bs^4Ult!?`C#(WOlq#J|$Su)jttXCFwpof^}x26;iJI;B)9Ftr2F zpnP3WQ^?D>PF_kl7MEN{r2V1=)QRJ{^+RH{hR^CiB(%(tt#z_@TlHCn7YRP4ZG_{Mo#_jnJ4?285L<$?o* zvxxWIYO%i+*o?MN(y49G0ungbuAvXv_T_46BKw}v3{smY@NQgv7XaYa&nq7N)_|_4 z|Hzkq0hpPo{4`s5VnV|FLHe;x;g14RKqSy#EZ#7XT>w##M0n?EO6Aq_FwW9iWc{{V zgF*LM5y51t_;KCfW{`%mz36L~R(y{i4IjL>CrXkLx79`9R}EKxtcq;^sC0vyVD_nH zPrwSQDychgpGG3aSu``qq>?4ZC8wa=9s{ z^!vb=gngcs>VoHgpAKIeFBAZPVh2h+jwkpqn)%&Nzzz>Ueii`6E{?8{SsCw^pQWtj&fO-e!++uWZjq$ajt+SLak=X7pyyR{iy#YX%2yn@I!f+R^ zy1*!hR(^pZMYg^B(x zIWmwg#SfH_HQF(p`IHy9_>^XH$tHf<&64;!>#6SLR-_2_;vjhkB$~HuR@#u_0vx!5 zHEHAFphAsyhx7E$&VkQPRSwCOOQ)W^21Y)MI(7B=RPZw^u!|ThaEiP>*>4nhd1OEd z@^<6tS_-a8+lmEHWi(lixX^5}WIGI3{ZzSDVVr>%s%8_TTCf55bxuagSu`Nb zlBUW*G1z=rDWw<42X?_Wd(C?h&Qmkzi#No*00Y{%##=T#e#CBnpQ}i=_SP3L9!&Qh z8A%T}uYN(X8Ty6^Za^W*USFF252&eo_dOmb`v+Y8^_{Uzo``AIdB(x$9GmvqwrIfU zIeDDwYSGG^F5hIhiO=0{Apr${U5u|S-b2YLx7r_l)YtdL$_ZsRF^S{$lpb4dF-3`F z{epq?YvehX>xBz-86YK>`K@vii3oSi%ASCt_fdZ8{zi5bhYe-1o#5&*2%`=eizQ-y z03UuJFfkI|H9X!1$Wixh@y4A5KsDr)7Hnx#&D$9xI_7XVh`&Rs=;BkhXKE){z7oS+U9 z$G}jl5Rtfgk1t{^`^H}D4f?U$5(nM@xnu#EsyLntUmJJB&U1XM3HD5{vjsUPFMc)* z@*e&+Kuhl*gONLm4|^*%RyPt=R=@R8_wUpbg0Yl%R)C`Jmt03-p@B`9Ci1$#(h+1q+AT zu+m0gOgflHCW!2!ct>=hsggivX4Bww#TC1`M@9h?`quuN$|fVJyt&WkBbO$A7HT4H zu7f+Cw6?Yeo~$DK!}So5rO;cuoW^@$=8=MOqJ^83(E8zXWP@}2r&|CEm#|h+6MPT= zG(t;Z%8*Aqo2SgJMB`pm0VC z9Do97lx`0!hEr1~^~G#N!nX12>qm_AJl7$;NORtEKt@T*Le@(lZ&9N2HPGdHA0MBY z!JWntxaJebsTFZ+wdgmdfG4gim< zgZ1wQpa5p-GnB@1u~=5*b8zH#~i zSmbpCAn429Z11gr>7>5u{t2VeLtOm=R;+k}-8a-@8#G3i%fBNG7;qJPe4c^A+$D~3 zQ1$}uf&`6&G~#|yxihOqk0KAQZwX4HfOs6oQBcLkM;!efx<9DS0UKW7WH6cm!lrea z`vi_n4ZNHT9G@9=fI3WPCD~Ndx(!Lz=hqV&=~a&S&4bTx$6`a`vq&PiLU!p^g+}@W zSLik)gV)TD_X@HjwN_xEvq%4Iz5)5GOn0mSBkt>bmFbWB!uC~3GOx^sZ(9$;jE6@> zKk7f@w8FO*_z3?5kG`h&Ip^9F3K=bC=7LO_|J@y^`N={5vjuU2rF1yn`OGKt&fV(* zk=#q$B2#a_tanUmG)fp>Q>=N&{lP-fW#^hlYzT0GuJb(1mR1FHGkr5Xz4X*yz-CM* zL&!eHy;&^32-$)KeH?+TlGQqVG~P6wj=1u1z6)Y4fT_ zk@3!NW0;k-RrfR9lJ$HtsS`>?uR6Ki(ha^8cS{(hLn-g3NRkL*av7;k9n%x%Krz-C zpz)L|5psTAv6iekx0yoC4sB3qin(s3h1}SXXrKBuEj945$1zzbHV~g@lC<>ARC0W4 zn8oDHW+I^|X@2va&CLqM8AlRYWvWgJWYgU^;!u~BG#tjfxysJy4 zjQ8t@65pRlKb}}?RPGZiB$t2tn1%U~;b%Hmy0`KwMCwKgVFFWP=iR+&O`d<)9S64j zKau$R`zL&WIjvpz87#+rzI*h@VOfBcJmRQGrU*Q6h7EpqTFoWJW~y3eoe5Tp_@q7`(7;azTxYiB5=>8;H zF?%zjOl5dsFVb@guSzl6(L+lF{q6S81*Kkw1ifIQW`v{jLFuXos?4LL&RCIEmQ5(xXj6{99Fp`3)o6gpqM=w_UtDyu~cRKtSuL+ly{g1s$AiZ=cGq4#hR@G7 zE%+?ox$|47t7a||9k8uBGy*L>4p^%&;p(WRk@o&iQ*T`s%c|X0SS_ESq*xP+ z!@)+CX8G#{(VXv8owf;_e&5Yfu9ad1vrX^VTpE@>>7?8XU?HTI`h1lcKCa-!81oeG zkHN5>UZ&9{5{067DKKv1{N}B=MYG^A8}H)z{51uV^zYeTSM=(BBl(b95C6_?e zW?x?XZfxZOKJ-Jwpn0WXEAU<}8PwhnDU_!~%Z&$T;p{bUxhW?q_>+y-3k#+lbSaSH zfA70~5UhzJ<1t809`}hT2ru{oT!R_MxxYePaZC3s7Q;FTJUR*Q&d1mh*p^3XasXI)B+-z{UzE&eJA>6j zeJmE(Ss!bVDzL3=-D|}4nKM(IHarbQGFkI;|6=wpX=7MIp z*zfXNL3(TxH-s#O7KM#S(QVW7Q%E@$g|EToCZtQp%WYjeNmA{!^z9rko8SIcHlzG| z7Ltl5$&Vhvc{XbtVa#y%de^3@OFl1&Aete;EgzwCu(Q=GqVOgqTbB)NzY-?w%znHl-9>c)|0 zfhxNXHJyjH5;Ms3YPz(>L#T=Ce64FmA2rd2Qnhx#eCfS{g1{-g$DIxe%e}D7Sa5{fwUvEU1g8u&~y%Wscq0zPN@8|B_;Am%>R@iqwF*OjMNt@+vTl zyEb+=md*)}?s(j?^II=`aoKH zopxbu^gdr)w~K=z#@11SXw_i)aiuU$)7;O(1<16>Gqj;C^ zlPYbTgCVd=zlrohGO4i#ie9`Fbo-CBNniR!8|F~W+M?HfAM2|A=YiKNfbe;`AWj!` zj7B2?p*0*7fx!fH#Fi0_yrw7gv`B{_u z2AF8M)WwHOc9tjpYkf&fBnkq&LY_$4r6$W>s^-q_;kEJuylX_N7C*fJCLhQEb1181 zufN)W(pDP&6y2N1eM58`jhF7b`PTXH2n+xa$NLndxa;F%9}7+y&^L0)Y0vr{^n=;F zs$Xc4psbX2y!0@=I`Kt8$3|ggzR9pDO|b494-fB7^{k*ekC0P$Yy9r`Dm#QM!gN&W zB-P*0CK#C%5Jv@@LAF<11~w{8m@ z)fU@1I*=hgh!H=H6Njt6PP@)6}=BWBL z^^~knXFA99C;N_>o`lOkX3vtt!(>wPJ7A<_Eb2uXNhY^?D1UH?PaBgl_+287$I0BN zoXRDmz{b$wkE_qC@$aC|d+)#daYV)dDfncD_zLkg%$6`K7B7<3|F4hBRyZ)AcJrN| z-)uu5SWN!r&6Qhg6bg`viV9E!(~Ecw^|399v^#)`owhT5j->AOK)Uzuin|~SZ0zjp zlo~Zm<@Mnx=tp!g+UmhK78V`4b}Vt-K|xp0-!H*XQ;u{W5{VS`MZa$LyO+pHt}?}w zay4J;Q6m6CK48p&ePVW&R+cMzH$6QaaFF4#8fq!St8w4h)Kb4|F-tugI(HLN?XGZo zp5>9#t+&B5-1Qc$>_=9a#Ih+?eUDhyz!7SPcaPoky&qlIH|?F6 z0Ys93-^o+h#C>2sFG%ieZVYEvQK-s4`>ZZMR8%9Ap~h_aQ`mj$PN3G^I2SFel*_M4 zcNCJGZ7a&(#a*52$N)X})PI;QrCB1gc-dcg52w?1!99|PEpprpCNRag`pBn0@0$r`@5*(l`;;Hd#U-NAT%tT#`1-w&!?%{_>Ec$|0fd0 zyUgOj>ku`t=zFMujwvTn)~gtgvqRg9Yu%Eenm<5|ma_qNn=h(2i+b-f)3BDZpY1LG z(VSqCk6cvPX>@^Kl?&6)RJ5UV`pv*bJH&uylB(r-v>5Ft`{d3ix1k(B#*s%5*iceJ z^(~7+>niAN#dqEpCEct4ZtPWkI)00PRwBsu8HHq3*PVOWJr+vH&y4kI78H`IirBuS z7h{Q+RX2UvZ(1^4)z`>5SRbr>L#^PjyE9$sc8w~GN-KaldeG3R4nK2M;=qOG-O#rw zv54P#H7d!sC~gXlE}EO0Ti8MI$|c7IXopwHT-VhhNq5*^z-VQkyke)!`DpWs1kM?m z%WETj7TNreNIzOr_A3I*VkCAz7`U*H@T@Ob6^`PVkVh zs!&;B&~~0a3AaC*+sf64YR8;ZQLIx=@7a33d}RoS--(Kgt_)VM5hM*&x%XIDC>&WS zk2fVnNF>KI6X8gdozfjl7SUd8n4;8H?0iqLR=hqyn*kSluWfU{1qde~F;pTl_it5B zYtG*EZvRe3!A#}Mus<|JW)ObOOw8f<@NiL=a+KPT_iLekqYJgNfjN{Q_}H9grvfQd%vTHIesp(Q{*c+co`D**83b4 z^;c^>fP`6u+8T5PJZ}P)K*;E|BE>+i=lf7ZC?Y|d@5{hS!Xan_sYNciC{I}V+X_Wm zrfkjnAJ6%j<&Px`xjs}=DCwue!5y+&DY@|)e^T*iF1oKbtD2A0NRv23KQ>Uv*HE}` z=i`(7)%M!T&`flFoNv#S;++0R;T}v zAJMJWn8>IpY#2`G#=3hN7B<9`qpq?g`(*5ER~1L$@bV!!i-mZaS<9rx&7LUnxaNM5 z(aw(iZ3Np#TAS{{5OI+n-n_SSlN{Y)seJTjxRD0Xg3{*&Pj&st=_I z%;G{2|CtM@kZY`GA>^KZ9_te5c9|yyYHMf0iI;7A_d4)I+l?KfBJdPu=UqebGoMX; z?w8AwcojFD=gpKJdLJ&X(A(B_bxYRo=7iOI!L60z)qcRwP zsU&>U(*m-p*^D7i48O%)ZAO2+HiWUsO(Oal+c&>jqa~aUtg(qfNj}fr6k8~#!X<6|pBxLQgeR3GxED$cSm{7%= zd0f>*aI36R&@VQ3xZr2R-KmGXTOatBc6B-uvMVK}GlRPXH@F$o0Kf-K?-`89-`%54-5+AQ>_|GK0h-*hQ6H;XU zv6bdfP`osOuRd-2U5AcqMTnTjw&$n5_V8-l8ni;3`9 z{y!kr)QBb3oSb2W2>-eLPs*`^GQ~*6TEVA8kU}4p)LX~?-wH^=K7Y%U0Ew);_mem4 zKzq+y@WNpLJO!L2SZRWRh9`@Xxy734a{0v#`5FLt7q9w}+kf zARTKgN9J`IiR0KCsOQp;B9P7NqNvseg=-ITMQmua{_we1yp8JDgGDSL3kdqNnL9dG zPbQo~^A%`6FAqPyE4<1u`uhNfM%#%*Xk8RuGv~I&H8MY+)3ffWd1ravNFpgrav@Ob^Vzqa#oQi zvbvMpBl`-%LA>x)Uh&B>3xsTScPoLpF&P~L36F^S4-kP0I$v%7>41bz2HkSFM_RSo zZ*z0&gU@thZhu~KIzxoVUiQrm7O^j--yF>M37&>#2)S>a>@9Dw|5*~Jg{_GQ+&YzX z9eVneP>*N5>Q4znv|)|IGkn>Msq%(*u+$_+gZDh$!5!o^+451wO+J(vhc&THF5$6E z=AtAkmQ-P5G0CD=9|ULLD-;VxZEa;m2coS5J-~nz#qHZn$)e6*zFCAUXF7KAl}K7t zMJ&O`Wz1Vd*>39R?%w%5u^U&bSX+%okc`FLe^WFzmXJOAt+q*&Fv<_X>fKPBWYI*S z21jt2jeU-Nn8ZWCD|@0Cef>>~s72(D(;P1zhm?c6f<4kDn;nAKj8Re*N`7d3Slp1%Img-Oq@f zDw%Pc-r0Fvy(qyrWJ7Du+1DNZT0okea@dAw+r~F2HSLs7VU=B}GpGX|XReq!qD8kQ zC8Ps!T+-lkeY5;42roy)BF@D%XWH?4dOa*4?63yfg!n|`^!=`dW7NcY+!oA#U$Q@h zm|+W|2L*IU6?lFQet%g5(1Y5!x$&%8}m?Fd;8e#Ii$pIxTF5hIUy%cd_0;Q zl^B;7W@i3=zQ=Q(*&VzNt^)M)Z4CA zv!jCkz1<}nD};{`m}E+ zLP?RaOR%)VlFZG|Um{WC(9aEXyXLrU!H!ph9;!K;DdLI%- z`7b7m-KCN-VuFxm(Sm>A?57C{Q~DnkQ}_S-_bv<&vNk|f%9ilFeaOM_5QhoCE6Ah> z#KDEDM>>KE3jQ)|fT@upV7P!VzaCS%eIqODYOD)`UpS7AgzVnNT}+ksa~RRT7yJMB zrpiq$B4dlDJGw9 zuk~ouAG&Uw{d`HH8{2<7P&7ISN>z=qVKfN0$}lmzET>C^gm39X-{cn%{-tWz2^YS{ zHj=Zvj$L%VKi85~$8&x`u^n<$%lukru>O{?tm?mZ{YhD%TTVLj#Fc=R>l$9DgrT3H zz=AF}!qL$%QF`$=^aH~Cg#$OiB=>8*DRLtGeTEe`f9AW7Pni^KXNL_7KJU)B+$^Mq8Gks_?^}P8+Wf|>U?dAUY$KNz~y?LINu`f1PWH_GDg`;KKj51AqzjLLL zqL)8=zCAZwu8WPeVv8bw_gR>kf)M6%tn(0V8q%=&wOv$Zuc?-3-Nzta+R+(Hwe9(K zkm^wH$8XHbi}6nG=}z84ul{V;;h+C)n>-3_(hPElZk;00#0MKYcYA6`twt^tyf~hH zRU^4QWlXZkz;;uIJi)vspDpd_DxvTKQK5b?(Us$xXtf<4LiLVwt?r;#RWB$8f~BWT zA68~Ts8u4URN|vdb4a7SS?h=k$uJ1WOVk)G}(*05*0mi=n1K5=`kG7b-^Hp4&p z9%Qz#3!F2}vEcpT_#xDheR<8N^_GIy^I_+V3L8?c9WSp|KQDp1U&y2~*{ziXXaRX; zS70UUR58LCpt?8bY=5+S6_2~C6Z2!Xq&oe2SWEp$C;5hev$#@Q@PE6i)_T%zjQ~Yt z&>!;$X%D39b|i$x>7(4)mPGx`-diokXpl0{?nFXPDl^p8Utu^XkR10!(9Nz7A9QjP zlt!?b;a!_eiB=j>{Rr{;F`1Aj+>-M(+U2GQMJQ#1$yDXZv~qX8U+8amQF4D$N?1s* zZ!Js}KT_{!AaOx;AY~w8PdV9J(it2RFfhCPP&`e|D>&E6eL{ikh#>QDP_IEKWX{&@ zl&ii^3HX!chu%Ncb#W;Fbn0@@_uSQ3YBDX`L3*$0?T^b%65<-hIo$>YOuB zx%{+kmc5+*0yq=P{}C#WqFq8(*HfJr5`HZS3CqRoqWesR#xqn7p9}`A$CpX89lZGt z9$0v8Q{Qv*zbd^H3?bp>fxmIq_^bM#_jg!9(4q{>&*=?#K>~MwqPbOn%?A4?4y(Z))2=#&n_fUYYrMv4OhTqxQm}F1Pnf=*i zsRl=F_(Wtsg!iefR#3L>dV^-nj#bp^_#IyvPH zpGj5-HC|EioO&=$bB@qHh_Sld!LF)pPf| z`QcQOz*uu_+ch-F{~BvEMP%m@S|y)?DbheMcK4N=pchLcQ$uBo=W*X*zb_S#`llz+ zicic{aL3KYtM##(3*R1AyqbW7Tp6rCRO%h><@%+|j&`})Kfb{K;AWL?Vm2&pQeLXP_r(fa zQv2<(;8v@|KocSfp;7*Ev5DN^_A;lEKn7f6eeqYH;W0#5{KwQgRQsC_qIZ$EIZ}+L zyxF0g^Cx^6QwB2(%kN7nZW}5KmK&+HH3?^{W<9yuNd77tBERn}_AZb=lU`Uz$d$wj zjEQhjp>VK6<1Sb`Vg+eT;>}l^nMZdj*k40ZDXRt^LzjccvDeYRTJi0 zU?tXgQry$Y&K@+cB z`saZ!k*Vp)BK#t5?^v7o?>H?CC)|X1$SlJZ9yhuB7L=4goBV~+Lk7}4N=FVIwrCvUf8M))j_@P){W6e7S+-Lzv$*@~h(DTIf|i1fc;u^ZqJ=CW zcZjS@BH#O?N5amv@K2z1sedEpdnnZRIEbcXih;{g_*q-^g1S@PtEE!YJL3EU3sO$E zNZjeZgipx-x4(o^%#V8igNN#RKv1PB$5c7|N?suLmh9CE8Rj?y31kUY;~p%9H@rI^ zxl@seX^;;=_A{?%XBJxG4JvyDFhYfsHh5Rn^3OaS)bVGY!!;ts6Hy{j#|mwFY;g7$ z8LaR^ojj${%5JTc1uN3P{q9YR?F6tcPqZ-y5)z&|M7dMGuDONpLImbz)l^j6JUx}$ zlz&nYMhR8UZg_IUKd&ZPXEUA7yX_bl1Zh~8sF09L4XEO2PoMk-8{~;+`1t@fIx0O8 za}m3v$___6mo%Hdh2#BA^Z#3~Z*jrrnnZ7-QxO-8yCmwbjK9(m50X&~*PTv>w}V-w zMMEcA0@*yg%uO4=Cn>_i;x0}$8>JTehcz{k618l1wr|P_KQGJ;Y95U_vg^L@61rC&oCW zTa?86D0+3AvUH|hzp?$A4?r6$Yim!y(Si1;fPg|vN?3zaUrwa* zXz_i!C;joG9@AcsMinM>8^|EW7-`fv`ZAkr0xJS8E}9a z6g}ThpOb&FiD{niUeEAdl^<-hpr9jMKlw5Ln*|=U%|bh_Km4n|Sx89e=k#M3?FSBE zPbe4^MMWx57aEu5Q1c{BRMYycYXw9c$nQ-~d;ZGLz`%Rr!X;3R0WteL5Ua7L>SZb- zfM9e{JzzgBoSIvyEuvbv7A8;{kP>Eiet3M0nrk>d+#Ka!@H?2Uv?iq$cpm~YbN58l z3%@fSid_-o<3o>Wt2Y5jg1C#<>1rYvl`hK=e0QDHM6*p1DCvC*RIiQH+u|9mo@K) zU@xMuXT9qFq2f1gcr2}Wc?o=#9bN!~7#SBG)Z+Uq+MY)K-z&`_(sW*^5P_9Tl<$7% zAMAd!r)Z-uyemU|7tHH!+>Q#^ZE^RE(4^kEjD(^5_e0eKH-@7Y-W`A3)G_(a#YI^5 z7%n3nh4}4|@*K3AW|oI0C!t_I0We`-qM~45apNd$kymsRTpS|WTlONH8-Yk%uk`ga zFzDUgr_ZrKRf7}c)*g3t5&RJY5^J#3fQTY8el8Gu2{aGoT;LmwPxnK>R6dxsJ{&95 zuCcgOyFs$zVis#q+xVHUzha_qhGiC^e@yO!G@lYE%Fnlc`<9&a&+pA?pe^tR-{Co- zAb+6Tzo~oN>O0R9%E!xl1e6-hhH|D~9>HF&5 ztI2p_6c$bzFrc>+2tU;uK6>q;Kw+%kRyqAx&O$rS%4A0}Y$}v*(E*Hbld4&!%k{kZ z^N%8J9T9bteNS<#2@vK7V9ysoM2f);1&9G5cFVK+k+ z(=G3GG(4@a&hSqV;08K-AjyeHNfYtj$8FaDigAExZjN)_(NJnSv z%S3@W<*TKg4R|(7qb@Gi#j(H(<(-Wiz5XtX5WMoaro4O?Z0=ayHo*!Rt3q%S^IxxE z!C?fR068@hC<(rA<76OAXxiRxkL?mD2FRorJ0xbhfkOCf82r&ZA5^7yauJ5(KPu%i zv;GAS%ecX@8sp}$PkBO>=aG*^qr(uyAyx!IHrM(|Wa13*VjKYj_uvmHt(Tw8M!Jhd zAbTVC-Ay)`?2i>w8rB|fZLy-=Q?Ho?9)#bNZ9wUDD)WJKT|v57e;2c}hC3=QMQWVBqO7P3-LPEmnCt=M>x?zwOrggrUN`XKnR zn5hu))1i-1SXc{&UZgSK_aXzcCy&Ma!TO*sqXG`O#iWI}n@+Bap|}ZARYw=S$6NAe zZLP@GD?ZFnDne-WMkjY6BRT#iEi@z&08%*Z`D;Btp>2$`?B# zo&B6}jmL05VICd`>F6U9;HtO@@%)T{&7MW|Ka$dwmZ(3OjTWFO;XY@kIm414)d!$Fi10SCp)9__w;ETJ=WFD0%;O(-eT;1y6}7x z6NK3`!EPNxDu%s#usiC35e1$eU&=$=cF`@Ekx7$4g!t2g!tSQnUDsNEh`BvJ>hpZF zzpxe9-kHg%SGqG(&6&s)F41vtmyfxzp`jra5_bpxS8*U1+oBV2c|V3*Hq{7JI$--O z!Rh$|HlQ!;D|tD1-%HT*^#bB9ZsSG;#V4<8cX0|mptmC2YdOs2_6Gq&{(#PVh1?BE zkAc&#t*wzMG&8mVY0*yN2eU_xpNz*tgp>sQmC7i^&A66k@2PD+5D&OWFm$>{M^|G# z=gTtdZ(tY%AtfP6hAfYKJ17e{Dh5)GSWIUDPz$`Qr`f{*Z~udq7u%1)4HwyDoOG~h zY-C*SSo_63m{ZNC7j%V)=15QnBJ52nyIsy@Kc!!e<%@N02K9|@?ySU}yjf5Dpo?1i z-zWONfX=ds+3&P&qu2RyIGPDQ_aHnZaX#(~B0#$>+aLZakXh`PwSdq};YogY#1H<%1q|&I_tg_TKq%j!Mk#kvOF3$XPI-w4eG-yuZ{oWhfT})b zfRi>JOJDkHn_yco>n)et*w$3A^T^p{r^RQ>`Gg3Mt$gni)g&Ppcp4t~|B(0AQBkg6 z*sw=MQaY5B7AX~^!$Tt_prWKm=O8(f#!w<4-5{YzcbCqPQqnO4I5Z3~bi;cOCp^FR z`~Lj?`_^JDX9@E>ckFxbeeb=mYkzOm?U+peo=@t7V5c`3E5J;y3N4hCr25egRdA_ru~x z0V3ZB_?6tL-`YmC)a{RnP4?m2+rk|$Jhfy`w$tSU;faJr`tYlVo}jXRA< zO8kgFlb&!vi&P;UhhHM|#l9Q8$8-n)Jpiji9Px$aY#wqtFL*jFxHy1w0FDo~K(Q5p zxO;aMPD6Jr0w1k<&q`+N`XT8hD~8Y6aRAwUMw!s$xpPapv*Wd!OXbHw2#{N=)K>vQ z-`746nD~{+^HPFH>)rF{<-cM4{4&@~mkLz*Xws){eeDO+=7aqtRzY;9mU4Jom z9{i{_d^NQ$@nF|sJ&GA#<*o@{%ht*+6V&9tO7F_V~&3tjS7Q^`IQiT6Cq!`uWV0t|0HD52q>oVR)t5u*e&GzmtWuW_e4i1))65T!8u@w-el z5grpqr{DsDM?)4^dQ*7wxJ~wW1Y&%`ZhYeBhrz$OH{~~DV}A_v+1XB}mGO&(BHsf^ zZ%O*4MNG=^L9g*lFEpPwX2l11Rxj628*_L)Cc``Y*3;6?hGo6a=1k5oZ8C??B6s3o zO$R-db9*I<0r2mq-%lsL?&or%m=OWRHIP6hnt;GStu&d?LpC{XMkOVsm4aW%X6|bn z*%O+Ej$Djt%5Yo-X%x3si zeO+A#nLn)iNO*2YquteN_?7(tPmMV3oz^M}o8}~$Ljd|y&GrV10hm4(aHijhD5AZ> zZsa2tXFJ72{G7RdmfaLJ>ebeg#IdU=K=;vs;wfTN#jdrfZ~6K8Up{c!AIj64X#1nm zbs>Ngogsw_GX2hxVR+D;SJqU-Os#)?J8h_{p=AEx&c^_)CnnUg#21t_25#)c#Xuo(D;?0pH5ur z+D3>+Jtl(8J6c}A#B&e(ldt0eoL3E6_pTwCZ$-sTAPx!YwFLrGBt1y4Ue(alWE*0X z`{5ql@J?d-*(b+p9sn!G%k%}*6yhjn99!9X-MUOO$qbvtd%%n41o)<=-+;=QI6;$n zB)C!l9-ic5;8!6~g@!e6dqP7)DXu~kF6Pi_YO0f#q}x6PF-NNNV=sGnxv9=aMWk|talBLE#(+cZX`gR1;`Ug0%xzQ{OFM!=irCs#opo%Pie@a zVR4ArA4hUFUPYx{J8@16 zHny3;i{{El-X`n8V+m>q#GYkYRm)6O03!Qs`oTjeEB(}UwXVzZA(G>}Uv#n$4+Tg` zlUG*6;)6mZAY2BM zDqpz;(I1eD!{ph7+`;@eOel&1k@?j*(AmR1{b$Mf@?24Ow0Ldkuj&@tJ`?<$D~m_- zEClAYI&S2&MkxqC*=V_ky?PVP5UPI^#~cj{iTuCviE8kfh~V8pd#(mC5l>pev}Et~ zZ{I2m*Ir1K$TE&Gy;q+`=WK7NA9eWq2iDd>)VSQar-?&jvEwrkz8vEUm>_hl{9GkX9)(a^&~C=R0aWYz^1Q`ov}zfemUy z@M1jPz`VFS8dO!`AvqO(OMhbgJ;PfSI{`}QKdJYADB<4a1!*)C$|e#W6v{o*jEYiU zS#3|fwjwJk?)z$Ca41E0PhD$L`krs^?C1phyEw199x}4Y#BRn1%?mRnHP+^D$%9FX zu4VL5$lbdmYIUq)9`wBEW|{xVHeGs=$9vgJ4lm^l3@+0^><|cX`Qn^ve;Olr#XW=2 z70qCI0MORc)00T)nR8x~uQ%khdmT93l^bd;k9LRcDH`Hkdb#%pG$Oa4`uX=7y_yNH zBO@Xb?@KyKY!Ht8nMDPpz;0fou=R?7$c;#+zUDWdzp|eve{?xe^N6_Y=1gK3bo>5Y z-`i0V{YW26``OLr(U|npkO?L$q^_t{B)3q`X9ar`zFJ=T1S1hF7RsNS^Y9&PK)FFW zs)bUscbEfB(Tk`e0dg@@+OU3`7?QQ8NCVXjnO?=KlCX<&5*>S+fW1g4_|(7AG^ezY zfgB?ST4_JH6zA#ykb@b)m(;YC&kg+H11R74*u=fq2c*ByB4x_~k#fwTq?9j8BB0*F z5!Ysf5|V$ITk3QqK<#hQGqE>M`}949aE#N0xhiGUIB_t9nNtrlTDRuw!KpUZ%rmm< z>^LYL1!J*JV#}V;o!p1dCGmWp*1I%3uAs(C9ms$prYjVxtF|(J3c9%oOadm?8cBVYb;s3fh{DI(R`|Ctc>d_QvtMvD;W? z5-Friyy{+n{Pid&FDU~9hD_1z8@5qs&8MOI)Ln58hKAn|38CNUkXh!bUJN-Z|Ku{% zRH5V3={1A2MxKO&!UbHx3E>{`UwK1LscY>VRMIsoNm1zbN}pIF045S1eiLZm#3@B> zZ0zSqxki{XkI9pjGlr+=ec3bbzjO`iWqE_P+yCv-MM9I)SDe%@HKQyDP5ccXyA$@^ zP=^=fgal65#bIgqwFeany6uqv=59IAzTn;s);|u0|ITneGyVBp`i?AJ6{8}bFOdz_ z;$f5U0tHudYSeSR$7tNx7PC(x3BPy*BN> zALn#Rgj4N(_^Jb%3%!<~vs86-*`8-i=@UxAsR-u9Q}f!8a#G<*G&Yi#?xKE}zOfTE0i+c=;X0!YNuoIPra(P=`4!na7u>DGQ&KuVkQoWm5F~!zo|%*>JDu zJnbb9?`|>E4c>RXdoR9+B)k74*u2O^@V@fx=y4z@gjwIhTW>5>f=@(vzWG&?g7LwJ-ci-o%Uz#}l#K>b4@#Sa zUq0tq4#HLS>X;CU7P3sWu}5SzoZd>h-dPb&HQHVoPJVY)Uh~?cr)M7@>}5`+I|-1h zOR}n36Sop=d*NH*#ce$=?q;{g8x<=mEe=+iX}I_4!HEDC`xaz!i-hj&6d|8+p3TI- zAkP1zhLlwHclf4^>dn0F{){E9bPISlk$#_!;Y}riRu1O8a71 zRfYcwlV0a{WAC|FCoy_k)Wf1;{8O}3jqFcCVFowFU+2w?u`R>tg|XeK6}>b^uA$e1 zFW#zhH=ya2HBmc+Ydr;MRZ$^~Y$|o=c~KFeh&Pwi@sp1*$S3RfCKKR+nX@ z_ytV?KOr3_D72O!A#b^I{+i)b^11OvG#Xa`yyp)b!5CD%^Vb**= z;T#9zgy63VIDIBC)>CjCVGtmRphB$xhsixVuhIiz0}2qI;Lhk@4*?G+U{%Ni<{a*K zu1UeeKyls>PKM6`%Cz8Nas++LxL>m1RNG$_b=GdUVUgih1y}%31y?Td|H&{Qd7eLY z+OkCKy&R7z-`k!~^xrW;qF`R5`<&E6zl#8ao>-NsLN%`H7@l;im(^(w{-+cWEClqE zN`0rco!2mL_fD1^VbX+K#+AR;^PgE=sz`Fdiy$3>({B~f#U%X)J@(g1wBRawQ4X++ zT7|~x6A%Qic2r@V_%OX+OYY;M#qKo;ZBXI&-&NscFm-dR&wv&wwok<8QSJYj1NdT+ z|5+$(Waz^`--UG7+=sJQ6x%{f{3rEpHgTY={&xrB>|rlnR{RkjcQK;UVGsE<)mqW^ z?yf#h@tX70lP52y{(;o|y~{lSg6=$Rq+>{2U;C}F!4l)k(5w5bMKlZ+r~Ci>0_P9qHySBvzxDTARD!SA8|^;sRF6jaWt)oK5`^dP|%aJ_oT^=^XtoND$!0>(xV zzH4x7P7yQY2ks^`Yda9uUI*bV%kjm%Pl)>zwBP3uUOygc_B2%A z!Caf-Kn-`O!B0T7(0`5{RGd(BCBojFw;EvomzVE0RoFOt*dj%4}E-iPOfnSkMEWh;%Zq)>N(1n3@WNKQ` z@Vx4^G~*?Htg(mI_fzLVQQs^Aj4J>s{&!Z`Di{M^Oo5FI$}0qV2XEQnemS*BP;P2% zvmh;m&V!Vh^ZK2S9Kh5ki0he*i<{D^ z%^z8m;EJT106jyAM*S`0C`*X=Z8yb>BZ4F-0+qkFA5^V)oQZl=t=?@-UF|$G%WwNo?&bDs zL2~MPdt93AstXg*4PX)MZ*rjL#yOxTUM5!z+hqGZxM1aUggy$pnBK=RAZUxFUQ2GJ_ zo}Q{=aR*QN+;>;T03K`Bxki5k=CXfF;PxcK%gd{+tdMnX~?cw!#i zZOIK%ak5fUsIf6swhnV=Yinx|P6CK;5TsS-pu|Plg%Y|(N3R8c#8d9>>4~HdkQEYo z=N#1u^1~o{*`7G)==S_wmOA`^V-sW(+TUIzPct_wxfZj!zRoS^4iu$ME+EGbMv1$l z-h8O7tqp1dT`1fg-EPqJv$h2X6E4G`)T^y-Q5H#elJyG7e5YRk+MF!oLoY1@fZTGOn$1Tbat%@uk}sz( zA4QlpUz;-4*O%}+l``1~KB1&yu|&>~Qv8w+Buz(I3$?9*5)+Vx0s&Q$;OFxIFb=J{ z#IU-$%4iqTUowXyMa7~K_+-o+GHFn;Bc5d@57hTmq+?y^MJIybt?ZaBfNeg0g@)si z)_Wh^oz|Gi(#g>!eH5spEh5$Q=ZGe5Qe5?V?nnS)2tIeKfM)IlC6V_d+ge-6^+{fN zdeW;ZHL1Fon!ZUXD=WK+nVx1NMGXwR+n%lk8^B0Ue^H=zn`=p&ek!M5cOJqu*Zy83i`9f*GKwKDuN3 zK*dJ&{Y4ToG8Y1W(+|$G?G!I`p6{BCuW%4QP|Nwa1=J#c&^K<2Uk1BTzL^54xYNmx z+Yx2T#spBXpJja$XIMHx1nUM~+2TxFcxHvzGGIu!%2rJlJ02Sv^1sdg4OBRB zN$$=pGe)X#XiC~6D?=St=qu)_AZ>0Cz_jPQgu%!M0IrEE`1q=6uA{1rDl%WXwM8Y1 zBh-n?~lbrr@Q0?H@3N1=N^9KATeMaGRq^L@ZSN~P~PHT}zqxy4aF zzb`sGB*&;4mk$>JxhZpaTgj6y+PFs7CCUUxKqeyl((HvZAATv-9P_nn-@L#{i7o zM{oSQ2Ycu&Yl8DV80SnTma6my_~CljsYV|_wERq8AD(e^bTsCNhWmq)KF2mRFfg!> z1zaTBhFSC{TyVh8ASq7U$wW^NPbOoMXhwe(bN2z<%s2Wu{G~DnR!n6rHEnp9c?=so zW7JjC5aPLcd7aE%!}ENRxK=Shz$J62#c_33fGY*0oH8C|b)14MMQN1&Lwomew~#FI zQ>c_CQ8X@3WWgN+AMHq~%(%8PQmjP4+I;1^mqnFyB(}S&mXuQwac*da1<;JuATSO@ z(QtfiYHf8@8k|(e52e#ckb=G`K!IHa7^xmAZq4US3e|vw^S~Am=~a#&gHw6!F2gQg zd1@*%>B9Y87lO6Wa`tcF+?UbJ&nlWnT6}!-xopb#qa~&07|>CgDH4ibs6Xt3)JJl1 z@(LhAs|PKQx> zRz#a~lDBRZT!1Wxy@Yw_h+-3i+Q?>y$9jj3?^wd6XU`YGVcbRkG(LN`H@^iGun zQt2>ASR%B$r?=O`-93^bwM(=Ad&a^0OTnrV_GU{@H^9j(d3hP<&e_`Dig}Q52~?=X znRg6MN(77|h0yu596fLTu;PPjqI|8bgRYV!n;%5UIR~8(eMA3Y^&-u|JyM2WemYtR z;BIAT121!PakUrz*ai14R{DQse}_r!z0$b@m2U)Z$X|P4BETL%N)xu}iK8i*`HVwlfr^e$E_solK zBTtt(y%lZ#?~VTbEqYe;&MkwvtOvdfP613U(MS{4M32Hc#+_V1u0zmQ88FotLWNt} zM7N6}hksGj_=roae7&d!dR1SWGO&-DO?+T%=QE*mFR$@^(LU~HJ$#?GV7T+)_nr2n zAP#S#0WR-zZ%&7fP%vJC%YT&=FNfwKI?uDEXi3L>X@tJ%%jV6-Y2Nc6J$#wbv!C?QGSd}GWFHJ!crs7u4;iH z(yE-4!LAY$socCk*92{vGwS3}Th}FkE_i?S5g2*sVmZ>K$7;0Z_KdR8=GnjJSB;4? znO8cv`Nf?83KFhskIQi`!3)l6YXIE_a#9+h!ciSx3psr1@A|5!6C9e3S~6}$`M-9c zf>C<5{;6bw`U{-MooJ^?6M}EcJ!qnRcKA>h9; zWngW{0udy1(;L;-tBEs-c9n5HRSBh`(yFC^RPyRqAr=8bI?Vll@n)Ss(@@(lzIH~| zd!Sh7f6h{b;&zP9sjr;pWwDyA@H(1P5uhBFLGbc#h#Jm7G{g!$w1z;mvIh#i)S7U2 zbo~+wi!amek1eb=s0KkQu<ZEp?v49sCV{BX z+uwH*S$1+naz8(eK z3J0U;{$Lai;2a(@^~q`gCSqj$FD&a>9n9r9h`Wm%_c}sBAWjkxNdb>1J3AY2nbuWT1C{!7 za?;ot+eg46;L#-U%Jf$>E;>$Ou&}UgCkJg9Yvv%k6u4x`$!N-{>HpxoXBN+Q5zS= z3A%bQj$go~GVB8;8i+~sZ!y`4aJoXJvJR@iHq2d&NXDLI&vY#Ab#-+CE>)m%4`#I} z&BF`y7or><;Rs-XRhfyBE%E5Lfk4_C#@<;{g@%Fztvc7cZ-6=p3(6agJ6K0$OwdT` zOWamT`Qosvq*mPif5O#zqHsqSLpH)C&~3Q1(w7YqE)RbL)rOVf*OhLdfkV`q*7tmn zZVY&%@D6Q9k>U+B+BErkoKmuzU=$* zUE7)=dem&v{z}qjCWdRYSvhyZAx{s88Xy9BMt;?-DzH52ZY1c4ejA)mAPueQS%4(b z2vlzf{zj!@xX{hw?kiveuT8H`rfW2Q-2ofoK+l+ynv`TlW=2}Mw~CDGFt@M%IWf`A zOidqWDf?FA&6t_iy{fFPJm4UTEjI^KIT|yDnPp<4u|L!0&CU1TQyl>I6ouP_Kna6+ zGh8l$xrCNgEaK6bfY;Og&+BUpNyl2FR$E0nBmTT{x64=Nj#dBC`Z|!ZcFQtQ5(w(! zHRr!a?X8Txd+*`q#?Qxh1Kr$~M`6>rjKP@6`Z0sDGP47sx=GN1F-NARjHRK9X9JV~ zlv0t25^|G%F6G&nC#?a;lK{bs_D)oxGxP?k&GG0p2Hcibnu%UgjGz$jWL7abA>q1^Q$_?IzK&)y^BZo# z#tN%S)A@#m2F9mrpXY!!X_lCYaT561GR{Xx=a%(*vq2bz`F((FukyOe#}vZ*Rad_x$;5vwe%{yfIV!m9>4wQ z(Rn!LnJA%X#g)6IvR4&qlgsNOI^R5sllpiP3q{p*BO`OMnAf7dh8_Xn^65a)PJiLK6MgPV?}S2- zz{ylEQ;y4!!9{5q*8BzxlOg~FnKp#RuxXXjSo&|L8~lCKpVP<|rqP>Lzq*+}5U`%p zB|xZE#IAtrv-|-|rvu=rCScX4Hkh%bTr-~HM9YD65=iMNy@6#9ECHJu*v2@jB37L8 z6|Y6l`|qDAf(?gI2JmicZCoJi>fS|r*;v3CYkv3H$(-!cr{9BRU~M$|aWH#}rO{Ur zNMTScF6K3L^CR}rE&ssGsY+M=*tQ0k-n+jYeceU#^u!=p@9=h|@E zMy6&h@8Hrya^-CE3m_A#5Q4jUfVor`Se4P(O=j2NBtJv(4(?3$Wt@K*%mf(QDcneH z&PRHlPeMb$=W=1(Gy`x35r!27pY`d%FMV;DD-f+?0H66o!7rP@RSYc9G58!5tArao z@aw@tPr+veHC*Nj0-g@m&<}jhI1d(!0zZPC)J-+_^;5S;RAm$`xU;1lEXj(#j12j_PE2hftx(AfmKVQ^^V`PGG_rWJ1(FI;$hYNymue8ssq2XO~*DnFD zPrDc87Ngr8vk;rdlJZF0JI8HS8>5}qsR+&z=HpY+K9qCuYI7PT+eQJ*$^OW>-Jpp3 z@&tJ>jNo%2p=R^bB&y0fR?3M#gOuL+qr2lfzpLJ63^k9Ful5ih-A#%}iDpmfOpnEA zOEn%-UDA711O+z&YZ~0*!FD3BqdUyZ{1!7JbH3R&0J^*7LPI&;h z7uIh)igiL#@V

!;h}yrX_y-5nFGm2G$6IyVY_J8o`MRPYCgKW`t7P>Q-B(TontL^KXAfe)-|(o1kN}ZEYWpmfFis zg<@)eq6CLgo7MW8jz-S!Q^tQ5wWz-a7vi-*O5 z6e*ufCa9`;QMRz`XB1Umr&ai#lCzVHRQzmuDyVAjm|+xkG~9nx&*VD|!TTd&N$TvudffjYDL!5;QT+lT8^Z>3(TbmT0hq~;Ldf4%Zqt#oLJ=4}0C?iy$| zN|!wLvwyq5yK-*l*+G@%+>ES1!h z<82CJ%1VSW56UWEMDk-KwK`+_S4f4}8>+<11SMVCR_u0ro7_zr3(Wdy$b?f)qFQU0 zcJAqt$!rcb(JefOjXden4tBaWs37(z_*9HZ8YKm@i!AN1s*IY|3=@eGhvW4`G2h1=sb2%++sVI|UnGTCR1IqlL=SXt_@m zU+NRJbl5v;c|6jTDdSX^sN+{xD|D`mw$!x|d8A(YIdjY0BiiyzGzuB%h#Vend^Mk6 zuzHX!9~f?z_;>{iBM8(5f^(~nh0c;#R7-}4nS;s8d~sK%>F5@`PJLsu6*T>kJXS9p z7+#WFyYR?=dTFtuBIhDd@LGRZliC#u(-n%l^_lX~1a2|nw5 zgfQPSfR|UU-qiH=$575D}Cp*z=Q}w2C`XXBZ-jc-m7>UD!*4x zW~vzIAF#X-^obe&PM~%5z%q<^mW}bsJqpst8EJ)KG8xm9i*rJ~k`<0|ccdOtOn#o_ zJAA&M9@VcXc7KRR3F`d4?BTD>UQMmE7G! zi+EWPXI<01s;#6Bh#=^I_+#~;z?3wlI<>1(-fuiLc{sJhv2(t%w_`0u7SoBDZyc1^ ziTDDjcC(bS1J2eDThC&=^IrKZZ$r4o`e`Ug-zDO9c!46dzBQal(tYr&c1Q4{krG54 zEW*@g3;?>U*dvC6_E>?P_4Dw)$H|2-oJ3=;RbYn%&Nz+aWgf~s?qcD5pVjp z@DJGGVJ6c^0>RX`zED`U5Pp~_JPPaB<4Eda4&$aGl5*W&=}NU5Co_~>Xh~Sg7N~)a zV_QqEvY&lJf}LJ}FPbi2)u|>eGWsbsS5+pYwqP!6-?>5()ygi>NshlqNzUTEM zy=Es$-BD4bN<3~Vvez%B+xL#ea)7<#$oB9m|Cmuhji7!hzW!a4O3NmR<=MuY-3z92 zRe_$6o)r?8n=PW{^P*f)-85EDEVod{m9HE+5g!tkJl;dS(!m$j}Il=aehRc@L>4A!pQ^g1sT5| zAsH-V-_S+r5P(SJ(oEhzAu6-%CURh06R2Svf2jzK^BA1l&BD?N814i7GV5`q)EGO& zAPkFjg>O-!FAQZ@HhvLvv+}pOwmY#Dbtc7IC!ua&35mDg{gpLPN2ba3=w+LOhaWa? z$^+%T5W6eBu9>v)Jzv~zVWQX??UMv)Ff((XXGr+i-Qql{mzZLt<>(df>aG?)+%>Q6 zXeZIcJy5y#^I+K|IvYAv)it@h9h;C);L^{qU@3C%3s=^%fXcm710TehhGv%RG2H$$ zzYzuc_SayiuaqYg=yYxQ3g!ciU&=~QFRvreIq2Y-Kc`wO3S@O~-ADm&hhg{UT;GyR zF!ly&J~HpA7}GmawFyG5T8jc&TXeDiS8=f;IgpLAiOT0Tzhjv9Lu;B0v=kHO#f z67@Dbs>TFLBCQHi#l5=8)}A%)ufk`}jN+?)&6p`cMXGp%u|FqGefMYJdl12{nlI!M z^+^O|Q40G(tsxA{g4F-f6b^0SEPmyfjBv5-6C|!-5CAVV`JNmCY|$DZxvmCn83A;x z58L~*waluQG@Nx1@^ox)kec~^^<$qUq1lca49E$R8o)I*C0EOxWdobISQNAxwfV*< znP1J(WpPd=#^xK=y4ol!xAn=mq$)t4^1%Pd)p6$X)X3%P`gy2rx@_zBb9{5Q>v(t< z73*yYISnF!I7|#BPDXY(dbz18x23|ZkVN6fJ~jePSp_F4Nlj;i_-WL}J`KgckdUtU z)9)zpw2BJ;w0m}Q<-Aj!7IE1IB8K;@_E0sQmc6G?OXTqGaV)A!bhB(S`JUBG6=b|= z@g&U3#N-ZUIb3LUoh!x{JGk9%UYdUF;V*eLs+ZboHbFN~RP({TV`ZP^DU6-`Q&HEg z3iB8RF%nxg##{wI1!y(;4!cyegMk4Ov{I!NG?udX5&d(>bOuI9bqc_*4FH$|`u1Uh z`mHK)93)lDBO#o>JaK;;;TWB#z2LnXv-R)N@bD)@EiRElxdGr)? zPZKn%72ftd!@nlm*Qn{Z6;i^7M*)!GZwN;A@VQmHGM=xj8vGo$I$)UM`j& zJlq5_?6@EPbIIoe0sghY$mdMsw>i45=X|-1uw74(nhW;!RDKyoiRNE=ytI<27iUjQ zaVlH1^0cY|`y=DhRLxwlq~AWO^s#@OF~8xx^$q`~(>C4^cYzbv5&TY%!YO^|YQbY- zzmi=mVm(2Fx`P(Qb(CwTZq?MzadsNbBowl6i46QxSB$~H#~u9l;%H7cN8?y)5RBK+ zOI=~NRo{n8)O{{tRoK72lQXbvhtZMt4W|9AMOaNh-|*u#_G37);4#1gmjg0~hvTZS zz~QTCrZtIUA=D9QB1WBf06_NuF8z)B-eYUIW4Oh2FF#n}IJ*2!Ln|KnBQ| z01XRr@Z9S0#n6pufLT1n^`!3LRat~z>(oI2eF=r>OQRgHgW7D=G1WCf7yBwgrf%__ zYTf%CgIvE+)X0o4rhP{C)V9BI7( z3)+yGIg{csNr(hTgT=On5p7WAcmk9w3!C?BdjvW_6^83%0`@mO542Zda%6Dd!J%ov zK_I#VIE2S{Fe8CJxCV))Ueykxri;tPVhX;aTAMT$M!ssGC*{{C`0P@T8RkC>J~Oqo zg``RCM0i^o7`S`IQB3ximep98+11Vrc}%U3`aS=C)xUDrh}iMz&W}8Q4M}3JCV~3n zaJi$EvsYUUm-0lZ*UpOEJWdwV&erw9r8ZU~(;K}0WE8a3~QW0k7`^? zW?V=59|R_V`3O)!$l4BmEl$HDrsc@a3}==;#I-P9!~)_Uq1nAU&JjbG?$Hu61T)-G z9xHpemU;9WPXcAnI zlVj79Ebdp}1mOJY@wZu}0zg}jDc1oFGAZmgAnR0>xW2Z=)lO(okOnk`D<}w?02pG{ zL7*CB5x`6}NyA*f9u@;W9A@H@6C6+*r%2=Pn8Deb07r}bXka@qbZznL8kp@zeXEVNuxiL?R|*KP6l9_{8y~Y{P^52JSBL?CkYVe|1Y* zc8!krm!6F5(WD-(!!<Z^rR_`j@dIGi=85p zJ<~|PS5-A&?u;B5b65g`r0(bt|8geL&b9}{U}&-(6@W0c_6IqAwFFL4^L-{lt@olR z?L}+}4O}+r@M@?uPCVv-RUxEF@$RoH$?BY0?qDZ5BB`R6_H-T98kOH1`8kSCW6hF} z;x>Nud zs_cFiX>+zv9$V!iBumO8vwrC2vLKlDYDl@@pb9v4mYcj~4tdV27{siV59KiIA{o_b zw4;}mZD;9m^3|nd846NY)$?1^I-xA6LzHWDA3%59S#T2U(B8-@L}az_E?(WzPokm7 zb*MarzsA8-PCPKSr_xWx!L(P4C*2ch~%*5ZO!+R|Gl+9kXlFn~lgoeA0 z7dct;Erf!3O}dOb3i&koxc|0UtFcVJ`}TLb^zkzI;7IsQ4UHDpKVgVZTsWU?AOcH{ z?(iR|6~C6sahMPoO{T#u+xZ8}nrB!19o0P1d-wcLxVn1m46<%({X(s zT{%ZkgfguGWwH@J>LfS)u5&QR9|RJtZ=2kQT_SPkrbU~=1yx}Sr6yKFCjJDyma=4c zz;e5|V+OC|OV)GNaS!YsH0LA@9Gs+A$$Q0d+t-x7QXYl2M29Uc?Gzk@k)uD4xpWb* zNYCE|hW-XFwj#?Wg#zjAl1!!0<73b9km+P`wDgn|_6o z2b?x5Kf=BB9)zXoowB?U@FyVCQiff6lrVlcejC3jBY&g(w!AG;uzDZ=U}dusm2IR1 zRSaD3G2gZ9H=*S&YZCiR0O^LLMvRR{n&>5?0&XW%PVkR071Ur|^jzFsUdd%>K_(LH z(8%|KArGXeb$ouQg2^5rk(B z+&S0`-k3X!`Ix+ITV zvI1&a*>A6wU6n0_5gNOiFzpfG&@o>b$m1LG^p!K_2M8TS^f0RUa+wIl_Bg80K@6li zq*4qFlCjy-CZhg?$na!x^g(4<7a3viS?P+<3J}LBDq?uXNN4#@_w{)kkiytR z;##WVMTgr;`|U&>x;Onc4Z-GyW-5YEkc{_SmLhPCcLkK85-DA~x6{9s)r5|rK#ZDQ z>cDs1_dT}wF7^J0`FjhgXP7~3oq||$B8YmX&stO$vDFWFIC_jVD0lwam+<#EI9#4P zF648%kSn)QzB)emaXR{KedR8^ww7?`YE;xeejqIr|%edKogOI2(%K3-R z>s|BiOo`H_cgK7_AJl_*Y@J$J8NRN58V`s)CQt8-mU@*PXbp^64Cwm_oo3Vph9^Fi zoSse$coaW=*t!X$o9t(N_gAc(W*I@CWxN!n7R>@N3uk;dCBYGlb$$a}@7w`5*vOzU zt%UdSK8r@eCUSnhC85h;Y?vRMyD|fp113{BuZ7JzvhGO^cv;O!dP7;wr9ewRo84$j zy=-x*nJAuuURroGyHpY~&d%WF-1eUQ8&O|OKw>K)BEWTk}Z59J6XAoo~8&CSbgFMIk@`JK}WtZVfKZkn}(^aUE9cr$wzvbOq-Q1V!;}# z&2kg2Tzxpa&)z3Z@UJ|1h1oO0|CxYcb(Q&#A~ktu2aXsc&~u)#A`&})-#K@0aW?g23ZX0XYE)X1*wR=hR|9mZnU1vECC+eTg;P(8X@0shEjK=! zcfMvOsnhtX5W1hcHTfuRe9+@;XR7cPEfnD|PoD@6auT}-B0q8<^0Pz3G}*1^ACp;i zd~}C1J819V@P|dHZrj@2RyKYx$17taDjACWLg%;3NeNZ0Z`ToTj#cK07dp+=oX%zXBwGfh zs|+$(Up79-hg&_0Pkdx%vux*i{DKp1`j86!(Eo5<1-f~FO&x?}Us*HuSt4>eDL-va z6}<3H*@J?aNXi#RCs}Xm{Zc22o`EZSS@5b?VU8?s*Ux|fMX}^*-1+!Hpp{rtn5D%g zwS;v{etKbV&+|gA-sWTD6kVyBN67T%0v?S!u6A~9G}+rDKXk`*b4)+03O`ZL*d7T; zZ%dvgoNq~!mpkG%k&4ZQzPLp(}5|`ljQTo1VtV`+ zY@ZtC?n7^}*UKbK&#a(zY5ux=gNHI3XDNzZ$bKyof^AHcUp3OZj>c!fg^A;VE_xJ( z{Zv`GU{%F5vttaqbia4VjrNCl{oSyenoDKV_}pVUd``f8T?ShJYbU|F1!dY(SelgQ z7V3{rf*c9_aP%W?aKtzT5orIK;}Yzlv5BVled1cOWGof_YZ*598#dfG8PTeN-1rEu zv5ZUlKYf0|(!?%oM>KxGVy6Y*rXMN6?%p!_far8N*tBo3>HC9!QN{_b#dzURC2}nE z^L6bz9kDY(*c%E^4UJH6SU}Q>W_jB)Spn*^E<-ExlVZYv`?&d!PvP4dFmAe6#DZ(A z@Tg@FeW=E*Y6@FDSy2S(g>6lQ_Lpp1wI6}s@eeidolrQD(4;VyVQ)B_e0br4^gtc~ zS%s3=9$y%zx!rEX;r@<3zU=LkRo&>jAYuBNPSs z7_J_CZ5kpEmN8)n;RQ0>T9_Fa98{EK7#J9o1RnxPfldx~28IR?pxF!zE=q?O85k6V zfO;7iCUz76$qtuMEu$eeoDxiz@;NOA2DY!BE{-9?$zP*>87OLT5fUn;rj`y{7-JlxX&x~2- z;a&BTvxUG3a|VVvXR1UW#%}vE(eqAiQFz#w4RJ4wjsn}b3=Hc1jC~*8ecrw8xRy1r zM-4Qw$ZDt3p66F}L9GOd5;nU-+Mjp7b5S)HhZyur;ZVEW)8f0absoDfFTG;*wEQop zW_d-?a)=u*bGlV5SH6AMy}}&qhvj`)9UqFiAdX6_6?o`gRvPbGu__%b+kA+@rQ*E) znP(m#w_IHkE?BpP@BOA-{tsTgusxqYpA*<@dGN%XLC|iA)6Z?Ob^bd*4VeYAfbG`r zF4JxAXg)r@eA9I6J%3~$++A(M2yBmoJ+M;w&xz#6U%Z9C+P!}#SoM<9O)eGso#2qK0~w0e@Z#(KkQ`tCXG!@Ma6^o-{MUrb z-okZ~Mjgs~=G<`yP9R8uELACtXp#SQTw$JZ+q^(a=6@AEhQ}n5fy9hYOV0hcZty=W zSooQRl8xt`%jYc4!kW0?Ml_@){qWZ*j{pA!`naF{-}q@Q&>0M#u6{1-oD!M Date: Sun, 21 Jun 2026 18:26:22 +0100 Subject: [PATCH 3/6] Fix PR review comments --- .../server/src/auth/ServerSecretStore.test.ts | 16 ++++++ apps/server/src/auth/ServerSecretStore.ts | 20 ++++---- apps/server/src/integrations.test.ts | 16 ++++-- apps/server/src/integrations.ts | 8 ++- apps/server/src/serverSettings.test.ts | 51 +++++++++++++++++++ apps/server/src/serverSettings.ts | 21 ++++---- .../settings/IntegrationsSettings.tsx | 1 + packages/contracts/src/settings.ts | 3 +- 8 files changed, 111 insertions(+), 25 deletions(-) diff --git a/apps/server/src/auth/ServerSecretStore.test.ts b/apps/server/src/auth/ServerSecretStore.test.ts index 0534bcf8043..817312121a0 100644 --- a/apps/server/src/auth/ServerSecretStore.test.ts +++ b/apps/server/src/auth/ServerSecretStore.test.ts @@ -245,6 +245,22 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { }).pipe(Effect.provide(makeServerSecretStoreLayer())), ); + it.effect("returns a read error when the secret payload cannot be decrypted", () => + Effect.gen(function* () { + const serverConfig = yield* ServerConfig.ServerConfig; + const fileSystem = yield* FileSystem.FileSystem; + const secretStore = yield* ServerSecretStore.ServerSecretStore; + const secretPath = NodePath.join(serverConfig.secretsDir, "session-signing-key.bin"); + + yield* fileSystem.writeFile(secretPath, Uint8Array.from([0, 1, 2, 3])); + + const error = yield* Effect.flip(secretStore.get("session-signing-key")); + + assert.instanceOf(error, ServerSecretStore.SecretStoreReadError); + assert.include(error.message, "Failed to read secret session-signing-key."); + }).pipe(Effect.provide(makeServerSecretStoreLayer())), + ); + it.effect("propagates read failures other than missing-file errors", () => Effect.gen(function* () { const secretStore = yield* ServerSecretStore.ServerSecretStore; diff --git a/apps/server/src/auth/ServerSecretStore.ts b/apps/server/src/auth/ServerSecretStore.ts index f1d76084c24..d62e9768df4 100644 --- a/apps/server/src/auth/ServerSecretStore.ts +++ b/apps/server/src/auth/ServerSecretStore.ts @@ -294,17 +294,17 @@ export const make = Effect.gen(function* () { const get: ServerSecretStore["Service"]["get"] = (name) => fileSystem.readFile(resolveSecretPath(name)).pipe( - Effect.map((bytes) => Option.some(decryptSecretBytes(encryptionKey, bytes))), - Effect.catch((cause) => - cause.reason._tag === "NotFound" - ? Effect.succeed(Option.none()) - : Effect.fail( - new SecretStoreReadError({ - resource: `secret ${name}`, - cause, - }), - ), + Effect.flatMap((bytes) => + Effect.try({ + try: () => Option.some(decryptSecretBytes(encryptionKey, bytes)), + catch: (cause) => + new SecretStoreReadError({ + resource: `secret ${name}`, + cause, + }), + }), ), + Effect.catchIf((cause) => cause.reason._tag === "NotFound", () => Effect.succeed(Option.none())), Effect.withSpan("ServerSecretStore.get"), ); diff --git a/apps/server/src/integrations.test.ts b/apps/server/src/integrations.test.ts index 8ad07b9bb26..7fd38c69c40 100644 --- a/apps/server/src/integrations.test.ts +++ b/apps/server/src/integrations.test.ts @@ -6,18 +6,22 @@ import { type IntegrationAccountTokenValidationInput } from "@t3tools/contracts" import { testIntegrationToken } from "./integrations.ts"; -function makeHttpClient(response: Response) { +function makeHttpClient(response: Response, onRequest?: (request: any) => void) { return HttpClient.make((request) => - Effect.succeed(HttpClientResponse.fromWeb(request, response)), + Effect.sync(() => { + onRequest?.(request); + return HttpClientResponse.fromWeb(request, response); + }), ); } function provideHttpClient( input: T, response: Response, + onRequest?: (request: any) => void, ) { return testIntegrationToken(input).pipe( - Effect.provideService(HttpClient.HttpClient, makeHttpClient(response)), + Effect.provideService(HttpClient.HttpClient, makeHttpClient(response, onRequest)), ); } @@ -49,6 +53,7 @@ describe("testIntegrationToken", () => { const result = yield* provideHttpClient( { kind: "jira", + accountName: "jira@example.test", baseUrl: "https://jira.example.test", apiKey: "jira_token", }, @@ -56,6 +61,11 @@ describe("testIntegrationToken", () => { { displayName: "Jira User", emailAddress: "jira@example.test" }, { status: 200 }, ), + (request) => { + expect(request.headers.authorization).toBe( + `Basic ${Buffer.from("jira@example.test:jira_token").toString("base64")}`, + ); + }, ); expect(result.accountLabel).toBe("Jira User"); diff --git a/apps/server/src/integrations.ts b/apps/server/src/integrations.ts index 0126aab2b75..a492b1f54fe 100644 --- a/apps/server/src/integrations.ts +++ b/apps/server/src/integrations.ts @@ -118,8 +118,14 @@ const INTEGRATION_VALIDATORS: Record< ); } + if (input.accountName === undefined || input.accountName.length === 0) { + return Effect.fail( + validationError(input, "Jira requires an account name before testing the token."), + ); + } + const request = HttpClientRequest.get(makeUrl(input.baseUrl, "/rest/api/3/myself")).pipe( - HttpClientRequest.bearerToken(input.apiKey), + HttpClientRequest.basicAuth(input.accountName, input.apiKey), HttpClientRequest.acceptJson, ); diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index 77d908f4605..da3c1b6f1f1 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -645,4 +645,55 @@ it.layer(NodeServices.layer)("server settings", (it) => { ); }).pipe(Effect.provide(makeServerSettingsLayer())), ); + + it.effect("migrates plaintext integration keys into the secret store on save", () => + Effect.gen(function* () { + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; + const serverConfig = yield* ServerConfig.ServerConfig; + const fileSystem = yield* FileSystem.FileSystem; + + const next = yield* serverSettings.updateSettings({ + integrations: { + github: [ + { + id: IntegrationAccountId.make("github_legacy_plaintext"), + name: "Legacy", + apiKey: "ghp_legacy_secret", + }, + ], + gitlab: [], + jira: [], + linear: [], + }, + }); + + assert.deepEqual(next.integrations.github[0], { + id: IntegrationAccountId.make("github_legacy_plaintext"), + name: "Legacy", + apiKey: "ghp_legacy_secret", + apiKeyRedacted: true, + }); + + const raw = yield* fileSystem.readFileString(serverConfig.settingsPath); + assert.notInclude(raw, "ghp_legacy_secret"); + // @effect-diagnostics-next-line preferSchemaOverJson:off + assert.deepEqual(JSON.parse(raw).integrations.github, [ + { + id: IntegrationAccountId.make("github_legacy_plaintext"), + name: "Legacy", + apiKey: "", + apiKeyRedacted: true, + }, + ]); + + assert.deepEqual(ServerSettingsModule.redactServerSettingsForClient(next).integrations.github, [ + { + id: IntegrationAccountId.make("github_legacy_plaintext"), + name: "Legacy", + apiKey: "", + apiKeyRedacted: true, + }, + ]); + }).pipe(Effect.provide(makeServerSettingsLayer())), + ); }); diff --git a/apps/server/src/serverSettings.ts b/apps/server/src/serverSettings.ts index 67664f515d6..e75718f1a0d 100644 --- a/apps/server/src/serverSettings.ts +++ b/apps/server/src/serverSettings.ts @@ -103,7 +103,7 @@ function redactProviderEnvironmentVariable( } function redactIntegrationAccount(account: IntegrationAccount): IntegrationAccount { - if (!account.apiKeyRedacted) { + if (!account.apiKeyRedacted && account.apiKey.length === 0) { const { apiKeyRedacted: _omit, ...rest } = account; return rest; } @@ -556,37 +556,38 @@ const make = Effect.gen(function* () { const persistedAccounts: IntegrationAccount[] = []; for (const account of accounts) { const secretName = integrationAccountSecretName({ kind, accountId: account.id }); - if (!account.apiKeyRedacted) { - yield* secretStore.remove(secretName).pipe( + if (account.apiKey.length > 0) { + yield* secretStore.set(secretName, textEncoder.encode(account.apiKey)).pipe( Effect.mapError( (cause) => new ServerSettingsError({ settingsPath, - operation: "remove-secret", + operation: "write-secret", cause, }), ), ); + nextSecretKeys.add(secretName); persistedAccounts.push(redactIntegrationAccount(account)); continue; } - nextSecretKeys.add(secretName); - if (account.apiKey.length > 0) { - yield* secretStore.set(secretName, textEncoder.encode(account.apiKey)).pipe( + if (!account.apiKeyRedacted) { + yield* secretStore.remove(secretName).pipe( Effect.mapError( (cause) => new ServerSettingsError({ settingsPath, - operation: "write-secret", + operation: "remove-secret", cause, }), ), ); - persistedAccounts.push({ ...account, apiKey: "", apiKeyRedacted: true }); } else { - persistedAccounts.push(redactIntegrationAccount(account)); + nextSecretKeys.add(secretName); } + + persistedAccounts.push(redactIntegrationAccount(account)); } integrations[kind] = persistedAccounts; } diff --git a/apps/web/src/components/settings/IntegrationsSettings.tsx b/apps/web/src/components/settings/IntegrationsSettings.tsx index 138e2548c5e..8df5452d69c 100644 --- a/apps/web/src/components/settings/IntegrationsSettings.tsx +++ b/apps/web/src/components/settings/IntegrationsSettings.tsx @@ -216,6 +216,7 @@ function AccountDialog({ environmentId, input: { kind: state.kind, + accountName: trimmedName, ...(normalizedBaseUrl !== null ? { baseUrl: normalizedBaseUrl } : {}), apiKey: trimmedKey, }, diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 00a66cba438..eaf1d4e23da 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -80,7 +80,7 @@ export const INTEGRATION_DEFINITIONS: Record Date: Sun, 21 Jun 2026 18:49:00 +0100 Subject: [PATCH 4/6] Address remaining integration review comments --- apps/server/src/auth/ServerSecretStore.ts | 116 ++++++++++-------- apps/server/src/integrations.ts | 5 + apps/server/src/ws.ts | 50 +++++++- .../settings/IntegrationsSettings.tsx | 45 ++++--- packages/contracts/src/settings.ts | 4 +- 5 files changed, 151 insertions(+), 69 deletions(-) diff --git a/apps/server/src/auth/ServerSecretStore.ts b/apps/server/src/auth/ServerSecretStore.ts index d62e9768df4..73ae566d60e 100644 --- a/apps/server/src/auth/ServerSecretStore.ts +++ b/apps/server/src/auth/ServerSecretStore.ts @@ -304,68 +304,88 @@ export const make = Effect.gen(function* () { }), }), ), - Effect.catchIf((cause) => cause.reason._tag === "NotFound", () => Effect.succeed(Option.none())), + Effect.catchIf((cause) => cause.reason?._tag === "NotFound", () => Effect.succeed(Option.none())), Effect.withSpan("ServerSecretStore.get"), ); const set: ServerSecretStore["Service"]["set"] = (name, value) => { const secretPath = resolveSecretPath(name); - const encryptedValue = encryptSecretBytes(encryptionKey, value); - return crypto.randomUUIDv4.pipe( - Effect.mapError( - (cause) => - new SecretStoreTemporaryPathError({ - resource: `secret ${name}`, - cause, - }), - ), - Effect.flatMap((uuid) => { - const tempPath = `${secretPath}.${uuid}.tmp`; - return Effect.gen(function* () { - yield* fileSystem.writeFile(tempPath, encryptedValue); - yield* fileSystem.chmod(tempPath, 0o600); - yield* fileSystem.rename(tempPath, secretPath); - yield* fileSystem.chmod(secretPath, 0o600); - }).pipe( - Effect.catch((cause) => - fileSystem.remove(tempPath).pipe( - Effect.ignore, - Effect.flatMap(() => - Effect.fail( - new SecretStorePersistError({ - resource: `secret ${name}`, - cause, - }), + return Effect.try({ + try: () => encryptSecretBytes(encryptionKey, value), + catch: (cause) => + new SecretStoreEncodeError({ + resource: `secret ${name}`, + cause, + }), + }).pipe( + Effect.flatMap((encryptedValue) => + crypto.randomUUIDv4.pipe( + Effect.mapError( + (cause) => + new SecretStoreTemporaryPathError({ + resource: `secret ${name}`, + cause, + }), + ), + Effect.flatMap((uuid) => { + const tempPath = `${secretPath}.${uuid}.tmp`; + return Effect.gen(function* () { + yield* fileSystem.writeFile(tempPath, encryptedValue); + yield* fileSystem.chmod(tempPath, 0o600); + yield* fileSystem.rename(tempPath, secretPath); + yield* fileSystem.chmod(secretPath, 0o600); + }).pipe( + Effect.catch((cause) => + fileSystem.remove(tempPath).pipe( + Effect.ignore, + Effect.flatMap(() => + Effect.fail( + new SecretStorePersistError({ + resource: `secret ${name}`, + cause, + }), + ), + ), ), ), - ), - ), - ); - }), + ); + }), + ), + ), Effect.withSpan("ServerSecretStore.set"), ); }; const create: ServerSecretStore["Service"]["create"] = (name, value) => { const secretPath = resolveSecretPath(name); - const encryptedValue = encryptSecretBytes(encryptionKey, value); - return Effect.scoped( - Effect.gen(function* () { - const file = yield* fileSystem.open(secretPath, { - flag: "wx", - mode: 0o600, - }); - yield* file.writeAll(encryptedValue); - yield* file.sync; - yield* fileSystem.chmod(secretPath, 0o600); - }), - ).pipe( - Effect.mapError( - (cause) => - new SecretStorePersistError({ - resource: `secret ${name}`, - cause, + return Effect.try({ + try: () => encryptSecretBytes(encryptionKey, value), + catch: (cause) => + new SecretStoreEncodeError({ + resource: `secret ${name}`, + cause, + }), + }).pipe( + Effect.flatMap((encryptedValue) => + Effect.scoped( + Effect.gen(function* () { + const file = yield* fileSystem.open(secretPath, { + flag: "wx", + mode: 0o600, + }); + yield* file.writeAll(encryptedValue); + yield* file.sync; + yield* fileSystem.chmod(secretPath, 0o600); }), + ).pipe( + Effect.mapError( + (cause) => + new SecretStorePersistError({ + resource: `secret ${name}`, + cause, + }), + ), + ), ), ); }; diff --git a/apps/server/src/integrations.ts b/apps/server/src/integrations.ts index a492b1f54fe..9903a3c0981 100644 --- a/apps/server/src/integrations.ts +++ b/apps/server/src/integrations.ts @@ -189,6 +189,11 @@ export const testIntegrationToken = ( HttpClient.HttpClient > => Effect.gen(function* () { + if (input.apiKey === undefined) { + return yield* Effect.fail( + validationError(input, "An API key is required to test the token."), + ); + } const httpClient = yield* HttpClient.HttpClient; return yield* INTEGRATION_VALIDATORS[input.kind](input, httpClient); }); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 094c807fbef..2577d07d0da 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -49,6 +49,7 @@ import { AssetWorkspaceContextNotFoundError, AssetWorkspaceContextResolutionError, EnvironmentAuthorizationError, + IntegrationAccountTokenValidationError, ThreadId, type TerminalAttachStreamEvent, type TerminalError, @@ -1231,7 +1232,54 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => [WS_METHODS.serverTestIntegrationToken]: (input) => observeRpcEffect( WS_METHODS.serverTestIntegrationToken, - Integrations.testIntegrationToken(input), + Effect.gen(function* () { + if ((input.useStoredToken ?? false) || input.apiKey === undefined) { + if (input.accountId === undefined) { + return yield* Effect.fail( + new IntegrationAccountTokenValidationError({ + kind: input.kind, + detail: "An account id is required to retest a stored token.", + }), + ); + } + + const settings = yield* serverSettings.getSettings; + const accounts = settings.integrations[input.kind]; + const account = accounts.find((candidate) => candidate.id === input.accountId); + if (account === undefined) { + return yield* Effect.fail( + new IntegrationAccountTokenValidationError({ + kind: input.kind, + detail: "Stored integration account not found.", + }), + ); + } + + if (account.apiKey.length === 0) { + return yield* Effect.fail( + new IntegrationAccountTokenValidationError({ + kind: input.kind, + detail: "Stored integration token is unavailable.", + }), + ); + } + + return yield* Integrations.testIntegrationToken({ + kind: input.kind, + accountName: input.accountName ?? account.name, + ...(input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {}), + apiKey: account.apiKey, + }); + } + + return yield* Integrations.testIntegrationToken({ + kind: input.kind, + ...(input.accountId !== undefined ? { accountId: input.accountId } : {}), + ...(input.accountName !== undefined ? { accountName: input.accountName } : {}), + ...(input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {}), + apiKey: input.apiKey, + }); + }), { "rpc.aggregate": "server", }, diff --git a/apps/web/src/components/settings/IntegrationsSettings.tsx b/apps/web/src/components/settings/IntegrationsSettings.tsx index 8df5452d69c..ad66248cbb4 100644 --- a/apps/web/src/components/settings/IntegrationsSettings.tsx +++ b/apps/web/src/components/settings/IntegrationsSettings.tsx @@ -211,28 +211,35 @@ function AccountDialog({ try { setIsTesting(true); - if (!preserveSavedToken) { - const result = await testIntegrationToken({ - environmentId, - input: { - kind: state.kind, - accountName: trimmedName, - ...(normalizedBaseUrl !== null ? { baseUrl: normalizedBaseUrl } : {}), - apiKey: trimmedKey, - }, - }); - - if (result._tag !== "Success") { - throw new Error("Could not verify the token."); - } + const result = await testIntegrationToken({ + environmentId, + input: preserveSavedToken + ? { + kind: state.kind, + accountId: state.account?.id, + accountName: trimmedName, + ...(normalizedBaseUrl !== null ? { baseUrl: normalizedBaseUrl } : {}), + useStoredToken: true, + } + : { + kind: state.kind, + accountId: state.account?.id, + accountName: trimmedName, + ...(normalizedBaseUrl !== null ? { baseUrl: normalizedBaseUrl } : {}), + apiKey: trimmedKey, + }, + }); - toastManager.add({ - type: "success", - title: `${INTEGRATION_DISPLAY_NAMES[state.kind]} token verified`, - description: `Connected to ${result.value.accountLabel}.`, - }); + if (result._tag !== "Success") { + throw new Error("Could not verify the token."); } + toastManager.add({ + type: "success", + title: `${INTEGRATION_DISPLAY_NAMES[state.kind]} token verified`, + description: `Connected to ${result.value.accountLabel}.`, + }); + const existing = state.account; const nextId = existing?.id ?? diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index eaf1d4e23da..684d01d6b58 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -127,9 +127,11 @@ export const DEFAULT_INTEGRATIONS_SETTINGS: IntegrationsSettings = Schema.decode export const IntegrationAccountTokenValidationInput = Schema.Struct({ kind: IntegrationKind, + accountId: Schema.optionalKey(IntegrationAccountId), accountName: Schema.optionalKey(TrimmedString), baseUrl: Schema.optionalKey(TrimmedString), - apiKey: TrimmedNonEmptyString, + apiKey: Schema.optionalKey(TrimmedNonEmptyString), + useStoredToken: Schema.optionalKey(Schema.Boolean), }); export type IntegrationAccountTokenValidationInput = typeof IntegrationAccountTokenValidationInput.Type; From c8c9cb256edf4d1974392d66d6652ca007803a67 Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Sun, 21 Jun 2026 19:03:08 +0100 Subject: [PATCH 5/6] Fix integration patching and retest flow --- apps/server/src/auth/ServerSecretStore.ts | 5 ++++- apps/server/src/serverSettings.test.ts | 19 +++++++++++-------- apps/server/src/ws.ts | 6 +++++- packages/contracts/src/settings.ts | 9 ++++++++- packages/shared/src/serverSettings.ts | 3 +-- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/apps/server/src/auth/ServerSecretStore.ts b/apps/server/src/auth/ServerSecretStore.ts index 73ae566d60e..0f35e13d09a 100644 --- a/apps/server/src/auth/ServerSecretStore.ts +++ b/apps/server/src/auth/ServerSecretStore.ts @@ -304,7 +304,10 @@ export const make = Effect.gen(function* () { }), }), ), - Effect.catchIf((cause) => cause.reason?._tag === "NotFound", () => Effect.succeed(Option.none())), + Effect.catchIf( + (cause) => cause.reason?._tag === "NotFound", + () => Effect.succeed(Option.none()), + ), Effect.withSpan("ServerSecretStore.get"), ); diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index da3c1b6f1f1..62ddc9efc15 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -686,14 +686,17 @@ it.layer(NodeServices.layer)("server settings", (it) => { }, ]); - assert.deepEqual(ServerSettingsModule.redactServerSettingsForClient(next).integrations.github, [ - { - id: IntegrationAccountId.make("github_legacy_plaintext"), - name: "Legacy", - apiKey: "", - apiKeyRedacted: true, - }, - ]); + assert.deepEqual( + ServerSettingsModule.redactServerSettingsForClient(next).integrations.github, + [ + { + id: IntegrationAccountId.make("github_legacy_plaintext"), + name: "Legacy", + apiKey: "", + apiKeyRedacted: true, + }, + ], + ); }).pipe(Effect.provide(makeServerSettingsLayer())), ); }); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 2577d07d0da..4d02586c23e 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -1267,7 +1267,11 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => return yield* Integrations.testIntegrationToken({ kind: input.kind, accountName: input.accountName ?? account.name, - ...(input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {}), + ...(input.baseUrl !== undefined + ? { baseUrl: input.baseUrl } + : account.baseUrl !== undefined + ? { baseUrl: account.baseUrl } + : {}), apiKey: account.apiKey, }); } diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 684d01d6b58..a0ed3a3fbd8 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -649,7 +649,14 @@ export const ServerSettingsPatch = Schema.Struct({ // patches risk leaving driver-specific config in a half-merged state. // The web UI sends a fully-formed map every time it edits this field. providerInstances: Schema.optionalKey(Schema.Record(ProviderInstanceId, ProviderInstanceConfig)), - integrations: Schema.optionalKey(IntegrationsSettings), + integrations: Schema.optionalKey( + Schema.Struct({ + github: Schema.optionalKey(Schema.Array(IntegrationAccount)), + gitlab: Schema.optionalKey(Schema.Array(IntegrationAccount)), + jira: Schema.optionalKey(Schema.Array(IntegrationAccount)), + linear: Schema.optionalKey(Schema.Array(IntegrationAccount)), + }), + ), }); export type ServerSettingsPatch = typeof ServerSettingsPatch.Type; diff --git a/packages/shared/src/serverSettings.ts b/packages/shared/src/serverSettings.ts index 465bb9e232b..1bbf466f60b 100644 --- a/packages/shared/src/serverSettings.ts +++ b/packages/shared/src/serverSettings.ts @@ -76,14 +76,13 @@ export function applyServerSettingsPatch( patch: ServerSettingsPatch, ): ServerSettings { const selectionPatch = patch.textGenerationModelSelection; - const { automaticGitFetchInterval, integrations, ...patchForMerge } = patch; + const { automaticGitFetchInterval, ...patchForMerge } = patch; const next = deepMerge(current, patchForMerge); const nextWithReplacements = { ...next, ...(patch.providerInstances !== undefined ? { providerInstances: patch.providerInstances } : {}), - ...(integrations !== undefined ? { integrations } : {}), ...(automaticGitFetchInterval !== undefined ? { automaticGitFetchInterval } : {}), }; if (!selectionPatch) { From b2782aae5e53df7054926e82fea720cc0ecfa120 Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Sun, 21 Jun 2026 19:04:48 +0100 Subject: [PATCH 6/6] Fix integration wizard review details --- apps/web/src/components/settings/IntegrationsSettings.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/settings/IntegrationsSettings.tsx b/apps/web/src/components/settings/IntegrationsSettings.tsx index ad66248cbb4..1fb2474f77a 100644 --- a/apps/web/src/components/settings/IntegrationsSettings.tsx +++ b/apps/web/src/components/settings/IntegrationsSettings.tsx @@ -151,7 +151,7 @@ function AccountDialog({ setError(`${definition.baseUrlLabel ?? "Base URL"} must be a valid URL.`); return null; } - }, [baseUrl, definition.baseUrlLabel, requiresBaseUrl]); + }, [baseUrl, definition.baseUrlLabel, requiresBaseUrl, state.account?.baseUrl]); const handleNext = useCallback(() => { setError(null); @@ -284,6 +284,8 @@ function AccountDialog({ validateName, ]); + const reviewBaseUrl = preserveSavedBaseUrl ? state.account?.baseUrl : baseUrl.trim(); + const submitLabel = preserveSavedToken ? "Save changes" : isEdit @@ -416,7 +418,7 @@ function AccountDialog({ {requiresBaseUrl ? (

Base URL
-
{baseUrl.trim() || "Not set"}
+
{reviewBaseUrl || "Not set"}
) : null}