Skip to content

Commit 555cf46

Browse files
committed
feat(permissions): add scope presets for common use cases
- Add ScopePresets object with 14 pre-built permission sets - Include EMAIL_READ, PROFILE_READ/WRITE, POST_WRITE presets - Add SOCIAL_WRITE for likes/reposts/follows - Add MEDIA_UPLOAD and IMAGE_UPLOAD presets - Add POSTING_APP preset combining posts and media - Add READ_ONLY and FULL_ACCESS presets - Include EMAIL_AND_PROFILE combo preset - Add transitional scope presets for backward compatibility - Add 18 comprehensive tests for all presets - All 167 tests passing
1 parent 68ed479 commit 555cf46

File tree

2 files changed

+303
-0
lines changed

2 files changed

+303
-0
lines changed

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

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,202 @@ export function buildScope(permissions: string[]): string {
769769
return permissions.join(" ");
770770
}
771771

772+
/**
773+
* Pre-built scope presets for common use cases.
774+
*
775+
* These presets provide ready-to-use permission sets for typical application scenarios.
776+
*/
777+
export const ScopePresets = {
778+
/**
779+
* Email access scope - allows reading user's email address.
780+
*
781+
* Includes:
782+
* - account:email?action=read
783+
*
784+
* @example
785+
* ```typescript
786+
* const scope = ScopePresets.EMAIL_READ;
787+
* // Use in OAuth flow to request email access
788+
* ```
789+
*/
790+
EMAIL_READ: buildScope(new PermissionBuilder().accountEmail("read").build()),
791+
792+
/**
793+
* Profile read scope - allows reading user's profile.
794+
*
795+
* Includes:
796+
* - repo:app.bsky.actor.profile (read-only)
797+
*
798+
* @example
799+
* ```typescript
800+
* const scope = ScopePresets.PROFILE_READ;
801+
* ```
802+
*/
803+
PROFILE_READ: buildScope(new PermissionBuilder().repoRead("app.bsky.actor.profile").build()),
804+
805+
/**
806+
* Profile write scope - allows updating user's profile.
807+
*
808+
* Includes:
809+
* - repo:app.bsky.actor.profile (create + update)
810+
*
811+
* @example
812+
* ```typescript
813+
* const scope = ScopePresets.PROFILE_WRITE;
814+
* ```
815+
*/
816+
PROFILE_WRITE: buildScope(new PermissionBuilder().repoWrite("app.bsky.actor.profile").build()),
817+
818+
/**
819+
* Post creation scope - allows creating and updating posts.
820+
*
821+
* Includes:
822+
* - repo:app.bsky.feed.post (create + update)
823+
*
824+
* @example
825+
* ```typescript
826+
* const scope = ScopePresets.POST_WRITE;
827+
* ```
828+
*/
829+
POST_WRITE: buildScope(new PermissionBuilder().repoWrite("app.bsky.feed.post").build()),
830+
831+
/**
832+
* Social interactions scope - allows liking, reposting, and following.
833+
*
834+
* Includes:
835+
* - repo:app.bsky.feed.like (create + update)
836+
* - repo:app.bsky.feed.repost (create + update)
837+
* - repo:app.bsky.graph.follow (create + update)
838+
*
839+
* @example
840+
* ```typescript
841+
* const scope = ScopePresets.SOCIAL_WRITE;
842+
* ```
843+
*/
844+
SOCIAL_WRITE: buildScope(
845+
new PermissionBuilder()
846+
.repoWrite("app.bsky.feed.like")
847+
.repoWrite("app.bsky.feed.repost")
848+
.repoWrite("app.bsky.graph.follow")
849+
.build(),
850+
),
851+
852+
/**
853+
* Media upload scope - allows uploading images and videos.
854+
*
855+
* Includes:
856+
* - blob permissions for image/* and video/*
857+
*
858+
* @example
859+
* ```typescript
860+
* const scope = ScopePresets.MEDIA_UPLOAD;
861+
* ```
862+
*/
863+
MEDIA_UPLOAD: buildScope(new PermissionBuilder().blob(["image/*", "video/*"]).build()),
864+
865+
/**
866+
* Image upload only scope - allows uploading images.
867+
*
868+
* Includes:
869+
* - blob:image/*
870+
*
871+
* @example
872+
* ```typescript
873+
* const scope = ScopePresets.IMAGE_UPLOAD;
874+
* ```
875+
*/
876+
IMAGE_UPLOAD: buildScope(new PermissionBuilder().blob("image/*").build()),
877+
878+
/**
879+
* Posting app scope - full posting capabilities including media.
880+
*
881+
* Includes:
882+
* - repo:app.bsky.feed.post (create + update)
883+
* - repo:app.bsky.feed.like (create + update)
884+
* - repo:app.bsky.feed.repost (create + update)
885+
* - blob permissions for image/* and video/*
886+
*
887+
* @example
888+
* ```typescript
889+
* const scope = ScopePresets.POSTING_APP;
890+
* ```
891+
*/
892+
POSTING_APP: buildScope(
893+
new PermissionBuilder()
894+
.repoWrite("app.bsky.feed.post")
895+
.repoWrite("app.bsky.feed.like")
896+
.repoWrite("app.bsky.feed.repost")
897+
.blob(["image/*", "video/*"])
898+
.build(),
899+
),
900+
901+
/**
902+
* Read-only app scope - allows reading all repository data.
903+
*
904+
* Includes:
905+
* - repo:* (read-only, no actions)
906+
*
907+
* @example
908+
* ```typescript
909+
* const scope = ScopePresets.READ_ONLY;
910+
* ```
911+
*/
912+
READ_ONLY: buildScope(new PermissionBuilder().repoRead("*").build()),
913+
914+
/**
915+
* Full access scope - allows all repository operations.
916+
*
917+
* Includes:
918+
* - repo:* (create + update + delete)
919+
*
920+
* @example
921+
* ```typescript
922+
* const scope = ScopePresets.FULL_ACCESS;
923+
* ```
924+
*/
925+
FULL_ACCESS: buildScope(new PermissionBuilder().repoFull("*").build()),
926+
927+
/**
928+
* Email + Profile scope - common combination for user identification.
929+
*
930+
* Includes:
931+
* - account:email?action=read
932+
* - repo:app.bsky.actor.profile (read-only)
933+
*
934+
* @example
935+
* ```typescript
936+
* const scope = ScopePresets.EMAIL_AND_PROFILE;
937+
* ```
938+
*/
939+
EMAIL_AND_PROFILE: buildScope(
940+
new PermissionBuilder().accountEmail("read").repoRead("app.bsky.actor.profile").build(),
941+
),
942+
943+
/**
944+
* Transitional email scope (legacy).
945+
*
946+
* Uses the transitional scope format for backward compatibility.
947+
*
948+
* @example
949+
* ```typescript
950+
* const scope = ScopePresets.TRANSITION_EMAIL;
951+
* ```
952+
*/
953+
TRANSITION_EMAIL: buildScope(new PermissionBuilder().transition("email").build()),
954+
955+
/**
956+
* Transitional generic scope (legacy).
957+
*
958+
* Uses the transitional scope format for backward compatibility.
959+
*
960+
* @example
961+
* ```typescript
962+
* const scope = ScopePresets.TRANSITION_GENERIC;
963+
* ```
964+
*/
965+
TRANSITION_GENERIC: buildScope(new PermissionBuilder().transition("generic").build()),
966+
} as const;
967+
772968
/**
773969
* Parse a scope string into an array of individual permissions.
774970
*

packages/sdk-core/tests/auth/permissions.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
IncludePermissionSchema,
1818
PermissionSchema,
1919
PermissionBuilder,
20+
ScopePresets,
2021
buildScope,
2122
parseScope,
2223
hasPermission,
@@ -1121,3 +1122,109 @@ describe("Scope Utility Functions", () => {
11211122
});
11221123
});
11231124
});
1125+
1126+
describe("ScopePresets", () => {
1127+
describe("Basic Presets", () => {
1128+
it("should provide EMAIL_READ preset", () => {
1129+
expect(ScopePresets.EMAIL_READ).toBe("account:email?action=read");
1130+
});
1131+
1132+
it("should provide PROFILE_READ preset", () => {
1133+
expect(ScopePresets.PROFILE_READ).toBe("repo:app.bsky.actor.profile");
1134+
});
1135+
1136+
it("should provide PROFILE_WRITE preset", () => {
1137+
expect(ScopePresets.PROFILE_WRITE).toBe("repo:app.bsky.actor.profile?action=create&action=update");
1138+
});
1139+
1140+
it("should provide POST_WRITE preset", () => {
1141+
expect(ScopePresets.POST_WRITE).toBe("repo:app.bsky.feed.post?action=create&action=update");
1142+
});
1143+
1144+
it("should provide IMAGE_UPLOAD preset", () => {
1145+
expect(ScopePresets.IMAGE_UPLOAD).toBe("blob:image/*");
1146+
});
1147+
1148+
it("should provide MEDIA_UPLOAD preset", () => {
1149+
expect(ScopePresets.MEDIA_UPLOAD).toBe("blob?accept=image%2F*&accept=video%2F*");
1150+
});
1151+
});
1152+
1153+
describe("Complex Presets", () => {
1154+
it("should provide SOCIAL_WRITE preset", () => {
1155+
const permissions = parseScope(ScopePresets.SOCIAL_WRITE);
1156+
expect(permissions).toHaveLength(3);
1157+
expect(permissions).toContain("repo:app.bsky.feed.like?action=create&action=update");
1158+
expect(permissions).toContain("repo:app.bsky.feed.repost?action=create&action=update");
1159+
expect(permissions).toContain("repo:app.bsky.graph.follow?action=create&action=update");
1160+
});
1161+
1162+
it("should provide POSTING_APP preset", () => {
1163+
const permissions = parseScope(ScopePresets.POSTING_APP);
1164+
expect(permissions).toHaveLength(4);
1165+
expect(permissions).toContain("repo:app.bsky.feed.post?action=create&action=update");
1166+
expect(permissions).toContain("repo:app.bsky.feed.like?action=create&action=update");
1167+
expect(permissions).toContain("repo:app.bsky.feed.repost?action=create&action=update");
1168+
expect(permissions).toContain("blob?accept=image%2F*&accept=video%2F*");
1169+
});
1170+
1171+
it("should provide EMAIL_AND_PROFILE preset", () => {
1172+
const permissions = parseScope(ScopePresets.EMAIL_AND_PROFILE);
1173+
expect(permissions).toHaveLength(2);
1174+
expect(permissions).toContain("account:email?action=read");
1175+
expect(permissions).toContain("repo:app.bsky.actor.profile");
1176+
});
1177+
});
1178+
1179+
describe("Access Level Presets", () => {
1180+
it("should provide READ_ONLY preset", () => {
1181+
expect(ScopePresets.READ_ONLY).toBe("repo:*");
1182+
});
1183+
1184+
it("should provide FULL_ACCESS preset", () => {
1185+
expect(ScopePresets.FULL_ACCESS).toBe("repo:*?action=create&action=update&action=delete");
1186+
});
1187+
});
1188+
1189+
describe("Transitional Presets", () => {
1190+
it("should provide TRANSITION_EMAIL preset", () => {
1191+
expect(ScopePresets.TRANSITION_EMAIL).toBe("transition:email");
1192+
});
1193+
1194+
it("should provide TRANSITION_GENERIC preset", () => {
1195+
expect(ScopePresets.TRANSITION_GENERIC).toBe("transition:generic");
1196+
});
1197+
});
1198+
1199+
describe("Preset Usage", () => {
1200+
it("should work with parseScope", () => {
1201+
const permissions = parseScope(ScopePresets.POSTING_APP);
1202+
expect(permissions.length).toBeGreaterThan(0);
1203+
});
1204+
1205+
it("should work with hasPermission", () => {
1206+
expect(hasPermission(ScopePresets.EMAIL_READ, "account:email?action=read")).toBe(true);
1207+
expect(hasPermission(ScopePresets.EMAIL_READ, "account:repo")).toBe(false);
1208+
});
1209+
1210+
it("should work with mergeScopes", () => {
1211+
const merged = mergeScopes([ScopePresets.EMAIL_READ, ScopePresets.POST_WRITE]);
1212+
expect(hasPermission(merged, "account:email?action=read")).toBe(true);
1213+
expect(hasPermission(merged, "repo:app.bsky.feed.post?action=create&action=update")).toBe(true);
1214+
});
1215+
1216+
it("should be valid scopes", () => {
1217+
expect(validateScope(ScopePresets.EMAIL_READ).isValid).toBe(true);
1218+
expect(validateScope(ScopePresets.POSTING_APP).isValid).toBe(true);
1219+
expect(validateScope(ScopePresets.FULL_ACCESS).isValid).toBe(true);
1220+
expect(validateScope(ScopePresets.TRANSITION_EMAIL).isValid).toBe(true);
1221+
});
1222+
});
1223+
1224+
describe("Preset Immutability", () => {
1225+
it("should be readonly (const assertion)", () => {
1226+
expect(typeof ScopePresets.EMAIL_READ).toBe("string");
1227+
expect(typeof ScopePresets.POSTING_APP).toBe("string");
1228+
});
1229+
});
1230+
});

0 commit comments

Comments
 (0)