@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/README.md +46 -2
- package/dist/index.cjs +1195 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1183 -7
- package/dist/index.mjs +1170 -5
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.ts +26 -1
- package/dist/types.cjs +27 -2
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.ts +89 -5
- package/dist/types.mjs +27 -2
- package/dist/types.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
*
|
|
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
|