Skip to content

Commit 473c3ca

Browse files
authored
Merge pull request #65 from hypercerts-org/feat/oauth_scopes_and_email
OAuth scopes and email
2 parents ea9a720 + ec17a44 commit 473c3ca

File tree

12 files changed

+2968
-11
lines changed

12 files changed

+2968
-11
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@hypercerts-org/sdk-core": minor
3+
---
4+
5+
feat(auth): add OAuth scopes and granular permissions system
6+
7+
Add comprehensive OAuth permissions system with support for granular permissions and easy email access:
8+
9+
**Permission System**
10+
- Zod schemas for all ATProto permission types (account, repo, blob, rpc, identity, include)
11+
- Support for both transitional (legacy) and granular permission models
12+
- Type-safe permission builder with fluent API
13+
- 14 pre-built scope presets (EMAIL_READ, POSTING_APP, FULL_ACCESS, etc.)
14+
- 8 utility functions for working with scopes
15+
16+
**Email Access**
17+
- New `getAccountEmail()` method to retrieve user email from authenticated session
18+
- Returns null when permission not granted
19+
- Comprehensive error handling
20+
21+
**Enhanced OAuth Integration**
22+
- Automatic scope validation with helpful warnings
23+
- Migration suggestions from transitional to granular permissions
24+
- Improved documentation with comprehensive examples
25+
26+
**Breaking Changes**: None - fully backward compatible
27+
28+
**New Exports**:
29+
- `PermissionBuilder` - Fluent API for building type-safe scopes
30+
- `ScopePresets` - 14 ready-to-use permission presets
31+
- Utility functions: `buildScope()`, `parseScope()`, `hasPermission()`, `validateScope()`, etc.
32+
- Permission schemas and types for TypeScript consumers
33+
34+
See README for usage examples and migration guide.

