From eae520b57e249d834a6f98db0032c3d0461ce0b7 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 2 Aug 2026 08:06:42 -0700 Subject: [PATCH 1/3] feat(app): show masked keys, timestamps, and remove in connect-provider dialog --- .../components/dialog-connect-provider.tsx | 92 ++++++++++++++++++- .../app/src/components/settings-providers.tsx | 6 +- packages/app/src/utils/server-compat.ts | 26 ++++-- packages/opencode/src/auth/index.ts | 12 ++- packages/opencode/src/provider/auth.ts | 16 +++- 5 files changed, 141 insertions(+), 11 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 417c47cfbe57..90c88577dee6 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -414,7 +414,7 @@ function ProviderConnection(props: { label: language.t("provider.connect.method.apiKey"), }, ]) - const [integration] = createResource( + const [integration, { refetch }] = createResource( () => ({ provider: props.provider, directory: directory() }), (input) => serverSDK() @@ -431,6 +431,17 @@ function ProviderConnection(props: { ) return values?.length ? values : fallback() }) + const storedKeys = createMemo(() => + (integration.latest?.connections?.filter((c) => c.type === "credential") ?? []) as Array<{ + type: "credential" + id: string + label: string + displayPrefix?: string + displaySuffix?: string + createdAt?: number + cooldownUntil?: number + }>, + ) const [store, setStore] = createStore({ methodIndex: undefined as undefined | number, authorization: undefined as undefined | IntegrationOauthConnectOutput["data"], @@ -1167,6 +1178,85 @@ function ProviderConnection(props: { + 0}> +
+
+ Stored Keys +
+ + {(key) => { + const prefix = () => key.displayPrefix ?? "" + const suffix = () => key.displaySuffix ?? "" + const createdAt = () => + key.createdAt + ? new Date(key.createdAt).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }) + : undefined + const onCooldown = () => + key.cooldownUntil && key.cooldownUntil > Date.now() + const cooldownRemaining = () => { + if (!key.cooldownUntil) return undefined + const ms = key.cooldownUntil - Date.now() + if (ms <= 0) return undefined + const h = Math.floor(ms / 3_600_000) + const m = Math.floor((ms % 3_600_000) / 60_000) + return h > 0 ? `${h}h ${m}m` : `${m}m` + } + return ( +
+
+
+ + {prefix() && suffix() + ? `${prefix()}...${suffix()}` + : key.label} + + + + Rate-limited{cooldownRemaining() ? ` · ${cooldownRemaining()}` : ""} + + +
+
+ {key.label} + + + {createdAt()} + +
+
+ +
+ ) + }} +
+
+
diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 2e84cd8f85a0..3048d87298db 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -287,7 +287,11 @@ export const CredentialRows: Component<{ integrationID: string }> = (props) => { {(connection) => (
- {connection.label || "API key"}: ******** + + {(connection as any).displayPrefix && (connection as any).displaySuffix + ? `${(connection as any).displayPrefix}...${(connection as any).displaySuffix}` + : `${connection.label || "API key"}: ********`} + diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts index 73ce77fbfd10..d5e2f25937c2 100644 --- a/packages/app/src/utils/server-compat.ts +++ b/packages/app/src/utils/server-compat.ts @@ -390,18 +390,32 @@ function createV1Api(input: CompatibleInput): CompatibleApi { type: "api" | "oauth" label: string prompts?: Extract["prompts"] + credentialID?: string + displayPrefix?: string + displaySuffix?: string + createdAt?: number + cooldownUntil?: number }> const methods = authMethods.map((method, index) => method.type === "api" ? { type: "key" as const, label: method.label } : { type: "oauth" as const, id: String(index), label: method.label, prompts: method.prompts }, ) - let apiIndex = 0 - const connections = authMethods.flatMap((method) => { - if (method.type !== "api") return [] - const credentialID = `${value.integrationID}:${apiIndex++}` - return [{ type: "credential" as const, id: credentialID, label: method.label }] - }) + const connections = authMethods.flatMap((method) => + method.type === "api" && method.credentialID + ? [ + { + type: "credential" as const, + id: method.credentialID, + label: method.label, + displayPrefix: method.displayPrefix, + displaySuffix: method.displaySuffix, + createdAt: method.createdAt, + cooldownUntil: method.cooldownUntil, + }, + ] + : [], + ) return located( { id: value.integrationID, diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index a53d843e7095..b3221d445ed1 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -84,11 +84,21 @@ const layer = Layer.effect( const data = yield* all() if (norm !== key) delete data[key] delete data[norm + "/"] + const existing = data[norm] const stored = info.type === "api" && multiKeyProviders.has(norm) ? new Api({ ...info, - metadata: { ...info.metadata, keys: JSON.stringify(uniqueKeys(parseKeys(data[norm]), info.key)) }, + metadata: { + ...info.metadata, + keys: JSON.stringify(uniqueKeys(parseKeys(existing), info.key)), + keysCreatedAt: JSON.stringify([ + ...(existing?.metadata?.keysCreatedAt + ? (JSON.parse(existing.metadata.keysCreatedAt) as number[]) + : parseKeys(existing).map(() => 0)), + ...(parseKeys(existing).includes(info.key) ? [] : [Date.now()]), + ]), + }, }) : info yield* fsys diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index 705b2111ee49..b76f6ecf6458 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -42,6 +42,11 @@ export class Method extends Schema.Class("ProviderAuthMethod")({ type: Schema.Literals(["oauth", "api"]), label: Schema.String, prompts: optional(Schema.Array(Prompt)), + credentialID: optional(Schema.String), + displayPrefix: optional(Schema.String), + displaySuffix: optional(Schema.String), + createdAt: optional(Schema.Number), + cooldownUntil: optional(Schema.Number), }) {} export const Methods = Schema.Record(Schema.String, Schema.Array(Method)) @@ -166,15 +171,22 @@ const layer: Layer.Layer = Layer. try { const keys: unknown = info.metadata?.keys ? JSON.parse(info.metadata.keys) : [info.key] if (!Array.isArray(keys)) continue + const createdAt: number[] = info.metadata?.keysCreatedAt + ? (JSON.parse(info.metadata.keysCreatedAt) as number[]) + : [] result[providerID] = [ ...(result[providerID] ?? []), ...keys .filter((key): key is string => typeof key === "string") - .map((_, index) => + .map((key, index) => Schema.decodeUnknownSync(Method)({ type: "api", - label: `Stored API key ${index + 1}`, + label: `API key ${index + 1}`, prompts: [], + credentialID: `${providerID}:${index}`, + displayPrefix: key.slice(0, 4), + displaySuffix: key.slice(-4), + createdAt: createdAt[index] ?? undefined, }), ), ] From 9be908eae470c6294f119e14dcb614915deb7f11 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 2 Aug 2026 08:27:21 -0700 Subject: [PATCH 2/3] fix(app): preserve stored keys across legacy auth compatibility --- packages/app/src/utils/server-compat.ts | 22 ++++++++++++++++------ packages/opencode/src/provider/auth.ts | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts index 3b44a720a82e..339dbf7f8b24 100644 --- a/packages/app/src/utils/server-compat.ts +++ b/packages/app/src/utils/server-compat.ts @@ -396,20 +396,30 @@ function createV1Api(input: CompatibleInput): CompatibleApi { createdAt?: number cooldownUntil?: number }> - const methods = authMethods.filter((method) => !method.credentialID).map((method, index) => + const stored = authMethods.map((method) => { + if (method.credentialID) return method.credentialID + const match = method.type === "api" ? method.label.match(/^Stored API key (\d+) \(([^.]+)\.\.\.([^)]+)\)$/) : undefined + if (!match) return undefined + return { + credentialID: `${value.integrationID}:${Number(match[1]) - 1}`, + displayPrefix: match[2], + displaySuffix: match[3], + } + }) + const methods = authMethods.filter((_, index) => !stored[index]).map((method, index) => method.type === "api" ? { type: "key" as const, label: method.label } : { type: "oauth" as const, id: String(index), label: method.label, prompts: method.prompts }, ) - const connections = authMethods.flatMap((method) => - method.type === "api" && method.credentialID + const connections = authMethods.flatMap((method, index) => + method.type === "api" && stored[index] ? [ { type: "credential" as const, - id: method.credentialID, + id: typeof stored[index] === "string" ? stored[index] : stored[index].credentialID, label: method.label, - displayPrefix: method.displayPrefix, - displaySuffix: method.displaySuffix, + displayPrefix: typeof stored[index] === "string" ? method.displayPrefix : stored[index].displayPrefix, + displaySuffix: typeof stored[index] === "string" ? method.displaySuffix : stored[index].displaySuffix, createdAt: method.createdAt, cooldownUntil: method.cooldownUntil, }, diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index b76f6ecf6458..49d5812ecf67 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -181,7 +181,7 @@ const layer: Layer.Layer = Layer. .map((key, index) => Schema.decodeUnknownSync(Method)({ type: "api", - label: `API key ${index + 1}`, + label: `Stored API key ${index + 1} (${key.slice(0, 4)}...${key.slice(-4)})`, prompts: [], credentialID: `${providerID}:${index}`, displayPrefix: key.slice(0, 4), From 9ee9b9b79f8098968e4d4b02c294cc5fe9661241 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 2 Aug 2026 08:46:18 -0700 Subject: [PATCH 3/3] fix(app): render stored keys from legacy methods --- .../components/dialog-connect-provider.tsx | 28 ++++++++++++------- packages/app/src/utils/server-compat.ts | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index dfb36db66157..407c930fd55b 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -427,20 +427,28 @@ function ProviderConnection(props: { const loading = createMemo(() => integration.loading) const methods = createMemo(() => { const values = integration.latest?.methods.filter( - (method): method is ConnectMethod => method.type === "key" || method.type === "oauth", + (method): method is ConnectMethod => + (method.type === "key" || method.type === "oauth") && !(method.label ?? "").startsWith("Stored API key "), ) return values?.length ? values : fallback() }) const storedKeys = createMemo(() => - (integration.latest?.connections?.filter((c) => c.type === "credential") ?? []) as Array<{ - type: "credential" - id: string - label: string - displayPrefix?: string - displaySuffix?: string - createdAt?: number - cooldownUntil?: number - }>, + (integration.latest?.methods ?? []).flatMap((method) => { + if (method.type !== "key") return [] + const match = (method.label ?? "").match(/^Stored API key (\d+) \(([^.]+)\.\.\.([^)]+)\)$/) + if (!match) return [] + return [ + { + type: "credential" as const, + id: `${props.provider}:${Number(match[1]) - 1}`, + label: `Workspace ${match[1]}`, + displayPrefix: match[2], + displaySuffix: match[3], + createdAt: undefined, + cooldownUntil: undefined, + }, + ] + }), ) const [store, setStore] = createStore({ methodIndex: undefined as undefined | number, diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts index 339dbf7f8b24..0199e8803ca6 100644 --- a/packages/app/src/utils/server-compat.ts +++ b/packages/app/src/utils/server-compat.ts @@ -406,7 +406,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi { displaySuffix: match[3], } }) - const methods = authMethods.filter((_, index) => !stored[index]).map((method, index) => + const methods = authMethods.map((method, index) => method.type === "api" ? { type: "key" as const, label: method.label } : { type: "oauth" as const, id: String(index), label: method.label, prompts: method.prompts },