From d9d55c25f81818d0602be4992d73f6a92f2ff1d5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 3 Aug 2026 10:12:55 -0700 Subject: [PATCH] refactor(credentials): drop the principal abstraction, keep the identity fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #6201 introduced a ServiceAccountPrincipal union mirrored centrally into audit and blob metadata, and replaced 21 provider-named audit keys with uniform ones. Nothing reads any of it. It was built for an identity UI that was deliberately not shipped, and the audit-key rename is a breaking change for anything consuming those rows. The gap it was meant to close needed a fraction of that: Atlassian already resolved its accountId, it just was not recorded where every other provider records its identifier. Removes principal.ts, the required-nullable field on all three registry result types, and the central mirroring. Restores the per-provider audit keys, so the only breaking change in #6201 is undone. 14 validators, both registry server.ts files, errors.ts, Zoom, Zoho Desk and 12 test files are byte-identical to main again — each verified as a pure principal swap with no fix inside. Keeps every bug fix: atlassianAccountId + email, googleClientEmail/projectId and slackBotUserId now land in auditMetadata alongside the existing keys; Box and Salesforce log identity-lookup failures (neither file had a logger, so a degraded connect left no trace); Shopify no longer rejects a working credential on a partial-scope error; Google/Slack rotation still re-labels and records the new identity. Also drops providerFailureReason, which became dead code once the minters were rebuilt on main's shape. --- .../minters/box.test.ts | 38 +++-------- .../client-credential-accounts/minters/box.ts | 65 ++++++------------- .../minters/salesforce.test.ts | 29 +-------- .../minters/salesforce.ts | 64 +++++------------- .../minters/zoho-desk.test.ts | 7 +- .../minters/zoho-desk.ts | 8 +-- .../minters/zoom.test.ts | 3 +- .../minters/zoom.ts | 6 +- .../client-credential-accounts/server.ts | 15 +---- .../credentials/orchestration/index.test.ts | 5 +- apps/sim/lib/credentials/principal.ts | 62 ------------------ .../service-account-secret.test.ts | 41 ++---------- .../lib/credentials/service-account-secret.ts | 51 ++------------- .../token-service-accounts/errors.ts | 14 ---- .../token-service-accounts/server.ts | 15 +---- .../validators/airtable.test.ts | 10 ++- .../validators/airtable.ts | 6 +- .../validators/asana.test.ts | 6 +- .../validators/asana.ts | 7 +- .../validators/attio.test.ts | 4 +- .../validators/attio.ts | 15 ++--- .../validators/calcom.test.ts | 4 +- .../validators/calcom.ts | 8 +-- .../validators/claude-platform.ts | 5 -- .../validators/clickup.ts | 7 +- .../validators/hubspot.test.ts | 9 ++- .../validators/hubspot.ts | 18 ++--- .../validators/linear.test.ts | 3 +- .../validators/linear.ts | 5 +- .../validators/monday.test.ts | 5 +- .../validators/monday.ts | 5 +- .../validators/notion.test.ts | 13 ++-- .../validators/notion.ts | 8 +-- .../validators/pipedrive.test.ts | 6 +- .../validators/pipedrive.ts | 4 +- .../validators/shopify.test.ts | 4 +- .../validators/shopify.ts | 13 ++-- .../validators/trello.test.ts | 4 +- .../validators/trello.ts | 11 ++-- .../validators/wealthbox.test.ts | 4 +- .../validators/wealthbox.ts | 16 ++--- .../validators/webflow.test.ts | 6 +- .../validators/webflow.ts | 6 +- 43 files changed, 160 insertions(+), 475 deletions(-) delete mode 100644 apps/sim/lib/credentials/principal.ts diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/box.test.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/box.test.ts index 7b09b9b2ad5..f380e8d8742 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/box.test.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/box.test.ts @@ -67,7 +67,6 @@ describe('mintBoxServiceAccountToken', () => { .mockResolvedValueOnce(jsonResponse(200, { access_token: 'box-access', expires_in: 3600 })) .mockResolvedValueOnce( jsonResponse(200, { - id: '33445566', name: 'Sim Automation', login: 'AutomationUser_123_abc@boxdevedition.com', }) @@ -80,13 +79,14 @@ describe('mintBoxServiceAccountToken', () => { expiresInSeconds: 3600, identity: { displayName: 'Sim Automation', - principal: { - kind: 'user', - id: '33445566', - label: 'AutomationUser_123_abc@boxdevedition.com', + auditMetadata: { + boxEnterpriseId: '1234567', + boxServiceAccountLogin: 'AutomationUser_123_abc@boxdevedition.com', + }, + storedMetadata: { + enterpriseId: '1234567', + serviceAccountLogin: 'AutomationUser_123_abc@boxdevedition.com', }, - auditMetadata: { boxEnterpriseId: '1234567' }, - storedMetadata: { enterpriseId: '1234567' }, }, }) expect(mockFetch).toHaveBeenCalledTimes(2) @@ -94,7 +94,7 @@ describe('mintBoxServiceAccountToken', () => { expectIdentityCall() }) - it('marks the principal as lookup_failed when users/me fails', async () => { + it('still succeeds with a fallback identity when users/me fails', async () => { mockFetch .mockResolvedValueOnce(jsonResponse(200, { access_token: 'box-access', expires_in: 2400 })) .mockResolvedValueOnce(jsonResponse(500, { message: 'boom' })) @@ -105,26 +105,8 @@ describe('mintBoxServiceAccountToken', () => { expect(result.expiresInSeconds).toBe(2400) expect(result.identity).toEqual({ displayName: 'Box enterprise 1234567', - principal: { kind: 'lookup_failed', reason: 'HTTP 500' }, auditMetadata: { boxEnterpriseId: '1234567' }, - storedMetadata: { enterpriseId: '1234567' }, - }) - }) - - it('marks the principal as lookup_failed when users/me omits the user id', async () => { - mockFetch - .mockResolvedValueOnce(jsonResponse(200, { access_token: 'box-access', expires_in: 3600 })) - .mockResolvedValueOnce(jsonResponse(200, { name: 'Sim Automation' })) - - const result = await mintBoxServiceAccountToken(FIELDS) - - expect(result.identity?.principal).toEqual({ - kind: 'lookup_failed', - reason: 'response missing user id', }) - // Only the principal degrades — a name that did come back still beats the - // Enterprise-ID fallback, so the credential does not lose its label. - expect(result.identity?.displayName).toBe('Sim Automation') }) it('still succeeds when the identity request itself throws', async () => { @@ -136,10 +118,6 @@ describe('mintBoxServiceAccountToken', () => { expect(result.accessToken).toBe('box-access') expect(result.identity?.displayName).toBe('Box enterprise 1234567') - expect(result.identity?.principal).toEqual({ - kind: 'lookup_failed', - reason: 'provider_unavailable (HTTP 502)', - }) }) it('throws invalid_credentials on 400 invalid_client', async () => { diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/box.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/box.ts index 25e3b609080..571cb32d1f7 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/box.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/box.ts @@ -10,15 +10,12 @@ import { fetchProvider, isTransientProviderStatus, parseProviderJson, - providerFailureReason, readProviderErrorSnippet, TokenServiceAccountValidationError, } from '@/lib/credentials/token-service-accounts/errors' const logger = createLogger('BoxServiceAccountMinter') -const IDENTITY_STEP = 'box_identity' - const BOX_TOKEN_URL = 'https://api.box.com/oauth2/token' const BOX_CURRENT_USER_URL = 'https://api.box.com/2.0/users/me' @@ -27,14 +24,7 @@ interface BoxTokenResponse { expires_in?: number } -/** - * `id`, `name`, and `login` are all in the standard field set `GET /2.0/users/me` - * returns without a `fields` parameter, so capturing the Service Account's user - * id costs no extra request. - * @see https://developer.box.com/reference/get-users-me/ - */ interface BoxCurrentUserResponse { - id?: string name?: string login?: string } @@ -67,67 +57,52 @@ function boxErrorHint(body: string): string | undefined { /** * Best-effort identity lookup for the app's Service Account user. A failure - * never fails the mint — the credential degrades to an Enterprise-ID-derived - * display name with a `lookup_failed` principal, so the audit record shows the - * identity was not captured rather than implying none exists. + * never fails the mint — the caller falls back to an Enterprise-ID-derived + * display name. */ async function fetchBoxServiceAccountIdentity( accessToken: string, orgId: string ): Promise { - /** - * `label` keeps whatever human name the lookup did return. A response can - * carry `name`/`login` but no `id` — the principal is then unusable, but the - * label still beats the Enterprise-ID fallback, so only the principal - * degrades and the credential does not silently lose its name. - */ - const degraded = (reason: string, label?: string): ClientCredentialAccountIdentity => ({ - displayName: label ?? `Box enterprise ${orgId}`, - principal: { kind: 'lookup_failed', reason }, + const fallback: ClientCredentialAccountIdentity = { + displayName: `Box enterprise ${orgId}`, auditMetadata: { boxEnterpriseId: orgId }, - storedMetadata: { enterpriseId: orgId }, - }) + } try { const res = await fetchProvider( BOX_CURRENT_USER_URL, { headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' } }, - IDENTITY_STEP + 'box_identity' ) if (!res.ok) { logger.warn('Box service-account identity lookup failed', { - step: IDENTITY_STEP, + step: 'box_identity', status: res.status, enterpriseId: orgId, }) - return degraded(`HTTP ${res.status}`) + return fallback } - const user = await parseProviderJson(res, IDENTITY_STEP) - const id = typeof user.id === 'string' && user.id ? user.id : undefined + const user = await parseProviderJson(res, 'box_identity') const login = typeof user.login === 'string' && user.login ? user.login : undefined const name = typeof user.name === 'string' && user.name ? user.name : undefined - if (!id) { - logger.warn('Box service-account identity response carried no user id', { - step: IDENTITY_STEP, - status: res.status, - enterpriseId: orgId, - }) - return degraded('response missing user id', name ?? login) - } return { - displayName: name ?? login ?? `Box enterprise ${orgId}`, - // The Service Account is a real Box user; `enterpriseId` is shared by - // every app in the enterprise and so is kept as separate context. - principal: { kind: 'user', id, ...(login ? { label: login } : {}) }, - auditMetadata: { boxEnterpriseId: orgId }, - storedMetadata: { enterpriseId: orgId }, + displayName: name ?? login ?? fallback.displayName, + auditMetadata: { + boxEnterpriseId: orgId, + ...(login ? { boxServiceAccountLogin: login } : {}), + }, + storedMetadata: { + enterpriseId: orgId, + ...(login ? { serviceAccountLogin: login } : {}), + }, } } catch (error) { logger.warn('Box service-account identity lookup threw', { - step: IDENTITY_STEP, + step: 'box_identity', enterpriseId: orgId, error: getErrorMessage(error), }) - return degraded(providerFailureReason(error)) + return fallback } } diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/salesforce.test.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/salesforce.test.ts index 0b874321280..6e0aad65f22 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/salesforce.test.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/salesforce.test.ts @@ -78,7 +78,6 @@ describe('mintSalesforceServiceAccountToken', () => { name: 'Integration User', preferred_username: 'integration@yourorg.com', organization_id: '00Dxx0000000001EAA', - user_id: '005xx000001Sv6DAAS', }) ) @@ -91,19 +90,16 @@ describe('mintSalesforceServiceAccountToken', () => { grantedScopes: ['api'], identity: { displayName: 'Integration User', - principal: { - kind: 'user', - id: '005xx000001Sv6DAAS', - label: 'integration@yourorg.com', - }, auditMetadata: { salesforceMyDomainHost: HOST, salesforceOrgId: '00Dxx0000000001EAA', + salesforceRunAsUsername: 'integration@yourorg.com', }, storedMetadata: { myDomainHost: HOST, instanceUrl: INSTANCE_URL, orgId: '00Dxx0000000001EAA', + runAsUsername: 'integration@yourorg.com', grantedScopes: 'api', }, }, @@ -263,7 +259,7 @@ describe('mintSalesforceServiceAccountToken', () => { }) }) - it('marks the principal as lookup_failed when the userinfo call throws', async () => { + it('falls back to a host-derived identity when the userinfo call fails', async () => { mockFetch .mockResolvedValueOnce( jsonResponse(200, { access_token: 'sf-access', instance_url: INSTANCE_URL }) @@ -275,30 +271,11 @@ describe('mintSalesforceServiceAccountToken', () => { expect(result.accessToken).toBe('sf-access') expect(result.identity).toEqual({ displayName: `Salesforce ${HOST}`, - principal: { kind: 'lookup_failed', reason: 'provider_unavailable (HTTP 502)' }, auditMetadata: { salesforceMyDomainHost: HOST }, storedMetadata: { myDomainHost: HOST, instanceUrl: INSTANCE_URL }, }) }) - it('marks the principal as lookup_failed when userinfo omits user_id', async () => { - mockFetch - .mockResolvedValueOnce( - jsonResponse(200, { access_token: 'sf-access', instance_url: INSTANCE_URL }) - ) - .mockResolvedValueOnce(jsonResponse(200, { name: 'Integration User' })) - - const result = await mintSalesforceServiceAccountToken(FIELDS) - - expect(result.identity?.principal).toEqual({ - kind: 'lookup_failed', - reason: 'response missing user_id', - }) - // Only the principal degrades — a name that did come back still beats the - // host fallback, so the credential does not lose its label. - expect(result.identity?.displayName).toBe('Integration User') - }) - it('ignores a non-Salesforce instance_url and falls back to the validated host', async () => { mockFetch .mockResolvedValueOnce( diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/salesforce.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/salesforce.ts index e8e702c98c7..3b3498d846f 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/salesforce.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/salesforce.ts @@ -10,12 +10,10 @@ import type { ClientCredentialAccountMintOptions, ClientCredentialAccountMintResult, } from '@/lib/credentials/client-credential-accounts/server' -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, isTransientProviderStatus, parseProviderJson, - providerFailureReason, readProviderErrorSnippet, TokenServiceAccountValidationError, } from '@/lib/credentials/token-service-accounts/errors' @@ -26,31 +24,20 @@ import { * 15min minimum), unknowable from the response. Cache conservatively for 10 * minutes (safely below the 15-minute floor) and rely on re-minting. */ -const SALESFORCE_TOKEN_TTL_SECONDS = 600 - -const IDENTITY_STEP = 'salesforce_identity' - const logger = createLogger('SalesforceServiceAccountMinter') +const SALESFORCE_TOKEN_TTL_SECONDS = 600 + interface SalesforceTokenResponse { access_token?: string instance_url?: string scope?: string } -/** - * `/services/oauth2/userinfo` returns `user_id`, `organization_id`, - * `preferred_username`, and `name` in the same call the display name already - * needs, so capturing the run-as user id costs no extra request. `sub` is - * deliberately unused — Salesforce documents it as the UserInfo endpoint URL, - * not a subject identifier. - * @see https://help.salesforce.com/s/articleView?id=sf.remoteaccess_using_userinfo_endpoint.htm&type=5 - */ interface SalesforceUserinfoResponse { name?: string preferred_username?: string organization_id?: string - user_id?: string } /** @@ -108,43 +95,34 @@ function salesforceTokenTtlSeconds(accessToken: string): number { /** * Best-effort identity lookup for the run-as integration user via the - * standard userinfo endpoint. A failure never fails the mint — the credential - * degrades to a host-derived display name with a `lookup_failed` principal, so - * the audit record shows the identity was not captured rather than implying - * none exists. + * standard userinfo endpoint. A failure never fails the mint — the caller + * falls back to a host-derived display name. */ async function fetchSalesforceIdentity( accessToken: string, instanceUrl: string, host: string ): Promise { - /** - * `label` keeps whatever human name userinfo did return. A response can carry - * `name`/`preferred_username` but no `user_id` — the principal is then - * unusable, but the label still beats the host fallback, so only the - * principal degrades and the credential does not silently lose its name. - */ - const degraded = (reason: string, label?: string): ClientCredentialAccountIdentity => ({ - displayName: label ?? `Salesforce ${host}`, - principal: { kind: 'lookup_failed', reason }, + const fallback: ClientCredentialAccountIdentity = { + displayName: `Salesforce ${host}`, auditMetadata: { salesforceMyDomainHost: host }, storedMetadata: { myDomainHost: host, instanceUrl }, - }) + } try { const res = await fetchProvider( `${instanceUrl}/services/oauth2/userinfo`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' } }, - IDENTITY_STEP + 'salesforce_identity' ) if (!res.ok) { logger.warn('Salesforce run-as identity lookup failed', { - step: IDENTITY_STEP, + step: 'salesforce_identity', status: res.status, host, }) - return degraded(`HTTP ${res.status}`) + return fallback } - const user = await parseProviderJson(res, IDENTITY_STEP) + const user = await parseProviderJson(res, 'salesforce_identity') const username = typeof user.preferred_username === 'string' && user.preferred_username ? user.preferred_username @@ -154,37 +132,27 @@ async function fetchSalesforceIdentity( typeof user.organization_id === 'string' && user.organization_id ? user.organization_id : undefined - const userId = typeof user.user_id === 'string' && user.user_id ? user.user_id : undefined - if (!userId) { - logger.warn('Salesforce userinfo response carried no user_id', { - step: IDENTITY_STEP, - status: res.status, - host, - }) - return degraded('response missing user_id', name ?? username) - } return { - displayName: name ?? username ?? `Salesforce ${host}`, - // The 18-char user id is immutable; `preferred_username` is renameable, - // so it is only a label. - principal: userPrincipal(userId, username), + displayName: name ?? username ?? fallback.displayName, auditMetadata: { salesforceMyDomainHost: host, ...(orgId ? { salesforceOrgId: orgId } : {}), + ...(username ? { salesforceRunAsUsername: username } : {}), }, storedMetadata: { myDomainHost: host, instanceUrl, ...(orgId ? { orgId } : {}), + ...(username ? { runAsUsername: username } : {}), }, } } catch (error) { logger.warn('Salesforce run-as identity lookup threw', { - step: IDENTITY_STEP, + step: 'salesforce_identity', host, error: getErrorMessage(error), }) - return degraded(providerFailureReason(error)) + return fallback } } diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/zoho-desk.test.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/zoho-desk.test.ts index 6bf48fd65c1..839e452202a 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/zoho-desk.test.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/zoho-desk.test.ts @@ -130,9 +130,12 @@ describe('mintZohoDeskServiceAccountToken', () => { grantedScopes: ['Desk.tickets.READ', 'Desk.contacts.READ'], identity: { displayName: 'Zoho Desk org 600123456', - principal: { kind: 'tenant', id: 'ZohoDesk.600123456' }, - auditMetadata: { zohoDeskClientId: 'zoho-cid' }, + auditMetadata: { + zohoDeskSoid: 'ZohoDesk.600123456', + zohoDeskClientId: 'zoho-cid', + }, storedMetadata: { + soid: 'ZohoDesk.600123456', apiDomain: 'https://desk.zoho.com', dataCenter: 'us', grantedScopes: 'Desk.tickets.READ Desk.contacts.READ', diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/zoho-desk.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/zoho-desk.ts index 7ceff1748a9..35f3f6f7963 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/zoho-desk.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/zoho-desk.ts @@ -12,7 +12,6 @@ import type { ClientCredentialAccountMintOptions, ClientCredentialAccountMintResult, } from '@/lib/credentials/client-credential-accounts/server' -import { tenantPrincipal } from '@/lib/credentials/principal' import { fetchProvider, isTransientProviderStatus, @@ -256,7 +255,7 @@ export async function mintZohoDeskServiceAccountToken( return { accessToken: payload.access_token, expiresInSeconds, apiDomain, grantedScopes } } - const storedMetadata: Record = { apiDomain, dataCenter: dataCenter.id } + const storedMetadata: Record = { soid, apiDomain, dataCenter: dataCenter.id } if (grantedScopes?.length) { storedMetadata.grantedScopes = grantedScopes.join(' ') } @@ -268,10 +267,7 @@ export async function mintZohoDeskServiceAccountToken( grantedScopes, identity: { displayName: `Zoho Desk org ${fields.orgId.trim()}`, - // The Self Client grant is scoped to the organization (`soid`) and never - // hits the Accounts profile endpoint, so no agent identity exists here. - principal: tenantPrincipal(soid), - auditMetadata: { zohoDeskClientId: fields.clientId }, + auditMetadata: { zohoDeskSoid: soid, zohoDeskClientId: fields.clientId }, storedMetadata, }, } diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/zoom.test.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/zoom.test.ts index afa900ed610..dcfd2822a14 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/zoom.test.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/zoom.test.ts @@ -74,8 +74,7 @@ describe('mintZoomServiceAccountToken', () => { grantedScopes: ['meeting:read:meeting:admin', 'user:read:user:admin'], identity: { displayName: 'Zoom account AbCdEf123', - principal: { kind: 'tenant', id: 'AbCdEf123' }, - auditMetadata: { zoomClientId: 'zoom-cid' }, + auditMetadata: { zoomAccountId: 'AbCdEf123', zoomClientId: 'zoom-cid' }, storedMetadata: { apiUrl: 'https://api.zoom.us', grantedScopes: 'meeting:read:meeting:admin user:read:user:admin', diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/zoom.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/zoom.ts index 218eee37c1e..978409ae0ee 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/zoom.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/zoom.ts @@ -3,7 +3,6 @@ import type { ClientCredentialAccountMintOptions, ClientCredentialAccountMintResult, } from '@/lib/credentials/client-credential-accounts/server' -import { tenantPrincipal } from '@/lib/credentials/principal' import { fetchProvider, isTransientProviderStatus, @@ -125,10 +124,7 @@ export async function mintZoomServiceAccountToken( grantedScopes, identity: { displayName: `Zoom account ${fields.orgId}`, - // A Server-to-Server app authenticates as the account, not as a Zoom - // user; the grant exposes no user identifier at all. - principal: tenantPrincipal(fields.orgId), - auditMetadata: { zoomClientId: fields.clientId }, + auditMetadata: { zoomAccountId: fields.orgId, zoomClientId: fields.clientId }, ...(Object.keys(storedMetadata).length > 0 ? { storedMetadata } : {}), }, } diff --git a/apps/sim/lib/credentials/client-credential-accounts/server.ts b/apps/sim/lib/credentials/client-credential-accounts/server.ts index 2a1f6bd2214..44b216f8406 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/server.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/server.ts @@ -11,7 +11,6 @@ import { mintBoxServiceAccountToken } from '@/lib/credentials/client-credential- import { mintSalesforceServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/salesforce' import { mintZohoDeskServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/zoho-desk' import { mintZoomServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/zoom' -import type { ServiceAccountPrincipal } from '@/lib/credentials/principal' /** Raw fields a client-credential minter receives (already trimmed). */ export interface ClientCredentialAccountFields { @@ -34,21 +33,11 @@ export interface ClientCredentialAccountFields { export interface ClientCredentialAccountIdentity { /** Default display name when the user didn't provide one. */ displayName: string - /** - * Identity the minted token acts as, or `null` when the provider exposes - * none. Required (never optional) so a new minter cannot be written without - * deciding. `verifyAndBuildServiceAccountSecret` mirrors it into both - * `auditMetadata` and `storedMetadata`, so minters must not repeat it. - */ - principal: ServiceAccountPrincipal | null - /** - * Non-secret identifiers recorded in the audit log that are NOT the - * principal (e.g. the enterprise id behind a service-account user). - */ + /** Non-secret identifiers recorded in the audit log (e.g. account/enterprise id). */ auditMetadata: Record /** * Non-secret metadata persisted inside the encrypted blob alongside the - * credentials (e.g. regional API host, granted scopes) for debugging. + * credentials (e.g. regional API host, service-account login) for debugging. */ storedMetadata?: Record } diff --git a/apps/sim/lib/credentials/orchestration/index.test.ts b/apps/sim/lib/credentials/orchestration/index.test.ts index 18e0cfb0d1d..25be0da325a 100644 --- a/apps/sim/lib/credentials/orchestration/index.test.ts +++ b/apps/sim/lib/credentials/orchestration/index.test.ts @@ -108,7 +108,7 @@ describe('performUpdateCredential — service-account secret rotation', () => { providerId: 'google-service-account', encryptedServiceAccountKey: 'new-cipher', displayName: NEW_EMAIL, - auditMetadata: { principalKind: 'user', principalId: NEW_EMAIL }, + auditMetadata: { googleClientEmail: NEW_EMAIL }, }) }) @@ -213,8 +213,7 @@ describe('performUpdateCredential — service-account secret rotation', () => { expect(auditMetadata()).toMatchObject({ credentialType: 'service_account', - principalKind: 'user', - principalId: NEW_EMAIL, + googleClientEmail: NEW_EMAIL, }) expect(auditMetadata().updatedFields).toEqual( expect.arrayContaining(['displayName', 'encryptedServiceAccountKey']) diff --git a/apps/sim/lib/credentials/principal.ts b/apps/sim/lib/credentials/principal.ts deleted file mode 100644 index 3339b00ec1a..00000000000 --- a/apps/sim/lib/credentials/principal.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Provider-identity primitives for service-account credentials. - * - * Deliberately a leaf module: the token and client-credential registries both - * need these, and `service-account-secret` imports values from both registries. - * Defining them there would close a runtime import cycle. - */ - -/** - * Provider identity captured while verifying a service-account credential. - * - * `tenant` exists because several providers can only ever report an - * org/workspace/site-level identifier (Attio, Shopify, Webflow, Zoom, Zoho - * Desk) — callers must never present those as the human actor behind the - * credential. `lookup_failed` records that the provider does expose a - * principal but the lookup did not complete, which is distinct from a - * provider that exposes no principal at all (`null`). - */ -export type ServiceAccountPrincipal = - | { kind: 'user'; id: string; label?: string } - | { kind: 'tenant'; id: string; label?: string } - | { kind: 'lookup_failed'; reason: string } - -/** - * The human actor a credential authenticates as. - * - * `label` accepts null/undefined because provider payloads routinely type an - * optional email or username that way, and is dropped when empty so - * {@link serviceAccountPrincipalMetadata} never emits a blank key. - */ -export function userPrincipal(id: string, label?: string | null): ServiceAccountPrincipal { - return { kind: 'user', id, ...(label ? { label } : {}) } -} - -/** - * An org/workspace/site-level identifier, for the providers that expose no - * actor at all. Kept distinct from {@link userPrincipal} so callers can never - * present a tenant id as the person behind the credential. - */ -export function tenantPrincipal(id: string, label?: string | null): ServiceAccountPrincipal { - return { kind: 'tenant', id, ...(label ? { label } : {}) } -} - -/** - * Flattens a principal into the string map mirrored into both `auditMetadata` - * (queryable on `audit_log.metadata`) and `storedMetadata` (inside the - * encrypted blob). Applied centrally by the builders below so no provider can - * capture a principal and forget to surface it. - */ -export function serviceAccountPrincipalMetadata( - principal: ServiceAccountPrincipal | null -): Record { - if (principal === null) return { principalKind: 'none' } - if (principal.kind === 'lookup_failed') { - return { principalKind: 'lookup_failed', principalLookupError: principal.reason } - } - return { - principalKind: principal.kind, - principalId: principal.id, - ...(principal.label ? { principalLabel: principal.label } : {}), - } -} diff --git a/apps/sim/lib/credentials/service-account-secret.test.ts b/apps/sim/lib/credentials/service-account-secret.test.ts index b874432a680..27fa472f15f 100644 --- a/apps/sim/lib/credentials/service-account-secret.test.ts +++ b/apps/sim/lib/credentials/service-account-secret.test.ts @@ -104,7 +104,6 @@ describe('verifyAndBuildServiceAccountSecret', () => { accountId: 'acc-1', displayName: 'Jira Bot', cloudId: 'cloud-1', - emailAddress: 'bot@acme.com', }) const result = await verifyAndBuildServiceAccountSecret(ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID, { apiToken: 'tok', @@ -113,9 +112,6 @@ describe('verifyAndBuildServiceAccountSecret', () => { expect(result.providerId).toBe(ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID) expect(result.displayName).toBe('Jira Bot') expect(result.auditMetadata.atlassianCloudId).toBe('cloud-1') - expect(result.principal).toEqual({ kind: 'user', id: 'acc-1', label: 'bot@acme.com' }) - expect(result.auditMetadata.principalId).toBe('acc-1') - expect(result.auditMetadata.principalLabel).toBe('bot@acme.com') const blob = JSON.parse(result.encryptedServiceAccountKey) expect(blob).toMatchObject({ apiToken: 'tok', @@ -131,32 +127,17 @@ describe('verifyAndBuildServiceAccountSecret', () => { }) it('validates and encrypts a Google service-account JSON key', async () => { - const json = JSON.stringify({ - type: 'service_account', - client_email: 'svc@proj.iam', - project_id: 'proj', - }) + const json = JSON.stringify({ type: 'service_account', client_email: 'svc@proj.iam' }) const result = await verifyAndBuildServiceAccountSecret('google-service-account', { serviceAccountJson: json, }) expect(result.providerId).toBe('google-service-account') expect(result.displayName).toBe('svc@proj.iam') expect(result.encryptedServiceAccountKey).toBe(json) - expect(result.principal).toEqual({ kind: 'user', id: 'svc@proj.iam' }) - expect(result.auditMetadata).toEqual({ - googleClientEmail: 'svc@proj.iam', - googleProjectId: 'proj', - principalKind: 'user', - principalId: 'svc@proj.iam', - }) }) it('accepts a legacy Google create with an empty providerId', async () => { - const json = JSON.stringify({ - type: 'service_account', - client_email: 'svc@proj.iam', - project_id: 'proj', - }) + const json = JSON.stringify({ type: 'service_account', client_email: 'svc@proj.iam' }) const result = await verifyAndBuildServiceAccountSecret('', { serviceAccountJson: json }) expect(result.providerId).toBe('google-service-account') }) @@ -176,7 +157,6 @@ describe('verifyAndBuildServiceAccountSecret', () => { expiresInSeconds: 3600, identity: { displayName: 'Zoom account acc-1', - principal: { kind: 'tenant', id: 'acc-1' }, auditMetadata: { zoomAccountId: 'acc-1' }, storedMetadata: { apiUrl: 'https://api.zoom.us' }, }, @@ -188,11 +168,7 @@ describe('verifyAndBuildServiceAccountSecret', () => { }) expect(result.providerId).toBe('zoom-service-account') expect(result.displayName).toBe('Zoom account acc-1') - expect(result.auditMetadata).toEqual({ - zoomAccountId: 'acc-1', - principalKind: 'tenant', - principalId: 'acc-1', - }) + expect(result.auditMetadata).toEqual({ zoomAccountId: 'acc-1' }) expect(mockClientCredentialMinter).toHaveBeenCalledWith({ clientId: 'cid', clientSecret: 'csec', @@ -205,11 +181,7 @@ describe('verifyAndBuildServiceAccountSecret', () => { clientId: 'cid', clientSecret: 'csec', orgId: 'acc-1', - metadata: { - apiUrl: 'https://api.zoom.us', - principalKind: 'tenant', - principalId: 'acc-1', - }, + metadata: { apiUrl: 'https://api.zoom.us' }, }) }) @@ -221,10 +193,9 @@ describe('verifyAndBuildServiceAccountSecret', () => { orgId: '999', }) expect(result.displayName).toBe('Box 999') - expect(result.principal).toBeNull() - expect(result.auditMetadata).toEqual({ principalKind: 'none' }) + expect(result.auditMetadata).toEqual({}) const blob = JSON.parse(result.encryptedServiceAccountKey) - expect(blob.metadata).toEqual({ principalKind: 'none' }) + expect(blob.metadata).toBeUndefined() }) it('throws when client-credential required fields are missing, without minting', async () => { diff --git a/apps/sim/lib/credentials/service-account-secret.ts b/apps/sim/lib/credentials/service-account-secret.ts index d6b678d4a17..c5418ccaf9c 100644 --- a/apps/sim/lib/credentials/service-account-secret.ts +++ b/apps/sim/lib/credentials/service-account-secret.ts @@ -16,10 +16,6 @@ import { getClientCredentialAccountMinter, } from '@/lib/credentials/client-credential-accounts/server' import { slackCustomBotDisplayName } from '@/lib/credentials/display-name' -import { - type ServiceAccountPrincipal, - serviceAccountPrincipalMetadata, -} from '@/lib/credentials/principal' import { getTokenServiceAccountDescriptor, isTokenServiceAccountProviderId, @@ -57,12 +53,6 @@ export interface ServiceAccountSecretResult { encryptedServiceAccountKey: string displayName: string auditMetadata: Record - /** - * Provider principal behind the credential, or `null` when the provider - * exposes none. Required (never optional) so a new provider cannot be added - * without deciding what identity it captures. - */ - principal: ServiceAccountPrincipal | null /** Slack custom bot: the derived bot user id (for reaction self-drop). */ botUserId?: string } @@ -89,11 +79,6 @@ async function buildAtlassianServiceAccountSecret( } const normalizedDomain = normalizeAtlassianDomain(domain) const validation = await validateAtlassianServiceAccount(apiToken, normalizedDomain) - const principal: ServiceAccountPrincipal = { - kind: 'user', - id: validation.accountId, - ...(validation.emailAddress ? { label: validation.emailAddress } : {}), - } // `atlassianAccountId` stays at the blob's top level: `getAtlassianServiceAccountSecret` // in `app/api/auth/oauth/utils.ts` reads it there on every existing credential. const blob = JSON.stringify({ @@ -102,7 +87,6 @@ async function buildAtlassianServiceAccountSecret( domain: normalizedDomain, cloudId: validation.cloudId, atlassianAccountId: validation.accountId, - metadata: serviceAccountPrincipalMetadata(principal), }) const { encrypted } = await encryptSecret(blob) return { @@ -112,9 +96,9 @@ async function buildAtlassianServiceAccountSecret( auditMetadata: { atlassianDomain: normalizedDomain, atlassianCloudId: validation.cloudId, - ...serviceAccountPrincipalMetadata(principal), + atlassianAccountId: validation.accountId, + ...(validation.emailAddress ? { atlassianAccountEmail: validation.emailAddress } : {}), }, - principal, } } @@ -144,11 +128,6 @@ async function buildSlackCustomBotSecret( `Could not verify the Slack bot token: ${getErrorMessage(error)}` ) } - // `auth.test` returns the bot user only for bot tokens; a token without one - // is workspace-scoped, so the team is the finest identity available. - const principal: ServiceAccountPrincipal = botUserId - ? { kind: 'user', id: botUserId } - : { kind: 'tenant', id: teamId, ...(teamName ? { label: teamName } : {}) } const blob = JSON.stringify({ type: SLACK_CUSTOM_BOT_SECRET_TYPE, signingSecret, @@ -156,15 +135,13 @@ async function buildSlackCustomBotSecret( teamId, botUserId, teamName, - metadata: serviceAccountPrincipalMetadata(principal), }) const { encrypted } = await encryptSecret(blob) return { providerId: SLACK_CUSTOM_BOT_PROVIDER_ID, encryptedServiceAccountKey: encrypted, displayName: slackCustomBotDisplayName(teamName), - auditMetadata: { slackTeamId: teamId, ...serviceAccountPrincipalMetadata(principal) }, - principal, + auditMetadata: { slackTeamId: teamId, ...(botUserId ? { slackBotUserId: botUserId } : {}) }, botUserId, } } @@ -190,11 +167,6 @@ async function buildGoogleServiceAccountSecret( ) } const { client_email: clientEmail, project_id: projectId } = jsonParseResult.data - // `client_email` is the principal a Google service account authenticates as - // (its `unique_id` is not guaranteed to be present in a downloaded key). - const principal: ServiceAccountPrincipal = { kind: 'user', id: clientEmail } - // The blob stays the verbatim GCP key — every consumer parses it as one — so - // the principal is mirrored into the audit metadata only. const { encrypted } = await encryptSecret(serviceAccountJson) return { providerId: GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID, @@ -203,9 +175,7 @@ async function buildGoogleServiceAccountSecret( auditMetadata: { googleClientEmail: clientEmail, googleProjectId: projectId, - ...serviceAccountPrincipalMetadata(principal), }, - principal, } } @@ -236,21 +206,19 @@ async function buildTokenServiceAccountSecret( ) } const validation = await validator({ apiToken, domain }) - const principalMetadata = serviceAccountPrincipalMetadata(validation.principal) const blob: TokenServiceAccountSecretBlob = { type: TOKEN_SERVICE_ACCOUNT_SECRET_TYPE, providerId, apiToken, ...(requiresDomain ? { domain: validation.normalizedDomain ?? domain } : {}), - metadata: { ...validation.storedMetadata, ...principalMetadata }, + ...(validation.storedMetadata ? { metadata: validation.storedMetadata } : {}), } const { encrypted } = await encryptSecret(JSON.stringify(blob)) return { providerId, encryptedServiceAccountKey: encrypted, displayName: validation.displayName, - auditMetadata: { ...validation.auditMetadata, ...principalMetadata }, - principal: validation.principal, + auditMetadata: validation.auditMetadata, } } @@ -287,10 +255,6 @@ async function buildClientCredentialAccountSecret( ) } const mint = await minter({ clientId, clientSecret, orgId, dataCenter }) - // `identity` is absent only on the `skipIdentity` execution-time path, which - // never reaches this builder; treat it as "no principal captured". - const principal = mint.identity?.principal ?? null - const principalMetadata = serviceAccountPrincipalMetadata(principal) const blob: ClientCredentialAccountSecretBlob = { type: CLIENT_CREDENTIAL_ACCOUNT_SECRET_TYPE, providerId, @@ -298,15 +262,14 @@ async function buildClientCredentialAccountSecret( clientSecret, orgId, ...(dataCenter ? { dataCenter } : {}), - metadata: { ...mint.identity?.storedMetadata, ...principalMetadata }, + ...(mint.identity?.storedMetadata ? { metadata: mint.identity.storedMetadata } : {}), } const { encrypted } = await encryptSecret(JSON.stringify(blob)) return { providerId, encryptedServiceAccountKey: encrypted, displayName: mint.identity?.displayName ?? `${descriptor.serviceLabel} ${orgId}`, - auditMetadata: { ...mint.identity?.auditMetadata, ...principalMetadata }, - principal, + auditMetadata: mint.identity?.auditMetadata ?? {}, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/errors.ts b/apps/sim/lib/credentials/token-service-accounts/errors.ts index 3e6ec4cbed6..abf0b003b96 100644 --- a/apps/sim/lib/credentials/token-service-accounts/errors.ts +++ b/apps/sim/lib/credentials/token-service-accounts/errors.ts @@ -1,4 +1,3 @@ -import { getErrorMessage } from '@sim/utils/errors' import { truncate } from '@sim/utils/string' /** @@ -24,19 +23,6 @@ export class TokenServiceAccountValidationError extends Error { const ERROR_SNIPPET_MAX_LENGTH = 500 -/** - * Short, stable description of a failed best-effort provider call, for callers - * that degrade instead of throwing. `TokenServiceAccountValidationError`'s - * message is only its code, so the status is appended to keep the reason - * diagnosable. - */ -export function providerFailureReason(error: unknown): string { - if (error instanceof TokenServiceAccountValidationError) { - return `${error.code} (HTTP ${error.status})` - } - return getErrorMessage(error, 'request failed') -} - /** * Transient statuses a provider token/verification endpoint can return that * say nothing about the submitted credentials (throttling, request timeout) — diff --git a/apps/sim/lib/credentials/token-service-accounts/server.ts b/apps/sim/lib/credentials/token-service-accounts/server.ts index 4fee7e16e40..a7a693b1ca0 100644 --- a/apps/sim/lib/credentials/token-service-accounts/server.ts +++ b/apps/sim/lib/credentials/token-service-accounts/server.ts @@ -1,4 +1,3 @@ -import type { ServiceAccountPrincipal } from '@/lib/credentials/principal' import { AIRTABLE_SERVICE_ACCOUNT_PROVIDER_ID, ASANA_SERVICE_ACCOUNT_PROVIDER_ID, @@ -45,21 +44,11 @@ export interface TokenServiceAccountFields { export interface TokenServiceAccountValidationResult { /** Default display name when the user didn't provide one. */ displayName: string - /** - * Identity the token authenticates as, or `null` when the provider exposes - * none. Required (never optional) so a new validator cannot be written - * without deciding. `verifyAndBuildServiceAccountSecret` mirrors it into both - * `auditMetadata` and `storedMetadata`, so validators must not repeat it. - */ - principal: ServiceAccountPrincipal | null - /** - * Non-secret identifiers recorded in the audit log that are NOT the - * principal (e.g. the org id behind a user principal). - */ + /** Non-secret identifiers recorded in the audit log (e.g. portal/workspace id). */ auditMetadata: Record /** * Non-secret metadata persisted inside the encrypted blob alongside the - * token (e.g. normalized store domain, granted scopes) for later debugging. + * token (e.g. normalized store domain, portal id) for later debugging. */ storedMetadata?: Record /** Normalized domain to persist instead of the raw user input (when collected). */ diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/airtable.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/airtable.test.ts index b71becb916b..b1f4797b509 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/airtable.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/airtable.test.ts @@ -37,9 +37,8 @@ describe('validateAirtableServiceAccount', () => { expect(result).toEqual({ displayName: 'svc@example.com', - principal: { kind: 'user', id: 'usrABC123', label: 'svc@example.com' }, - auditMetadata: {}, - storedMetadata: { scopes: 'data.records:read' }, + auditMetadata: { airtableUserId: 'usrABC123' }, + storedMetadata: { userId: 'usrABC123', scopes: 'data.records:read' }, }) expect(mockFetch).toHaveBeenCalledWith('https://api.airtable.com/v0/meta/whoami', { headers: { @@ -56,9 +55,8 @@ describe('validateAirtableServiceAccount', () => { const result = await validateAirtableServiceAccount({ apiToken: 'pat456.secret' }) expect(result.displayName).toBe('Airtable user usrXYZ789') - expect(result.principal).toEqual({ kind: 'user', id: 'usrXYZ789' }) - expect(result.auditMetadata).toEqual({}) - expect(result.storedMetadata).toEqual({}) + expect(result.auditMetadata).toEqual({ airtableUserId: 'usrXYZ789' }) + expect(result.storedMetadata).toEqual({ userId: 'usrXYZ789' }) }) it('throws invalid_credentials on 401', async () => { diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/airtable.ts b/apps/sim/lib/credentials/token-service-accounts/validators/airtable.ts index c70ed3c4b1b..ccab65691eb 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/airtable.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/airtable.ts @@ -1,4 +1,3 @@ -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -48,15 +47,14 @@ export async function validateAirtableServiceAccount( }) } - const storedMetadata: Record = {} + const storedMetadata: Record = { userId: whoami.id } if (whoami.scopes) { storedMetadata.scopes = whoami.scopes.join(' ') } return { displayName: whoami.email ?? `Airtable user ${whoami.id}`, - principal: userPrincipal(whoami.id, whoami.email), - auditMetadata: {}, + auditMetadata: { airtableUserId: whoami.id }, storedMetadata, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/asana.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/asana.test.ts index c1c57c7f815..6e950a55002 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/asana.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/asana.test.ts @@ -35,8 +35,8 @@ describe('validateAsanaServiceAccount', () => { expect(result).toEqual({ displayName: 'Sim Integration', - principal: { kind: 'user', id: '12345', label: 'bot@example.com' }, - auditMetadata: {}, + auditMetadata: { asanaUserGid: '12345' }, + storedMetadata: { userGid: '12345', email: 'bot@example.com' }, }) expect(mockFetch).toHaveBeenCalledWith( 'https://app.asana.com/api/1.0/users/me?opt_fields=gid,name,email', @@ -62,7 +62,7 @@ describe('validateAsanaServiceAccount', () => { const gidOnly = await validateAsanaServiceAccount({ apiToken: 'token-2' }) expect(gidOnly.displayName).toBe('Asana user 999') - expect(gidOnly.principal).toEqual({ kind: 'user', id: '999' }) + expect(gidOnly.storedMetadata).toEqual({ userGid: '999' }) }) it('maps 401 to invalid_credentials', async () => { diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/asana.ts b/apps/sim/lib/credentials/token-service-accounts/validators/asana.ts index e35f0adff37..c138258ee35 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/asana.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/asana.ts @@ -1,4 +1,3 @@ -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -52,10 +51,12 @@ export async function validateAsanaServiceAccount( const name = body.data?.name const email = body.data?.email + const storedMetadata: Record = { userGid: gid } + if (email) storedMetadata.email = email return { displayName: name || email || `Asana user ${gid}`, - principal: userPrincipal(gid, email), - auditMetadata: {}, + auditMetadata: { asanaUserGid: gid }, + storedMetadata, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/attio.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/attio.test.ts index 92002f1752c..7c193f063e5 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/attio.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/attio.test.ts @@ -60,8 +60,8 @@ describe('validateAttioServiceAccount', () => { }) expect(result).toEqual({ displayName: 'Acme CRM', - principal: { kind: 'tenant', id: 'ws-123', label: 'acme-crm' }, - auditMetadata: {}, + auditMetadata: { attioWorkspaceId: 'ws-123' }, + storedMetadata: { workspaceId: 'ws-123', workspaceSlug: 'acme-crm' }, }) }) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/attio.ts b/apps/sim/lib/credentials/token-service-accounts/validators/attio.ts index c0b7ccc1d0d..1969439c4ac 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/attio.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/attio.ts @@ -69,15 +69,14 @@ export async function validateAttioServiceAccount( }) } - // An Attio workspace access token is not bound to a member, so the workspace - // is the finest identity the token can ever report. + const storedMetadata: Record = { workspaceId: self.workspace_id } + if (self.workspace_slug) { + storedMetadata.workspaceSlug = self.workspace_slug + } + return { displayName: self.workspace_name || 'Attio workspace', - principal: { - kind: 'tenant', - id: self.workspace_id, - ...(self.workspace_slug ? { label: self.workspace_slug } : {}), - }, - auditMetadata: {}, + auditMetadata: { attioWorkspaceId: self.workspace_id }, + storedMetadata, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/calcom.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/calcom.test.ts index 78b0b7bbcd2..cbd22a5b1a2 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/calcom.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/calcom.test.ts @@ -36,8 +36,8 @@ describe('validateCalcomServiceAccount', () => { expect(result).toEqual({ displayName: 'sim-bot', - principal: { kind: 'user', id: '42', label: 'sim-bot' }, - auditMetadata: {}, + auditMetadata: { calcomUserId: '42' }, + storedMetadata: { userId: '42', email: 'bot@example.com' }, }) expect(mockFetch).toHaveBeenCalledWith('https://api.cal.com/v2/me', { headers: { diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/calcom.ts b/apps/sim/lib/credentials/token-service-accounts/validators/calcom.ts index bd8cc59ab07..c536bb43b1e 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/calcom.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/calcom.ts @@ -1,4 +1,3 @@ -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -55,11 +54,12 @@ export async function validateCalcomServiceAccount( const userId = String(body.data.id) const username = body.data.username const email = body.data.email - const label = username || email + const storedMetadata: Record = { userId } + if (email) storedMetadata.email = email return { displayName: username || email || 'Cal.com account', - principal: userPrincipal(userId, label), - auditMetadata: {}, + auditMetadata: { calcomUserId: userId }, + storedMetadata, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/claude-platform.ts b/apps/sim/lib/credentials/token-service-accounts/validators/claude-platform.ts index 566834f4fd2..f457d6be29d 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/claude-platform.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/claude-platform.ts @@ -35,13 +35,8 @@ export async function validateClaudePlatformServiceAccount( await throwForProviderResponse(res, 'agents_list') const suffix = fields.apiToken.slice(-4) - // Explicitly no principal: the Managed Agents API exposes no whoami endpoint - // and no workspace identifier on any response, so nothing about the key's - // owner is knowable at connect time. This is a provider limitation, not a - // failed lookup — see `ServiceAccountPrincipal`. return { displayName: `Claude Platform (…${suffix})`, - principal: null, auditMetadata: {}, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/clickup.ts b/apps/sim/lib/credentials/token-service-accounts/validators/clickup.ts index 6facd5d4b28..129fbb1fb02 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/clickup.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/clickup.ts @@ -1,4 +1,3 @@ -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -66,11 +65,9 @@ export async function validateClickupServiceAccount( }) } - const label = user.username || user.email - return { displayName: user.username || user.email || 'ClickUp account', - principal: userPrincipal(String(user.id), label), - auditMetadata: {}, + auditMetadata: { clickupUserId: String(user.id) }, + storedMetadata: { userId: String(user.id) }, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/hubspot.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/hubspot.test.ts index c37e2ad1a53..1259b11ac0a 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/hubspot.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/hubspot.test.ts @@ -74,9 +74,8 @@ describe('validateHubspotServiceAccount', () => { expect(result).toEqual({ displayName: 'HubSpot portal 12345', - principal: { kind: 'user', id: '111' }, auditMetadata: { hubspotHubId: '12345' }, - storedMetadata: { hubId: '12345', appId: '222' }, + storedMetadata: { hubId: '12345', appId: '222', userId: '111' }, }) expect(mockFetch).toHaveBeenCalledTimes(1) @@ -92,8 +91,8 @@ describe('validateHubspotServiceAccount', () => { expect(result).toEqual({ displayName: 'HubSpot portal 123', - principal: { kind: 'tenant', id: '123' }, - auditMetadata: {}, + auditMetadata: { hubspotHubId: '123' }, + storedMetadata: { hubId: '123' }, }) expect(mockFetch).toHaveBeenCalledTimes(2) @@ -128,8 +127,8 @@ describe('validateHubspotServiceAccount', () => { expect(result).toEqual({ displayName: 'HubSpot private app', - principal: null, auditMetadata: {}, + storedMetadata: {}, }) expect(mockFetch).toHaveBeenCalledTimes(2) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/hubspot.ts b/apps/sim/lib/credentials/token-service-accounts/validators/hubspot.ts index c22452e5b55..457710e32c8 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/hubspot.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/hubspot.ts @@ -1,4 +1,3 @@ -import { tenantPrincipal, userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -42,12 +41,10 @@ async function verifyViaAccountInfo( 'account_info' ) if (res.status === 403) { - // The token is live but the app cannot read account info, so neither the - // portal nor the creating user is knowable on this path. return { displayName: 'HubSpot private app', - principal: null, auditMetadata: {}, + storedMetadata: {}, } } await throwForProviderResponse(res, 'account_info') @@ -56,10 +53,8 @@ async function verifyViaAccountInfo( const hubId = typeof info?.portalId === 'number' ? String(info.portalId) : undefined return { displayName: hubId ? `HubSpot portal ${hubId}` : 'HubSpot private app', - // This route never reports the private app's creating user, so the portal - // is the finest identity available here. - principal: hubId ? tenantPrincipal(hubId) : null, - auditMetadata: {}, + auditMetadata: hubId ? { hubspotHubId: hubId } : {}, + storedMetadata: hubId ? { hubId } : {}, } } @@ -118,15 +113,10 @@ export async function validateHubspotServiceAccount( const storedMetadata: Record = { hubId } if (typeof tokenInfo.appId === 'number') storedMetadata.appId = String(tokenInfo.appId) + if (typeof tokenInfo.userId === 'number') storedMetadata.userId = String(tokenInfo.userId) return { displayName: `HubSpot portal ${hubId}`, - // `userId` is the HubSpot user the private app acts on behalf of; it is the - // actor, while `hubId` is only the portal it lives in. - principal: - typeof tokenInfo.userId === 'number' - ? userPrincipal(String(tokenInfo.userId)) - : tenantPrincipal(hubId), auditMetadata: { hubspotHubId: hubId }, storedMetadata, } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/linear.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/linear.test.ts index 37ef8fc156c..3007d161370 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/linear.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/linear.test.ts @@ -39,9 +39,8 @@ describe('validateLinearServiceAccount', () => { expect(result).toEqual({ displayName: 'Acme', - principal: { kind: 'user', id: 'viewer-1', label: 'jane@acme.com' }, auditMetadata: { linearOrganizationId: 'org-1' }, - storedMetadata: { organizationId: 'org-1' }, + storedMetadata: { viewerId: 'viewer-1', organizationId: 'org-1' }, }) const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit] diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/linear.ts b/apps/sim/lib/credentials/token-service-accounts/validators/linear.ts index 46c98aef7b0..ea4e297f374 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/linear.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/linear.ts @@ -1,4 +1,3 @@ -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -128,17 +127,15 @@ export async function validateLinearServiceAccount( } const organization = payload.data?.organization - const storedMetadata: Record = {} + const storedMetadata: Record = { viewerId: viewer.id } const auditMetadata: Record = {} if (organization?.id) { storedMetadata.organizationId = organization.id auditMetadata.linearOrganizationId = organization.id } - const label = viewer.email || viewer.name || undefined return { displayName: organization?.name || viewer.name || viewer.email || 'Linear workspace', - principal: userPrincipal(viewer.id, label), auditMetadata, storedMetadata, } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/monday.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/monday.test.ts index 590d425c79d..e96e7006b72 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/monday.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/monday.test.ts @@ -38,9 +38,8 @@ describe('validateMondayServiceAccount', () => { expect(result).toEqual({ displayName: 'Acme', - principal: { kind: 'user', id: '12345', label: 'jane@example.com' }, auditMetadata: { mondayAccountId: '987' }, - storedMetadata: { accountId: '987', accountSlug: 'acme' }, + storedMetadata: { accountId: '987', accountSlug: 'acme', userId: '12345' }, }) expect(mockFetch).toHaveBeenCalledWith('https://api.monday.com/v2', { method: 'POST', @@ -160,6 +159,6 @@ describe('validateMondayServiceAccount', () => { ) const result = await validateMondayServiceAccount({ apiToken: 'token' }) expect(result.displayName).toBe('Acme') - expect(result.principal).toEqual({ kind: 'user', id: '77', label: 'Bot User' }) + expect(result.storedMetadata?.userId).toBe('77') }) }) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/monday.ts b/apps/sim/lib/credentials/token-service-accounts/validators/monday.ts index 75fa3fdbe9e..9ff853d71ff 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/monday.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/monday.ts @@ -1,4 +1,3 @@ -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -116,7 +115,7 @@ export async function validateMondayServiceAccount( const userId = String(me.id) const accountId = account?.id != null ? String(account.id) : '' - const storedMetadata: Record = { accountId } + const storedMetadata: Record = { accountId, userId } if (account?.slug) { storedMetadata.accountSlug = account.slug } @@ -124,11 +123,9 @@ export async function validateMondayServiceAccount( if (accountId) { auditMetadata.mondayAccountId = accountId } - const label = me.email || me.name return { displayName: account?.name || me.name || me.email || `monday user ${userId}`, - principal: userPrincipal(userId, label), auditMetadata, storedMetadata, } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/notion.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/notion.test.ts index adffbfb5a1f..fad5f227b30 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/notion.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/notion.test.ts @@ -39,9 +39,8 @@ describe('validateNotionServiceAccount', () => { expect(result).toEqual({ displayName: 'Ops Integration', - principal: { kind: 'user', id: 'bot-123', label: 'Ops Integration' }, - auditMetadata: {}, - storedMetadata: { workspaceName: 'Acme Workspace' }, + auditMetadata: { notionBotId: 'bot-123' }, + storedMetadata: { botId: 'bot-123', workspaceName: 'Acme Workspace' }, }) expect(mockFetch).toHaveBeenCalledWith('https://api.notion.com/v1/users/me', { headers: { @@ -67,9 +66,11 @@ describe('validateNotionServiceAccount', () => { const result = await validateNotionServiceAccount({ apiToken: 'secret_legacy' }) expect(result.displayName).toBe('Acme Workspace') - expect(result.principal).toEqual({ kind: 'user', id: 'bot-456' }) - expect(result.auditMetadata).toEqual({}) - expect(result.storedMetadata).toEqual({ workspaceName: 'Acme Workspace' }) + expect(result.auditMetadata).toEqual({ notionBotId: 'bot-456' }) + expect(result.storedMetadata).toEqual({ + botId: 'bot-456', + workspaceName: 'Acme Workspace', + }) }) it('throws invalid_credentials on 401', async () => { diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/notion.ts b/apps/sim/lib/credentials/token-service-accounts/validators/notion.ts index e2b75762e48..4321ba5f3c4 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/notion.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/notion.ts @@ -1,4 +1,3 @@ -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -57,17 +56,14 @@ export async function validateNotionServiceAccount( } const workspaceName = me.bot?.workspace_name || undefined - const storedMetadata: Record = {} + const storedMetadata: Record = { botId: me.id } if (workspaceName) { storedMetadata.workspaceName = workspaceName } return { displayName: me.name || workspaceName || 'Notion integration', - // The integration authenticates as its own bot user, which is the actor - // recorded on every page/database change it makes. - principal: userPrincipal(me.id, me.name), - auditMetadata: {}, + auditMetadata: { notionBotId: me.id }, storedMetadata, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/pipedrive.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/pipedrive.test.ts index 4e75faeb57f..1e73129649b 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/pipedrive.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/pipedrive.test.ts @@ -48,9 +48,8 @@ describe('validatePipedriveServiceAccount', () => { expect(result).toEqual({ displayName: 'Jane Doe (Acme Inc)', - principal: { kind: 'user', id: '42', label: 'Jane Doe' }, auditMetadata: { pipedriveCompanyId: '777' }, - storedMetadata: { companyId: '777', companyDomain: 'acme' }, + storedMetadata: { userId: '42', companyId: '777', companyDomain: 'acme' }, }) expect(mockFetch).toHaveBeenCalledTimes(1) @@ -68,8 +67,7 @@ describe('validatePipedriveServiceAccount', () => { const result = await validatePipedriveServiceAccount(FIELDS) expect(result.displayName).toBe('Pipedrive company 777') - expect(result.principal).toEqual({ kind: 'user', id: '42' }) - expect(result.storedMetadata).toEqual({ companyId: '777' }) + expect(result.storedMetadata).toEqual({ userId: '42', companyId: '777' }) }) it('throws invalid_credentials on 401', async () => { diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/pipedrive.ts b/apps/sim/lib/credentials/token-service-accounts/validators/pipedrive.ts index 66da8628dfa..3fa20e90ec6 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/pipedrive.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/pipedrive.ts @@ -1,4 +1,3 @@ -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -63,7 +62,7 @@ export async function validatePipedriveServiceAccount( const companyDomain = typeof user.company_domain === 'string' && user.company_domain ? user.company_domain : undefined - const storedMetadata: Record = {} + const storedMetadata: Record = { userId: String(user.id) } if (companyId) storedMetadata.companyId = companyId if (companyDomain) storedMetadata.companyDomain = companyDomain @@ -77,7 +76,6 @@ export async function validatePipedriveServiceAccount( return { displayName, - principal: userPrincipal(String(user.id), userName), auditMetadata: companyId ? { pipedriveCompanyId: companyId } : {}, storedMetadata, } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/shopify.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/shopify.test.ts index 36eff828aac..dbd47bf5876 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/shopify.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/shopify.test.ts @@ -44,8 +44,8 @@ describe('validateShopifyServiceAccount', () => { expect(result).toEqual({ displayName: 'Acme Store', - principal: { kind: 'tenant', id: 'acme-store.myshopify.com', label: 'Acme Store' }, - auditMetadata: {}, + auditMetadata: { shopifyShopDomain: 'acme-store.myshopify.com' }, + storedMetadata: { shopDomain: 'acme-store.myshopify.com', shopName: 'Acme Store' }, normalizedDomain: 'acme-store.myshopify.com', }) expect(mockFetch).toHaveBeenCalledWith( diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/shopify.ts b/apps/sim/lib/credentials/token-service-accounts/validators/shopify.ts index 4d18a625334..ad1ec8a8cce 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/shopify.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/shopify.ts @@ -141,16 +141,13 @@ export async function validateShopifyServiceAccount( : undefined const canonicalDomain = apiDomain && SHOPIFY_HOST_REGEX.test(apiDomain) ? apiDomain : domain - // A custom-app Admin API token belongs to the app, not to a staff member, so - // the store is the finest identity it can ever report. + const storedMetadata: Record = { shopDomain: canonicalDomain } + if (shopName) storedMetadata.shopName = shopName + return { displayName: shopName ?? canonicalDomain, - principal: { - kind: 'tenant', - id: canonicalDomain, - ...(shopName ? { label: shopName } : {}), - }, - auditMetadata: {}, + auditMetadata: { shopifyShopDomain: canonicalDomain }, + storedMetadata, normalizedDomain: canonicalDomain, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/trello.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/trello.test.ts index 82c76793363..314da932186 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/trello.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/trello.test.ts @@ -46,8 +46,8 @@ describe('validateTrelloServiceAccount', () => { expect(result).toEqual({ displayName: 'Sim Bot', - principal: { kind: 'user', id: 'abc123', label: 'simbot' }, - auditMetadata: {}, + auditMetadata: { trelloMemberId: 'abc123' }, + storedMetadata: { memberId: 'abc123', username: 'simbot' }, }) const [url] = mockFetch.mock.calls[0] diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/trello.ts b/apps/sim/lib/credentials/token-service-accounts/validators/trello.ts index 500a8c6b537..209a43e8b2e 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/trello.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/trello.ts @@ -1,5 +1,4 @@ import { env } from '@/lib/core/config/env' -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -80,12 +79,14 @@ export async function validateTrelloServiceAccount( }) } - const username = - typeof member.username === 'string' && member.username ? member.username : undefined + const storedMetadata: Record = { memberId: member.id } + if (typeof member.username === 'string' && member.username) { + storedMetadata.username = member.username + } return { displayName: member.fullName || member.username || `Trello member ${member.id}`, - principal: userPrincipal(member.id, username), - auditMetadata: {}, + auditMetadata: { trelloMemberId: member.id }, + storedMetadata, } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/wealthbox.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/wealthbox.test.ts index 153faef2706..50f8739d879 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/wealthbox.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/wealthbox.test.ts @@ -43,8 +43,8 @@ describe('validateWealthboxServiceAccount', () => { expect(result).toEqual({ displayName: 'Bill Jones', - principal: { kind: 'user', id: '42', label: 'bill@example.com' }, - auditMetadata: {}, + auditMetadata: { wealthboxUserId: '42' }, + storedMetadata: { userId: '42', email: 'bill@example.com' }, }) expect(mockFetch).toHaveBeenCalledTimes(1) diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/wealthbox.ts b/apps/sim/lib/credentials/token-service-accounts/validators/wealthbox.ts index ac97ca76474..94e85821f33 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/wealthbox.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/wealthbox.ts @@ -1,4 +1,3 @@ -import { userPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -95,11 +94,12 @@ export async function validateWealthboxServiceAccount( const userId = typeof me.current_user?.id === 'number' ? String(me.current_user.id) : undefined const email = me.email || me.current_user?.email - // `/v1/me` omits `current_user` for some token types; without it Wealthbox - // reports no identifier of any kind on this response. - return { - displayName, - principal: userId ? userPrincipal(userId, email) : null, - auditMetadata: {}, - } + const auditMetadata: Record = {} + if (userId) auditMetadata.wealthboxUserId = userId + + const storedMetadata: Record = {} + if (userId) storedMetadata.userId = userId + if (email) storedMetadata.email = email + + return { displayName, auditMetadata, storedMetadata } } diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/webflow.test.ts b/apps/sim/lib/credentials/token-service-accounts/validators/webflow.test.ts index 109f862070b..1cb3f9018c8 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/webflow.test.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/webflow.test.ts @@ -35,8 +35,8 @@ describe('validateWebflowServiceAccount', () => { expect(result).toEqual({ displayName: 'Acme Marketing', - principal: { kind: 'tenant', id: 'site123', label: 'Acme Marketing' }, - auditMetadata: {}, + auditMetadata: { webflowSiteId: 'site123' }, + storedMetadata: { siteId: 'site123', siteName: 'Acme Marketing' }, }) expect(mockFetch).toHaveBeenCalledWith('https://api.webflow.com/v2/sites', { headers: { @@ -55,7 +55,7 @@ describe('validateWebflowServiceAccount', () => { const result = await validateWebflowServiceAccount({ apiToken: 'wf-token' }) expect(result.displayName).toBe('acme') - expect(result.principal).toEqual({ kind: 'tenant', id: 'site456', label: 'acme' }) + expect(result.storedMetadata).toEqual({ siteId: 'site456', siteName: 'acme' }) }) it('throws invalid_credentials on 401', async () => { diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/webflow.ts b/apps/sim/lib/credentials/token-service-accounts/validators/webflow.ts index 4da3aeba131..034558532b2 100644 --- a/apps/sim/lib/credentials/token-service-accounts/validators/webflow.ts +++ b/apps/sim/lib/credentials/token-service-accounts/validators/webflow.ts @@ -1,4 +1,3 @@ -import { tenantPrincipal } from '@/lib/credentials/principal' import { fetchProvider, parseProviderJson, @@ -52,10 +51,9 @@ export async function validateWebflowServiceAccount( const displayName = site.displayName || site.shortName || 'Webflow site' - // A site API token is bound to a site, never to a Webflow user. return { displayName, - principal: tenantPrincipal(site.id, displayName), - auditMetadata: {}, + auditMetadata: { webflowSiteId: site.id }, + storedMetadata: { siteId: site.id, siteName: displayName }, } }