packages/sdk-core/README.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,11 @@ const orgRepo = sdsRepo.repo(organizationDid);
141141
// Teammate can now access orgRepo and create hypercerts
142142
```
143143

144-
### 2. Authentication
144+
### 2. Authentication & OAuth Permissions
145145

146-
The SDK uses OAuth 2.0 for authentication with support for both PDS (Personal Data Server) and SDS (Shared Data Server).
146+
The SDK uses OAuth 2.0 for authentication with granular permission control.
147+
148+
#### Basic Authentication
147149

148150
```typescript
149151
// First-time user authentication
@@ -164,6 +166,48 @@ const session = await sdk.restoreSession("did:plc:user123");
164166
const repo = sdk.getRepository(session);
165167
```
166168

169+
#### OAuth Scopes & Permissions
170+
171+
Control exactly what your app can access using type-safe permission builders:
172+
173+
```typescript
174+
import { PermissionBuilder, ScopePresets, buildScope } from '@hypercerts-org/sdk-core';
175+
176+
// Use ready-made presets
177+
const scope = ScopePresets.EMAIL_AND_PROFILE; // Request email + profile access
178+
const scope = ScopePresets.POSTING_APP; // Full posting capabilities
179+
180+
// Or build custom permissions
181+
const scope = buildScope(
182+
new PermissionBuilder()
183+
.accountEmail('read') // Read user's email
184+
.repoWrite('app.bsky.feed.post') // Create/update posts
185+
.blob(['image/*', 'video/*']) // Upload media
186+
.build()
187+
);
188+
189+
// Use in OAuth configuration
190+
const sdk = createATProtoSDK({
191+
oauth: {
192+
clientId: 'your-client-id',
193+
redirectUri: 'https://your-app.com/callback',
194+
scope: scope, // Your custom scope
195+
// ... other config
196+
}
197+
});
198+
```
199+
200+
**Available Presets:**
201+
- `EMAIL_READ` - User's email address
202+
- `PROFILE_READ` / `PROFILE_WRITE` - Profile access
203+
- `POST_WRITE` - Create posts
204+
- `SOCIAL_WRITE` - Likes, reposts, follows
205+
- `MEDIA_UPLOAD` - Image and video uploads
206+
- `POSTING_APP` - Full posting with media
207+
- `EMAIL_AND_PROFILE` - Common combination
208+
209+
See [OAuth Permissions Documentation](./docs/implementations/atproto_oauth_scopes.md) for detailed usage.
210+
167211
### 3. Working with Hypercerts
168212

169213
#### Creating a Hypercert

packages/sdk-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hypercerts-org/sdk-core",
3-
"version": "0.8.0",
3+
"version": "0.9.0",
44
"description": "Framework-agnostic ATProto SDK core for authentication, repository operations, and lexicon management",
55
"main": "dist/index.cjs",
66
"repository": {

packages/sdk-core/src/auth/OAuthClient.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ATProtoSDKConfig } from "../core/config.js";
44
import { AuthenticationError, NetworkError } from "../core/errors.js";
55
import { InMemorySessionStore } from "../storage/InMemorySessionStore.js";
66
import { InMemoryStateStore } from "../storage/InMemoryStateStore.js";
7+
import { parseScope, validateScope, ATPROTO_SCOPE } from "./permissions.js";
78

89
/**
910
* Options for the OAuth authorization flow.
@@ -179,7 +180,7 @@ export class OAuthClient {
179180
*/
180181
private buildClientMetadata() {
181182
const clientIdUrl = new URL(this.config.oauth.clientId);
182-
return {
183+
const metadata = {
183184
client_id: this.config.oauth.clientId,
184185
client_name: "ATProto SDK Client",
185186
client_uri: clientIdUrl.origin,
@@ -193,6 +194,89 @@ export class OAuthClient {
193194
dpop_bound_access_tokens: true,
194195
jwks_uri: this.config.oauth.jwksUri,
195196
} as const;
197+
198+
// Validate scope before returning metadata
199+
this.validateClientMetadataScope(metadata.scope);
200+
201+
return metadata;
202+
}
203+
204+
/**
205+
* Validates the OAuth scope in client metadata and logs warnings/suggestions.
206+
*
207+
* This method:
208+
* 1. Checks if the scope is well-formed using permission utilities
209+
* 2. Detects mixing of transitional and granular permissions
210+
* 3. Logs warnings for missing `atproto` scope
211+
* 4. Suggests migration to granular permissions for transitional scopes
212+
*
213+
* @param scope - The OAuth scope string to validate
214+
* @internal
215+
*/
216+
private validateClientMetadataScope(scope: string): void {
217+
// Parse the scope into individual permissions
218+
const permissions = parseScope(scope);
219+
220+
// Validate well-formedness
221+
const validation = validateScope(scope);
222+
if (!validation.isValid) {
223+
this.logger?.error("Invalid OAuth scope detected", {
224+
invalidPermissions: validation.invalidPermissions,
225+
scope,
226+
});
227+
}
228+
229+
// Check for atproto scope
230+
const hasAtproto = permissions.includes(ATPROTO_SCOPE);
231+
if (!hasAtproto) {
232+
this.logger?.warn("OAuth scope missing 'atproto' - basic API access may be limited", {
233+
scope,
234+
suggestion: "Add 'atproto' to your scope for basic API access",
235+
});
236+
}
237+
238+
// Detect transitional scopes
239+
const transitionalScopes = permissions.filter((p) => p.startsWith("transition:"));
240+
const granularScopes = permissions.filter(
241+
(p) =>
242+
p.startsWith("account:") ||
243+
p.startsWith("repo:") ||
244+
p.startsWith("blob") ||
245+
p.startsWith("rpc:") ||
246+
p.startsWith("identity:") ||
247+
p.startsWith("include:"),
248+
);
249+
250+
// Log info about transitional scopes
251+
if (transitionalScopes.length > 0) {
252+
this.logger?.info("Using transitional OAuth scopes (legacy)", {
253+
transitionalScopes,
254+
note: "Transitional scopes are supported but granular permissions are recommended",
255+
});
256+
257+
// Suggest migration to granular permissions
258+
if (transitionalScopes.includes("transition:email")) {
259+
this.logger?.info("Consider migrating 'transition:email' to granular permissions", {
260+
suggestion: "Use: account:email?action=read",
261+
example: "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.EMAIL_READ",
262+
});
263+
}
264+
if (transitionalScopes.includes("transition:generic")) {
265+
this.logger?.info("Consider migrating 'transition:generic' to granular permissions", {
266+
suggestion: "Use specific permissions like: repo:* account:repo?action=read",
267+
example: "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.FULL_ACCESS",
268+
});
269+
}
270+
}
271+
272+
// Warn if mixing transitional and granular
273+
if (transitionalScopes.length > 0 && granularScopes.length > 0) {
274+
this.logger?.warn("Mixing transitional and granular OAuth scopes", {
275+
transitionalScopes,
276+
granularScopes,
277+
note: "While supported, it's recommended to use either transitional or granular permissions consistently",
278+
});
279+
}
196280
}
197281

198282
/**

0 commit comments

Comments
 (0)