Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/oauth-permissions-and-email.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@hypercerts-org/sdk-core": minor
---

feat(auth): add OAuth scopes and granular permissions system

Add comprehensive OAuth permissions system with support for granular permissions and easy email access:

**Permission System**
- Zod schemas for all ATProto permission types (account, repo, blob, rpc, identity, include)
- Support for both transitional (legacy) and granular permission models
- Type-safe permission builder with fluent API
- 14 pre-built scope presets (EMAIL_READ, POSTING_APP, FULL_ACCESS, etc.)
- 8 utility functions for working with scopes

**Email Access**
- New `getAccountEmail()` method to retrieve user email from authenticated session
- Returns null when permission not granted
- Comprehensive error handling

**Enhanced OAuth Integration**
- Automatic scope validation with helpful warnings
- Migration suggestions from transitional to granular permissions
- Improved documentation with comprehensive examples

**Breaking Changes**: None - fully backward compatible

**New Exports**:
- `PermissionBuilder` - Fluent API for building type-safe scopes
- `ScopePresets` - 14 ready-to-use permission presets
- Utility functions: `buildScope()`, `parseScope()`, `hasPermission()`, `validateScope()`, etc.
- Permission schemas and types for TypeScript consumers

See README for usage examples and migration guide.
48 changes: 46 additions & 2 deletions packages/sdk-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,11 @@ const orgRepo = sdsRepo.repo(organizationDid);
// Teammate can now access orgRepo and create hypercerts
```

### 2. Authentication
### 2. Authentication & OAuth Permissions

The SDK uses OAuth 2.0 for authentication with support for both PDS (Personal Data Server) and SDS (Shared Data Server).
The SDK uses OAuth 2.0 for authentication with granular permission control.

#### Basic Authentication

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

#### OAuth Scopes & Permissions

Control exactly what your app can access using type-safe permission builders:

```typescript
import { PermissionBuilder, ScopePresets, buildScope } from '@hypercerts-org/sdk-core';

// Use ready-made presets
const scope = ScopePresets.EMAIL_AND_PROFILE; // Request email + profile access
const scope = ScopePresets.POSTING_APP; // Full posting capabilities

// Or build custom permissions
const scope = buildScope(
new PermissionBuilder()
.accountEmail('read') // Read user's email
.repoWrite('app.bsky.feed.post') // Create/update posts
.blob(['image/*', 'video/*']) // Upload media
.build()
);

// Use in OAuth configuration
const sdk = createATProtoSDK({
oauth: {
clientId: 'your-client-id',
redirectUri: 'https://your-app.com/callback',
scope: scope, // Your custom scope
// ... other config
}
});
```

**Available Presets:**
- `EMAIL_READ` - User's email address
- `PROFILE_READ` / `PROFILE_WRITE` - Profile access
- `POST_WRITE` - Create posts
- `SOCIAL_WRITE` - Likes, reposts, follows
- `MEDIA_UPLOAD` - Image and video uploads
- `POSTING_APP` - Full posting with media
- `EMAIL_AND_PROFILE` - Common combination

See [OAuth Permissions Documentation](./docs/implementations/atproto_oauth_scopes.md) for detailed usage.

### 3. Working with Hypercerts

#### Creating a Hypercert
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hypercerts-org/sdk-core",
"version": "0.8.0",
"version": "0.9.0",
"description": "Framework-agnostic ATProto SDK core for authentication, repository operations, and lexicon management",
"main": "dist/index.cjs",
"repository": {
Expand Down
86 changes: 85 additions & 1 deletion packages/sdk-core/src/auth/OAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ATProtoSDKConfig } from "../core/config.js";
import { AuthenticationError, NetworkError } from "../core/errors.js";
import { InMemorySessionStore } from "../storage/InMemorySessionStore.js";
import { InMemoryStateStore } from "../storage/InMemoryStateStore.js";
import { parseScope, validateScope, ATPROTO_SCOPE } from "./permissions.js";

/**
* Options for the OAuth authorization flow.
Expand Down Expand Up @@ -179,7 +180,7 @@ export class OAuthClient {
*/
private buildClientMetadata() {
const clientIdUrl = new URL(this.config.oauth.clientId);
return {
const metadata = {
client_id: this.config.oauth.clientId,
client_name: "ATProto SDK Client",
client_uri: clientIdUrl.origin,
Expand All @@ -193,6 +194,89 @@ export class OAuthClient {
dpop_bound_access_tokens: true,
jwks_uri: this.config.oauth.jwksUri,
} as const;

// Validate scope before returning metadata
this.validateClientMetadataScope(metadata.scope);

return metadata;
}

/**
* Validates the OAuth scope in client metadata and logs warnings/suggestions.
*
* This method:
* 1. Checks if the scope is well-formed using permission utilities
* 2. Detects mixing of transitional and granular permissions
* 3. Logs warnings for missing `atproto` scope
* 4. Suggests migration to granular permissions for transitional scopes
*
* @param scope - The OAuth scope string to validate
* @internal
*/
private validateClientMetadataScope(scope: string): void {
// Parse the scope into individual permissions
const permissions = parseScope(scope);

// Validate well-formedness
const validation = validateScope(scope);
if (!validation.isValid) {
this.logger?.error("Invalid OAuth scope detected", {
invalidPermissions: validation.invalidPermissions,
scope,
});
}

// Check for atproto scope
const hasAtproto = permissions.includes(ATPROTO_SCOPE);
if (!hasAtproto) {
this.logger?.warn("OAuth scope missing 'atproto' - basic API access may be limited", {
scope,
suggestion: "Add 'atproto' to your scope for basic API access",
});
}

// Detect transitional scopes
const transitionalScopes = permissions.filter((p) => p.startsWith("transition:"));
const granularScopes = permissions.filter(
(p) =>
p.startsWith("account:") ||
p.startsWith("repo:") ||
p.startsWith("blob") ||
p.startsWith("rpc:") ||
p.startsWith("identity:") ||
p.startsWith("include:"),
);

// Log info about transitional scopes
if (transitionalScopes.length > 0) {
this.logger?.info("Using transitional OAuth scopes (legacy)", {
transitionalScopes,
note: "Transitional scopes are supported but granular permissions are recommended",
});

// Suggest migration to granular permissions
if (transitionalScopes.includes("transition:email")) {
this.logger?.info("Consider migrating 'transition:email' to granular permissions", {
suggestion: "Use: account:email?action=read",
example: "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.EMAIL_READ",
});
}
if (transitionalScopes.includes("transition:generic")) {
this.logger?.info("Consider migrating 'transition:generic' to granular permissions", {
suggestion: "Use specific permissions like: repo:* account:repo?action=read",
example: "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.FULL_ACCESS",
});
}
}

// Warn if mixing transitional and granular
if (transitionalScopes.length > 0 && granularScopes.length > 0) {
this.logger?.warn("Mixing transitional and granular OAuth scopes", {
transitionalScopes,
granularScopes,
note: "While supported, it's recommended to use either transitional or granular permissions consistently",
});
}
}

/**
Expand Down
Loading