Skip to content

Commit 8927ac3

Browse files
waleedlatif1claude
andcommitted
fix(oauth): fix stale scope-descriptions.ts references and add test coverage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 198a19a commit 8927ac3

File tree

4 files changed

+119
-9
lines changed

4 files changed

+119
-9
lines changed

.claude/commands/add-block.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export const {ServiceName}Block: BlockConfig = {
125125

126126
**Scopes:** Always use `getScopesForService(serviceId)` from `@/lib/oauth/utils` for `requiredScopes`. Never hardcode scope arrays — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`.
127127

128-
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `lib/oauth/scope-descriptions.ts`.
128+
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`.
129129

130130
### Selectors (with dynamic options)
131131
```typescript
@@ -801,7 +801,7 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
801801
- [ ] DependsOn set for fields that need other values
802802
- [ ] Required fields marked correctly (boolean or condition)
803803
- [ ] OAuth inputs have correct `serviceId` and `requiredScopes: getScopesForService(serviceId)`
804-
- [ ] Scope descriptions added to `lib/oauth/scope-descriptions.ts` for any new scopes
804+
- [ ] Scope descriptions added to `SCOPE_DESCRIPTIONS` in `lib/oauth/utils.ts` for any new scopes
805805
- [ ] Tools.access lists all tool IDs (snake_case)
806806
- [ ] Tools.config.tool returns correct tool ID (snake_case)
807807
- [ ] Outputs match tool outputs

.claude/commands/add-integration.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ If creating V2 versions (API-aligned outputs):
423423

424424
### OAuth Scopes (if OAuth service)
425425
- [ ] Defined scopes in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS`
426-
- [ ] Added scope descriptions in `lib/oauth/scope-descriptions.ts`
426+
- [ ] Added scope descriptions in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
427427
- [ ] Used `getCanonicalScopesForProvider()` in `auth.ts` (never hardcode)
428428
- [ ] Used `getScopesForService()` in block `requiredScopes` (never hardcode)
429429

@@ -730,7 +730,7 @@ Use `wandConfig` for fields that are hard to fill out manually:
730730
Scopes are maintained in a single source of truth and reused everywhere:
731731

732732
1. **Define scopes** in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS[provider].services[service].scopes`
733-
2. **Add descriptions** in `lib/oauth/scope-descriptions.ts` for the OAuth modal UI
733+
2. **Add descriptions** in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for the OAuth modal UI
734734
3. **Reference in auth.ts** using `getCanonicalScopesForProvider(providerId)` from `@/lib/oauth/utils`
735735
4. **Reference in blocks** using `getScopesForService(serviceId)` from `@/lib/oauth/utils`
736736

@@ -757,4 +757,4 @@ requiredScopes: getScopesForService('{service}'),
757757
9. **Optional fields use advanced mode** - Set `mode: 'advanced'` on rarely-used optional fields
758758
10. **Complex inputs need wandConfig** - Timestamps, JSON arrays, and other hard-to-type values should have `wandConfig` enabled
759759
11. **Never hardcode scopes** - Use `getScopesForService()` in blocks and `getCanonicalScopesForProvider()` in auth.ts
760-
12. **Always add scope descriptions** - New scopes must have entries in `lib/oauth/scope-descriptions.ts`
760+
12. **Always add scope descriptions** - New scopes must have entries in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`

.claude/commands/validate-integration.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ apps/sim/blocks/registry.ts # Block registry entry for this service
2828
apps/sim/components/icons.tsx # Icon definition
2929
apps/sim/lib/auth/auth.ts # OAuth config — should use getCanonicalScopesForProvider()
3030
apps/sim/lib/oauth/oauth.ts # OAuth provider config — single source of truth for scopes
31-
apps/sim/lib/oauth/scope-descriptions.ts # Human-readable scope descriptions for modal UI
31+
apps/sim/lib/oauth/utils.ts # Scope utilities, SCOPE_DESCRIPTIONS for modal UI
3232
```
3333

3434
## Step 2: Pull API Documentation
@@ -206,7 +206,7 @@ Scopes are centralized — the single source of truth is `OAUTH_PROVIDERS` in `l
206206
- [ ] `auth.ts` uses `getCanonicalScopesForProvider(providerId)` — NOT a hardcoded array
207207
- [ ] Block `requiredScopes` uses `getScopesForService(serviceId)` — NOT a hardcoded array
208208
- [ ] No hardcoded scope arrays in `auth.ts` or block files (should all use utility functions)
209-
- [ ] Each scope has a human-readable description in `lib/oauth/scope-descriptions.ts`
209+
- [ ] Each scope has a human-readable description in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
210210
- [ ] No excess scopes that aren't needed by any tool
211211

212212
## Step 6: Validate Pagination Consistency
@@ -249,7 +249,7 @@ Group findings by severity:
249249
- Missing `?? null` on nullable response fields
250250
- Block condition array missing an operation that uses that field
251251
- Hardcoded scope arrays instead of using `getScopesForService()` / `getCanonicalScopesForProvider()`
252-
- Missing scope description in `lib/oauth/scope-descriptions.ts`
252+
- Missing scope description in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
253253

254254
**Suggestion** (minor improvements):
255255
- Better description text
@@ -279,7 +279,7 @@ After fixing, confirm:
279279
- [ ] Validated tools.config mapping, tool selector, and type coercions
280280
- [ ] Validated block outputs match what tools return, with typed JSON where possible
281281
- [ ] Validated OAuth scopes use centralized utilities (getScopesForService, getCanonicalScopesForProvider) — no hardcoded arrays
282-
- [ ] Validated scope descriptions exist in `lib/oauth/scope-descriptions.ts` for all scopes
282+
- [ ] Validated scope descriptions exist in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for all scopes
283283
- [ ] Validated pagination consistency across tools and block
284284
- [ ] Validated error handling (error checks, meaningful messages)
285285
- [ ] Validated registry entries (tools and block, alphabetical, correct imports)

apps/sim/lib/oauth/utils.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import type { OAuthProvider, OAuthServiceMetadata } from './types'
33
import {
44
getAllOAuthServices,
55
getCanonicalScopesForProvider,
6+
getMissingRequiredScopes,
67
getProviderIdFromServiceId,
8+
getScopesForService,
79
getServiceByProviderAndId,
810
getServiceConfigByProviderId,
911
parseProvider,
@@ -597,3 +599,111 @@ describe('parseProvider', () => {
597599
expect(config.featureType).toBe('sharepoint')
598600
})
599601
})
602+
603+
describe('getScopesForService', () => {
604+
it.concurrent('should return scopes for a valid serviceId', () => {
605+
const scopes = getScopesForService('gmail')
606+
607+
expect(Array.isArray(scopes)).toBe(true)
608+
expect(scopes.length).toBeGreaterThan(0)
609+
expect(scopes).toContain('https://www.googleapis.com/auth/gmail.send')
610+
})
611+
612+
it.concurrent('should return empty array for unknown serviceId', () => {
613+
const scopes = getScopesForService('nonexistent-service')
614+
615+
expect(Array.isArray(scopes)).toBe(true)
616+
expect(scopes.length).toBe(0)
617+
})
618+
619+
it.concurrent('should return new array instance (not reference)', () => {
620+
const scopes1 = getScopesForService('gmail')
621+
const scopes2 = getScopesForService('gmail')
622+
623+
expect(scopes1).not.toBe(scopes2)
624+
expect(scopes1).toEqual(scopes2)
625+
})
626+
627+
it.concurrent('should work for Microsoft services', () => {
628+
const scopes = getScopesForService('outlook')
629+
630+
expect(scopes.length).toBeGreaterThan(0)
631+
expect(scopes).toContain('Mail.ReadWrite')
632+
})
633+
634+
it.concurrent('should return empty array for empty string', () => {
635+
const scopes = getScopesForService('')
636+
637+
expect(Array.isArray(scopes)).toBe(true)
638+
expect(scopes.length).toBe(0)
639+
})
640+
})
641+
642+
describe('getMissingRequiredScopes', () => {
643+
it.concurrent('should return empty array when all scopes are granted', () => {
644+
const credential = { scopes: ['read', 'write'] }
645+
const missing = getMissingRequiredScopes(credential, ['read', 'write'])
646+
647+
expect(missing).toEqual([])
648+
})
649+
650+
it.concurrent('should return missing scopes', () => {
651+
const credential = { scopes: ['read'] }
652+
const missing = getMissingRequiredScopes(credential, ['read', 'write'])
653+
654+
expect(missing).toEqual(['write'])
655+
})
656+
657+
it.concurrent('should return all required scopes when credential is undefined', () => {
658+
const missing = getMissingRequiredScopes(undefined, ['read', 'write'])
659+
660+
expect(missing).toEqual(['read', 'write'])
661+
})
662+
663+
it.concurrent('should return all required scopes when credential has undefined scopes', () => {
664+
const missing = getMissingRequiredScopes({ scopes: undefined }, ['read', 'write'])
665+
666+
expect(missing).toEqual(['read', 'write'])
667+
})
668+
669+
it.concurrent('should ignore offline_access in required scopes', () => {
670+
const credential = { scopes: ['read'] }
671+
const missing = getMissingRequiredScopes(credential, ['read', 'offline_access'])
672+
673+
expect(missing).toEqual([])
674+
})
675+
676+
it.concurrent('should ignore refresh_token in required scopes', () => {
677+
const credential = { scopes: ['read'] }
678+
const missing = getMissingRequiredScopes(credential, ['read', 'refresh_token'])
679+
680+
expect(missing).toEqual([])
681+
})
682+
683+
it.concurrent('should ignore offline.access in required scopes', () => {
684+
const credential = { scopes: ['read'] }
685+
const missing = getMissingRequiredScopes(credential, ['read', 'offline.access'])
686+
687+
expect(missing).toEqual([])
688+
})
689+
690+
it.concurrent('should filter ignored scopes even when credential is undefined', () => {
691+
const missing = getMissingRequiredScopes(undefined, ['read', 'offline_access', 'refresh_token'])
692+
693+
expect(missing).toEqual(['read'])
694+
})
695+
696+
it.concurrent('should return empty array when requiredScopes is empty', () => {
697+
const credential = { scopes: ['read'] }
698+
const missing = getMissingRequiredScopes(credential, [])
699+
700+
expect(missing).toEqual([])
701+
})
702+
703+
it.concurrent('should return empty array when requiredScopes defaults to empty', () => {
704+
const credential = { scopes: ['read'] }
705+
const missing = getMissingRequiredScopes(credential)
706+
707+
expect(missing).toEqual([])
708+
})
709+
})

0 commit comments

Comments
 (0)