Skip to content

Commit 63c913e

Browse files
committed
fix(knowledge): keep long-lived saved AWS keys per run and gate saved-credential edits on rollout
- Reuse a saved AWS credential without a session token for the whole run instead of reloading it on every page and hydration - Hide the edit form's saved-credential replacement while saved-credential setup is disabled, matching the server gate - Classify a connector whose credential service has no config as internal, not caller-fixable - State in the v2 catalog that apiKey connectors with savedCredential accept credentialId
1 parent d72284c commit 63c913e

9 files changed

Lines changed: 41 additions & 9 deletions

File tree

‎apps/docs/openapi-v2-resources.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21700,7 +21700,7 @@
2170021700
"mode": {
2170121701
"type": "string",
2170221702
"const": "apiKey",
21703-
"description": "Authenticates with a stored API key; pass `apiKey`, and no `credentialId`, when creating the connector."
21703+
"description": "Authenticates with a stored API key; pass `apiKey` when creating the connector, or a saved `credentialId` instead when `savedCredential` is present."
2170421704
},
2170521705
"label": {
2170621706
"description": "Label shown above the key field.",

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,7 @@ export function KnowledgeBase({
15161516
isLoading={isLoadingConnectors}
15171517
canEdit={userPermissions.canEdit}
15181518
className='mt-0'
1519+
savedCredentialSetupEnabled={savedCredentialSetupEnabled}
15191520
/>
15201521
</ChipModalBody>
15211522
</ChipModal>

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface ConnectorsSectionProps {
3131
isLoading: boolean
3232
canEdit: boolean
3333
className?: string
34+
savedCredentialSetupEnabled?: boolean
3435
}
3536

3637
const STATUS_CONFIG = {
@@ -51,6 +52,7 @@ export function ConnectorsSection({
5152
isLoading,
5253
canEdit,
5354
className,
55+
savedCredentialSetupEnabled = false,
5456
}: ConnectorsSectionProps) {
5557
const scope = explicitScope ?? resourceScopeFromOwner({ workspaceId })
5658
const [editingConnector, setEditingConnector] = useState<ConnectorData | null>(null)
@@ -84,6 +86,7 @@ export function ConnectorsSection({
8486
knowledgeBaseId={knowledgeBaseId}
8587
isSearchIndex={isSearchIndex}
8688
connector={editingConnector}
89+
savedCredentialSetupEnabled={savedCredentialSetupEnabled}
8790
/>
8891
)}
8992
</div>

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface EditConnectorModalProps {
2626
knowledgeBaseId: string
2727
isSearchIndex?: boolean
2828
connector: ConnectorData
29+
savedCredentialSetupEnabled?: boolean
2930
}
3031

3132
export function EditConnectorModal({
@@ -35,6 +36,7 @@ export function EditConnectorModal({
3536
isSearchIndex = false,
3637
connector,
3738
scope,
39+
savedCredentialSetupEnabled = false,
3840
}: EditConnectorModalProps) {
3941
const [activeTab, setActiveTab] = useState('settings')
4042
const [documentFilter, setDocumentFilter] = useState<ConnectorDocumentFilter>('active')
@@ -44,6 +46,7 @@ export function EditConnectorModal({
4446
isSearchIndex,
4547
connector,
4648
onSaved: () => onOpenChange(false),
49+
savedCredentialSetupEnabled,
4750
})
4851

4952
return (

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ interface UseConnectorSettingsFormOptions {
127127
connector: ConnectorData
128128
onSaved: (connector: ConnectorData) => void
129129
syncing?: boolean
130+
/** Whether a key connector's saved credential may be replaced; the server refuses it while setup is off. */
131+
savedCredentialSetupEnabled?: boolean
130132
}
131133

132134
/** Editable connector settings shared by the source page and knowledge-base modal. */
@@ -137,6 +139,7 @@ export function useConnectorSettingsForm({
137139
connector,
138140
onSaved,
139141
syncing = isConnectorSyncingOrPending(connector),
142+
savedCredentialSetupEnabled = false,
140143
}: UseConnectorSettingsFormOptions) {
141144
const liveSearch = isSearchIndex
142145
const connectorConfig = liveSearchSourceMeta(
@@ -236,7 +239,8 @@ export function useConnectorSettingsForm({
236239

237240
/**
238241
* A key connector that syncs with its saved credential, or did until the credential was
239-
* removed: its credential stays replaceable here, and its key-only settings do not apply.
242+
* removed: its credential stays replaceable here while saved-credential setup is enabled,
243+
* and its key-only settings do not apply.
240244
*/
241245
const usesSavedCredential =
242246
connectorConfig?.auth.mode === 'apiKey' &&
@@ -293,7 +297,7 @@ export function useConnectorSettingsForm({
293297
accessDirty && !accessModeChanged && isContentEngineAccessMode(access.accessMode)
294298
/** Exposes credential selection for mode changes and administrator credential recovery. */
295299
const needsWorkspaceCredential =
296-
usesSavedCredential ||
300+
(usesSavedCredential && savedCredentialSetupEnabled) ||
297301
(connectorConfig?.auth.mode === 'oauth' &&
298302
isContentEngineAccessMode(access.accessMode) &&
299303
(persistedAccess.accessMode === 'members' ||

‎apps/sim/lib/api/contracts/v2/catalog.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ export const v2ConnectorTypeSchema = z
737737
mode: z
738738
.literal('apiKey')
739739
.describe(
740-
'Authenticates with a stored API key; pass `apiKey`, and no `credentialId`, when creating the connector.'
740+
'Authenticates with a stored API key; pass `apiKey` when creating the connector, or a saved `credentialId` instead when `savedCredential` is present.'
741741
),
742742
label: z.string().optional().describe('Label shown above the key field.'),
743743
placeholder: z.string().optional().describe('Placeholder shown in the key field.'),

‎apps/sim/lib/knowledge/connectors/access-token.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,23 @@ describe('createConnectorRunToken', () => {
536536
expect(runToken.current().accessToken).toBe('fresh-oauth-token')
537537
})
538538

539+
it('keeps long-lived saved AWS keys for the whole run instead of reloading them on every page', async () => {
540+
const resolve = vi.fn()
541+
const runToken = createConnectorRunToken({
542+
token: {
543+
accessToken: '',
544+
awsSession: { accessKeyId: 'AKIA1', secretAccessKey: 'AKIA1-secret' },
545+
},
546+
syncContext: {},
547+
resolve,
548+
})
549+
550+
await runToken.refresh()
551+
552+
expect(resolve).not.toHaveBeenCalled()
553+
expect(runToken.current().awsSession?.accessKeyId).toBe('AKIA1')
554+
})
555+
539556
it('never re-resolves a stored key', async () => {
540557
const resolve = vi.fn()
541558
const runToken = createConnectorRunToken({

‎apps/sim/lib/knowledge/connectors/access-token.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ const TOKEN_REUSE_MARGIN_MS = 5 * 60 * 1000
345345
/** A run's connector token, kept usable for the length of the run. */
346346
export interface ConnectorRunToken {
347347
current(): ConnectorAccessToken
348-
/** Re-resolves the token before a later call, unless it states an expiry still well ahead. */
348+
/** Re-resolves the token before a later call, unless it is long-lived or states an expiry still well ahead. */
349349
refresh(): Promise<void>
350350
}
351351

@@ -354,8 +354,9 @@ export interface ConnectorRunToken {
354354
* outlives a short-lived token, and re-seeds what the token binds into the run's
355355
* `syncContext`, so a connector reading its credential's site or AWS session from there signs
356356
* with the current one. A token that states its expiry (an AWS STS session) is kept until
357-
* shortly before it rather than exchanged again on every page. `resolve` is absent for a
358-
* stored key, which never changes during a run.
357+
* shortly before it rather than exchanged again on every page. Long-lived saved AWS keys (an
358+
* AWS session with no session token) are kept for the whole run, like a stored key, for which
359+
* `resolve` is absent.
359360
*/
360361
export function createConnectorRunToken(input: {
361362
token: ConnectorAccessToken
@@ -367,6 +368,7 @@ export function createConnectorRunToken(input: {
367368
current: () => token,
368369
async refresh() {
369370
if (!input.resolve) return
371+
if (token.awsSession && !token.awsSession.sessionToken) return
370372
if (token.expiresAt && token.expiresAt.getTime() - Date.now() > TOKEN_REUSE_MARGIN_MS) return
371373
token = await input.resolve()
372374
Object.assign(input.syncContext, syncContextForToken(token))

‎apps/sim/lib/knowledge/connectors/credential-service.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import type { ConnectorMeta } from '@/connectors/types'
1111
* The OAuth service a connector's credential must belong to. Setting up a key
1212
* connector on its saved credential and every access update resolve it here, so
1313
* a connector whose declared service has no config is refused instead of
14-
* skipping the provider check.
14+
* skipping the provider check. Callers reach it only for a connector that takes
15+
* a credential, so a missing service is a connector-definition fault, not one
16+
* the caller can fix.
1517
*/
1618
export function requireConnectorCredentialService(
1719
connectorMeta: Pick<ConnectorMeta, 'name' | 'auth'>
@@ -23,7 +25,7 @@ export function requireConnectorCredentialService(
2325
: null
2426
if (!service) {
2527
throw new OrchestrationError(
26-
'validation',
28+
'internal',
2729
`${connectorMeta.name} has no OAuth service to validate the credential against`
2830
)
2931
}

0 commit comments

Comments
 (0)