Skip to content

Commit e1a3f29

Browse files
authored
feat(credentials): saved credentials for 21 token-and-host blocks on one host-aware check factory (#9849)
* refactor(credentials): host-aware token checks in the shared validator factory tokenCheckValidator now takes a domain normalizer (stored as the credential's domain), an account-id normalizer, an SSRF-guarded egress profile for hosts the user chose, and a flag that keeps token-echoing error bodies out of logs. GitLab becomes a declaration on it, so every token check reads the same way. * feat(credentials): saved credentials for 20 token-and-host blocks Algolia, Azure DevOps, Convex, Devin, Dynatrace, Email Bison, Grafana, Infisical, Jotform, Jupyter (v3), Logfire, Mailgun, New Relic, Okta, Plane, Supabase, Tinybird, Twilio SMS and Voice (one shared Twilio credential), and Upstash get a credential that carries the token plus its host, region, or account id, and a credential-only successor. Every host-deciding param comes from the credential; a typed host is normalized as the tools do it and verified through the guarded egress, a region or account id is checked before any request, and Databricks subject tokens last up to an hour so long Genie polls keep their token. * feat(credentials): Thrive saved credential, and close the batch-3 audit findings Thrive gets a credential holding its key, tenant id, and region host (one of its four API hosts). Audit fixes: the connect form sends URL-shaped domains as typed so the provider check keeps their scheme and path; credentials default to a name with their domain or account id (GitLab keeps its token name) and record it in the audit log; Upstash accepts http and a read-only token refused PING; Infisical keeps http and a proxy path; Devin, Dynatrace, and Logfire refuse dot segments, non-http schemes, and mistyped regions. * improvement(credentials): simplify and clean up the token-and-host checks One normalizeHttpBaseUrl and one allowedDomain replace five URL normalizers and four region checks (fixing Grafana and Tinybird treating a non-http scheme as a host); region tables come from the tools; acceptsBody can name the credential (GitLab); Email Bison and Jupyter refuse credentials in the URL instead of dropping them; the Supabase public-URL policy is inlined; the Databricks token lifetime uses the issuer's cap. * fix(credentials): address review round 1 Azure DevOps tools build every project-scoped URL through one traversal-safe helper, so a project or build id can't climb into another organization with a saved token; Tinybird and Upstash restricted-token replies are covered by tests. * fix(credentials): address the final audit An Azure DevOps project or organization pasted already encoded is decoded once before the traversal-safe segment, so it isn't encoded twice; a base URL with a single-slash scheme is refused; Upstash accepts a proxy path like the legacy block; a Thrive tenant id must be printable ASCII. * docs(skills): teach the saved token credential pattern in the integration skills add-integration, add-block, add-tools, and validate-integration now describe the token credential (descriptor, tokenCheckValidator declaration with its host and account normalizers, shared policy mapping every host-deciding param) and credential-only successors for blocks that take a secret inline. * docs(skills): address review round 2 The token-credential skill steps name the real tokenCredentialOAuth factory, scope the checklist to token-credential and successor blocks, and keep the AuthMode.ApiKey path for token-only blocks; normalizeEmailBisonBaseUrl gains its TSDoc. * fix(credentials): refuse an Email Bison instance typed with a non-https scheme normalizeEmailBisonBaseUrl now keeps any typed scheme, so ftp://host is refused instead of becoming a host named ftp. * fix(credentials): close release-audit findings in the saved-credential executor - A policy that maps credential fields owns them: the generic domain, instanceUrl, apiDomain and cloudId fills no longer write a stored host into a same-named user param (Thrive create/update user sent its region host as the user's domain). - An agent tool with a saved credential selected is neither asked for nor allowed to send the params the credential supplies (Azure DevOps organization, Infisical base URL). - ENOTFOUND is read from the error or its cause, so a mistyped host or account id is site_not_found under Bun as well as Node. - A service-account secret is released only to a tool bound to its provider; a bare credential ID is refused. * fix(credentials): refuse non-http schemes on typed hosts; correct docs and copy - One refuseUnsafeTypedUrl guard refuses a typed instance URL with credentials or a non-http(s) scheme, for Email Bison, Jupyter and Logfire, before their normalizers can read the scheme as a host. The Email Bison tool normalizer is back to its staging behavior; the check lives in the credential validator, so legacy blocks are unchanged. - Docs for successor users: Jotform region, Dynatrace scopes, Logfire host, Plane instance, Infisical, Fathom and Supabase now point at the saved credential; self-hosting no longer says API keys live in blocks; the AWS keyless step names the dialog's Issuer. - ElevenLabs and Instantly copy no longer asks for a permission their checks don't need; Okta lists trexcloud.com.
1 parent 1174092 commit e1a3f29

519 files changed

Lines changed: 4058 additions & 1229 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/add-block/SKILL.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,40 @@ export const {ServiceName}Block: BlockConfig = {
159159

160160
Optional companions: `credentialLabels` (override the picker's section/connect-row copy) and `allowServiceAccounts: true` (trigger-mode only — list service accounts, which triggers otherwise exclude; set only when the trigger's polling path can resolve a service-account token). The connect modal, provider families (Google JSON key, Atlassian token, token-paste, client-credential, Slack bot), and the preview gate are all resolved from `serviceAccountProviderId` — you don't wire them per block.
161161

162+
### Credential-only successor (replacing inline secrets)
163+
164+
A block that takes a secret inline keeps running for existing workflows; the saved-credential
165+
version is a new block type that spreads it. Keep the successor an object literal with the picker
166+
written inline, because the docs generator reads the source text:
167+
168+
```typescript
169+
export const ServiceV2Block: BlockConfig = {
170+
...ServiceBlock,
171+
type: 'service_v2',
172+
authMode: AuthMode.OAuth,
173+
hideFromToolbar: false,
174+
sunset: undefined,
175+
...withSavedCredential(ServiceBlock, {
176+
id: 'credential',
177+
type: 'oauth-input',
178+
serviceId: 'service',
179+
title: 'Service Credential',
180+
}),
181+
}
182+
```
183+
184+
- The legacy block changes only by `hideFromToolbar: true` and `sunset: { status: 'legacy',
185+
replacedBy: 'service_v2' }` (an older `_v2` points at the new `_v3`). Register both types.
186+
- `withSavedCredential` (`blocks/utils.ts`) swaps exactly the fields its policy in
187+
`SAVED_CREDENTIAL_OAUTH` supplies for the picker, places it directly under the operation
188+
dropdown, inherits the replaced fields' `condition` and `required`, and leaves trigger-mode
189+
fields alone. Pass `required` when the replaced fields disagree (an optional host), and `params`
190+
only when the legacy params builder validates the secret: extract that builder so the legacy
191+
block calls it unchanged (`blocks/legacy-builders.test.ts`) and the successor skips the check.
192+
- Add the pair to `serializer/saved-credentials.test.ts` (`optionalIds` for replaced fields the
193+
legacy block never required), and grep for code that hard-codes the legacy type (`getBlock`,
194+
`integration.type ===`, availability lookups), which now resolves to the successor.
195+
162196
### OAuth deployment availability (required for integration blocks)
163197

164198
A visible tools-category block with OAuth is deployment-gated. Its `oauth-input.serviceId` is

‎.agents/skills/add-integration/SKILL.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,42 @@ Use explicit permission choices when the supported operations have different acc
6666
region and permission selections on reconnect unless the user changes them. Verify documented
6767
identity endpoints rather than guessing which person a client-credentials token represents.
6868

69+
### Token credentials (API key, personal access token, token + host)
70+
71+
A service authenticated by a pasted key or token takes it through a saved token credential, never an
72+
inline `password` field, so one shared, verified credential serves every block. Wire it by the
73+
service id:
74+
75+
1. `apps/sim/lib/credentials/token-service-accounts/descriptors.ts`: a provider id and descriptor.
76+
Fields are `apiToken`, plus `domain` for a host, instance URL, or region and
77+
`providerWorkspaceId` for an account or org id; every listed field is required. Set `url: true`
78+
on a `domain` that is a URL whose scheme or path matters, so the connect form sends it as typed.
79+
2. `validators/token-checks.ts`: one `tokenCheckValidator` declaration (register it in
80+
`server.ts`). Make one cheap, documented read on the tools' own host with the tools' own auth,
81+
reusing their header and URL builders. `domain` and `workspaceId` normalize to exactly the form
82+
the tools build URLs from and throw for anything that could steer the host or path
83+
(`normalizeHttpBaseUrl`, `allowedDomain`, `refuseUrlCredentials`); the normalized domain is what
84+
the credential stores. Set `egress` when the user chooses the host. Add `forbiddenMeansScoped`,
85+
`rejectsKey`, or `provesKey` only for documented replies (probe a fake key to see how a bad one
86+
answers), and `errorBodyEchoesToken` when an error body echoes the token.
87+
3. `apps/sim/lib/oauth/oauth.ts` and `types.ts`: the service entry (`authType: 'service_account'`,
88+
`serviceAccountProviderId`).
89+
4. `apps/sim/tools/shared/saved-credentials.ts`: `tokenCredentialOAuth('<service>', { <toolParam>:
90+
'accessToken' | 'domain' | 'providerWorkspaceId' })`, listed in `SAVED_CREDENTIAL_OAUTH`, and set
91+
as `oauth` on every tool. Map every param that decides the request host; the executor overwrites
92+
mapped params after authorizing the credential, and `check-block-registry` probes the tools' real
93+
URL builders and fails on a host-steering param left unmapped.
94+
5. A credential-only block (including a successor) uses `authMode: AuthMode.OAuth`, so the
95+
generated catalog maps the service; a token-only block that keeps `AuthMode.ApiKey`, as Coda
96+
does, registers in `tokenCredentialIntegrationTypes` instead (see below). The credential picker
97+
sits directly under the operation dropdown (`add-block` → OAuth/Credentials). An existing block that already takes the secret inline is never edited in
98+
place: add a credential-only successor (`add-block` → Credential-only successor).
99+
100+
Then run `generate-docs`, `generate:block-successors`, `generate:deployment-config`, and
101+
`generate:tool-metadata`, and `check-block-registry.ts origin/staging`. This pattern does not fit
102+
hosted-key tools, model-provider keys, two-secret or username/password logins, or a host chosen
103+
per call.
104+
69105
### Pair saved credentials with block and resource selectors
70106

71107
`oauth-input` is the shared saved-credential picker, including for service accounts that never

‎.agents/skills/add-tools/SKILL.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export const {serviceName}{Action}Tool: ToolConfig<
8989
provider: '{service}', // Must match the service id in OAUTH_PROVIDERS
9090
// credentialKind: 'service-account', // service-account-only integrations
9191
},
92+
// A token credential (API key, PAT, token + host) instead uses the service's shared policy from
93+
// tools/shared/saved-credentials.ts, built with tokenCredentialOAuth('{service}', { apiKey:
94+
// 'accessToken' }). Its keys are the tool's own token/host/account params, which the executor
95+
// fills after authorizing the credential.
9296

9397
params: {
9498
// Hidden params (system-injected, e.g. the OAuth accessToken)

‎.agents/skills/validate-integration/SKILL.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,17 @@ For **each tool** in `tools.access`:
218218
- True/false → `switch` (a Yes/No `dropdown` only when the tool needs a third "unset" state)
219219
- Credentials → `oauth-input` with correct `serviceId`
220220
- Every tool with a `credential`/`oauthCredential` param declares `oauth: { required: true, provider }` matching that `serviceId`, plus `credentialKind: 'service-account'` when only service accounts are supported
221+
- [ ] In a new block or credential-only successor, a pasted key, token, or token + host comes from a
222+
saved token credential (add-integration → Token credentials), not an inline `password` field
223+
(legacy blocks keep theirs); a successor changes its legacy block only by `hideFromToolbar`
224+
and `sunset`, and its picker sits under the operation
225+
- [ ] For a token-credential integration, every tool in `tools.access` declares the service's
226+
`tokenCredentialOAuth` policy, every
227+
param that decides the request host is mapped, and the stored domain is in the form the tools
228+
build URLs from (`check-block-registry.ts origin/staging` passes)
229+
- [ ] The `tokenCheckValidator` request uses the tools' host and auth, its normalizers refuse
230+
anything that could steer the host or path, and a fake key is rejected without a valid
231+
scoped key being refused
221232
- [ ] Dropdown `value: () => 'default'` is set for dropdowns with a sensible default
222233
- [ ] Every `short-input`, `long-input`, `code`, and selector subBlock has a `placeholder` — including
223234
password fields (`Enter your API key`). Formatted values show the shape

‎apps/docs/components/ui/icon-mapping.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
343343
ahrefs_v2: AhrefsIcon,
344344
airtable: AirtableIcon,
345345
algolia: AlgoliaIcon,
346+
algolia_v2: AlgoliaIcon,
346347
amplitude: AmplitudeIcon,
347348
api: ApiIcon,
348349
api_trigger: ApiIcon,
@@ -361,6 +362,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
361362
attio: AttioIcon,
362363
azure_data_explorer: AzureDataExplorerIcon,
363364
azure_devops: AzureIcon,
365+
azure_devops_v2: AzureIcon,
364366
bitbucket: BitbucketIcon,
365367
box: BoxCompanyIcon,
366368
box_v2: BoxCompanyIcon,
@@ -403,6 +405,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
403405
confluence_v2: ConfluenceIcon,
404406
context_dev: ContextDevIcon,
405407
convex: ConvexIcon,
408+
convex_v2: ConvexIcon,
406409
credential: CredentialIcon,
407410
credential_group: GridOffset,
408411
crowdstrike: CrowdStrikeIcon,
@@ -420,6 +423,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
420423
daytona_v2: DaytonaIcon,
421424
deployments: Rocket,
422425
devin: DevinIcon,
426+
devin_v2: DevinIcon,
423427
discord: DiscordIcon,
424428
discord_v2: DiscordIcon,
425429
docusign: DocuSignIcon,
@@ -435,12 +439,14 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
435439
dynamodb: DynamoDBIcon,
436440
dynamodb_v2: DynamoDBIcon,
437441
dynatrace: DynatraceIcon,
442+
dynatrace_v2: DynatraceIcon,
438443
ec2: EC2Icon,
439444
ec2_v2: EC2Icon,
440445
elasticsearch: ElasticsearchIcon,
441446
elevenlabs: ElevenLabsIcon,
442447
elevenlabs_v2: ElevenLabsIcon,
443448
emailbison: EmailBisonIcon,
449+
emailbison_v2: EmailBisonIcon,
444450
embeddings: EmbeddingsIcon,
445451
enrich: EnrichSoIcon,
446452
enrich_v2: EnrichSoIcon,
@@ -506,6 +512,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
506512
'google-drive': GoogleDriveIcon,
507513
'google-sheets': GoogleSheetsIcon,
508514
grafana: GrafanaIcon,
515+
grafana_v2: GrafanaIcon,
509516
grain: GrainIcon,
510517
grain_v2: GrainIcon,
511518
grain_v3: GrainIcon,
@@ -537,6 +544,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
537544
incidentio: IncidentioIcon,
538545
incidentio_v2: IncidentioIcon,
539546
infisical: InfisicalIcon,
547+
infisical_v2: InfisicalIcon,
540548
input_trigger: FormInput,
541549
instantly: InstantlyIcon,
542550
instantly_v2: InstantlyIcon,
@@ -549,9 +557,11 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
549557
jira: JiraIcon,
550558
jira_service_management: JiraServiceManagementIcon,
551559
jotform: JotformIcon,
560+
jotform_v2: JotformIcon,
552561
jsm: JiraServiceManagementIcon,
553562
jupyter: JupyterIcon,
554563
jupyter_v2: JupyterIcon,
564+
jupyter_v3: JupyterIcon,
555565
kalshi: KalshiIcon,
556566
kalshi_v2: KalshiIcon,
557567
ketch: KetchIcon,
@@ -573,6 +583,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
573583
linq: LinqIcon,
574584
linq_v2: LinqIcon,
575585
logfire: LogfireIcon,
586+
logfire_v2: LogfireIcon,
576587
logrocket: LogRocketIcon,
577588
logs: Library,
578589
logs_v2: Library,
@@ -583,6 +594,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
583594
mailchimp: MailchimpIcon,
584595
mailchimp_v2: MailchimpIcon,
585596
mailgun: MailgunIcon,
597+
mailgun_v2: MailgunIcon,
586598
managed_agent: ClaudeIcon,
587599
manageengine_sdp: ManageEngineIcon,
588600
manual_trigger: Play,
@@ -615,12 +627,14 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
615627
netsuite: NetSuiteIcon,
616628
neverbounce: NeverBounceIcon,
617629
new_relic: NewRelicIcon,
630+
new_relic_v2: NewRelicIcon,
618631
note: NoteIcon,
619632
notion: NotionIcon,
620633
notion_v2: NotionIcon,
621634
obsidian: ObsidianIcon,
622635
oci: OracleIcon,
623636
okta: OktaIcon,
637+
okta_v2: OktaIcon,
624638
onedrive: MicrosoftOneDriveIcon,
625639
onepassword: OnePasswordIcon,
626640
openai: OpenAIIcon,
@@ -641,6 +655,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
641655
pipedrive: PipedriveIcon,
642656
pitchbook: PitchBookIcon,
643657
plane: PlaneIcon,
658+
plane_v2: PlaneIcon,
644659
planetscale: PlanetScaleIcon,
645660
polymarket: PolymarketIcon,
646661
postgresql: PostgresIcon,
@@ -743,6 +758,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
743758
stt: STTIcon,
744759
stt_v2: STTIcon,
745760
supabase: SupabaseIcon,
761+
supabase_v2: SupabaseIcon,
746762
table: Table,
747763
table_v2: Table,
748764
tailscale: TailscaleIcon,
@@ -757,8 +773,10 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
757773
textract_v3: TextractIcon,
758774
thinking: BrainIcon,
759775
thrive: ThriveIcon,
776+
thrive_v2: ThriveIcon,
760777
tiktok: TikTokIcon,
761778
tinybird: TinybirdIcon,
779+
tinybird_v2: TinybirdIcon,
762780
tinyfish: TinyFishIcon,
763781
translate: TranslateIcon,
764782
trello: TrelloIcon,
@@ -767,10 +785,13 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
767785
tts: TTSIcon,
768786
twilio: TwilioIcon,
769787
twilio_sms: TwilioIcon,
788+
twilio_sms_v2: TwilioIcon,
770789
twilio_voice: TwilioIcon,
790+
twilio_voice_v2: TwilioIcon,
771791
typeform: TypeformIcon,
772792
typeform_v2: TypeformIcon,
773793
upstash: UpstashIcon,
794+
upstash_v2: UpstashIcon,
774795
uptimerobot: UptimeRobotIcon,
775796
uptimerobot_v2: UptimeRobotIcon,
776797
vanta: VantaIcon,

0 commit comments

Comments
 (0)