@hypercerts-org/sdk-core 0.9.0-beta.0 → 0.10.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { JoseKey, NodeOAuthClient } from '@atproto/oauth-client-node';
2
+ import { z } from 'zod';
2
3
  import { Lexicons } from '@atproto/lexicon';
3
4
  import { HYPERCERT_COLLECTIONS, HYPERCERT_LEXICONS } from '@hypercerts-org/lexicon';
4
5
  export { AppCertifiedLocation, ComAtprotoRepoStrongRef, HYPERCERT_COLLECTIONS, HYPERCERT_LEXICONS, OrgHypercertsClaim, OrgHypercertsClaimContribution, OrgHypercertsClaimEvaluation, OrgHypercertsClaimEvidence, OrgHypercertsClaimMeasurement, OrgHypercertsClaimRights, OrgHypercertsCollection, ids, lexicons, schemaDict, schemas, validate } from '@hypercerts-org/lexicon';
5
6
  import { Agent } from '@atproto/api';
6
7
  import { EventEmitter } from 'eventemitter3';
7
- import { z } from 'zod';
8
8
 
9
9
  /**
10
10
  * Base error class for all SDK errors.
@@ -522,6 +522,990 @@ class InMemoryStateStore {
522
522
  }
523
523
  }
524
524
 
525
+ /**
526
+ * OAuth Scopes and Granular Permissions
527
+ *
528
+ * This module provides type-safe, Zod-validated OAuth scope and permission management
529
+ * for the ATProto SDK. It supports both legacy transitional scopes and the new
530
+ * granular permissions model.
531
+ *
532
+ * @see https://atproto.com/specs/oauth
533
+ * @see https://atproto.com/specs/permission
534
+ *
535
+ * @module auth/permissions
536
+ */
537
+ /**
538
+ * Base OAuth scope - required for all sessions
539
+ *
540
+ * @constant
541
+ */
542
+ const ATPROTO_SCOPE = "atproto";
543
+ /**
544
+ * Transitional OAuth scopes for legacy compatibility.
545
+ *
546
+ * These scopes provide broad access and are maintained for backwards compatibility.
547
+ * New applications should use granular permissions instead.
548
+ *
549
+ * @deprecated Use granular permissions (account:*, repo:*, etc.) for better control
550
+ * @constant
551
+ */
552
+ const TRANSITION_SCOPES = {
553
+ /** Broad PDS permissions including record creation, blob uploads, and preferences */
554
+ GENERIC: "transition:generic",
555
+ /** Direct messages access (requires transition:generic) */
556
+ CHAT: "transition:chat.bsky",
557
+ /** Email address and confirmation status */
558
+ EMAIL: "transition:email",
559
+ };
560
+ /**
561
+ * Zod schema for transitional scopes.
562
+ *
563
+ * Validates that a scope string is one of the known transitional scopes.
564
+ *
565
+ * @example
566
+ * ```typescript
567
+ * TransitionScopeSchema.parse('transition:email'); // Valid
568
+ * TransitionScopeSchema.parse('invalid'); // Throws ZodError
569
+ * ```
570
+ */
571
+ const TransitionScopeSchema = z
572
+ .enum(["transition:generic", "transition:chat.bsky", "transition:email"])
573
+ .describe("Legacy transitional OAuth scopes");
574
+ /**
575
+ * Zod schema for account permission attributes.
576
+ *
577
+ * Account attributes specify what aspect of the account is being accessed.
578
+ */
579
+ const AccountAttrSchema = z.enum(["email", "repo"]);
580
+ /**
581
+ * Zod schema for account actions.
582
+ *
583
+ * Account actions specify the level of access (read-only or management).
584
+ */
585
+ const AccountActionSchema = z.enum(["read", "manage"]);
586
+ /**
587
+ * Zod schema for repository actions.
588
+ *
589
+ * Repository actions specify what operations can be performed on records.
590
+ */
591
+ const RepoActionSchema = z.enum(["create", "update", "delete"]);
592
+ /**
593
+ * Zod schema for identity permission attributes.
594
+ *
595
+ * Identity attributes specify what identity information can be managed.
596
+ */
597
+ const IdentityAttrSchema = z.enum(["handle", "*"]);
598
+ /**
599
+ * Zod schema for MIME type patterns.
600
+ *
601
+ * Validates MIME type strings like "image/*" or "video/mp4".
602
+ *
603
+ * @example
604
+ * ```typescript
605
+ * MimeTypeSchema.parse('image/*'); // Valid
606
+ * MimeTypeSchema.parse('video/mp4'); // Valid
607
+ * MimeTypeSchema.parse('invalid'); // Throws ZodError
608
+ * ```
609
+ */
610
+ const MimeTypeSchema = z
611
+ .string()
612
+ .regex(/^[a-z]+\/[a-z0-9*+-]+$/i, 'Invalid MIME type pattern. Expected format: type/subtype (e.g., "image/*" or "video/mp4")');
613
+ /**
614
+ * Zod schema for NSID (Namespaced Identifier).
615
+ *
616
+ * NSIDs are reverse-DNS style identifiers used throughout ATProto
617
+ * (e.g., "app.bsky.feed.post" or "com.example.myrecord").
618
+ *
619
+ * @see https://atproto.com/specs/nsid
620
+ *
621
+ * @example
622
+ * ```typescript
623
+ * NsidSchema.parse('app.bsky.feed.post'); // Valid
624
+ * NsidSchema.parse('com.example.myrecord'); // Valid
625
+ * NsidSchema.parse('InvalidNSID'); // Throws ZodError
626
+ * ```
627
+ */
628
+ const NsidSchema = z
629
+ .string()
630
+ .regex(/^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z][a-zA-Z0-9-]*)+$/, 'Invalid NSID format. Expected reverse-DNS format (e.g., "app.bsky.feed.post")');
631
+ /**
632
+ * Zod schema for account permission.
633
+ *
634
+ * Account permissions control access to account-level information like email
635
+ * and repository management.
636
+ *
637
+ * @example Without action (read-only)
638
+ * ```typescript
639
+ * const input = { type: 'account', attr: 'email' };
640
+ * AccountPermissionSchema.parse(input); // Returns: "account:email"
641
+ * ```
642
+ *
643
+ * @example With action
644
+ * ```typescript
645
+ * const input = { type: 'account', attr: 'email', action: 'manage' };
646
+ * AccountPermissionSchema.parse(input); // Returns: "account:email?action=manage"
647
+ * ```
648
+ */
649
+ const AccountPermissionSchema = z
650
+ .object({
651
+ type: z.literal("account"),
652
+ attr: AccountAttrSchema,
653
+ action: AccountActionSchema.optional(),
654
+ })
655
+ .transform(({ attr, action }) => {
656
+ let perm = `account:${attr}`;
657
+ if (action) {
658
+ perm += `?action=${action}`;
659
+ }
660
+ return perm;
661
+ });
662
+ /**
663
+ * Zod schema for repository permission.
664
+ *
665
+ * Repository permissions control write access to records by collection type.
666
+ * The collection must be a valid NSID or wildcard (*).
667
+ *
668
+ * @example Without actions (all actions allowed)
669
+ * ```typescript
670
+ * const input = { type: 'repo', collection: 'app.bsky.feed.post' };
671
+ * RepoPermissionSchema.parse(input); // Returns: "repo:app.bsky.feed.post"
672
+ * ```
673
+ *
674
+ * @example With specific actions
675
+ * ```typescript
676
+ * const input = {
677
+ * type: 'repo',
678
+ * collection: 'app.bsky.feed.post',
679
+ * actions: ['create', 'update']
680
+ * };
681
+ * RepoPermissionSchema.parse(input); // Returns: "repo:app.bsky.feed.post?action=create&action=update"
682
+ * ```
683
+ *
684
+ * @example With wildcard collection
685
+ * ```typescript
686
+ * const input = { type: 'repo', collection: '*', actions: ['delete'] };
687
+ * RepoPermissionSchema.parse(input); // Returns: "repo:*?action=delete"
688
+ * ```
689
+ */
690
+ const RepoPermissionSchema = z
691
+ .object({
692
+ type: z.literal("repo"),
693
+ collection: NsidSchema.or(z.literal("*")),
694
+ actions: z.array(RepoActionSchema).optional(),
695
+ })
696
+ .transform(({ collection, actions }) => {
697
+ let perm = `repo:${collection}`;
698
+ if (actions && actions.length > 0) {
699
+ const params = actions.map((a) => `action=${a}`).join("&");
700
+ perm += `?${params}`;
701
+ }
702
+ return perm;
703
+ });
704
+ /**
705
+ * Zod schema for blob permission.
706
+ *
707
+ * Blob permissions control media file uploads constrained by MIME type patterns.
708
+ *
709
+ * @example Single MIME type
710
+ * ```typescript
711
+ * const input = { type: 'blob', mimeTypes: ['image/*'] };
712
+ * BlobPermissionSchema.parse(input); // Returns: "blob:image/*"
713
+ * ```
714
+ *
715
+ * @example Multiple MIME types
716
+ * ```typescript
717
+ * const input = { type: 'blob', mimeTypes: ['image/*', 'video/*'] };
718
+ * BlobPermissionSchema.parse(input); // Returns: "blob?accept=image/*&accept=video/*"
719
+ * ```
720
+ */
721
+ const BlobPermissionSchema = z
722
+ .object({
723
+ type: z.literal("blob"),
724
+ mimeTypes: z.array(MimeTypeSchema).min(1, "At least one MIME type required"),
725
+ })
726
+ .transform(({ mimeTypes }) => {
727
+ if (mimeTypes.length === 1) {
728
+ return `blob:${mimeTypes[0]}`;
729
+ }
730
+ const accepts = mimeTypes.map((t) => `accept=${encodeURIComponent(t)}`).join("&");
731
+ return `blob?${accepts}`;
732
+ });
733
+ /**
734
+ * Zod schema for RPC permission.
735
+ *
736
+ * RPC permissions control authenticated API calls to remote services.
737
+ * At least one of lexicon or aud must be restricted (both cannot be wildcards).
738
+ *
739
+ * @example Specific lexicon with wildcard audience
740
+ * ```typescript
741
+ * const input = {
742
+ * type: 'rpc',
743
+ * lexicon: 'com.atproto.repo.createRecord',
744
+ * aud: '*'
745
+ * };
746
+ * RpcPermissionSchema.parse(input);
747
+ * // Returns: "rpc:com.atproto.repo.createRecord?aud=*"
748
+ * ```
749
+ *
750
+ * @example With specific audience
751
+ * ```typescript
752
+ * const input = {
753
+ * type: 'rpc',
754
+ * lexicon: 'com.atproto.repo.createRecord',
755
+ * aud: 'did:web:api.example.com',
756
+ * inheritAud: true
757
+ * };
758
+ * RpcPermissionSchema.parse(input);
759
+ * // Returns: "rpc:com.atproto.repo.createRecord?aud=did%3Aweb%3Aapi.example.com&inheritAud=true"
760
+ * ```
761
+ */
762
+ const RpcPermissionSchema = z
763
+ .object({
764
+ type: z.literal("rpc"),
765
+ lexicon: NsidSchema.or(z.literal("*")),
766
+ aud: z.string().min(1, "Audience is required"),
767
+ inheritAud: z.boolean().optional(),
768
+ })
769
+ .refine(({ lexicon, aud }) => lexicon !== "*" || aud !== "*", "At least one of lexicon or aud must be restricted (wildcards cannot both be used)")
770
+ .transform(({ lexicon, aud, inheritAud }) => {
771
+ let perm = `rpc:${lexicon}?aud=${encodeURIComponent(aud)}`;
772
+ if (inheritAud) {
773
+ perm += "&inheritAud=true";
774
+ }
775
+ return perm;
776
+ });
777
+ /**
778
+ * Zod schema for identity permission.
779
+ *
780
+ * Identity permissions control access to DID documents and handles.
781
+ *
782
+ * @example Handle management
783
+ * ```typescript
784
+ * const input = { type: 'identity', attr: 'handle' };
785
+ * IdentityPermissionSchema.parse(input); // Returns: "identity:handle"
786
+ * ```
787
+ *
788
+ * @example All identity attributes
789
+ * ```typescript
790
+ * const input = { type: 'identity', attr: '*' };
791
+ * IdentityPermissionSchema.parse(input); // Returns: "identity:*"
792
+ * ```
793
+ */
794
+ const IdentityPermissionSchema = z
795
+ .object({
796
+ type: z.literal("identity"),
797
+ attr: IdentityAttrSchema,
798
+ })
799
+ .transform(({ attr }) => `identity:${attr}`);
800
+ /**
801
+ * Zod schema for permission set inclusion.
802
+ *
803
+ * Include permissions reference permission sets bundled under a single NSID.
804
+ *
805
+ * @example Without audience
806
+ * ```typescript
807
+ * const input = { type: 'include', nsid: 'com.example.authBasicFeatures' };
808
+ * IncludePermissionSchema.parse(input);
809
+ * // Returns: "include:com.example.authBasicFeatures"
810
+ * ```
811
+ *
812
+ * @example With audience
813
+ * ```typescript
814
+ * const input = {
815
+ * type: 'include',
816
+ * nsid: 'com.example.authBasicFeatures',
817
+ * aud: 'did:web:api.example.com'
818
+ * };
819
+ * IncludePermissionSchema.parse(input);
820
+ * // Returns: "include:com.example.authBasicFeatures?aud=did%3Aweb%3Aapi.example.com"
821
+ * ```
822
+ */
823
+ const IncludePermissionSchema = z
824
+ .object({
825
+ type: z.literal("include"),
826
+ nsid: NsidSchema,
827
+ aud: z.string().optional(),
828
+ })
829
+ .transform(({ nsid, aud }) => {
830
+ let perm = `include:${nsid}`;
831
+ if (aud) {
832
+ perm += `?aud=${encodeURIComponent(aud)}`;
833
+ }
834
+ return perm;
835
+ });
836
+ /**
837
+ * Union schema for all permission types.
838
+ *
839
+ * This schema accepts any of the supported permission types and validates
840
+ * them according to their specific rules.
841
+ */
842
+ const PermissionSchema = z.union([
843
+ AccountPermissionSchema,
844
+ RepoPermissionSchema,
845
+ BlobPermissionSchema,
846
+ RpcPermissionSchema,
847
+ IdentityPermissionSchema,
848
+ IncludePermissionSchema,
849
+ ]);
850
+ /**
851
+ * Fluent builder for constructing OAuth permission arrays.
852
+ *
853
+ * This class provides a convenient, type-safe way to build arrays of permissions
854
+ * using method chaining.
855
+ *
856
+ * @example Basic usage
857
+ * ```typescript
858
+ * const builder = new PermissionBuilder()
859
+ * .accountEmail('read')
860
+ * .repoWrite('app.bsky.feed.post')
861
+ * .blob(['image/*', 'video/*']);
862
+ *
863
+ * const permissions = builder.build();
864
+ * // Returns: ['account:email?action=read', 'repo:app.bsky.feed.post?action=create&action=update', 'blob:image/*,video/*']
865
+ * ```
866
+ *
867
+ * @example With transitional scopes
868
+ * ```typescript
869
+ * const builder = new PermissionBuilder()
870
+ * .transition('email')
871
+ * .transition('generic');
872
+ *
873
+ * const scopes = builder.build();
874
+ * // Returns: ['transition:email', 'transition:generic']
875
+ * ```
876
+ */
877
+ class PermissionBuilder {
878
+ constructor() {
879
+ this.permissions = [];
880
+ }
881
+ /**
882
+ * Add a transitional scope.
883
+ *
884
+ * @param scope - The transitional scope name ('email', 'generic', or 'chat.bsky')
885
+ * @returns This builder for chaining
886
+ *
887
+ * @example
888
+ * ```typescript
889
+ * builder.transition('email').transition('generic');
890
+ * ```
891
+ */
892
+ transition(scope) {
893
+ const fullScope = `transition:${scope}`;
894
+ const validated = TransitionScopeSchema.parse(fullScope);
895
+ this.permissions.push(validated);
896
+ return this;
897
+ }
898
+ /**
899
+ * Add an account permission.
900
+ *
901
+ * @param attr - The account attribute ('email' or 'repo')
902
+ * @param action - Optional action ('read' or 'manage')
903
+ * @returns This builder for chaining
904
+ *
905
+ * @example
906
+ * ```typescript
907
+ * builder.accountEmail('read').accountRepo('manage');
908
+ * ```
909
+ */
910
+ account(attr, action) {
911
+ const permission = AccountPermissionSchema.parse({
912
+ type: "account",
913
+ attr,
914
+ action,
915
+ });
916
+ this.permissions.push(permission);
917
+ return this;
918
+ }
919
+ /**
920
+ * Convenience method for account:email permission.
921
+ *
922
+ * @param action - Optional action ('read' or 'manage')
923
+ * @returns This builder for chaining
924
+ *
925
+ * @example
926
+ * ```typescript
927
+ * builder.accountEmail('read');
928
+ * ```
929
+ */
930
+ accountEmail(action) {
931
+ return this.account("email", action);
932
+ }
933
+ /**
934
+ * Convenience method for account:repo permission.
935
+ *
936
+ * @param action - Optional action ('read' or 'manage')
937
+ * @returns This builder for chaining
938
+ *
939
+ * @example
940
+ * ```typescript
941
+ * builder.accountRepo('manage');
942
+ * ```
943
+ */
944
+ accountRepo(action) {
945
+ return this.account("repo", action);
946
+ }
947
+ /**
948
+ * Add a repository permission.
949
+ *
950
+ * @param collection - The NSID of the collection or '*' for all
951
+ * @param actions - Optional array of actions ('create', 'update', 'delete')
952
+ * @returns This builder for chaining
953
+ *
954
+ * @example
955
+ * ```typescript
956
+ * builder.repo('app.bsky.feed.post', ['create', 'update']);
957
+ * ```
958
+ */
959
+ repo(collection, actions) {
960
+ const permission = RepoPermissionSchema.parse({
961
+ type: "repo",
962
+ collection,
963
+ actions,
964
+ });
965
+ this.permissions.push(permission);
966
+ return this;
967
+ }
968
+ /**
969
+ * Convenience method for repository write permissions (create + update).
970
+ *
971
+ * @param collection - The NSID of the collection or '*' for all
972
+ * @returns This builder for chaining
973
+ *
974
+ * @example
975
+ * ```typescript
976
+ * builder.repoWrite('app.bsky.feed.post');
977
+ * ```
978
+ */
979
+ repoWrite(collection) {
980
+ return this.repo(collection, ["create", "update"]);
981
+ }
982
+ /**
983
+ * Convenience method for repository read permission (no actions).
984
+ *
985
+ * @param collection - The NSID of the collection or '*' for all
986
+ * @returns This builder for chaining
987
+ *
988
+ * @example
989
+ * ```typescript
990
+ * builder.repoRead('app.bsky.feed.post');
991
+ * ```
992
+ */
993
+ repoRead(collection) {
994
+ return this.repo(collection, []);
995
+ }
996
+ /**
997
+ * Convenience method for full repository permissions (create + update + delete).
998
+ *
999
+ * @param collection - The NSID of the collection or '*' for all
1000
+ * @returns This builder for chaining
1001
+ *
1002
+ * @example
1003
+ * ```typescript
1004
+ * builder.repoFull('app.bsky.feed.post');
1005
+ * ```
1006
+ */
1007
+ repoFull(collection) {
1008
+ return this.repo(collection, ["create", "update", "delete"]);
1009
+ }
1010
+ /**
1011
+ * Add a blob permission.
1012
+ *
1013
+ * @param mimeTypes - Array of MIME types or a single MIME type
1014
+ * @returns This builder for chaining
1015
+ *
1016
+ * @example
1017
+ * ```typescript
1018
+ * builder.blob(['image/*', 'video/*']);
1019
+ * builder.blob('image/*');
1020
+ * ```
1021
+ */
1022
+ blob(mimeTypes) {
1023
+ const types = Array.isArray(mimeTypes) ? mimeTypes : [mimeTypes];
1024
+ const permission = BlobPermissionSchema.parse({
1025
+ type: "blob",
1026
+ mimeTypes: types,
1027
+ });
1028
+ this.permissions.push(permission);
1029
+ return this;
1030
+ }
1031
+ /**
1032
+ * Add an RPC permission.
1033
+ *
1034
+ * @param lexicon - The NSID of the lexicon or '*' for all
1035
+ * @param aud - The audience (DID or URL)
1036
+ * @param inheritAud - Whether to inherit audience
1037
+ * @returns This builder for chaining
1038
+ *
1039
+ * @example
1040
+ * ```typescript
1041
+ * builder.rpc('com.atproto.repo.createRecord', 'did:web:api.example.com');
1042
+ * ```
1043
+ */
1044
+ rpc(lexicon, aud, inheritAud) {
1045
+ const permission = RpcPermissionSchema.parse({
1046
+ type: "rpc",
1047
+ lexicon,
1048
+ aud,
1049
+ inheritAud,
1050
+ });
1051
+ this.permissions.push(permission);
1052
+ return this;
1053
+ }
1054
+ /**
1055
+ * Add an identity permission.
1056
+ *
1057
+ * @param attr - The identity attribute ('handle' or '*')
1058
+ * @returns This builder for chaining
1059
+ *
1060
+ * @example
1061
+ * ```typescript
1062
+ * builder.identity('handle');
1063
+ * ```
1064
+ */
1065
+ identity(attr) {
1066
+ const permission = IdentityPermissionSchema.parse({
1067
+ type: "identity",
1068
+ attr,
1069
+ });
1070
+ this.permissions.push(permission);
1071
+ return this;
1072
+ }
1073
+ /**
1074
+ * Add an include permission.
1075
+ *
1076
+ * @param nsid - The NSID of the scope set to include
1077
+ * @param aud - Optional audience restriction
1078
+ * @returns This builder for chaining
1079
+ *
1080
+ * @example
1081
+ * ```typescript
1082
+ * builder.include('com.example.authBasicFeatures');
1083
+ * ```
1084
+ */
1085
+ include(nsid, aud) {
1086
+ const permission = IncludePermissionSchema.parse({
1087
+ type: "include",
1088
+ nsid,
1089
+ aud,
1090
+ });
1091
+ this.permissions.push(permission);
1092
+ return this;
1093
+ }
1094
+ /**
1095
+ * Add a custom permission string directly (bypasses validation).
1096
+ *
1097
+ * Use this for testing or special cases where you need to add
1098
+ * a permission that doesn't fit the standard types.
1099
+ *
1100
+ * @param permission - The permission string
1101
+ * @returns This builder for chaining
1102
+ *
1103
+ * @example
1104
+ * ```typescript
1105
+ * builder.custom('atproto');
1106
+ * ```
1107
+ */
1108
+ custom(permission) {
1109
+ this.permissions.push(permission);
1110
+ return this;
1111
+ }
1112
+ /**
1113
+ * Add the base atproto scope.
1114
+ *
1115
+ * @returns This builder for chaining
1116
+ *
1117
+ * @example
1118
+ * ```typescript
1119
+ * builder.atproto();
1120
+ * ```
1121
+ */
1122
+ atproto() {
1123
+ this.permissions.push(ATPROTO_SCOPE);
1124
+ return this;
1125
+ }
1126
+ /**
1127
+ * Build and return the array of permission strings.
1128
+ *
1129
+ * @returns Array of permission strings
1130
+ *
1131
+ * @example
1132
+ * ```typescript
1133
+ * const permissions = builder.build();
1134
+ * ```
1135
+ */
1136
+ build() {
1137
+ return [...this.permissions];
1138
+ }
1139
+ /**
1140
+ * Clear all permissions from the builder.
1141
+ *
1142
+ * @returns This builder for chaining
1143
+ *
1144
+ * @example
1145
+ * ```typescript
1146
+ * builder.clear().accountEmail('read');
1147
+ * ```
1148
+ */
1149
+ clear() {
1150
+ this.permissions = [];
1151
+ return this;
1152
+ }
1153
+ /**
1154
+ * Get the current number of permissions.
1155
+ *
1156
+ * @returns The number of permissions
1157
+ *
1158
+ * @example
1159
+ * ```typescript
1160
+ * const count = builder.count();
1161
+ * ```
1162
+ */
1163
+ count() {
1164
+ return this.permissions.length;
1165
+ }
1166
+ }
1167
+ /**
1168
+ * Build a scope string from an array of permissions.
1169
+ *
1170
+ * This is a convenience function that joins permission strings with spaces,
1171
+ * which is the standard format for OAuth scope parameters.
1172
+ *
1173
+ * @param permissions - Array of permission strings
1174
+ * @returns Space-separated scope string
1175
+ *
1176
+ * @example
1177
+ * ```typescript
1178
+ * const permissions = ['account:email?action=read', 'repo:app.bsky.feed.post'];
1179
+ * const scope = buildScope(permissions);
1180
+ * // Returns: "account:email?action=read repo:app.bsky.feed.post"
1181
+ * ```
1182
+ */
1183
+ function buildScope(permissions) {
1184
+ return permissions.join(" ");
1185
+ }
1186
+ /**
1187
+ * Pre-built scope presets for common use cases.
1188
+ *
1189
+ * These presets provide ready-to-use permission sets for typical application scenarios.
1190
+ */
1191
+ const ScopePresets = {
1192
+ /**
1193
+ * Email access scope - allows reading user's email address.
1194
+ *
1195
+ * Includes:
1196
+ * - account:email?action=read
1197
+ *
1198
+ * @example
1199
+ * ```typescript
1200
+ * const scope = ScopePresets.EMAIL_READ;
1201
+ * // Use in OAuth flow to request email access
1202
+ * ```
1203
+ */
1204
+ EMAIL_READ: buildScope(new PermissionBuilder().accountEmail("read").build()),
1205
+ /**
1206
+ * Profile read scope - allows reading user's profile.
1207
+ *
1208
+ * Includes:
1209
+ * - repo:app.bsky.actor.profile (read-only)
1210
+ *
1211
+ * @example
1212
+ * ```typescript
1213
+ * const scope = ScopePresets.PROFILE_READ;
1214
+ * ```
1215
+ */
1216
+ PROFILE_READ: buildScope(new PermissionBuilder().repoRead("app.bsky.actor.profile").build()),
1217
+ /**
1218
+ * Profile write scope - allows updating user's profile.
1219
+ *
1220
+ * Includes:
1221
+ * - repo:app.bsky.actor.profile (create + update)
1222
+ *
1223
+ * @example
1224
+ * ```typescript
1225
+ * const scope = ScopePresets.PROFILE_WRITE;
1226
+ * ```
1227
+ */
1228
+ PROFILE_WRITE: buildScope(new PermissionBuilder().repoWrite("app.bsky.actor.profile").build()),
1229
+ /**
1230
+ * Post creation scope - allows creating and updating posts.
1231
+ *
1232
+ * Includes:
1233
+ * - repo:app.bsky.feed.post (create + update)
1234
+ *
1235
+ * @example
1236
+ * ```typescript
1237
+ * const scope = ScopePresets.POST_WRITE;
1238
+ * ```
1239
+ */
1240
+ POST_WRITE: buildScope(new PermissionBuilder().repoWrite("app.bsky.feed.post").build()),
1241
+ /**
1242
+ * Social interactions scope - allows liking, reposting, and following.
1243
+ *
1244
+ * Includes:
1245
+ * - repo:app.bsky.feed.like (create + update)
1246
+ * - repo:app.bsky.feed.repost (create + update)
1247
+ * - repo:app.bsky.graph.follow (create + update)
1248
+ *
1249
+ * @example
1250
+ * ```typescript
1251
+ * const scope = ScopePresets.SOCIAL_WRITE;
1252
+ * ```
1253
+ */
1254
+ SOCIAL_WRITE: buildScope(new PermissionBuilder()
1255
+ .repoWrite("app.bsky.feed.like")
1256
+ .repoWrite("app.bsky.feed.repost")
1257
+ .repoWrite("app.bsky.graph.follow")
1258
+ .build()),
1259
+ /**
1260
+ * Media upload scope - allows uploading images and videos.
1261
+ *
1262
+ * Includes:
1263
+ * - blob permissions for image/* and video/*
1264
+ *
1265
+ * @example
1266
+ * ```typescript
1267
+ * const scope = ScopePresets.MEDIA_UPLOAD;
1268
+ * ```
1269
+ */
1270
+ MEDIA_UPLOAD: buildScope(new PermissionBuilder().blob(["image/*", "video/*"]).build()),
1271
+ /**
1272
+ * Image upload only scope - allows uploading images.
1273
+ *
1274
+ * Includes:
1275
+ * - blob:image/*
1276
+ *
1277
+ * @example
1278
+ * ```typescript
1279
+ * const scope = ScopePresets.IMAGE_UPLOAD;
1280
+ * ```
1281
+ */
1282
+ IMAGE_UPLOAD: buildScope(new PermissionBuilder().blob("image/*").build()),
1283
+ /**
1284
+ * Posting app scope - full posting capabilities including media.
1285
+ *
1286
+ * Includes:
1287
+ * - repo:app.bsky.feed.post (create + update)
1288
+ * - repo:app.bsky.feed.like (create + update)
1289
+ * - repo:app.bsky.feed.repost (create + update)
1290
+ * - blob permissions for image/* and video/*
1291
+ *
1292
+ * @example
1293
+ * ```typescript
1294
+ * const scope = ScopePresets.POSTING_APP;
1295
+ * ```
1296
+ */
1297
+ POSTING_APP: buildScope(new PermissionBuilder()
1298
+ .repoWrite("app.bsky.feed.post")
1299
+ .repoWrite("app.bsky.feed.like")
1300
+ .repoWrite("app.bsky.feed.repost")
1301
+ .blob(["image/*", "video/*"])
1302
+ .build()),
1303
+ /**
1304
+ * Read-only app scope - allows reading all repository data.
1305
+ *
1306
+ * Includes:
1307
+ * - repo:* (read-only, no actions)
1308
+ *
1309
+ * @example
1310
+ * ```typescript
1311
+ * const scope = ScopePresets.READ_ONLY;
1312
+ * ```
1313
+ */
1314
+ READ_ONLY: buildScope(new PermissionBuilder().repoRead("*").build()),
1315
+ /**
1316
+ * Full access scope - allows all repository operations.
1317
+ *
1318
+ * Includes:
1319
+ * - repo:* (create + update + delete)
1320
+ *
1321
+ * @example
1322
+ * ```typescript
1323
+ * const scope = ScopePresets.FULL_ACCESS;
1324
+ * ```
1325
+ */
1326
+ FULL_ACCESS: buildScope(new PermissionBuilder().repoFull("*").build()),
1327
+ /**
1328
+ * Email + Profile scope - common combination for user identification.
1329
+ *
1330
+ * Includes:
1331
+ * - account:email?action=read
1332
+ * - repo:app.bsky.actor.profile (read-only)
1333
+ *
1334
+ * @example
1335
+ * ```typescript
1336
+ * const scope = ScopePresets.EMAIL_AND_PROFILE;
1337
+ * ```
1338
+ */
1339
+ EMAIL_AND_PROFILE: buildScope(new PermissionBuilder().accountEmail("read").repoRead("app.bsky.actor.profile").build()),
1340
+ /**
1341
+ * Transitional email scope (legacy).
1342
+ *
1343
+ * Uses the transitional scope format for backward compatibility.
1344
+ *
1345
+ * @example
1346
+ * ```typescript
1347
+ * const scope = ScopePresets.TRANSITION_EMAIL;
1348
+ * ```
1349
+ */
1350
+ TRANSITION_EMAIL: buildScope(new PermissionBuilder().transition("email").build()),
1351
+ /**
1352
+ * Transitional generic scope (legacy).
1353
+ *
1354
+ * Uses the transitional scope format for backward compatibility.
1355
+ *
1356
+ * @example
1357
+ * ```typescript
1358
+ * const scope = ScopePresets.TRANSITION_GENERIC;
1359
+ * ```
1360
+ */
1361
+ TRANSITION_GENERIC: buildScope(new PermissionBuilder().transition("generic").build()),
1362
+ };
1363
+ /**
1364
+ * Parse a scope string into an array of individual permissions.
1365
+ *
1366
+ * This splits a space-separated scope string into individual permission strings.
1367
+ *
1368
+ * @param scope - Space-separated scope string
1369
+ * @returns Array of permission strings
1370
+ *
1371
+ * @example
1372
+ * ```typescript
1373
+ * const scope = "account:email?action=read repo:app.bsky.feed.post";
1374
+ * const permissions = parseScope(scope);
1375
+ * // Returns: ['account:email?action=read', 'repo:app.bsky.feed.post']
1376
+ * ```
1377
+ */
1378
+ function parseScope(scope) {
1379
+ return scope.trim().split(/\s+/).filter(Boolean);
1380
+ }
1381
+ /**
1382
+ * Check if a scope string contains a specific permission.
1383
+ *
1384
+ * This function performs exact string matching. For more advanced
1385
+ * permission checking (e.g., wildcard matching), you'll need to
1386
+ * implement custom logic.
1387
+ *
1388
+ * @param scope - Space-separated scope string
1389
+ * @param permission - The permission to check for
1390
+ * @returns True if the scope contains the permission
1391
+ *
1392
+ * @example
1393
+ * ```typescript
1394
+ * const scope = "account:email?action=read repo:app.bsky.feed.post";
1395
+ * hasPermission(scope, "account:email?action=read"); // true
1396
+ * hasPermission(scope, "account:repo"); // false
1397
+ * ```
1398
+ */
1399
+ function hasPermission(scope, permission) {
1400
+ const permissions = parseScope(scope);
1401
+ return permissions.includes(permission);
1402
+ }
1403
+ /**
1404
+ * Check if a scope string contains all of the specified permissions.
1405
+ *
1406
+ * @param scope - Space-separated scope string
1407
+ * @param requiredPermissions - Array of permissions to check for
1408
+ * @returns True if the scope contains all required permissions
1409
+ *
1410
+ * @example
1411
+ * ```typescript
1412
+ * const scope = "account:email?action=read repo:app.bsky.feed.post blob:image/*";
1413
+ * hasAllPermissions(scope, ["account:email?action=read", "blob:image/*"]); // true
1414
+ * hasAllPermissions(scope, ["account:email?action=read", "account:repo"]); // false
1415
+ * ```
1416
+ */
1417
+ function hasAllPermissions(scope, requiredPermissions) {
1418
+ const permissions = parseScope(scope);
1419
+ return requiredPermissions.every((required) => permissions.includes(required));
1420
+ }
1421
+ /**
1422
+ * Check if a scope string contains any of the specified permissions.
1423
+ *
1424
+ * @param scope - Space-separated scope string
1425
+ * @param checkPermissions - Array of permissions to check for
1426
+ * @returns True if the scope contains at least one of the permissions
1427
+ *
1428
+ * @example
1429
+ * ```typescript
1430
+ * const scope = "account:email?action=read repo:app.bsky.feed.post";
1431
+ * hasAnyPermission(scope, ["account:email?action=read", "account:repo"]); // true
1432
+ * hasAnyPermission(scope, ["account:repo", "identity:handle"]); // false
1433
+ * ```
1434
+ */
1435
+ function hasAnyPermission(scope, checkPermissions) {
1436
+ const permissions = parseScope(scope);
1437
+ return checkPermissions.some((check) => permissions.includes(check));
1438
+ }
1439
+ /**
1440
+ * Merge multiple scope strings into a single scope string with deduplicated permissions.
1441
+ *
1442
+ * @param scopes - Array of scope strings to merge
1443
+ * @returns Merged scope string with unique permissions
1444
+ *
1445
+ * @example
1446
+ * ```typescript
1447
+ * const scope1 = "account:email?action=read repo:app.bsky.feed.post";
1448
+ * const scope2 = "repo:app.bsky.feed.post blob:image/*";
1449
+ * const merged = mergeScopes([scope1, scope2]);
1450
+ * // Returns: "account:email?action=read repo:app.bsky.feed.post blob:image/*"
1451
+ * ```
1452
+ */
1453
+ function mergeScopes(scopes) {
1454
+ const allPermissions = scopes.flatMap(parseScope);
1455
+ const uniquePermissions = [...new Set(allPermissions)];
1456
+ return buildScope(uniquePermissions);
1457
+ }
1458
+ /**
1459
+ * Remove specific permissions from a scope string.
1460
+ *
1461
+ * @param scope - Space-separated scope string
1462
+ * @param permissionsToRemove - Array of permissions to remove
1463
+ * @returns New scope string without the specified permissions
1464
+ *
1465
+ * @example
1466
+ * ```typescript
1467
+ * const scope = "account:email?action=read repo:app.bsky.feed.post blob:image/*";
1468
+ * const filtered = removePermissions(scope, ["blob:image/*"]);
1469
+ * // Returns: "account:email?action=read repo:app.bsky.feed.post"
1470
+ * ```
1471
+ */
1472
+ function removePermissions(scope, permissionsToRemove) {
1473
+ const permissions = parseScope(scope);
1474
+ const filtered = permissions.filter((p) => !permissionsToRemove.includes(p));
1475
+ return buildScope(filtered);
1476
+ }
1477
+ /**
1478
+ * Validate that all permissions in a scope string are well-formed.
1479
+ *
1480
+ * This checks that each permission matches expected patterns for transitional
1481
+ * or granular permissions. It does NOT validate against the full Zod schemas.
1482
+ *
1483
+ * @param scope - Space-separated scope string
1484
+ * @returns Object with isValid flag and array of invalid permissions
1485
+ *
1486
+ * @example
1487
+ * ```typescript
1488
+ * const scope = "account:email?action=read invalid:permission";
1489
+ * const result = validateScope(scope);
1490
+ * // Returns: { isValid: false, invalidPermissions: ['invalid:permission'] }
1491
+ * ```
1492
+ */
1493
+ function validateScope(scope) {
1494
+ const permissions = parseScope(scope);
1495
+ const invalidPermissions = [];
1496
+ // Pattern for valid permission prefixes
1497
+ const validPrefixes = /^(atproto|transition:|account:|repo:|blob:?|rpc:|identity:|include:)/;
1498
+ for (const permission of permissions) {
1499
+ if (!validPrefixes.test(permission)) {
1500
+ invalidPermissions.push(permission);
1501
+ }
1502
+ }
1503
+ return {
1504
+ isValid: invalidPermissions.length === 0,
1505
+ invalidPermissions,
1506
+ };
1507
+ }
1508
+
525
1509
  /**
526
1510
  * OAuth 2.0 client for AT Protocol authentication with DPoP support.
527
1511
  *
@@ -656,7 +1640,7 @@ class OAuthClient {
656
1640
  */
657
1641
  buildClientMetadata() {
658
1642
  const clientIdUrl = new URL(this.config.oauth.clientId);
659
- return {
1643
+ const metadata = {
660
1644
  client_id: this.config.oauth.clientId,
661
1645
  client_name: "ATProto SDK Client",
662
1646
  client_uri: clientIdUrl.origin,
@@ -670,6 +1654,77 @@ class OAuthClient {
670
1654
  dpop_bound_access_tokens: true,
671
1655
  jwks_uri: this.config.oauth.jwksUri,
672
1656
  };
1657
+ // Validate scope before returning metadata
1658
+ this.validateClientMetadataScope(metadata.scope);
1659
+ return metadata;
1660
+ }
1661
+ /**
1662
+ * Validates the OAuth scope in client metadata and logs warnings/suggestions.
1663
+ *
1664
+ * This method:
1665
+ * 1. Checks if the scope is well-formed using permission utilities
1666
+ * 2. Detects mixing of transitional and granular permissions
1667
+ * 3. Logs warnings for missing `atproto` scope
1668
+ * 4. Suggests migration to granular permissions for transitional scopes
1669
+ *
1670
+ * @param scope - The OAuth scope string to validate
1671
+ * @internal
1672
+ */
1673
+ validateClientMetadataScope(scope) {
1674
+ // Parse the scope into individual permissions
1675
+ const permissions = parseScope(scope);
1676
+ // Validate well-formedness
1677
+ const validation = validateScope(scope);
1678
+ if (!validation.isValid) {
1679
+ this.logger?.error("Invalid OAuth scope detected", {
1680
+ invalidPermissions: validation.invalidPermissions,
1681
+ scope,
1682
+ });
1683
+ }
1684
+ // Check for atproto scope
1685
+ const hasAtproto = permissions.includes(ATPROTO_SCOPE);
1686
+ if (!hasAtproto) {
1687
+ this.logger?.warn("OAuth scope missing 'atproto' - basic API access may be limited", {
1688
+ scope,
1689
+ suggestion: "Add 'atproto' to your scope for basic API access",
1690
+ });
1691
+ }
1692
+ // Detect transitional scopes
1693
+ const transitionalScopes = permissions.filter((p) => p.startsWith("transition:"));
1694
+ const granularScopes = permissions.filter((p) => p.startsWith("account:") ||
1695
+ p.startsWith("repo:") ||
1696
+ p.startsWith("blob") ||
1697
+ p.startsWith("rpc:") ||
1698
+ p.startsWith("identity:") ||
1699
+ p.startsWith("include:"));
1700
+ // Log info about transitional scopes
1701
+ if (transitionalScopes.length > 0) {
1702
+ this.logger?.info("Using transitional OAuth scopes (legacy)", {
1703
+ transitionalScopes,
1704
+ note: "Transitional scopes are supported but granular permissions are recommended",
1705
+ });
1706
+ // Suggest migration to granular permissions
1707
+ if (transitionalScopes.includes("transition:email")) {
1708
+ this.logger?.info("Consider migrating 'transition:email' to granular permissions", {
1709
+ suggestion: "Use: account:email?action=read",
1710
+ example: "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.EMAIL_READ",
1711
+ });
1712
+ }
1713
+ if (transitionalScopes.includes("transition:generic")) {
1714
+ this.logger?.info("Consider migrating 'transition:generic' to granular permissions", {
1715
+ suggestion: "Use specific permissions like: repo:* account:repo?action=read",
1716
+ example: "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.FULL_ACCESS",
1717
+ });
1718
+ }
1719
+ }
1720
+ // Warn if mixing transitional and granular
1721
+ if (transitionalScopes.length > 0 && granularScopes.length > 0) {
1722
+ this.logger?.warn("Mixing transitional and granular OAuth scopes", {
1723
+ transitionalScopes,
1724
+ granularScopes,
1725
+ note: "While supported, it's recommended to use either transitional or granular permissions consistently",
1726
+ });
1727
+ }
673
1728
  }
674
1729
  /**
675
1730
  * Creates a fetch handler with timeout support.
@@ -4182,9 +5237,34 @@ const OAuthConfigSchema = z.object({
4182
5237
  redirectUri: z.string().url(),
4183
5238
  /**
4184
5239
  * OAuth scopes to request, space-separated.
4185
- * Common scopes: "atproto", "transition:generic"
5240
+ *
5241
+ * Can be a string of space-separated permissions or use the permission system:
5242
+ *
5243
+ * @example Using presets
5244
+ * ```typescript
5245
+ * import { ScopePresets } from '@hypercerts-org/sdk-core';
5246
+ * scope: ScopePresets.EMAIL_AND_PROFILE
5247
+ * ```
5248
+ *
5249
+ * @example Building custom scopes
5250
+ * ```typescript
5251
+ * import { PermissionBuilder, buildScope } from '@hypercerts-org/sdk-core';
5252
+ * scope: buildScope(
5253
+ * new PermissionBuilder()
5254
+ * .accountEmail('read')
5255
+ * .repoWrite('app.bsky.feed.post')
5256
+ * .build()
5257
+ * )
5258
+ * ```
5259
+ *
5260
+ * @example Legacy scopes
5261
+ * ```typescript
5262
+ * scope: "atproto transition:generic"
5263
+ * ```
5264
+ *
5265
+ * @see https://atproto.com/specs/permission for permission details
4186
5266
  */
4187
- scope: z.string(),
5267
+ scope: z.string().min(1, "OAuth scope is required"),
4188
5268
  /**
4189
5269
  * URL to your public JWKS (JSON Web Key Set) endpoint.
4190
5270
  * Used by the authorization server to verify your client's signatures.
@@ -4476,6 +5556,91 @@ class ATProtoSDK {
4476
5556
  }
4477
5557
  return this.oauthClient.revoke(did.trim());
4478
5558
  }
5559
+ /**
5560
+ * Gets the account email address from the authenticated session.
5561
+ *
5562
+ * This method retrieves the email address associated with the user's account
5563
+ * by calling the `com.atproto.server.getSession` endpoint. The email will only
5564
+ * be returned if the appropriate OAuth scope was granted during authorization.
5565
+ *
5566
+ * Required OAuth scopes:
5567
+ * - **Granular permissions**: `account:email?action=read` or `account:email`
5568
+ * - **Transitional permissions**: `transition:email`
5569
+ *
5570
+ * @param session - An authenticated OAuth session
5571
+ * @returns A Promise resolving to email info, or `null` if permission not granted
5572
+ * @throws {@link ValidationError} if the session is invalid
5573
+ * @throws {@link NetworkError} if the API request fails
5574
+ *
5575
+ * @example Using granular permissions
5576
+ * ```typescript
5577
+ * import { ScopePresets } from '@hypercerts-org/sdk-core';
5578
+ *
5579
+ * // Authorize with email scope
5580
+ * const authUrl = await sdk.authorize("user.bsky.social", {
5581
+ * scope: ScopePresets.EMAIL_READ
5582
+ * });
5583
+ *
5584
+ * // After callback...
5585
+ * const emailInfo = await sdk.getAccountEmail(session);
5586
+ * if (emailInfo) {
5587
+ * console.log(`Email: ${emailInfo.email}`);
5588
+ * console.log(`Confirmed: ${emailInfo.emailConfirmed}`);
5589
+ * } else {
5590
+ * console.log("Email permission not granted");
5591
+ * }
5592
+ * ```
5593
+ *
5594
+ * @example Using transitional permissions (legacy)
5595
+ * ```typescript
5596
+ * // Authorize with transition:email scope
5597
+ * const authUrl = await sdk.authorize("user.bsky.social", {
5598
+ * scope: "atproto transition:email"
5599
+ * });
5600
+ *
5601
+ * // After callback...
5602
+ * const emailInfo = await sdk.getAccountEmail(session);
5603
+ * ```
5604
+ */
5605
+ async getAccountEmail(session) {
5606
+ if (!session) {
5607
+ throw new ValidationError("Session is required");
5608
+ }
5609
+ try {
5610
+ // Determine PDS URL from session or config
5611
+ const pdsUrl = this.config.servers?.pds;
5612
+ if (!pdsUrl) {
5613
+ throw new ValidationError("PDS server URL not configured");
5614
+ }
5615
+ // Call com.atproto.server.getSession endpoint using session's fetchHandler
5616
+ // which automatically includes proper authorization with DPoP
5617
+ const response = await session.fetchHandler("/xrpc/com.atproto.server.getSession", {
5618
+ method: "GET",
5619
+ headers: {
5620
+ "Content-Type": "application/json",
5621
+ },
5622
+ });
5623
+ if (!response.ok) {
5624
+ throw new NetworkError(`Failed to get session info: ${response.status} ${response.statusText}`);
5625
+ }
5626
+ const data = (await response.json());
5627
+ // Return null if email not present (permission not granted)
5628
+ if (!data.email) {
5629
+ return null;
5630
+ }
5631
+ return {
5632
+ email: data.email,
5633
+ emailConfirmed: data.emailConfirmed ?? false,
5634
+ };
5635
+ }
5636
+ catch (error) {
5637
+ this.logger?.error("Failed to get account email", { error });
5638
+ if (error instanceof ValidationError || error instanceof NetworkError) {
5639
+ throw error;
5640
+ }
5641
+ throw new NetworkError(`Failed to get account email: ${error instanceof Error ? error.message : String(error)}`, error);
5642
+ }
5643
+ }
4479
5644
  /**
4480
5645
  * Creates a repository instance for data operations.
4481
5646
  *
@@ -4726,5 +5891,5 @@ const CollaboratorSchema = z.object({
4726
5891
  revokedAt: z.string().optional(),
4727
5892
  });
4728
5893
 
4729
- export { ATProtoSDK, ATProtoSDKConfigSchema, ATProtoSDKError, AuthenticationError, CollaboratorPermissionsSchema, CollaboratorSchema, ConfigurableAgent, InMemorySessionStore, InMemoryStateStore, LexiconRegistry, NetworkError, OAuthConfigSchema, OrganizationSchema, Repository, SDSRequiredError, ServerConfigSchema, SessionExpiredError, TimeoutConfigSchema, ValidationError, createATProtoSDK };
5894
+ export { ATPROTO_SCOPE, ATProtoSDK, ATProtoSDKConfigSchema, ATProtoSDKError, AccountActionSchema, AccountAttrSchema, AccountPermissionSchema, AuthenticationError, BlobPermissionSchema, CollaboratorPermissionsSchema, CollaboratorSchema, ConfigurableAgent, IdentityAttrSchema, IdentityPermissionSchema, InMemorySessionStore, InMemoryStateStore, IncludePermissionSchema, LexiconRegistry, MimeTypeSchema, NetworkError, NsidSchema, OAuthConfigSchema, OrganizationSchema, PermissionBuilder, PermissionSchema, RepoActionSchema, RepoPermissionSchema, Repository, RpcPermissionSchema, SDSRequiredError, ScopePresets, ServerConfigSchema, SessionExpiredError, TRANSITION_SCOPES, TimeoutConfigSchema, TransitionScopeSchema, ValidationError, buildScope, createATProtoSDK, hasAllPermissions, hasAnyPermission, hasPermission, mergeScopes, parseScope, removePermissions, validateScope };
4730
5895
  //# sourceMappingURL=index.mjs.map