@hypercerts-org/sdk-core 0.9.0-beta.0 → 0.10.0-beta.1
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/CHANGELOG.md +273 -0
- package/README.md +132 -105
- package/dist/index.cjs +1756 -664
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1341 -276
- package/dist/index.mjs +1700 -647
- package/dist/index.mjs.map +1 -1
- package/dist/lexicons.cjs +103 -374
- package/dist/lexicons.cjs.map +1 -1
- package/dist/lexicons.d.ts +90 -188
- package/dist/lexicons.mjs +103 -367
- package/dist/lexicons.mjs.map +1 -1
- package/dist/testing.d.ts +26 -1
- package/dist/types.cjs +33 -2
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.ts +113 -42
- package/dist/types.mjs +29 -2
- package/dist/types.mjs.map +1 -1
- package/package.json +10 -9
package/dist/index.cjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var oauthClientNode = require('@atproto/oauth-client-node');
|
|
4
|
-
var
|
|
5
|
-
var lexicon = require('@hypercerts-org/lexicon');
|
|
4
|
+
var zod = require('zod');
|
|
6
5
|
var api = require('@atproto/api');
|
|
7
6
|
var eventemitter3 = require('eventemitter3');
|
|
8
|
-
var
|
|
7
|
+
var lexicon = require('@hypercerts-org/lexicon');
|
|
8
|
+
require('@atproto/lexicon');
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Base error class for all SDK errors.
|
|
@@ -524,162 +524,1217 @@ class InMemoryStateStore {
|
|
|
524
524
|
}
|
|
525
525
|
|
|
526
526
|
/**
|
|
527
|
-
* OAuth
|
|
527
|
+
* OAuth Scopes and Granular Permissions
|
|
528
528
|
*
|
|
529
|
-
* This
|
|
530
|
-
*
|
|
529
|
+
* This module provides type-safe, Zod-validated OAuth scope and permission management
|
|
530
|
+
* for the ATProto SDK. It supports both legacy transitional scopes and the new
|
|
531
|
+
* granular permissions model.
|
|
531
532
|
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
* - **PKCE (Proof Key for Code Exchange)**: Protects against authorization code interception
|
|
535
|
-
* - **Automatic Token Refresh**: Transparently refreshes expired access tokens
|
|
536
|
-
* - **Session Persistence**: Stores sessions in configurable storage backends
|
|
533
|
+
* @see https://atproto.com/specs/oauth
|
|
534
|
+
* @see https://atproto.com/specs/permission
|
|
537
535
|
*
|
|
538
|
-
* @
|
|
539
|
-
|
|
540
|
-
|
|
536
|
+
* @module auth/permissions
|
|
537
|
+
*/
|
|
538
|
+
/**
|
|
539
|
+
* Base OAuth scope - required for all sessions
|
|
541
540
|
*
|
|
542
|
-
*
|
|
543
|
-
|
|
544
|
-
|
|
541
|
+
* @constant
|
|
542
|
+
*/
|
|
543
|
+
const ATPROTO_SCOPE = "atproto";
|
|
544
|
+
/**
|
|
545
|
+
* Transitional OAuth scopes for legacy compatibility.
|
|
545
546
|
*
|
|
546
|
-
*
|
|
547
|
+
* These scopes provide broad access and are maintained for backwards compatibility.
|
|
548
|
+
* New applications should use granular permissions instead.
|
|
549
|
+
*
|
|
550
|
+
* @deprecated Use granular permissions (account:*, repo:*, etc.) for better control
|
|
551
|
+
* @constant
|
|
552
|
+
*/
|
|
553
|
+
const TRANSITION_SCOPES = {
|
|
554
|
+
/** Broad PDS permissions including record creation, blob uploads, and preferences */
|
|
555
|
+
GENERIC: "transition:generic",
|
|
556
|
+
/** Direct messages access (requires transition:generic) */
|
|
557
|
+
CHAT: "transition:chat.bsky",
|
|
558
|
+
/** Email address and confirmation status */
|
|
559
|
+
EMAIL: "transition:email",
|
|
560
|
+
};
|
|
561
|
+
/**
|
|
562
|
+
* Zod schema for transitional scopes.
|
|
563
|
+
*
|
|
564
|
+
* Validates that a scope string is one of the known transitional scopes.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
547
567
|
* ```typescript
|
|
548
|
-
*
|
|
568
|
+
* TransitionScopeSchema.parse('transition:email'); // Valid
|
|
569
|
+
* TransitionScopeSchema.parse('invalid'); // Throws ZodError
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
572
|
+
const TransitionScopeSchema = zod.z
|
|
573
|
+
.enum(["transition:generic", "transition:chat.bsky", "transition:email"])
|
|
574
|
+
.describe("Legacy transitional OAuth scopes");
|
|
575
|
+
/**
|
|
576
|
+
* Zod schema for account permission attributes.
|
|
549
577
|
*
|
|
550
|
-
*
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
*
|
|
555
|
-
* jwksUri: "https://my-app.com/.well-known/jwks.json",
|
|
556
|
-
* jwkPrivate: process.env.JWK_PRIVATE_KEY!,
|
|
557
|
-
* },
|
|
558
|
-
* servers: { pds: "https://bsky.social" },
|
|
559
|
-
* });
|
|
578
|
+
* Account attributes specify what aspect of the account is being accessed.
|
|
579
|
+
*/
|
|
580
|
+
const AccountAttrSchema = zod.z.enum(["email", "repo"]);
|
|
581
|
+
/**
|
|
582
|
+
* Zod schema for account actions.
|
|
560
583
|
*
|
|
561
|
-
*
|
|
562
|
-
|
|
584
|
+
* Account actions specify the level of access (read-only or management).
|
|
585
|
+
*/
|
|
586
|
+
const AccountActionSchema = zod.z.enum(["read", "manage"]);
|
|
587
|
+
/**
|
|
588
|
+
* Zod schema for repository actions.
|
|
563
589
|
*
|
|
564
|
-
*
|
|
565
|
-
|
|
590
|
+
* Repository actions specify what operations can be performed on records.
|
|
591
|
+
*/
|
|
592
|
+
const RepoActionSchema = zod.z.enum(["create", "update", "delete"]);
|
|
593
|
+
/**
|
|
594
|
+
* Zod schema for identity permission attributes.
|
|
595
|
+
*
|
|
596
|
+
* Identity attributes specify what identity information can be managed.
|
|
597
|
+
*/
|
|
598
|
+
const IdentityAttrSchema = zod.z.enum(["handle", "*"]);
|
|
599
|
+
/**
|
|
600
|
+
* Zod schema for MIME type patterns.
|
|
601
|
+
*
|
|
602
|
+
* Validates MIME type strings like "image/*" or "video/mp4".
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* ```typescript
|
|
606
|
+
* MimeTypeSchema.parse('image/*'); // Valid
|
|
607
|
+
* MimeTypeSchema.parse('video/mp4'); // Valid
|
|
608
|
+
* MimeTypeSchema.parse('invalid'); // Throws ZodError
|
|
566
609
|
* ```
|
|
610
|
+
*/
|
|
611
|
+
const MimeTypeSchema = zod.z
|
|
612
|
+
.string()
|
|
613
|
+
.regex(/^[a-z]+\/[a-z0-9*+-]+$/i, 'Invalid MIME type pattern. Expected format: type/subtype (e.g., "image/*" or "video/mp4")');
|
|
614
|
+
/**
|
|
615
|
+
* Zod schema for NSID (Namespaced Identifier).
|
|
567
616
|
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
617
|
+
* NSIDs are reverse-DNS style identifiers used throughout ATProto
|
|
618
|
+
* (e.g., "app.bsky.feed.post" or "com.example.myrecord").
|
|
619
|
+
*
|
|
620
|
+
* @see https://atproto.com/specs/nsid
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* ```typescript
|
|
624
|
+
* NsidSchema.parse('app.bsky.feed.post'); // Valid
|
|
625
|
+
* NsidSchema.parse('com.example.myrecord'); // Valid
|
|
626
|
+
* NsidSchema.parse('InvalidNSID'); // Throws ZodError
|
|
627
|
+
* ```
|
|
570
628
|
*/
|
|
571
|
-
|
|
629
|
+
const NsidSchema = zod.z
|
|
630
|
+
.string()
|
|
631
|
+
.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")');
|
|
632
|
+
/**
|
|
633
|
+
* Zod schema for account permission.
|
|
634
|
+
*
|
|
635
|
+
* Account permissions control access to account-level information like email
|
|
636
|
+
* and repository management.
|
|
637
|
+
*
|
|
638
|
+
* @example Without action (read-only)
|
|
639
|
+
* ```typescript
|
|
640
|
+
* const input = { type: 'account', attr: 'email' };
|
|
641
|
+
* AccountPermissionSchema.parse(input); // Returns: "account:email"
|
|
642
|
+
* ```
|
|
643
|
+
*
|
|
644
|
+
* @example With action
|
|
645
|
+
* ```typescript
|
|
646
|
+
* const input = { type: 'account', attr: 'email', action: 'manage' };
|
|
647
|
+
* AccountPermissionSchema.parse(input); // Returns: "account:email?action=manage"
|
|
648
|
+
* ```
|
|
649
|
+
*/
|
|
650
|
+
const AccountPermissionSchema = zod.z
|
|
651
|
+
.object({
|
|
652
|
+
type: zod.z.literal("account"),
|
|
653
|
+
attr: AccountAttrSchema,
|
|
654
|
+
action: AccountActionSchema.optional(),
|
|
655
|
+
})
|
|
656
|
+
.transform(({ attr, action }) => {
|
|
657
|
+
let perm = `account:${attr}`;
|
|
658
|
+
if (action) {
|
|
659
|
+
perm += `?action=${action}`;
|
|
660
|
+
}
|
|
661
|
+
return perm;
|
|
662
|
+
});
|
|
663
|
+
/**
|
|
664
|
+
* Zod schema for repository permission.
|
|
665
|
+
*
|
|
666
|
+
* Repository permissions control write access to records by collection type.
|
|
667
|
+
* The collection must be a valid NSID or wildcard (*).
|
|
668
|
+
*
|
|
669
|
+
* @example Without actions (all actions allowed)
|
|
670
|
+
* ```typescript
|
|
671
|
+
* const input = { type: 'repo', collection: 'app.bsky.feed.post' };
|
|
672
|
+
* RepoPermissionSchema.parse(input); // Returns: "repo:app.bsky.feed.post"
|
|
673
|
+
* ```
|
|
674
|
+
*
|
|
675
|
+
* @example With specific actions
|
|
676
|
+
* ```typescript
|
|
677
|
+
* const input = {
|
|
678
|
+
* type: 'repo',
|
|
679
|
+
* collection: 'app.bsky.feed.post',
|
|
680
|
+
* actions: ['create', 'update']
|
|
681
|
+
* };
|
|
682
|
+
* RepoPermissionSchema.parse(input); // Returns: "repo:app.bsky.feed.post?action=create&action=update"
|
|
683
|
+
* ```
|
|
684
|
+
*
|
|
685
|
+
* @example With wildcard collection
|
|
686
|
+
* ```typescript
|
|
687
|
+
* const input = { type: 'repo', collection: '*', actions: ['delete'] };
|
|
688
|
+
* RepoPermissionSchema.parse(input); // Returns: "repo:*?action=delete"
|
|
689
|
+
* ```
|
|
690
|
+
*/
|
|
691
|
+
const RepoPermissionSchema = zod.z
|
|
692
|
+
.object({
|
|
693
|
+
type: zod.z.literal("repo"),
|
|
694
|
+
collection: NsidSchema.or(zod.z.literal("*")),
|
|
695
|
+
actions: zod.z.array(RepoActionSchema).optional(),
|
|
696
|
+
})
|
|
697
|
+
.transform(({ collection, actions }) => {
|
|
698
|
+
let perm = `repo:${collection}`;
|
|
699
|
+
if (actions && actions.length > 0) {
|
|
700
|
+
const params = actions.map((a) => `action=${a}`).join("&");
|
|
701
|
+
perm += `?${params}`;
|
|
702
|
+
}
|
|
703
|
+
return perm;
|
|
704
|
+
});
|
|
705
|
+
/**
|
|
706
|
+
* Zod schema for blob permission.
|
|
707
|
+
*
|
|
708
|
+
* Blob permissions control media file uploads constrained by MIME type patterns.
|
|
709
|
+
*
|
|
710
|
+
* @example Single MIME type
|
|
711
|
+
* ```typescript
|
|
712
|
+
* const input = { type: 'blob', mimeTypes: ['image/*'] };
|
|
713
|
+
* BlobPermissionSchema.parse(input); // Returns: "blob:image/*"
|
|
714
|
+
* ```
|
|
715
|
+
*
|
|
716
|
+
* @example Multiple MIME types
|
|
717
|
+
* ```typescript
|
|
718
|
+
* const input = { type: 'blob', mimeTypes: ['image/*', 'video/*'] };
|
|
719
|
+
* BlobPermissionSchema.parse(input); // Returns: "blob?accept=image/*&accept=video/*"
|
|
720
|
+
* ```
|
|
721
|
+
*/
|
|
722
|
+
const BlobPermissionSchema = zod.z
|
|
723
|
+
.object({
|
|
724
|
+
type: zod.z.literal("blob"),
|
|
725
|
+
mimeTypes: zod.z.array(MimeTypeSchema).min(1, "At least one MIME type required"),
|
|
726
|
+
})
|
|
727
|
+
.transform(({ mimeTypes }) => {
|
|
728
|
+
if (mimeTypes.length === 1) {
|
|
729
|
+
return `blob:${mimeTypes[0]}`;
|
|
730
|
+
}
|
|
731
|
+
const accepts = mimeTypes.map((t) => `accept=${encodeURIComponent(t)}`).join("&");
|
|
732
|
+
return `blob?${accepts}`;
|
|
733
|
+
});
|
|
734
|
+
/**
|
|
735
|
+
* Zod schema for RPC permission.
|
|
736
|
+
*
|
|
737
|
+
* RPC permissions control authenticated API calls to remote services.
|
|
738
|
+
* At least one of lexicon or aud must be restricted (both cannot be wildcards).
|
|
739
|
+
*
|
|
740
|
+
* @example Specific lexicon with wildcard audience
|
|
741
|
+
* ```typescript
|
|
742
|
+
* const input = {
|
|
743
|
+
* type: 'rpc',
|
|
744
|
+
* lexicon: 'com.atproto.repo.createRecord',
|
|
745
|
+
* aud: '*'
|
|
746
|
+
* };
|
|
747
|
+
* RpcPermissionSchema.parse(input);
|
|
748
|
+
* // Returns: "rpc:com.atproto.repo.createRecord?aud=*"
|
|
749
|
+
* ```
|
|
750
|
+
*
|
|
751
|
+
* @example With specific audience
|
|
752
|
+
* ```typescript
|
|
753
|
+
* const input = {
|
|
754
|
+
* type: 'rpc',
|
|
755
|
+
* lexicon: 'com.atproto.repo.createRecord',
|
|
756
|
+
* aud: 'did:web:api.example.com',
|
|
757
|
+
* inheritAud: true
|
|
758
|
+
* };
|
|
759
|
+
* RpcPermissionSchema.parse(input);
|
|
760
|
+
* // Returns: "rpc:com.atproto.repo.createRecord?aud=did%3Aweb%3Aapi.example.com&inheritAud=true"
|
|
761
|
+
* ```
|
|
762
|
+
*/
|
|
763
|
+
const RpcPermissionSchema = zod.z
|
|
764
|
+
.object({
|
|
765
|
+
type: zod.z.literal("rpc"),
|
|
766
|
+
lexicon: NsidSchema.or(zod.z.literal("*")),
|
|
767
|
+
aud: zod.z.string().min(1, "Audience is required"),
|
|
768
|
+
inheritAud: zod.z.boolean().optional(),
|
|
769
|
+
})
|
|
770
|
+
.refine(({ lexicon, aud }) => lexicon !== "*" || aud !== "*", "At least one of lexicon or aud must be restricted (wildcards cannot both be used)")
|
|
771
|
+
.transform(({ lexicon, aud, inheritAud }) => {
|
|
772
|
+
let perm = `rpc:${lexicon}?aud=${encodeURIComponent(aud)}`;
|
|
773
|
+
if (inheritAud) {
|
|
774
|
+
perm += "&inheritAud=true";
|
|
775
|
+
}
|
|
776
|
+
return perm;
|
|
777
|
+
});
|
|
778
|
+
/**
|
|
779
|
+
* Zod schema for identity permission.
|
|
780
|
+
*
|
|
781
|
+
* Identity permissions control access to DID documents and handles.
|
|
782
|
+
*
|
|
783
|
+
* @example Handle management
|
|
784
|
+
* ```typescript
|
|
785
|
+
* const input = { type: 'identity', attr: 'handle' };
|
|
786
|
+
* IdentityPermissionSchema.parse(input); // Returns: "identity:handle"
|
|
787
|
+
* ```
|
|
788
|
+
*
|
|
789
|
+
* @example All identity attributes
|
|
790
|
+
* ```typescript
|
|
791
|
+
* const input = { type: 'identity', attr: '*' };
|
|
792
|
+
* IdentityPermissionSchema.parse(input); // Returns: "identity:*"
|
|
793
|
+
* ```
|
|
794
|
+
*/
|
|
795
|
+
const IdentityPermissionSchema = zod.z
|
|
796
|
+
.object({
|
|
797
|
+
type: zod.z.literal("identity"),
|
|
798
|
+
attr: IdentityAttrSchema,
|
|
799
|
+
})
|
|
800
|
+
.transform(({ attr }) => `identity:${attr}`);
|
|
801
|
+
/**
|
|
802
|
+
* Zod schema for permission set inclusion.
|
|
803
|
+
*
|
|
804
|
+
* Include permissions reference permission sets bundled under a single NSID.
|
|
805
|
+
*
|
|
806
|
+
* @example Without audience
|
|
807
|
+
* ```typescript
|
|
808
|
+
* const input = { type: 'include', nsid: 'com.example.authBasicFeatures' };
|
|
809
|
+
* IncludePermissionSchema.parse(input);
|
|
810
|
+
* // Returns: "include:com.example.authBasicFeatures"
|
|
811
|
+
* ```
|
|
812
|
+
*
|
|
813
|
+
* @example With audience
|
|
814
|
+
* ```typescript
|
|
815
|
+
* const input = {
|
|
816
|
+
* type: 'include',
|
|
817
|
+
* nsid: 'com.example.authBasicFeatures',
|
|
818
|
+
* aud: 'did:web:api.example.com'
|
|
819
|
+
* };
|
|
820
|
+
* IncludePermissionSchema.parse(input);
|
|
821
|
+
* // Returns: "include:com.example.authBasicFeatures?aud=did%3Aweb%3Aapi.example.com"
|
|
822
|
+
* ```
|
|
823
|
+
*/
|
|
824
|
+
const IncludePermissionSchema = zod.z
|
|
825
|
+
.object({
|
|
826
|
+
type: zod.z.literal("include"),
|
|
827
|
+
nsid: NsidSchema,
|
|
828
|
+
aud: zod.z.string().optional(),
|
|
829
|
+
})
|
|
830
|
+
.transform(({ nsid, aud }) => {
|
|
831
|
+
let perm = `include:${nsid}`;
|
|
832
|
+
if (aud) {
|
|
833
|
+
perm += `?aud=${encodeURIComponent(aud)}`;
|
|
834
|
+
}
|
|
835
|
+
return perm;
|
|
836
|
+
});
|
|
837
|
+
/**
|
|
838
|
+
* Union schema for all permission types.
|
|
839
|
+
*
|
|
840
|
+
* This schema accepts any of the supported permission types and validates
|
|
841
|
+
* them according to their specific rules.
|
|
842
|
+
*/
|
|
843
|
+
const PermissionSchema = zod.z.union([
|
|
844
|
+
AccountPermissionSchema,
|
|
845
|
+
RepoPermissionSchema,
|
|
846
|
+
BlobPermissionSchema,
|
|
847
|
+
RpcPermissionSchema,
|
|
848
|
+
IdentityPermissionSchema,
|
|
849
|
+
IncludePermissionSchema,
|
|
850
|
+
]);
|
|
851
|
+
/**
|
|
852
|
+
* Fluent builder for constructing OAuth permission arrays.
|
|
853
|
+
*
|
|
854
|
+
* This class provides a convenient, type-safe way to build arrays of permissions
|
|
855
|
+
* using method chaining.
|
|
856
|
+
*
|
|
857
|
+
* @example Basic usage
|
|
858
|
+
* ```typescript
|
|
859
|
+
* const builder = new PermissionBuilder()
|
|
860
|
+
* .accountEmail('read')
|
|
861
|
+
* .repoWrite('app.bsky.feed.post')
|
|
862
|
+
* .blob(['image/*', 'video/*']);
|
|
863
|
+
*
|
|
864
|
+
* const permissions = builder.build();
|
|
865
|
+
* // Returns: ['account:email?action=read', 'repo:app.bsky.feed.post?action=create&action=update', 'blob:image/*,video/*']
|
|
866
|
+
* ```
|
|
867
|
+
*
|
|
868
|
+
* @example With transitional scopes
|
|
869
|
+
* ```typescript
|
|
870
|
+
* const builder = new PermissionBuilder()
|
|
871
|
+
* .transition('email')
|
|
872
|
+
* .transition('generic');
|
|
873
|
+
*
|
|
874
|
+
* const scopes = builder.build();
|
|
875
|
+
* // Returns: ['transition:email', 'transition:generic']
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
class PermissionBuilder {
|
|
879
|
+
constructor() {
|
|
880
|
+
this.permissions = [];
|
|
881
|
+
}
|
|
572
882
|
/**
|
|
573
|
-
*
|
|
883
|
+
* Add a transitional scope.
|
|
574
884
|
*
|
|
575
|
-
* @param
|
|
576
|
-
* @
|
|
885
|
+
* @param scope - The transitional scope name ('email', 'generic', or 'chat.bsky')
|
|
886
|
+
* @returns This builder for chaining
|
|
577
887
|
*
|
|
578
|
-
* @
|
|
579
|
-
*
|
|
580
|
-
*
|
|
888
|
+
* @example
|
|
889
|
+
* ```typescript
|
|
890
|
+
* builder.transition('email').transition('generic');
|
|
891
|
+
* ```
|
|
581
892
|
*/
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
this.
|
|
586
|
-
this
|
|
587
|
-
// Validate JWK format synchronously (before async initialization)
|
|
588
|
-
try {
|
|
589
|
-
JSON.parse(config.oauth.jwkPrivate);
|
|
590
|
-
}
|
|
591
|
-
catch (error) {
|
|
592
|
-
throw new AuthenticationError("Failed to parse JWK private key. Ensure it is valid JSON.", error);
|
|
593
|
-
}
|
|
594
|
-
// Initialize client lazily (async initialization)
|
|
595
|
-
this.clientPromise = this.initializeClient();
|
|
893
|
+
transition(scope) {
|
|
894
|
+
const fullScope = `transition:${scope}`;
|
|
895
|
+
const validated = TransitionScopeSchema.parse(fullScope);
|
|
896
|
+
this.permissions.push(validated);
|
|
897
|
+
return this;
|
|
596
898
|
}
|
|
597
899
|
/**
|
|
598
|
-
*
|
|
900
|
+
* Add an account permission.
|
|
599
901
|
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* 3. Creates the underlying NodeOAuthClient
|
|
902
|
+
* @param attr - The account attribute ('email' or 'repo')
|
|
903
|
+
* @param action - Optional action ('read' or 'manage')
|
|
904
|
+
* @returns This builder for chaining
|
|
604
905
|
*
|
|
605
|
-
* @
|
|
606
|
-
*
|
|
906
|
+
* @example
|
|
907
|
+
* ```typescript
|
|
908
|
+
* builder.accountEmail('read').accountRepo('manage');
|
|
909
|
+
* ```
|
|
607
910
|
*/
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const privateJWK = JSON.parse(this.config.oauth.jwkPrivate);
|
|
614
|
-
// Build client metadata
|
|
615
|
-
const clientMetadata = this.buildClientMetadata();
|
|
616
|
-
// Convert JWK keys to JoseKey instances (await here)
|
|
617
|
-
const keyset = await Promise.all(privateJWK.keys.map((key) => oauthClientNode.JoseKey.fromImportable(key, key.kid)));
|
|
618
|
-
// Create fetch with timeout
|
|
619
|
-
const fetchWithTimeout = this.createFetchWithTimeout(this.config.timeouts?.pdsMetadata ?? 30000);
|
|
620
|
-
// Use provided stores or fall back to in-memory implementations
|
|
621
|
-
const stateStore = this.config.storage?.stateStore ?? new InMemoryStateStore();
|
|
622
|
-
const sessionStore = this.config.storage?.sessionStore ?? new InMemorySessionStore();
|
|
623
|
-
this.client = new oauthClientNode.NodeOAuthClient({
|
|
624
|
-
clientMetadata,
|
|
625
|
-
keyset,
|
|
626
|
-
stateStore: this.createStateStoreAdapter(stateStore),
|
|
627
|
-
sessionStore: this.createSessionStoreAdapter(sessionStore),
|
|
628
|
-
handleResolver: this.config.servers?.pds,
|
|
629
|
-
fetch: this.config.fetch ?? fetchWithTimeout,
|
|
911
|
+
account(attr, action) {
|
|
912
|
+
const permission = AccountPermissionSchema.parse({
|
|
913
|
+
type: "account",
|
|
914
|
+
attr,
|
|
915
|
+
action,
|
|
630
916
|
});
|
|
631
|
-
|
|
917
|
+
this.permissions.push(permission);
|
|
918
|
+
return this;
|
|
632
919
|
}
|
|
633
920
|
/**
|
|
634
|
-
*
|
|
921
|
+
* Convenience method for account:email permission.
|
|
635
922
|
*
|
|
636
|
-
* @
|
|
637
|
-
* @
|
|
923
|
+
* @param action - Optional action ('read' or 'manage')
|
|
924
|
+
* @returns This builder for chaining
|
|
925
|
+
*
|
|
926
|
+
* @example
|
|
927
|
+
* ```typescript
|
|
928
|
+
* builder.accountEmail('read');
|
|
929
|
+
* ```
|
|
638
930
|
*/
|
|
639
|
-
|
|
640
|
-
return this.
|
|
931
|
+
accountEmail(action) {
|
|
932
|
+
return this.account("email", action);
|
|
641
933
|
}
|
|
642
934
|
/**
|
|
643
|
-
*
|
|
935
|
+
* Convenience method for account:repo permission.
|
|
644
936
|
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* @returns OAuth client metadata object
|
|
649
|
-
* @internal
|
|
937
|
+
* @param action - Optional action ('read' or 'manage')
|
|
938
|
+
* @returns This builder for chaining
|
|
650
939
|
*
|
|
651
|
-
* @
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
* - `dpop_bound_access_tokens`: Always true for AT Protocol
|
|
656
|
-
* - `token_endpoint_auth_method`: Uses private_key_jwt for security
|
|
940
|
+
* @example
|
|
941
|
+
* ```typescript
|
|
942
|
+
* builder.accountRepo('manage');
|
|
943
|
+
* ```
|
|
657
944
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return {
|
|
661
|
-
client_id: this.config.oauth.clientId,
|
|
662
|
-
client_name: "ATProto SDK Client",
|
|
663
|
-
client_uri: clientIdUrl.origin,
|
|
664
|
-
redirect_uris: [this.config.oauth.redirectUri],
|
|
665
|
-
scope: this.config.oauth.scope,
|
|
666
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
667
|
-
response_types: ["code"],
|
|
668
|
-
application_type: "web",
|
|
669
|
-
token_endpoint_auth_method: "private_key_jwt",
|
|
670
|
-
token_endpoint_auth_signing_alg: "ES256",
|
|
671
|
-
dpop_bound_access_tokens: true,
|
|
672
|
-
jwks_uri: this.config.oauth.jwksUri,
|
|
673
|
-
};
|
|
945
|
+
accountRepo(action) {
|
|
946
|
+
return this.account("repo", action);
|
|
674
947
|
}
|
|
675
948
|
/**
|
|
676
|
-
*
|
|
949
|
+
* Add a repository permission.
|
|
677
950
|
*
|
|
678
|
-
* @param
|
|
679
|
-
* @
|
|
680
|
-
* @
|
|
681
|
-
|
|
682
|
-
|
|
951
|
+
* @param collection - The NSID of the collection or '*' for all
|
|
952
|
+
* @param actions - Optional array of actions ('create', 'update', 'delete')
|
|
953
|
+
* @returns This builder for chaining
|
|
954
|
+
*
|
|
955
|
+
* @example
|
|
956
|
+
* ```typescript
|
|
957
|
+
* builder.repo('app.bsky.feed.post', ['create', 'update']);
|
|
958
|
+
* ```
|
|
959
|
+
*/
|
|
960
|
+
repo(collection, actions) {
|
|
961
|
+
const permission = RepoPermissionSchema.parse({
|
|
962
|
+
type: "repo",
|
|
963
|
+
collection,
|
|
964
|
+
actions,
|
|
965
|
+
});
|
|
966
|
+
this.permissions.push(permission);
|
|
967
|
+
return this;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Convenience method for repository write permissions (create + update).
|
|
971
|
+
*
|
|
972
|
+
* @param collection - The NSID of the collection or '*' for all
|
|
973
|
+
* @returns This builder for chaining
|
|
974
|
+
*
|
|
975
|
+
* @example
|
|
976
|
+
* ```typescript
|
|
977
|
+
* builder.repoWrite('app.bsky.feed.post');
|
|
978
|
+
* ```
|
|
979
|
+
*/
|
|
980
|
+
repoWrite(collection) {
|
|
981
|
+
return this.repo(collection, ["create", "update"]);
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Convenience method for repository read permission (no actions).
|
|
985
|
+
*
|
|
986
|
+
* @param collection - The NSID of the collection or '*' for all
|
|
987
|
+
* @returns This builder for chaining
|
|
988
|
+
*
|
|
989
|
+
* @example
|
|
990
|
+
* ```typescript
|
|
991
|
+
* builder.repoRead('app.bsky.feed.post');
|
|
992
|
+
* ```
|
|
993
|
+
*/
|
|
994
|
+
repoRead(collection) {
|
|
995
|
+
return this.repo(collection, []);
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Convenience method for full repository permissions (create + update + delete).
|
|
999
|
+
*
|
|
1000
|
+
* @param collection - The NSID of the collection or '*' for all
|
|
1001
|
+
* @returns This builder for chaining
|
|
1002
|
+
*
|
|
1003
|
+
* @example
|
|
1004
|
+
* ```typescript
|
|
1005
|
+
* builder.repoFull('app.bsky.feed.post');
|
|
1006
|
+
* ```
|
|
1007
|
+
*/
|
|
1008
|
+
repoFull(collection) {
|
|
1009
|
+
return this.repo(collection, ["create", "update", "delete"]);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Add a blob permission.
|
|
1013
|
+
*
|
|
1014
|
+
* @param mimeTypes - Array of MIME types or a single MIME type
|
|
1015
|
+
* @returns This builder for chaining
|
|
1016
|
+
*
|
|
1017
|
+
* @example
|
|
1018
|
+
* ```typescript
|
|
1019
|
+
* builder.blob(['image/*', 'video/*']);
|
|
1020
|
+
* builder.blob('image/*');
|
|
1021
|
+
* ```
|
|
1022
|
+
*/
|
|
1023
|
+
blob(mimeTypes) {
|
|
1024
|
+
const types = Array.isArray(mimeTypes) ? mimeTypes : [mimeTypes];
|
|
1025
|
+
const permission = BlobPermissionSchema.parse({
|
|
1026
|
+
type: "blob",
|
|
1027
|
+
mimeTypes: types,
|
|
1028
|
+
});
|
|
1029
|
+
this.permissions.push(permission);
|
|
1030
|
+
return this;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Add an RPC permission.
|
|
1034
|
+
*
|
|
1035
|
+
* @param lexicon - The NSID of the lexicon or '*' for all
|
|
1036
|
+
* @param aud - The audience (DID or URL)
|
|
1037
|
+
* @param inheritAud - Whether to inherit audience
|
|
1038
|
+
* @returns This builder for chaining
|
|
1039
|
+
*
|
|
1040
|
+
* @example
|
|
1041
|
+
* ```typescript
|
|
1042
|
+
* builder.rpc('com.atproto.repo.createRecord', 'did:web:api.example.com');
|
|
1043
|
+
* ```
|
|
1044
|
+
*/
|
|
1045
|
+
rpc(lexicon, aud, inheritAud) {
|
|
1046
|
+
const permission = RpcPermissionSchema.parse({
|
|
1047
|
+
type: "rpc",
|
|
1048
|
+
lexicon,
|
|
1049
|
+
aud,
|
|
1050
|
+
inheritAud,
|
|
1051
|
+
});
|
|
1052
|
+
this.permissions.push(permission);
|
|
1053
|
+
return this;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Add an identity permission.
|
|
1057
|
+
*
|
|
1058
|
+
* @param attr - The identity attribute ('handle' or '*')
|
|
1059
|
+
* @returns This builder for chaining
|
|
1060
|
+
*
|
|
1061
|
+
* @example
|
|
1062
|
+
* ```typescript
|
|
1063
|
+
* builder.identity('handle');
|
|
1064
|
+
* ```
|
|
1065
|
+
*/
|
|
1066
|
+
identity(attr) {
|
|
1067
|
+
const permission = IdentityPermissionSchema.parse({
|
|
1068
|
+
type: "identity",
|
|
1069
|
+
attr,
|
|
1070
|
+
});
|
|
1071
|
+
this.permissions.push(permission);
|
|
1072
|
+
return this;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Add an include permission.
|
|
1076
|
+
*
|
|
1077
|
+
* @param nsid - The NSID of the scope set to include
|
|
1078
|
+
* @param aud - Optional audience restriction
|
|
1079
|
+
* @returns This builder for chaining
|
|
1080
|
+
*
|
|
1081
|
+
* @example
|
|
1082
|
+
* ```typescript
|
|
1083
|
+
* builder.include('com.example.authBasicFeatures');
|
|
1084
|
+
* ```
|
|
1085
|
+
*/
|
|
1086
|
+
include(nsid, aud) {
|
|
1087
|
+
const permission = IncludePermissionSchema.parse({
|
|
1088
|
+
type: "include",
|
|
1089
|
+
nsid,
|
|
1090
|
+
aud,
|
|
1091
|
+
});
|
|
1092
|
+
this.permissions.push(permission);
|
|
1093
|
+
return this;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Add a custom permission string directly (bypasses validation).
|
|
1097
|
+
*
|
|
1098
|
+
* Use this for testing or special cases where you need to add
|
|
1099
|
+
* a permission that doesn't fit the standard types.
|
|
1100
|
+
*
|
|
1101
|
+
* @param permission - The permission string
|
|
1102
|
+
* @returns This builder for chaining
|
|
1103
|
+
*
|
|
1104
|
+
* @example
|
|
1105
|
+
* ```typescript
|
|
1106
|
+
* builder.custom('atproto');
|
|
1107
|
+
* ```
|
|
1108
|
+
*/
|
|
1109
|
+
custom(permission) {
|
|
1110
|
+
this.permissions.push(permission);
|
|
1111
|
+
return this;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Add the base atproto scope.
|
|
1115
|
+
*
|
|
1116
|
+
* @returns This builder for chaining
|
|
1117
|
+
*
|
|
1118
|
+
* @example
|
|
1119
|
+
* ```typescript
|
|
1120
|
+
* builder.atproto();
|
|
1121
|
+
* ```
|
|
1122
|
+
*/
|
|
1123
|
+
atproto() {
|
|
1124
|
+
this.permissions.push(ATPROTO_SCOPE);
|
|
1125
|
+
return this;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Build and return the array of permission strings.
|
|
1129
|
+
*
|
|
1130
|
+
* @returns Array of permission strings
|
|
1131
|
+
*
|
|
1132
|
+
* @example
|
|
1133
|
+
* ```typescript
|
|
1134
|
+
* const permissions = builder.build();
|
|
1135
|
+
* ```
|
|
1136
|
+
*/
|
|
1137
|
+
build() {
|
|
1138
|
+
return [...this.permissions];
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Clear all permissions from the builder.
|
|
1142
|
+
*
|
|
1143
|
+
* @returns This builder for chaining
|
|
1144
|
+
*
|
|
1145
|
+
* @example
|
|
1146
|
+
* ```typescript
|
|
1147
|
+
* builder.clear().accountEmail('read');
|
|
1148
|
+
* ```
|
|
1149
|
+
*/
|
|
1150
|
+
clear() {
|
|
1151
|
+
this.permissions = [];
|
|
1152
|
+
return this;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Get the current number of permissions.
|
|
1156
|
+
*
|
|
1157
|
+
* @returns The number of permissions
|
|
1158
|
+
*
|
|
1159
|
+
* @example
|
|
1160
|
+
* ```typescript
|
|
1161
|
+
* const count = builder.count();
|
|
1162
|
+
* ```
|
|
1163
|
+
*/
|
|
1164
|
+
count() {
|
|
1165
|
+
return this.permissions.length;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Build a scope string from an array of permissions.
|
|
1170
|
+
*
|
|
1171
|
+
* This is a convenience function that joins permission strings with spaces,
|
|
1172
|
+
* which is the standard format for OAuth scope parameters.
|
|
1173
|
+
*
|
|
1174
|
+
* @param permissions - Array of permission strings
|
|
1175
|
+
* @returns Space-separated scope string
|
|
1176
|
+
*
|
|
1177
|
+
* @example
|
|
1178
|
+
* ```typescript
|
|
1179
|
+
* const permissions = ['account:email?action=read', 'repo:app.bsky.feed.post'];
|
|
1180
|
+
* const scope = buildScope(permissions);
|
|
1181
|
+
* // Returns: "account:email?action=read repo:app.bsky.feed.post"
|
|
1182
|
+
* ```
|
|
1183
|
+
*/
|
|
1184
|
+
function buildScope(permissions) {
|
|
1185
|
+
return permissions.join(" ");
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Pre-built scope presets for common use cases.
|
|
1189
|
+
*
|
|
1190
|
+
* These presets provide ready-to-use permission sets for typical application scenarios.
|
|
1191
|
+
*/
|
|
1192
|
+
const ScopePresets = {
|
|
1193
|
+
/**
|
|
1194
|
+
* Email access scope - allows reading user's email address.
|
|
1195
|
+
*
|
|
1196
|
+
* Includes:
|
|
1197
|
+
* - account:email?action=read
|
|
1198
|
+
*
|
|
1199
|
+
* @example
|
|
1200
|
+
* ```typescript
|
|
1201
|
+
* const scope = ScopePresets.EMAIL_READ;
|
|
1202
|
+
* // Use in OAuth flow to request email access
|
|
1203
|
+
* ```
|
|
1204
|
+
*/
|
|
1205
|
+
EMAIL_READ: buildScope(new PermissionBuilder().accountEmail("read").build()),
|
|
1206
|
+
/**
|
|
1207
|
+
* Profile read scope - allows reading user's profile.
|
|
1208
|
+
*
|
|
1209
|
+
* Includes:
|
|
1210
|
+
* - repo:app.bsky.actor.profile (read-only)
|
|
1211
|
+
*
|
|
1212
|
+
* @example
|
|
1213
|
+
* ```typescript
|
|
1214
|
+
* const scope = ScopePresets.PROFILE_READ;
|
|
1215
|
+
* ```
|
|
1216
|
+
*/
|
|
1217
|
+
PROFILE_READ: buildScope(new PermissionBuilder().repoRead("app.bsky.actor.profile").build()),
|
|
1218
|
+
/**
|
|
1219
|
+
* Profile write scope - allows updating user's profile.
|
|
1220
|
+
*
|
|
1221
|
+
* Includes:
|
|
1222
|
+
* - repo:app.bsky.actor.profile (create + update)
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* ```typescript
|
|
1226
|
+
* const scope = ScopePresets.PROFILE_WRITE;
|
|
1227
|
+
* ```
|
|
1228
|
+
*/
|
|
1229
|
+
PROFILE_WRITE: buildScope(new PermissionBuilder().repoWrite("app.bsky.actor.profile").build()),
|
|
1230
|
+
/**
|
|
1231
|
+
* Post creation scope - allows creating and updating posts.
|
|
1232
|
+
*
|
|
1233
|
+
* Includes:
|
|
1234
|
+
* - repo:app.bsky.feed.post (create + update)
|
|
1235
|
+
*
|
|
1236
|
+
* @example
|
|
1237
|
+
* ```typescript
|
|
1238
|
+
* const scope = ScopePresets.POST_WRITE;
|
|
1239
|
+
* ```
|
|
1240
|
+
*/
|
|
1241
|
+
POST_WRITE: buildScope(new PermissionBuilder().repoWrite("app.bsky.feed.post").build()),
|
|
1242
|
+
/**
|
|
1243
|
+
* Social interactions scope - allows liking, reposting, and following.
|
|
1244
|
+
*
|
|
1245
|
+
* Includes:
|
|
1246
|
+
* - repo:app.bsky.feed.like (create + update)
|
|
1247
|
+
* - repo:app.bsky.feed.repost (create + update)
|
|
1248
|
+
* - repo:app.bsky.graph.follow (create + update)
|
|
1249
|
+
*
|
|
1250
|
+
* @example
|
|
1251
|
+
* ```typescript
|
|
1252
|
+
* const scope = ScopePresets.SOCIAL_WRITE;
|
|
1253
|
+
* ```
|
|
1254
|
+
*/
|
|
1255
|
+
SOCIAL_WRITE: buildScope(new PermissionBuilder()
|
|
1256
|
+
.repoWrite("app.bsky.feed.like")
|
|
1257
|
+
.repoWrite("app.bsky.feed.repost")
|
|
1258
|
+
.repoWrite("app.bsky.graph.follow")
|
|
1259
|
+
.build()),
|
|
1260
|
+
/**
|
|
1261
|
+
* Media upload scope - allows uploading images and videos.
|
|
1262
|
+
*
|
|
1263
|
+
* Includes:
|
|
1264
|
+
* - blob permissions for image/* and video/*
|
|
1265
|
+
*
|
|
1266
|
+
* @example
|
|
1267
|
+
* ```typescript
|
|
1268
|
+
* const scope = ScopePresets.MEDIA_UPLOAD;
|
|
1269
|
+
* ```
|
|
1270
|
+
*/
|
|
1271
|
+
MEDIA_UPLOAD: buildScope(new PermissionBuilder().blob(["image/*", "video/*"]).build()),
|
|
1272
|
+
/**
|
|
1273
|
+
* Image upload only scope - allows uploading images.
|
|
1274
|
+
*
|
|
1275
|
+
* Includes:
|
|
1276
|
+
* - blob:image/*
|
|
1277
|
+
*
|
|
1278
|
+
* @example
|
|
1279
|
+
* ```typescript
|
|
1280
|
+
* const scope = ScopePresets.IMAGE_UPLOAD;
|
|
1281
|
+
* ```
|
|
1282
|
+
*/
|
|
1283
|
+
IMAGE_UPLOAD: buildScope(new PermissionBuilder().blob("image/*").build()),
|
|
1284
|
+
/**
|
|
1285
|
+
* Posting app scope - full posting capabilities including media.
|
|
1286
|
+
*
|
|
1287
|
+
* Includes:
|
|
1288
|
+
* - repo:app.bsky.feed.post (create + update)
|
|
1289
|
+
* - repo:app.bsky.feed.like (create + update)
|
|
1290
|
+
* - repo:app.bsky.feed.repost (create + update)
|
|
1291
|
+
* - blob permissions for image/* and video/*
|
|
1292
|
+
*
|
|
1293
|
+
* @example
|
|
1294
|
+
* ```typescript
|
|
1295
|
+
* const scope = ScopePresets.POSTING_APP;
|
|
1296
|
+
* ```
|
|
1297
|
+
*/
|
|
1298
|
+
POSTING_APP: buildScope(new PermissionBuilder()
|
|
1299
|
+
.repoWrite("app.bsky.feed.post")
|
|
1300
|
+
.repoWrite("app.bsky.feed.like")
|
|
1301
|
+
.repoWrite("app.bsky.feed.repost")
|
|
1302
|
+
.blob(["image/*", "video/*"])
|
|
1303
|
+
.build()),
|
|
1304
|
+
/**
|
|
1305
|
+
* Read-only app scope - allows reading all repository data.
|
|
1306
|
+
*
|
|
1307
|
+
* Includes:
|
|
1308
|
+
* - repo:* (read-only, no actions)
|
|
1309
|
+
*
|
|
1310
|
+
* @example
|
|
1311
|
+
* ```typescript
|
|
1312
|
+
* const scope = ScopePresets.READ_ONLY;
|
|
1313
|
+
* ```
|
|
1314
|
+
*/
|
|
1315
|
+
READ_ONLY: buildScope(new PermissionBuilder().repoRead("*").build()),
|
|
1316
|
+
/**
|
|
1317
|
+
* Full access scope - allows all repository operations.
|
|
1318
|
+
*
|
|
1319
|
+
* Includes:
|
|
1320
|
+
* - repo:* (create + update + delete)
|
|
1321
|
+
*
|
|
1322
|
+
* @example
|
|
1323
|
+
* ```typescript
|
|
1324
|
+
* const scope = ScopePresets.FULL_ACCESS;
|
|
1325
|
+
* ```
|
|
1326
|
+
*/
|
|
1327
|
+
FULL_ACCESS: buildScope(new PermissionBuilder().repoFull("*").build()),
|
|
1328
|
+
/**
|
|
1329
|
+
* Email + Profile scope - common combination for user identification.
|
|
1330
|
+
*
|
|
1331
|
+
* Includes:
|
|
1332
|
+
* - account:email?action=read
|
|
1333
|
+
* - repo:app.bsky.actor.profile (read-only)
|
|
1334
|
+
*
|
|
1335
|
+
* @example
|
|
1336
|
+
* ```typescript
|
|
1337
|
+
* const scope = ScopePresets.EMAIL_AND_PROFILE;
|
|
1338
|
+
* ```
|
|
1339
|
+
*/
|
|
1340
|
+
EMAIL_AND_PROFILE: buildScope(new PermissionBuilder().accountEmail("read").repoRead("app.bsky.actor.profile").build()),
|
|
1341
|
+
/**
|
|
1342
|
+
* Transitional email scope (legacy).
|
|
1343
|
+
*
|
|
1344
|
+
* Uses the transitional scope format for backward compatibility.
|
|
1345
|
+
*
|
|
1346
|
+
* @example
|
|
1347
|
+
* ```typescript
|
|
1348
|
+
* const scope = ScopePresets.TRANSITION_EMAIL;
|
|
1349
|
+
* ```
|
|
1350
|
+
*/
|
|
1351
|
+
TRANSITION_EMAIL: buildScope(new PermissionBuilder().transition("email").build()),
|
|
1352
|
+
/**
|
|
1353
|
+
* Transitional generic scope (legacy).
|
|
1354
|
+
*
|
|
1355
|
+
* Uses the transitional scope format for backward compatibility.
|
|
1356
|
+
*
|
|
1357
|
+
* @example
|
|
1358
|
+
* ```typescript
|
|
1359
|
+
* const scope = ScopePresets.TRANSITION_GENERIC;
|
|
1360
|
+
* ```
|
|
1361
|
+
*/
|
|
1362
|
+
TRANSITION_GENERIC: buildScope(new PermissionBuilder().transition("generic").build()),
|
|
1363
|
+
};
|
|
1364
|
+
/**
|
|
1365
|
+
* Parse a scope string into an array of individual permissions.
|
|
1366
|
+
*
|
|
1367
|
+
* This splits a space-separated scope string into individual permission strings.
|
|
1368
|
+
*
|
|
1369
|
+
* @param scope - Space-separated scope string
|
|
1370
|
+
* @returns Array of permission strings
|
|
1371
|
+
*
|
|
1372
|
+
* @example
|
|
1373
|
+
* ```typescript
|
|
1374
|
+
* const scope = "account:email?action=read repo:app.bsky.feed.post";
|
|
1375
|
+
* const permissions = parseScope(scope);
|
|
1376
|
+
* // Returns: ['account:email?action=read', 'repo:app.bsky.feed.post']
|
|
1377
|
+
* ```
|
|
1378
|
+
*/
|
|
1379
|
+
function parseScope(scope) {
|
|
1380
|
+
return scope.trim().split(/\s+/).filter(Boolean);
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Check if a scope string contains a specific permission.
|
|
1384
|
+
*
|
|
1385
|
+
* This function performs exact string matching. For more advanced
|
|
1386
|
+
* permission checking (e.g., wildcard matching), you'll need to
|
|
1387
|
+
* implement custom logic.
|
|
1388
|
+
*
|
|
1389
|
+
* @param scope - Space-separated scope string
|
|
1390
|
+
* @param permission - The permission to check for
|
|
1391
|
+
* @returns True if the scope contains the permission
|
|
1392
|
+
*
|
|
1393
|
+
* @example
|
|
1394
|
+
* ```typescript
|
|
1395
|
+
* const scope = "account:email?action=read repo:app.bsky.feed.post";
|
|
1396
|
+
* hasPermission(scope, "account:email?action=read"); // true
|
|
1397
|
+
* hasPermission(scope, "account:repo"); // false
|
|
1398
|
+
* ```
|
|
1399
|
+
*/
|
|
1400
|
+
function hasPermission(scope, permission) {
|
|
1401
|
+
const permissions = parseScope(scope);
|
|
1402
|
+
return permissions.includes(permission);
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Check if a scope string contains all of the specified permissions.
|
|
1406
|
+
*
|
|
1407
|
+
* @param scope - Space-separated scope string
|
|
1408
|
+
* @param requiredPermissions - Array of permissions to check for
|
|
1409
|
+
* @returns True if the scope contains all required permissions
|
|
1410
|
+
*
|
|
1411
|
+
* @example
|
|
1412
|
+
* ```typescript
|
|
1413
|
+
* const scope = "account:email?action=read repo:app.bsky.feed.post blob:image/*";
|
|
1414
|
+
* hasAllPermissions(scope, ["account:email?action=read", "blob:image/*"]); // true
|
|
1415
|
+
* hasAllPermissions(scope, ["account:email?action=read", "account:repo"]); // false
|
|
1416
|
+
* ```
|
|
1417
|
+
*/
|
|
1418
|
+
function hasAllPermissions(scope, requiredPermissions) {
|
|
1419
|
+
const permissions = parseScope(scope);
|
|
1420
|
+
return requiredPermissions.every((required) => permissions.includes(required));
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Check if a scope string contains any of the specified permissions.
|
|
1424
|
+
*
|
|
1425
|
+
* @param scope - Space-separated scope string
|
|
1426
|
+
* @param checkPermissions - Array of permissions to check for
|
|
1427
|
+
* @returns True if the scope contains at least one of the permissions
|
|
1428
|
+
*
|
|
1429
|
+
* @example
|
|
1430
|
+
* ```typescript
|
|
1431
|
+
* const scope = "account:email?action=read repo:app.bsky.feed.post";
|
|
1432
|
+
* hasAnyPermission(scope, ["account:email?action=read", "account:repo"]); // true
|
|
1433
|
+
* hasAnyPermission(scope, ["account:repo", "identity:handle"]); // false
|
|
1434
|
+
* ```
|
|
1435
|
+
*/
|
|
1436
|
+
function hasAnyPermission(scope, checkPermissions) {
|
|
1437
|
+
const permissions = parseScope(scope);
|
|
1438
|
+
return checkPermissions.some((check) => permissions.includes(check));
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Merge multiple scope strings into a single scope string with deduplicated permissions.
|
|
1442
|
+
*
|
|
1443
|
+
* @param scopes - Array of scope strings to merge
|
|
1444
|
+
* @returns Merged scope string with unique permissions
|
|
1445
|
+
*
|
|
1446
|
+
* @example
|
|
1447
|
+
* ```typescript
|
|
1448
|
+
* const scope1 = "account:email?action=read repo:app.bsky.feed.post";
|
|
1449
|
+
* const scope2 = "repo:app.bsky.feed.post blob:image/*";
|
|
1450
|
+
* const merged = mergeScopes([scope1, scope2]);
|
|
1451
|
+
* // Returns: "account:email?action=read repo:app.bsky.feed.post blob:image/*"
|
|
1452
|
+
* ```
|
|
1453
|
+
*/
|
|
1454
|
+
function mergeScopes(scopes) {
|
|
1455
|
+
const allPermissions = scopes.flatMap(parseScope);
|
|
1456
|
+
const uniquePermissions = [...new Set(allPermissions)];
|
|
1457
|
+
return buildScope(uniquePermissions);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Remove specific permissions from a scope string.
|
|
1461
|
+
*
|
|
1462
|
+
* @param scope - Space-separated scope string
|
|
1463
|
+
* @param permissionsToRemove - Array of permissions to remove
|
|
1464
|
+
* @returns New scope string without the specified permissions
|
|
1465
|
+
*
|
|
1466
|
+
* @example
|
|
1467
|
+
* ```typescript
|
|
1468
|
+
* const scope = "account:email?action=read repo:app.bsky.feed.post blob:image/*";
|
|
1469
|
+
* const filtered = removePermissions(scope, ["blob:image/*"]);
|
|
1470
|
+
* // Returns: "account:email?action=read repo:app.bsky.feed.post"
|
|
1471
|
+
* ```
|
|
1472
|
+
*/
|
|
1473
|
+
function removePermissions(scope, permissionsToRemove) {
|
|
1474
|
+
const permissions = parseScope(scope);
|
|
1475
|
+
const filtered = permissions.filter((p) => !permissionsToRemove.includes(p));
|
|
1476
|
+
return buildScope(filtered);
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Validate that all permissions in a scope string are well-formed.
|
|
1480
|
+
*
|
|
1481
|
+
* This checks that each permission matches expected patterns for transitional
|
|
1482
|
+
* or granular permissions. It does NOT validate against the full Zod schemas.
|
|
1483
|
+
*
|
|
1484
|
+
* @param scope - Space-separated scope string
|
|
1485
|
+
* @returns Object with isValid flag and array of invalid permissions
|
|
1486
|
+
*
|
|
1487
|
+
* @example
|
|
1488
|
+
* ```typescript
|
|
1489
|
+
* const scope = "account:email?action=read invalid:permission";
|
|
1490
|
+
* const result = validateScope(scope);
|
|
1491
|
+
* // Returns: { isValid: false, invalidPermissions: ['invalid:permission'] }
|
|
1492
|
+
* ```
|
|
1493
|
+
*/
|
|
1494
|
+
function validateScope(scope) {
|
|
1495
|
+
const permissions = parseScope(scope);
|
|
1496
|
+
const invalidPermissions = [];
|
|
1497
|
+
// Pattern for valid permission prefixes
|
|
1498
|
+
const validPrefixes = /^(atproto|transition:|account:|repo:|blob:?|rpc:|identity:|include:)/;
|
|
1499
|
+
for (const permission of permissions) {
|
|
1500
|
+
if (!validPrefixes.test(permission)) {
|
|
1501
|
+
invalidPermissions.push(permission);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
isValid: invalidPermissions.length === 0,
|
|
1506
|
+
invalidPermissions,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* OAuth 2.0 client for AT Protocol authentication with DPoP support.
|
|
1512
|
+
*
|
|
1513
|
+
* This class wraps the `@atproto/oauth-client-node` library to provide
|
|
1514
|
+
* OAuth 2.0 authentication with the following features:
|
|
1515
|
+
*
|
|
1516
|
+
* - **DPoP (Demonstrating Proof of Possession)**: Binds tokens to cryptographic keys
|
|
1517
|
+
* to prevent token theft and replay attacks
|
|
1518
|
+
* - **PKCE (Proof Key for Code Exchange)**: Protects against authorization code interception
|
|
1519
|
+
* - **Automatic Token Refresh**: Transparently refreshes expired access tokens
|
|
1520
|
+
* - **Session Persistence**: Stores sessions in configurable storage backends
|
|
1521
|
+
*
|
|
1522
|
+
* @remarks
|
|
1523
|
+
* This class is typically used internally by {@link ATProtoSDK}. Direct usage
|
|
1524
|
+
* is only needed for advanced scenarios.
|
|
1525
|
+
*
|
|
1526
|
+
* The client uses lazy initialization - the underlying `NodeOAuthClient` is
|
|
1527
|
+
* created asynchronously on first use. This allows the constructor to return
|
|
1528
|
+
* synchronously while deferring async key parsing.
|
|
1529
|
+
*
|
|
1530
|
+
* @example Direct usage (advanced)
|
|
1531
|
+
* ```typescript
|
|
1532
|
+
* import { OAuthClient } from "@hypercerts-org/sdk";
|
|
1533
|
+
*
|
|
1534
|
+
* const client = new OAuthClient({
|
|
1535
|
+
* oauth: {
|
|
1536
|
+
* clientId: "https://my-app.com/client-metadata.json",
|
|
1537
|
+
* redirectUri: "https://my-app.com/callback",
|
|
1538
|
+
* scope: "atproto transition:generic",
|
|
1539
|
+
* jwksUri: "https://my-app.com/.well-known/jwks.json",
|
|
1540
|
+
* jwkPrivate: process.env.JWK_PRIVATE_KEY!,
|
|
1541
|
+
* },
|
|
1542
|
+
* servers: { pds: "https://bsky.social" },
|
|
1543
|
+
* });
|
|
1544
|
+
*
|
|
1545
|
+
* // Start authorization
|
|
1546
|
+
* const authUrl = await client.authorize("user.bsky.social");
|
|
1547
|
+
*
|
|
1548
|
+
* // Handle callback
|
|
1549
|
+
* const session = await client.callback(new URLSearchParams(callbackUrl.search));
|
|
1550
|
+
* ```
|
|
1551
|
+
*
|
|
1552
|
+
* @see {@link ATProtoSDK} for the recommended high-level API
|
|
1553
|
+
* @see https://atproto.com/specs/oauth for AT Protocol OAuth specification
|
|
1554
|
+
*/
|
|
1555
|
+
class OAuthClient {
|
|
1556
|
+
/**
|
|
1557
|
+
* Creates a new OAuth client.
|
|
1558
|
+
*
|
|
1559
|
+
* @param config - SDK configuration including OAuth credentials and server URLs
|
|
1560
|
+
* @throws {@link AuthenticationError} if the JWK private key is not valid JSON
|
|
1561
|
+
*
|
|
1562
|
+
* @remarks
|
|
1563
|
+
* The constructor validates the JWK format synchronously but defers
|
|
1564
|
+
* the actual client initialization to the first API call.
|
|
1565
|
+
*/
|
|
1566
|
+
constructor(config) {
|
|
1567
|
+
/** The underlying NodeOAuthClient instance (lazily initialized) */
|
|
1568
|
+
this.client = null;
|
|
1569
|
+
this.config = config;
|
|
1570
|
+
this.logger = config.logger;
|
|
1571
|
+
// Validate JWK format synchronously (before async initialization)
|
|
1572
|
+
try {
|
|
1573
|
+
JSON.parse(config.oauth.jwkPrivate);
|
|
1574
|
+
}
|
|
1575
|
+
catch (error) {
|
|
1576
|
+
throw new AuthenticationError("Failed to parse JWK private key. Ensure it is valid JSON.", error);
|
|
1577
|
+
}
|
|
1578
|
+
// Initialize client lazily (async initialization)
|
|
1579
|
+
this.clientPromise = this.initializeClient();
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Initializes the NodeOAuthClient asynchronously.
|
|
1583
|
+
*
|
|
1584
|
+
* This method is called lazily on first use. It:
|
|
1585
|
+
* 1. Parses the JWK private key(s)
|
|
1586
|
+
* 2. Builds OAuth client metadata
|
|
1587
|
+
* 3. Creates the underlying NodeOAuthClient
|
|
1588
|
+
*
|
|
1589
|
+
* @returns Promise resolving to the initialized client
|
|
1590
|
+
* @internal
|
|
1591
|
+
*/
|
|
1592
|
+
async initializeClient() {
|
|
1593
|
+
if (this.client) {
|
|
1594
|
+
return this.client;
|
|
1595
|
+
}
|
|
1596
|
+
// Parse JWK private key (already validated in constructor)
|
|
1597
|
+
const privateJWK = JSON.parse(this.config.oauth.jwkPrivate);
|
|
1598
|
+
// Build client metadata
|
|
1599
|
+
const clientMetadata = this.buildClientMetadata();
|
|
1600
|
+
// Convert JWK keys to JoseKey instances (await here)
|
|
1601
|
+
const keyset = await Promise.all(privateJWK.keys.map((key) => oauthClientNode.JoseKey.fromImportable(key, key.kid)));
|
|
1602
|
+
// Create fetch with timeout
|
|
1603
|
+
const fetchWithTimeout = this.createFetchWithTimeout(this.config.timeouts?.pdsMetadata ?? 30000);
|
|
1604
|
+
// Use provided stores or fall back to in-memory implementations
|
|
1605
|
+
const stateStore = this.config.storage?.stateStore ?? new InMemoryStateStore();
|
|
1606
|
+
const sessionStore = this.config.storage?.sessionStore ?? new InMemorySessionStore();
|
|
1607
|
+
this.client = new oauthClientNode.NodeOAuthClient({
|
|
1608
|
+
clientMetadata,
|
|
1609
|
+
keyset,
|
|
1610
|
+
stateStore: this.createStateStoreAdapter(stateStore),
|
|
1611
|
+
sessionStore: this.createSessionStoreAdapter(sessionStore),
|
|
1612
|
+
handleResolver: this.config.servers?.pds,
|
|
1613
|
+
fetch: this.config.fetch ?? fetchWithTimeout,
|
|
1614
|
+
});
|
|
1615
|
+
return this.client;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Gets the OAuth client instance, initializing if needed.
|
|
1619
|
+
*
|
|
1620
|
+
* @returns Promise resolving to the initialized client
|
|
1621
|
+
* @internal
|
|
1622
|
+
*/
|
|
1623
|
+
async getClient() {
|
|
1624
|
+
return this.clientPromise;
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Builds OAuth client metadata from configuration.
|
|
1628
|
+
*
|
|
1629
|
+
* The metadata describes your application to the authorization server
|
|
1630
|
+
* and must match what's published at your `clientId` URL.
|
|
1631
|
+
*
|
|
1632
|
+
* @returns OAuth client metadata object
|
|
1633
|
+
* @internal
|
|
1634
|
+
*
|
|
1635
|
+
* @remarks
|
|
1636
|
+
* Key metadata fields:
|
|
1637
|
+
* - `client_id`: URL to your client metadata JSON
|
|
1638
|
+
* - `redirect_uris`: Where to redirect after auth (must match config)
|
|
1639
|
+
* - `dpop_bound_access_tokens`: Always true for AT Protocol
|
|
1640
|
+
* - `token_endpoint_auth_method`: Uses private_key_jwt for security
|
|
1641
|
+
*/
|
|
1642
|
+
buildClientMetadata() {
|
|
1643
|
+
const clientIdUrl = new URL(this.config.oauth.clientId);
|
|
1644
|
+
const metadata = {
|
|
1645
|
+
client_id: this.config.oauth.clientId,
|
|
1646
|
+
client_name: "ATProto SDK Client",
|
|
1647
|
+
client_uri: clientIdUrl.origin,
|
|
1648
|
+
redirect_uris: [this.config.oauth.redirectUri],
|
|
1649
|
+
scope: this.config.oauth.scope,
|
|
1650
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
1651
|
+
response_types: ["code"],
|
|
1652
|
+
application_type: "web",
|
|
1653
|
+
token_endpoint_auth_method: "private_key_jwt",
|
|
1654
|
+
token_endpoint_auth_signing_alg: "ES256",
|
|
1655
|
+
dpop_bound_access_tokens: true,
|
|
1656
|
+
jwks_uri: this.config.oauth.jwksUri,
|
|
1657
|
+
};
|
|
1658
|
+
// Validate scope before returning metadata
|
|
1659
|
+
this.validateClientMetadataScope(metadata.scope);
|
|
1660
|
+
return metadata;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Validates the OAuth scope in client metadata and logs warnings/suggestions.
|
|
1664
|
+
*
|
|
1665
|
+
* This method:
|
|
1666
|
+
* 1. Checks if the scope is well-formed using permission utilities
|
|
1667
|
+
* 2. Detects mixing of transitional and granular permissions
|
|
1668
|
+
* 3. Logs warnings for missing `atproto` scope
|
|
1669
|
+
* 4. Suggests migration to granular permissions for transitional scopes
|
|
1670
|
+
*
|
|
1671
|
+
* @param scope - The OAuth scope string to validate
|
|
1672
|
+
* @internal
|
|
1673
|
+
*/
|
|
1674
|
+
validateClientMetadataScope(scope) {
|
|
1675
|
+
// Parse the scope into individual permissions
|
|
1676
|
+
const permissions = parseScope(scope);
|
|
1677
|
+
// Validate well-formedness
|
|
1678
|
+
const validation = validateScope(scope);
|
|
1679
|
+
if (!validation.isValid) {
|
|
1680
|
+
this.logger?.error("Invalid OAuth scope detected", {
|
|
1681
|
+
invalidPermissions: validation.invalidPermissions,
|
|
1682
|
+
scope,
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
// Check for atproto scope
|
|
1686
|
+
const hasAtproto = permissions.includes(ATPROTO_SCOPE);
|
|
1687
|
+
if (!hasAtproto) {
|
|
1688
|
+
this.logger?.warn("OAuth scope missing 'atproto' - basic API access may be limited", {
|
|
1689
|
+
scope,
|
|
1690
|
+
suggestion: "Add 'atproto' to your scope for basic API access",
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
// Detect transitional scopes
|
|
1694
|
+
const transitionalScopes = permissions.filter((p) => p.startsWith("transition:"));
|
|
1695
|
+
const granularScopes = permissions.filter((p) => p.startsWith("account:") ||
|
|
1696
|
+
p.startsWith("repo:") ||
|
|
1697
|
+
p.startsWith("blob") ||
|
|
1698
|
+
p.startsWith("rpc:") ||
|
|
1699
|
+
p.startsWith("identity:") ||
|
|
1700
|
+
p.startsWith("include:"));
|
|
1701
|
+
// Log info about transitional scopes
|
|
1702
|
+
if (transitionalScopes.length > 0) {
|
|
1703
|
+
this.logger?.info("Using transitional OAuth scopes (legacy)", {
|
|
1704
|
+
transitionalScopes,
|
|
1705
|
+
note: "Transitional scopes are supported but granular permissions are recommended",
|
|
1706
|
+
});
|
|
1707
|
+
// Suggest migration to granular permissions
|
|
1708
|
+
if (transitionalScopes.includes("transition:email")) {
|
|
1709
|
+
this.logger?.info("Consider migrating 'transition:email' to granular permissions", {
|
|
1710
|
+
suggestion: "Use: account:email?action=read",
|
|
1711
|
+
example: "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.EMAIL_READ",
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
if (transitionalScopes.includes("transition:generic")) {
|
|
1715
|
+
this.logger?.info("Consider migrating 'transition:generic' to granular permissions", {
|
|
1716
|
+
suggestion: "Use specific permissions like: repo:* account:repo?action=read",
|
|
1717
|
+
example: "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.FULL_ACCESS",
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
// Warn if mixing transitional and granular
|
|
1722
|
+
if (transitionalScopes.length > 0 && granularScopes.length > 0) {
|
|
1723
|
+
this.logger?.warn("Mixing transitional and granular OAuth scopes", {
|
|
1724
|
+
transitionalScopes,
|
|
1725
|
+
granularScopes,
|
|
1726
|
+
note: "While supported, it's recommended to use either transitional or granular permissions consistently",
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Creates a fetch handler with timeout support.
|
|
1732
|
+
*
|
|
1733
|
+
* @param timeoutMs - Request timeout in milliseconds
|
|
1734
|
+
* @returns A fetch function that aborts after the timeout
|
|
1735
|
+
* @internal
|
|
1736
|
+
*/
|
|
1737
|
+
createFetchWithTimeout(timeoutMs) {
|
|
683
1738
|
return async (input, init) => {
|
|
684
1739
|
const controller = new AbortController();
|
|
685
1740
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -941,308 +1996,11 @@ class OAuthClient {
|
|
|
941
1996
|
const client = await this.getClient();
|
|
942
1997
|
await client.revoke(did);
|
|
943
1998
|
this.logger?.info("Session revoked", { did });
|
|
944
|
-
}
|
|
945
|
-
catch (error) {
|
|
946
|
-
this.logger?.error("Failed to revoke session", { did, error });
|
|
947
|
-
throw new AuthenticationError(`Failed to revoke session: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
/**
|
|
953
|
-
* Registry for managing and validating AT Protocol lexicon schemas.
|
|
954
|
-
*
|
|
955
|
-
* Lexicons are schema definitions that describe the structure of records
|
|
956
|
-
* in the AT Protocol. This registry allows you to:
|
|
957
|
-
*
|
|
958
|
-
* - Register custom lexicons for your application's record types
|
|
959
|
-
* - Validate records against their lexicon schemas
|
|
960
|
-
* - Extend the AT Protocol Agent with custom lexicon support
|
|
961
|
-
*
|
|
962
|
-
* @remarks
|
|
963
|
-
* The SDK automatically registers hypercert lexicons when creating a Repository.
|
|
964
|
-
* You only need to use this class directly if you're working with custom
|
|
965
|
-
* record types.
|
|
966
|
-
*
|
|
967
|
-
* **Lexicon IDs** follow the NSID (Namespaced Identifier) format:
|
|
968
|
-
* `{authority}.{name}` (e.g., `org.hypercerts.hypercert`)
|
|
969
|
-
*
|
|
970
|
-
* @example Registering custom lexicons
|
|
971
|
-
* ```typescript
|
|
972
|
-
* const registry = sdk.getLexiconRegistry();
|
|
973
|
-
*
|
|
974
|
-
* // Register a single lexicon
|
|
975
|
-
* registry.register({
|
|
976
|
-
* lexicon: 1,
|
|
977
|
-
* id: "org.example.myRecord",
|
|
978
|
-
* defs: {
|
|
979
|
-
* main: {
|
|
980
|
-
* type: "record",
|
|
981
|
-
* key: "tid",
|
|
982
|
-
* record: {
|
|
983
|
-
* type: "object",
|
|
984
|
-
* required: ["title", "createdAt"],
|
|
985
|
-
* properties: {
|
|
986
|
-
* title: { type: "string" },
|
|
987
|
-
* description: { type: "string" },
|
|
988
|
-
* createdAt: { type: "string", format: "datetime" },
|
|
989
|
-
* },
|
|
990
|
-
* },
|
|
991
|
-
* },
|
|
992
|
-
* },
|
|
993
|
-
* });
|
|
994
|
-
*
|
|
995
|
-
* // Register multiple lexicons at once
|
|
996
|
-
* registry.registerMany([lexicon1, lexicon2, lexicon3]);
|
|
997
|
-
* ```
|
|
998
|
-
*
|
|
999
|
-
* @example Validating records
|
|
1000
|
-
* ```typescript
|
|
1001
|
-
* const result = registry.validate("org.example.myRecord", {
|
|
1002
|
-
* title: "Test",
|
|
1003
|
-
* createdAt: new Date().toISOString(),
|
|
1004
|
-
* });
|
|
1005
|
-
*
|
|
1006
|
-
* if (!result.valid) {
|
|
1007
|
-
* console.error(`Validation failed: ${result.error}`);
|
|
1008
|
-
* }
|
|
1009
|
-
* ```
|
|
1010
|
-
*
|
|
1011
|
-
* @see https://atproto.com/specs/lexicon for the Lexicon specification
|
|
1012
|
-
*/
|
|
1013
|
-
class LexiconRegistry {
|
|
1014
|
-
/**
|
|
1015
|
-
* Creates a new LexiconRegistry.
|
|
1016
|
-
*
|
|
1017
|
-
* The registry starts empty. Use {@link register} or {@link registerMany}
|
|
1018
|
-
* to add lexicons.
|
|
1019
|
-
*/
|
|
1020
|
-
constructor() {
|
|
1021
|
-
/** Map of lexicon ID to lexicon document */
|
|
1022
|
-
this.lexicons = new Map();
|
|
1023
|
-
this.lexiconsCollection = new lexicon$1.Lexicons();
|
|
1024
|
-
}
|
|
1025
|
-
/**
|
|
1026
|
-
* Registers a single lexicon schema.
|
|
1027
|
-
*
|
|
1028
|
-
* @param lexicon - The lexicon document to register
|
|
1029
|
-
* @throws {@link ValidationError} if the lexicon doesn't have an `id` field
|
|
1030
|
-
*
|
|
1031
|
-
* @remarks
|
|
1032
|
-
* If a lexicon with the same ID is already registered, it will be
|
|
1033
|
-
* replaced with the new definition. This is useful for testing but
|
|
1034
|
-
* should generally be avoided in production.
|
|
1035
|
-
*
|
|
1036
|
-
* @example
|
|
1037
|
-
* ```typescript
|
|
1038
|
-
* registry.register({
|
|
1039
|
-
* lexicon: 1,
|
|
1040
|
-
* id: "org.example.post",
|
|
1041
|
-
* defs: {
|
|
1042
|
-
* main: {
|
|
1043
|
-
* type: "record",
|
|
1044
|
-
* key: "tid",
|
|
1045
|
-
* record: {
|
|
1046
|
-
* type: "object",
|
|
1047
|
-
* required: ["text", "createdAt"],
|
|
1048
|
-
* properties: {
|
|
1049
|
-
* text: { type: "string", maxLength: 300 },
|
|
1050
|
-
* createdAt: { type: "string", format: "datetime" },
|
|
1051
|
-
* },
|
|
1052
|
-
* },
|
|
1053
|
-
* },
|
|
1054
|
-
* },
|
|
1055
|
-
* });
|
|
1056
|
-
* ```
|
|
1057
|
-
*/
|
|
1058
|
-
register(lexicon) {
|
|
1059
|
-
if (!lexicon.id) {
|
|
1060
|
-
throw new ValidationError("Lexicon must have an 'id' field");
|
|
1061
|
-
}
|
|
1062
|
-
// Remove existing lexicon if present (to allow overwriting)
|
|
1063
|
-
if (this.lexicons.has(lexicon.id)) {
|
|
1064
|
-
// Lexicons collection doesn't support removal, so we create a new one
|
|
1065
|
-
// This is a limitation - in practice, lexicons shouldn't be overwritten
|
|
1066
|
-
// But we allow it for testing and flexibility
|
|
1067
|
-
const existingLexicon = this.lexicons.get(lexicon.id);
|
|
1068
|
-
if (existingLexicon) {
|
|
1069
|
-
// Try to remove from collection (may fail if not supported)
|
|
1070
|
-
try {
|
|
1071
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1072
|
-
this.lexiconsCollection.remove?.(lexicon.id);
|
|
1073
|
-
}
|
|
1074
|
-
catch {
|
|
1075
|
-
// If removal fails, create a new collection
|
|
1076
|
-
this.lexiconsCollection = new lexicon$1.Lexicons();
|
|
1077
|
-
// Re-register all other lexicons
|
|
1078
|
-
for (const [id, lex] of this.lexicons.entries()) {
|
|
1079
|
-
if (id !== lexicon.id) {
|
|
1080
|
-
this.lexiconsCollection.add(lex);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
this.lexicons.set(lexicon.id, lexicon);
|
|
1087
|
-
this.lexiconsCollection.add(lexicon);
|
|
1088
|
-
}
|
|
1089
|
-
/**
|
|
1090
|
-
* Registers multiple lexicons at once.
|
|
1091
|
-
*
|
|
1092
|
-
* @param lexicons - Array of lexicon documents to register
|
|
1093
|
-
*
|
|
1094
|
-
* @example
|
|
1095
|
-
* ```typescript
|
|
1096
|
-
* import { HYPERCERT_LEXICONS } from "@hypercerts-org/sdk/lexicons";
|
|
1097
|
-
*
|
|
1098
|
-
* registry.registerMany(HYPERCERT_LEXICONS);
|
|
1099
|
-
* ```
|
|
1100
|
-
*/
|
|
1101
|
-
registerMany(lexicons) {
|
|
1102
|
-
for (const lexicon of lexicons) {
|
|
1103
|
-
this.register(lexicon);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Gets a lexicon document by ID.
|
|
1108
|
-
*
|
|
1109
|
-
* @param id - The lexicon NSID (e.g., "org.hypercerts.hypercert")
|
|
1110
|
-
* @returns The lexicon document, or `undefined` if not registered
|
|
1111
|
-
*
|
|
1112
|
-
* @example
|
|
1113
|
-
* ```typescript
|
|
1114
|
-
* const lexicon = registry.get("org.hypercerts.hypercert");
|
|
1115
|
-
* if (lexicon) {
|
|
1116
|
-
* console.log(`Found lexicon: ${lexicon.id}`);
|
|
1117
|
-
* }
|
|
1118
|
-
* ```
|
|
1119
|
-
*/
|
|
1120
|
-
get(id) {
|
|
1121
|
-
return this.lexicons.get(id);
|
|
1122
|
-
}
|
|
1123
|
-
/**
|
|
1124
|
-
* Validates a record against a collection's lexicon schema.
|
|
1125
|
-
*
|
|
1126
|
-
* @param collection - The collection NSID (same as lexicon ID)
|
|
1127
|
-
* @param record - The record data to validate
|
|
1128
|
-
* @returns Validation result with `valid` boolean and optional `error` message
|
|
1129
|
-
*
|
|
1130
|
-
* @remarks
|
|
1131
|
-
* - If no lexicon is registered for the collection, validation passes
|
|
1132
|
-
* (we can't validate against unknown schemas)
|
|
1133
|
-
* - Validation checks required fields and type constraints defined
|
|
1134
|
-
* in the lexicon schema
|
|
1135
|
-
*
|
|
1136
|
-
* @example
|
|
1137
|
-
* ```typescript
|
|
1138
|
-
* const result = registry.validate("org.hypercerts.hypercert", {
|
|
1139
|
-
* title: "My Hypercert",
|
|
1140
|
-
* description: "Description...",
|
|
1141
|
-
* // ... other fields
|
|
1142
|
-
* });
|
|
1143
|
-
*
|
|
1144
|
-
* if (!result.valid) {
|
|
1145
|
-
* throw new Error(`Invalid record: ${result.error}`);
|
|
1146
|
-
* }
|
|
1147
|
-
* ```
|
|
1148
|
-
*/
|
|
1149
|
-
validate(collection, record) {
|
|
1150
|
-
// Check if we have a lexicon registered for this collection
|
|
1151
|
-
// Collection format is typically "namespace.collection" (e.g., "app.bsky.feed.post")
|
|
1152
|
-
// Lexicon ID format is the same
|
|
1153
|
-
const lexiconId = collection;
|
|
1154
|
-
const lexicon = this.lexicons.get(lexiconId);
|
|
1155
|
-
if (!lexicon) {
|
|
1156
|
-
// No lexicon registered - validation passes (can't validate unknown schemas)
|
|
1157
|
-
return { valid: true };
|
|
1158
|
-
}
|
|
1159
|
-
// Check required fields if the lexicon defines them
|
|
1160
|
-
const recordDef = lexicon.defs?.record;
|
|
1161
|
-
if (recordDef && typeof recordDef === "object" && "record" in recordDef) {
|
|
1162
|
-
const recordSchema = recordDef.record;
|
|
1163
|
-
if (typeof recordSchema === "object" && "required" in recordSchema && Array.isArray(recordSchema.required)) {
|
|
1164
|
-
const recordObj = record;
|
|
1165
|
-
for (const requiredField of recordSchema.required) {
|
|
1166
|
-
if (typeof requiredField === "string" && !(requiredField in recordObj)) {
|
|
1167
|
-
return {
|
|
1168
|
-
valid: false,
|
|
1169
|
-
error: `Missing required field: ${requiredField}`,
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
try {
|
|
1176
|
-
this.lexiconsCollection.assertValidRecord(collection, record);
|
|
1177
|
-
return { valid: true };
|
|
1178
|
-
}
|
|
1179
|
-
catch (error) {
|
|
1180
|
-
// If error indicates lexicon not found, treat as validation pass
|
|
1181
|
-
// (the lexicon might exist in Agent's collection but not ours)
|
|
1182
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1183
|
-
if (errorMessage.includes("not found") || errorMessage.includes("Lexicon not found")) {
|
|
1184
|
-
return { valid: true };
|
|
1185
|
-
}
|
|
1186
|
-
return {
|
|
1187
|
-
valid: false,
|
|
1188
|
-
error: errorMessage,
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* Adds all registered lexicons to an AT Protocol Agent instance.
|
|
1194
|
-
*
|
|
1195
|
-
* This allows the Agent to understand custom lexicon types when making
|
|
1196
|
-
* API requests.
|
|
1197
|
-
*
|
|
1198
|
-
* @param agent - The Agent instance to extend
|
|
1199
|
-
*
|
|
1200
|
-
* @remarks
|
|
1201
|
-
* This is called automatically when creating a Repository. You typically
|
|
1202
|
-
* don't need to call this directly unless you're using the Agent
|
|
1203
|
-
* independently.
|
|
1204
|
-
*
|
|
1205
|
-
* @internal
|
|
1206
|
-
*/
|
|
1207
|
-
addToAgent(agent) {
|
|
1208
|
-
// Access the internal lexicons collection and merge our lexicons
|
|
1209
|
-
// The Agent's lex property is a Lexicons instance
|
|
1210
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1211
|
-
const agentLex = agent.lex;
|
|
1212
|
-
// Add each registered lexicon to the agent
|
|
1213
|
-
for (const lexicon of this.lexicons.values()) {
|
|
1214
|
-
agentLex.add(lexicon);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
/**
|
|
1218
|
-
* Gets all registered lexicon IDs.
|
|
1219
|
-
*
|
|
1220
|
-
* @returns Array of lexicon NSIDs
|
|
1221
|
-
*
|
|
1222
|
-
* @example
|
|
1223
|
-
* ```typescript
|
|
1224
|
-
* const ids = registry.getRegisteredIds();
|
|
1225
|
-
* console.log(`Registered lexicons: ${ids.join(", ")}`);
|
|
1226
|
-
* ```
|
|
1227
|
-
*/
|
|
1228
|
-
getRegisteredIds() {
|
|
1229
|
-
return Array.from(this.lexicons.keys());
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Checks if a lexicon is registered.
|
|
1233
|
-
*
|
|
1234
|
-
* @param id - The lexicon NSID to check
|
|
1235
|
-
* @returns `true` if the lexicon is registered
|
|
1236
|
-
*
|
|
1237
|
-
* @example
|
|
1238
|
-
* ```typescript
|
|
1239
|
-
* if (registry.has("org.hypercerts.hypercert")) {
|
|
1240
|
-
* // Hypercert lexicon is available
|
|
1241
|
-
* }
|
|
1242
|
-
* ```
|
|
1243
|
-
*/
|
|
1244
|
-
has(id) {
|
|
1245
|
-
return this.lexicons.has(id);
|
|
1999
|
+
}
|
|
2000
|
+
catch (error) {
|
|
2001
|
+
this.logger?.error("Failed to revoke session", { did, error });
|
|
2002
|
+
throw new AuthenticationError(`Failed to revoke session: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
2003
|
+
}
|
|
1246
2004
|
}
|
|
1247
2005
|
}
|
|
1248
2006
|
|
|
@@ -1374,14 +2132,12 @@ class RecordOperationsImpl {
|
|
|
1374
2132
|
*
|
|
1375
2133
|
* @param agent - AT Protocol Agent for making API calls
|
|
1376
2134
|
* @param repoDid - DID of the repository to operate on
|
|
1377
|
-
* @param lexiconRegistry - Registry for record validation
|
|
1378
2135
|
*
|
|
1379
2136
|
* @internal
|
|
1380
2137
|
*/
|
|
1381
|
-
constructor(agent, repoDid
|
|
2138
|
+
constructor(agent, repoDid) {
|
|
1382
2139
|
this.agent = agent;
|
|
1383
2140
|
this.repoDid = repoDid;
|
|
1384
|
-
this.lexiconRegistry = lexiconRegistry;
|
|
1385
2141
|
}
|
|
1386
2142
|
/**
|
|
1387
2143
|
* Creates a new record in the specified collection.
|
|
@@ -1417,10 +2173,6 @@ class RecordOperationsImpl {
|
|
|
1417
2173
|
* ```
|
|
1418
2174
|
*/
|
|
1419
2175
|
async create(params) {
|
|
1420
|
-
const validation = this.lexiconRegistry.validate(params.collection, params.record);
|
|
1421
|
-
if (!validation.valid) {
|
|
1422
|
-
throw new ValidationError(`Invalid record for collection ${params.collection}: ${validation.error}`);
|
|
1423
|
-
}
|
|
1424
2176
|
try {
|
|
1425
2177
|
const result = await this.agent.com.atproto.repo.createRecord({
|
|
1426
2178
|
repo: this.repoDid,
|
|
@@ -1475,10 +2227,6 @@ class RecordOperationsImpl {
|
|
|
1475
2227
|
* ```
|
|
1476
2228
|
*/
|
|
1477
2229
|
async update(params) {
|
|
1478
|
-
const validation = this.lexiconRegistry.validate(params.collection, params.record);
|
|
1479
|
-
if (!validation.valid) {
|
|
1480
|
-
throw new ValidationError(`Invalid record for collection ${params.collection}: ${validation.error}`);
|
|
1481
|
-
}
|
|
1482
2230
|
try {
|
|
1483
2231
|
const result = await this.agent.com.atproto.repo.putRecord({
|
|
1484
2232
|
repo: this.repoDid,
|
|
@@ -2070,6 +2818,148 @@ class ProfileOperationsImpl {
|
|
|
2070
2818
|
}
|
|
2071
2819
|
}
|
|
2072
2820
|
|
|
2821
|
+
/**
|
|
2822
|
+
* Lexicons entrypoint - Lexicon definitions and registry.
|
|
2823
|
+
*
|
|
2824
|
+
* This sub-entrypoint exports the lexicon registry and hypercert
|
|
2825
|
+
* lexicon constants for working with AT Protocol record schemas.
|
|
2826
|
+
*
|
|
2827
|
+
* @remarks
|
|
2828
|
+
* Import from `@hypercerts-org/sdk/lexicons`:
|
|
2829
|
+
*
|
|
2830
|
+
* ```typescript
|
|
2831
|
+
* import {
|
|
2832
|
+
* LexiconRegistry,
|
|
2833
|
+
* HYPERCERT_LEXICONS,
|
|
2834
|
+
* HYPERCERT_COLLECTIONS,
|
|
2835
|
+
* } from "@hypercerts-org/sdk/lexicons";
|
|
2836
|
+
* ```
|
|
2837
|
+
*
|
|
2838
|
+
* **Exports**:
|
|
2839
|
+
* - {@link LexiconRegistry} - Registry for managing and validating lexicons
|
|
2840
|
+
* - {@link HYPERCERT_LEXICONS} - Array of all hypercert lexicon documents
|
|
2841
|
+
* - {@link HYPERCERT_COLLECTIONS} - Constants for collection NSIDs
|
|
2842
|
+
*
|
|
2843
|
+
* @example Using collection constants
|
|
2844
|
+
* ```typescript
|
|
2845
|
+
* import { HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk/lexicons";
|
|
2846
|
+
*
|
|
2847
|
+
* // List hypercerts using the correct collection name
|
|
2848
|
+
* const records = await repo.records.list({
|
|
2849
|
+
* collection: HYPERCERT_COLLECTIONS.RECORD,
|
|
2850
|
+
* });
|
|
2851
|
+
*
|
|
2852
|
+
* // List contributions
|
|
2853
|
+
* const contributions = await repo.records.list({
|
|
2854
|
+
* collection: HYPERCERT_COLLECTIONS.CONTRIBUTION,
|
|
2855
|
+
* });
|
|
2856
|
+
* ```
|
|
2857
|
+
*
|
|
2858
|
+
* @example Custom lexicon registration
|
|
2859
|
+
* ```typescript
|
|
2860
|
+
* import { LexiconRegistry } from "@hypercerts-org/sdk/lexicons";
|
|
2861
|
+
*
|
|
2862
|
+
* const registry = sdk.getLexiconRegistry();
|
|
2863
|
+
*
|
|
2864
|
+
* // Register custom lexicon
|
|
2865
|
+
* registry.register({
|
|
2866
|
+
* lexicon: 1,
|
|
2867
|
+
* id: "org.myapp.customRecord",
|
|
2868
|
+
* defs: { ... },
|
|
2869
|
+
* });
|
|
2870
|
+
*
|
|
2871
|
+
* // Validate a record
|
|
2872
|
+
* const result = registry.validate("org.myapp.customRecord", record);
|
|
2873
|
+
* if (!result.valid) {
|
|
2874
|
+
* console.error(result.error);
|
|
2875
|
+
* }
|
|
2876
|
+
* ```
|
|
2877
|
+
*
|
|
2878
|
+
* @packageDocumentation
|
|
2879
|
+
*/
|
|
2880
|
+
/**
|
|
2881
|
+
* All hypercert-related lexicons for registration with AT Protocol Agent.
|
|
2882
|
+
* This array contains all lexicon documents from the published package.
|
|
2883
|
+
*/
|
|
2884
|
+
const HYPERCERT_LEXICONS = [
|
|
2885
|
+
lexicon.CERTIFIED_DEFS_LEXICON_JSON,
|
|
2886
|
+
lexicon.LOCATION_LEXICON_JSON,
|
|
2887
|
+
lexicon.STRONGREF_LEXICON_JSON,
|
|
2888
|
+
lexicon.HYPERCERTS_DEFS_LEXICON_JSON,
|
|
2889
|
+
lexicon.ACTIVITY_LEXICON_JSON,
|
|
2890
|
+
lexicon.COLLECTION_LEXICON_JSON,
|
|
2891
|
+
lexicon.CONTRIBUTION_LEXICON_JSON,
|
|
2892
|
+
lexicon.EVALUATION_LEXICON_JSON,
|
|
2893
|
+
lexicon.EVIDENCE_LEXICON_JSON,
|
|
2894
|
+
lexicon.MEASUREMENT_LEXICON_JSON,
|
|
2895
|
+
lexicon.RIGHTS_LEXICON_JSON,
|
|
2896
|
+
lexicon.PROJECT_LEXICON_JSON,
|
|
2897
|
+
lexicon.BADGE_AWARD_LEXICON_JSON,
|
|
2898
|
+
lexicon.BADGE_DEFINITION_LEXICON_JSON,
|
|
2899
|
+
lexicon.BADGE_RESPONSE_LEXICON_JSON,
|
|
2900
|
+
lexicon.FUNDING_RECEIPT_LEXICON_JSON,
|
|
2901
|
+
];
|
|
2902
|
+
/**
|
|
2903
|
+
* Collection NSIDs (Namespaced Identifiers) for hypercert records.
|
|
2904
|
+
*
|
|
2905
|
+
* Use these constants when performing record operations to ensure
|
|
2906
|
+
* correct collection names.
|
|
2907
|
+
*/
|
|
2908
|
+
const HYPERCERT_COLLECTIONS = {
|
|
2909
|
+
/**
|
|
2910
|
+
* Main hypercert claim record collection.
|
|
2911
|
+
*/
|
|
2912
|
+
CLAIM: lexicon.ACTIVITY_NSID,
|
|
2913
|
+
/**
|
|
2914
|
+
* Rights record collection.
|
|
2915
|
+
*/
|
|
2916
|
+
RIGHTS: lexicon.RIGHTS_NSID,
|
|
2917
|
+
/**
|
|
2918
|
+
* Location record collection (shared certified lexicon).
|
|
2919
|
+
*/
|
|
2920
|
+
LOCATION: lexicon.LOCATION_NSID,
|
|
2921
|
+
/**
|
|
2922
|
+
* Contribution record collection.
|
|
2923
|
+
*/
|
|
2924
|
+
CONTRIBUTION: lexicon.CONTRIBUTION_NSID,
|
|
2925
|
+
/**
|
|
2926
|
+
* Measurement record collection.
|
|
2927
|
+
*/
|
|
2928
|
+
MEASUREMENT: lexicon.MEASUREMENT_NSID,
|
|
2929
|
+
/**
|
|
2930
|
+
* Evaluation record collection.
|
|
2931
|
+
*/
|
|
2932
|
+
EVALUATION: lexicon.EVALUATION_NSID,
|
|
2933
|
+
/**
|
|
2934
|
+
* Evidence record collection.
|
|
2935
|
+
*/
|
|
2936
|
+
EVIDENCE: lexicon.EVIDENCE_NSID,
|
|
2937
|
+
/**
|
|
2938
|
+
* Collection record collection (groups of hypercerts).
|
|
2939
|
+
*/
|
|
2940
|
+
COLLECTION: lexicon.COLLECTION_NSID,
|
|
2941
|
+
/**
|
|
2942
|
+
* Project record collection.
|
|
2943
|
+
*/
|
|
2944
|
+
PROJECT: lexicon.PROJECT_NSID,
|
|
2945
|
+
/**
|
|
2946
|
+
* Badge award record collection.
|
|
2947
|
+
*/
|
|
2948
|
+
BADGE_AWARD: lexicon.BADGE_AWARD_NSID,
|
|
2949
|
+
/**
|
|
2950
|
+
* Badge definition record collection.
|
|
2951
|
+
*/
|
|
2952
|
+
BADGE_DEFINITION: lexicon.BADGE_DEFINITION_NSID,
|
|
2953
|
+
/**
|
|
2954
|
+
* Badge response record collection.
|
|
2955
|
+
*/
|
|
2956
|
+
BADGE_RESPONSE: lexicon.BADGE_RESPONSE_NSID,
|
|
2957
|
+
/**
|
|
2958
|
+
* Funding receipt record collection.
|
|
2959
|
+
*/
|
|
2960
|
+
FUNDING_RECEIPT: lexicon.FUNDING_RECEIPT_NSID,
|
|
2961
|
+
};
|
|
2962
|
+
|
|
2073
2963
|
/**
|
|
2074
2964
|
* HypercertOperationsImpl - High-level hypercert operations.
|
|
2075
2965
|
*
|
|
@@ -2133,17 +3023,15 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2133
3023
|
* @param agent - AT Protocol Agent for making API calls
|
|
2134
3024
|
* @param repoDid - DID of the repository to operate on
|
|
2135
3025
|
* @param _serverUrl - Server URL (reserved for future use)
|
|
2136
|
-
* @param lexiconRegistry - Registry for record validation
|
|
2137
3026
|
* @param logger - Optional logger for debugging
|
|
2138
3027
|
*
|
|
2139
3028
|
* @internal
|
|
2140
3029
|
*/
|
|
2141
|
-
constructor(agent, repoDid, _serverUrl,
|
|
3030
|
+
constructor(agent, repoDid, _serverUrl, logger) {
|
|
2142
3031
|
super();
|
|
2143
3032
|
this.agent = agent;
|
|
2144
3033
|
this.repoDid = repoDid;
|
|
2145
3034
|
this._serverUrl = _serverUrl;
|
|
2146
|
-
this.lexiconRegistry = lexiconRegistry;
|
|
2147
3035
|
this.logger = logger;
|
|
2148
3036
|
}
|
|
2149
3037
|
/**
|
|
@@ -2163,6 +3051,202 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2163
3051
|
}
|
|
2164
3052
|
}
|
|
2165
3053
|
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Uploads an image blob and returns a blob reference.
|
|
3056
|
+
*
|
|
3057
|
+
* @param image - Image blob to upload
|
|
3058
|
+
* @param onProgress - Optional progress callback
|
|
3059
|
+
* @returns Promise resolving to blob reference or undefined
|
|
3060
|
+
* @throws {@link NetworkError} if upload fails
|
|
3061
|
+
* @internal
|
|
3062
|
+
*/
|
|
3063
|
+
async uploadImageBlob(image, onProgress) {
|
|
3064
|
+
this.emitProgress(onProgress, { name: "uploadImage", status: "start" });
|
|
3065
|
+
try {
|
|
3066
|
+
const arrayBuffer = await image.arrayBuffer();
|
|
3067
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
3068
|
+
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
3069
|
+
encoding: image.type || "image/jpeg",
|
|
3070
|
+
});
|
|
3071
|
+
if (uploadResult.success) {
|
|
3072
|
+
const blobRef = {
|
|
3073
|
+
$type: "blob",
|
|
3074
|
+
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
3075
|
+
mimeType: uploadResult.data.blob.mimeType,
|
|
3076
|
+
size: uploadResult.data.blob.size,
|
|
3077
|
+
};
|
|
3078
|
+
this.emitProgress(onProgress, {
|
|
3079
|
+
name: "uploadImage",
|
|
3080
|
+
status: "success",
|
|
3081
|
+
data: { size: image.size },
|
|
3082
|
+
});
|
|
3083
|
+
return blobRef;
|
|
3084
|
+
}
|
|
3085
|
+
throw new NetworkError("Image upload succeeded but returned no blob reference");
|
|
3086
|
+
}
|
|
3087
|
+
catch (error) {
|
|
3088
|
+
this.emitProgress(onProgress, { name: "uploadImage", status: "error", error: error });
|
|
3089
|
+
throw new NetworkError(`Failed to upload image: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
/**
|
|
3093
|
+
* Creates a rights record for a hypercert.
|
|
3094
|
+
*
|
|
3095
|
+
* @param rights - Rights data
|
|
3096
|
+
* @param createdAt - ISO timestamp for creation
|
|
3097
|
+
* @param onProgress - Optional progress callback
|
|
3098
|
+
* @returns Promise resolving to rights URI and CID
|
|
3099
|
+
* @throws {@link ValidationError} if validation fails
|
|
3100
|
+
* @throws {@link NetworkError} if creation fails
|
|
3101
|
+
* @internal
|
|
3102
|
+
*/
|
|
3103
|
+
async createRightsRecord(rights, createdAt, onProgress) {
|
|
3104
|
+
this.emitProgress(onProgress, { name: "createRights", status: "start" });
|
|
3105
|
+
const rightsRecord = {
|
|
3106
|
+
$type: HYPERCERT_COLLECTIONS.RIGHTS,
|
|
3107
|
+
rightsName: rights.name,
|
|
3108
|
+
rightsType: rights.type,
|
|
3109
|
+
rightsDescription: rights.description,
|
|
3110
|
+
createdAt,
|
|
3111
|
+
};
|
|
3112
|
+
const rightsValidation = lexicon.validate(rightsRecord, HYPERCERT_COLLECTIONS.RIGHTS, "main", false);
|
|
3113
|
+
if (!rightsValidation.success) {
|
|
3114
|
+
throw new ValidationError(`Invalid rights record: ${rightsValidation.error?.message}`);
|
|
3115
|
+
}
|
|
3116
|
+
const rightsResult = await this.agent.com.atproto.repo.createRecord({
|
|
3117
|
+
repo: this.repoDid,
|
|
3118
|
+
collection: HYPERCERT_COLLECTIONS.RIGHTS,
|
|
3119
|
+
record: rightsRecord,
|
|
3120
|
+
});
|
|
3121
|
+
if (!rightsResult.success) {
|
|
3122
|
+
throw new NetworkError("Failed to create rights record");
|
|
3123
|
+
}
|
|
3124
|
+
const uri = rightsResult.data.uri;
|
|
3125
|
+
const cid = rightsResult.data.cid;
|
|
3126
|
+
this.emit("rightsCreated", { uri, cid });
|
|
3127
|
+
this.emitProgress(onProgress, {
|
|
3128
|
+
name: "createRights",
|
|
3129
|
+
status: "success",
|
|
3130
|
+
data: { uri },
|
|
3131
|
+
});
|
|
3132
|
+
return { uri, cid };
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Creates the main hypercert record.
|
|
3136
|
+
*
|
|
3137
|
+
* @param params - Hypercert creation parameters
|
|
3138
|
+
* @param rightsUri - URI of the associated rights record
|
|
3139
|
+
* @param rightsCid - CID of the associated rights record
|
|
3140
|
+
* @param imageBlobRef - Optional image blob reference
|
|
3141
|
+
* @param createdAt - ISO timestamp for creation
|
|
3142
|
+
* @param onProgress - Optional progress callback
|
|
3143
|
+
* @returns Promise resolving to hypercert URI and CID
|
|
3144
|
+
* @throws {@link ValidationError} if validation fails
|
|
3145
|
+
* @throws {@link NetworkError} if creation fails
|
|
3146
|
+
* @internal
|
|
3147
|
+
*/
|
|
3148
|
+
async createHypercertRecord(params, rightsUri, rightsCid, imageBlobRef, createdAt, onProgress) {
|
|
3149
|
+
this.emitProgress(onProgress, { name: "createHypercert", status: "start" });
|
|
3150
|
+
const hypercertRecord = {
|
|
3151
|
+
$type: HYPERCERT_COLLECTIONS.CLAIM,
|
|
3152
|
+
title: params.title,
|
|
3153
|
+
shortDescription: params.shortDescription,
|
|
3154
|
+
description: params.description,
|
|
3155
|
+
workScope: params.workScope,
|
|
3156
|
+
workTimeFrameFrom: params.workTimeFrameFrom,
|
|
3157
|
+
workTimeFrameTo: params.workTimeFrameTo,
|
|
3158
|
+
rights: { uri: rightsUri, cid: rightsCid },
|
|
3159
|
+
createdAt,
|
|
3160
|
+
};
|
|
3161
|
+
if (imageBlobRef) {
|
|
3162
|
+
hypercertRecord.image = imageBlobRef;
|
|
3163
|
+
}
|
|
3164
|
+
if (params.evidence && params.evidence.length > 0) {
|
|
3165
|
+
hypercertRecord.evidence = params.evidence;
|
|
3166
|
+
}
|
|
3167
|
+
const hypercertValidation = lexicon.validate(hypercertRecord, HYPERCERT_COLLECTIONS.CLAIM, "#main", false);
|
|
3168
|
+
if (!hypercertValidation.success) {
|
|
3169
|
+
throw new ValidationError(`Invalid hypercert record: ${hypercertValidation.error?.message}`);
|
|
3170
|
+
}
|
|
3171
|
+
const hypercertResult = await this.agent.com.atproto.repo.createRecord({
|
|
3172
|
+
repo: this.repoDid,
|
|
3173
|
+
collection: HYPERCERT_COLLECTIONS.CLAIM,
|
|
3174
|
+
record: hypercertRecord,
|
|
3175
|
+
});
|
|
3176
|
+
if (!hypercertResult.success) {
|
|
3177
|
+
throw new NetworkError("Failed to create hypercert record");
|
|
3178
|
+
}
|
|
3179
|
+
const uri = hypercertResult.data.uri;
|
|
3180
|
+
const cid = hypercertResult.data.cid;
|
|
3181
|
+
this.emit("recordCreated", { uri, cid });
|
|
3182
|
+
this.emitProgress(onProgress, {
|
|
3183
|
+
name: "createHypercert",
|
|
3184
|
+
status: "success",
|
|
3185
|
+
data: { uri },
|
|
3186
|
+
});
|
|
3187
|
+
return { uri, cid };
|
|
3188
|
+
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Attaches a location to a hypercert with progress tracking.
|
|
3191
|
+
*
|
|
3192
|
+
* @param hypercertUri - URI of the hypercert
|
|
3193
|
+
* @param location - Location data
|
|
3194
|
+
* @param onProgress - Optional progress callback
|
|
3195
|
+
* @returns Promise resolving to location URI
|
|
3196
|
+
* @internal
|
|
3197
|
+
*/
|
|
3198
|
+
async attachLocationWithProgress(hypercertUri, location, onProgress) {
|
|
3199
|
+
this.emitProgress(onProgress, { name: "attachLocation", status: "start" });
|
|
3200
|
+
try {
|
|
3201
|
+
const locationResult = await this.attachLocation(hypercertUri, location);
|
|
3202
|
+
this.emitProgress(onProgress, {
|
|
3203
|
+
name: "attachLocation",
|
|
3204
|
+
status: "success",
|
|
3205
|
+
data: { uri: locationResult.uri },
|
|
3206
|
+
});
|
|
3207
|
+
return locationResult.uri;
|
|
3208
|
+
}
|
|
3209
|
+
catch (error) {
|
|
3210
|
+
this.emitProgress(onProgress, { name: "attachLocation", status: "error", error: error });
|
|
3211
|
+
this.logger?.warn(`Failed to attach location: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
3212
|
+
throw error;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Creates contribution records with progress tracking.
|
|
3217
|
+
*
|
|
3218
|
+
* @param hypercertUri - URI of the hypercert
|
|
3219
|
+
* @param contributions - Array of contribution data
|
|
3220
|
+
* @param onProgress - Optional progress callback
|
|
3221
|
+
* @returns Promise resolving to array of contribution URIs
|
|
3222
|
+
* @internal
|
|
3223
|
+
*/
|
|
3224
|
+
async createContributionsWithProgress(hypercertUri, contributions, onProgress) {
|
|
3225
|
+
this.emitProgress(onProgress, { name: "createContributions", status: "start" });
|
|
3226
|
+
try {
|
|
3227
|
+
const contributionUris = [];
|
|
3228
|
+
for (const contrib of contributions) {
|
|
3229
|
+
const contribResult = await this.addContribution({
|
|
3230
|
+
hypercertUri,
|
|
3231
|
+
contributors: contrib.contributors,
|
|
3232
|
+
role: contrib.role,
|
|
3233
|
+
description: contrib.description,
|
|
3234
|
+
});
|
|
3235
|
+
contributionUris.push(contribResult.uri);
|
|
3236
|
+
}
|
|
3237
|
+
this.emitProgress(onProgress, {
|
|
3238
|
+
name: "createContributions",
|
|
3239
|
+
status: "success",
|
|
3240
|
+
data: { count: contributionUris.length },
|
|
3241
|
+
});
|
|
3242
|
+
return contributionUris;
|
|
3243
|
+
}
|
|
3244
|
+
catch (error) {
|
|
3245
|
+
this.emitProgress(onProgress, { name: "createContributions", status: "error", error: error });
|
|
3246
|
+
this.logger?.warn(`Failed to create contributions: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
3247
|
+
throw error;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
2166
3250
|
/**
|
|
2167
3251
|
* Creates a new hypercert with all related records.
|
|
2168
3252
|
*
|
|
@@ -2239,142 +3323,31 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2239
3323
|
};
|
|
2240
3324
|
try {
|
|
2241
3325
|
// Step 1: Upload image if provided
|
|
2242
|
-
|
|
2243
|
-
if (params.image) {
|
|
2244
|
-
this.emitProgress(params.onProgress, { name: "uploadImage", status: "start" });
|
|
2245
|
-
try {
|
|
2246
|
-
const arrayBuffer = await params.image.arrayBuffer();
|
|
2247
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
2248
|
-
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
2249
|
-
encoding: params.image.type || "image/jpeg",
|
|
2250
|
-
});
|
|
2251
|
-
if (uploadResult.success) {
|
|
2252
|
-
imageBlobRef = {
|
|
2253
|
-
$type: "blob",
|
|
2254
|
-
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
2255
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
2256
|
-
size: uploadResult.data.blob.size,
|
|
2257
|
-
};
|
|
2258
|
-
}
|
|
2259
|
-
this.emitProgress(params.onProgress, {
|
|
2260
|
-
name: "uploadImage",
|
|
2261
|
-
status: "success",
|
|
2262
|
-
data: { size: params.image.size },
|
|
2263
|
-
});
|
|
2264
|
-
}
|
|
2265
|
-
catch (error) {
|
|
2266
|
-
this.emitProgress(params.onProgress, { name: "uploadImage", status: "error", error: error });
|
|
2267
|
-
throw new NetworkError(`Failed to upload image: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
3326
|
+
const imageBlobRef = params.image ? await this.uploadImageBlob(params.image, params.onProgress) : undefined;
|
|
2270
3327
|
// Step 2: Create rights record
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
rightsName: params.rights.name,
|
|
2275
|
-
rightsType: params.rights.type,
|
|
2276
|
-
rightsDescription: params.rights.description,
|
|
2277
|
-
createdAt,
|
|
2278
|
-
};
|
|
2279
|
-
const rightsValidation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.RIGHTS, rightsRecord);
|
|
2280
|
-
if (!rightsValidation.valid) {
|
|
2281
|
-
throw new ValidationError(`Invalid rights record: ${rightsValidation.error}`);
|
|
2282
|
-
}
|
|
2283
|
-
const rightsResult = await this.agent.com.atproto.repo.createRecord({
|
|
2284
|
-
repo: this.repoDid,
|
|
2285
|
-
collection: lexicon.HYPERCERT_COLLECTIONS.RIGHTS,
|
|
2286
|
-
record: rightsRecord,
|
|
2287
|
-
});
|
|
2288
|
-
if (!rightsResult.success) {
|
|
2289
|
-
throw new NetworkError("Failed to create rights record");
|
|
2290
|
-
}
|
|
2291
|
-
result.rightsUri = rightsResult.data.uri;
|
|
2292
|
-
result.rightsCid = rightsResult.data.cid;
|
|
2293
|
-
this.emit("rightsCreated", { uri: result.rightsUri, cid: result.rightsCid });
|
|
2294
|
-
this.emitProgress(params.onProgress, {
|
|
2295
|
-
name: "createRights",
|
|
2296
|
-
status: "success",
|
|
2297
|
-
data: { uri: result.rightsUri },
|
|
2298
|
-
});
|
|
3328
|
+
const { uri: rightsUri, cid: rightsCid } = await this.createRightsRecord(params.rights, createdAt, params.onProgress);
|
|
3329
|
+
result.rightsUri = rightsUri;
|
|
3330
|
+
result.rightsCid = rightsCid;
|
|
2299
3331
|
// Step 3: Create hypercert record
|
|
2300
|
-
this.
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
title: params.title,
|
|
2304
|
-
shortDescription: params.shortDescription,
|
|
2305
|
-
description: params.description,
|
|
2306
|
-
workScope: params.workScope,
|
|
2307
|
-
workTimeFrameFrom: params.workTimeFrameFrom,
|
|
2308
|
-
workTimeFrameTo: params.workTimeFrameTo,
|
|
2309
|
-
rights: { uri: result.rightsUri, cid: result.rightsCid },
|
|
2310
|
-
createdAt,
|
|
2311
|
-
};
|
|
2312
|
-
if (imageBlobRef) {
|
|
2313
|
-
hypercertRecord.image = imageBlobRef;
|
|
2314
|
-
}
|
|
2315
|
-
if (params.evidence && params.evidence.length > 0) {
|
|
2316
|
-
hypercertRecord.evidence = params.evidence;
|
|
2317
|
-
}
|
|
2318
|
-
const hypercertValidation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.CLAIM, hypercertRecord);
|
|
2319
|
-
if (!hypercertValidation.valid) {
|
|
2320
|
-
throw new ValidationError(`Invalid hypercert record: ${hypercertValidation.error}`);
|
|
2321
|
-
}
|
|
2322
|
-
const hypercertResult = await this.agent.com.atproto.repo.createRecord({
|
|
2323
|
-
repo: this.repoDid,
|
|
2324
|
-
collection: lexicon.HYPERCERT_COLLECTIONS.CLAIM,
|
|
2325
|
-
record: hypercertRecord,
|
|
2326
|
-
});
|
|
2327
|
-
if (!hypercertResult.success) {
|
|
2328
|
-
throw new NetworkError("Failed to create hypercert record");
|
|
2329
|
-
}
|
|
2330
|
-
result.hypercertUri = hypercertResult.data.uri;
|
|
2331
|
-
result.hypercertCid = hypercertResult.data.cid;
|
|
2332
|
-
this.emit("recordCreated", { uri: result.hypercertUri, cid: result.hypercertCid });
|
|
2333
|
-
this.emitProgress(params.onProgress, {
|
|
2334
|
-
name: "createHypercert",
|
|
2335
|
-
status: "success",
|
|
2336
|
-
data: { uri: result.hypercertUri },
|
|
2337
|
-
});
|
|
3332
|
+
const { uri: hypercertUri, cid: hypercertCid } = await this.createHypercertRecord(params, rightsUri, rightsCid, imageBlobRef, createdAt, params.onProgress);
|
|
3333
|
+
result.hypercertUri = hypercertUri;
|
|
3334
|
+
result.hypercertCid = hypercertCid;
|
|
2338
3335
|
// Step 4: Attach location if provided
|
|
2339
3336
|
if (params.location) {
|
|
2340
|
-
this.emitProgress(params.onProgress, { name: "attachLocation", status: "start" });
|
|
2341
3337
|
try {
|
|
2342
|
-
|
|
2343
|
-
result.locationUri = locationResult.uri;
|
|
2344
|
-
this.emitProgress(params.onProgress, {
|
|
2345
|
-
name: "attachLocation",
|
|
2346
|
-
status: "success",
|
|
2347
|
-
data: { uri: result.locationUri },
|
|
2348
|
-
});
|
|
3338
|
+
result.locationUri = await this.attachLocationWithProgress(hypercertUri, params.location, params.onProgress);
|
|
2349
3339
|
}
|
|
2350
|
-
catch
|
|
2351
|
-
|
|
2352
|
-
this.logger?.warn(`Failed to attach location: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
3340
|
+
catch {
|
|
3341
|
+
// Error already logged and progress emitted
|
|
2353
3342
|
}
|
|
2354
3343
|
}
|
|
2355
3344
|
// Step 5: Create contributions if provided
|
|
2356
3345
|
if (params.contributions && params.contributions.length > 0) {
|
|
2357
|
-
this.emitProgress(params.onProgress, { name: "createContributions", status: "start" });
|
|
2358
|
-
result.contributionUris = [];
|
|
2359
3346
|
try {
|
|
2360
|
-
|
|
2361
|
-
const contribResult = await this.addContribution({
|
|
2362
|
-
hypercertUri: result.hypercertUri,
|
|
2363
|
-
contributors: contrib.contributors,
|
|
2364
|
-
role: contrib.role,
|
|
2365
|
-
description: contrib.description,
|
|
2366
|
-
});
|
|
2367
|
-
result.contributionUris.push(contribResult.uri);
|
|
2368
|
-
}
|
|
2369
|
-
this.emitProgress(params.onProgress, {
|
|
2370
|
-
name: "createContributions",
|
|
2371
|
-
status: "success",
|
|
2372
|
-
data: { count: result.contributionUris.length },
|
|
2373
|
-
});
|
|
3347
|
+
result.contributionUris = await this.createContributionsWithProgress(hypercertUri, params.contributions, params.onProgress);
|
|
2374
3348
|
}
|
|
2375
|
-
catch
|
|
2376
|
-
|
|
2377
|
-
this.logger?.warn(`Failed to create contributions: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
3349
|
+
catch {
|
|
3350
|
+
// Error already logged and progress emitted
|
|
2378
3351
|
}
|
|
2379
3352
|
}
|
|
2380
3353
|
return result;
|
|
@@ -2476,9 +3449,9 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2476
3449
|
// Preserve existing image
|
|
2477
3450
|
recordForUpdate.image = existingRecord.image;
|
|
2478
3451
|
}
|
|
2479
|
-
const validation =
|
|
2480
|
-
if (!validation.
|
|
2481
|
-
throw new ValidationError(`Invalid hypercert record: ${validation.error}`);
|
|
3452
|
+
const validation = lexicon.validate(recordForUpdate, collection, "main", false);
|
|
3453
|
+
if (!validation.success) {
|
|
3454
|
+
throw new ValidationError(`Invalid hypercert record: ${validation.error?.message}`);
|
|
2482
3455
|
}
|
|
2483
3456
|
const result = await this.agent.com.atproto.repo.putRecord({
|
|
2484
3457
|
repo: this.repoDid,
|
|
@@ -2527,10 +3500,10 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2527
3500
|
if (!result.success) {
|
|
2528
3501
|
throw new NetworkError("Failed to get hypercert");
|
|
2529
3502
|
}
|
|
2530
|
-
// Validate with lexicon
|
|
2531
|
-
const validation =
|
|
2532
|
-
if (!validation.
|
|
2533
|
-
throw new ValidationError(`Invalid hypercert record format: ${validation.error}`);
|
|
3503
|
+
// Validate with lexicon (more lenient - doesn't require $type)
|
|
3504
|
+
const validation = lexicon.validate(result.data.value, HYPERCERT_COLLECTIONS.CLAIM, "main", false);
|
|
3505
|
+
if (!validation.success) {
|
|
3506
|
+
throw new ValidationError(`Invalid hypercert record format: ${validation.error?.message}`);
|
|
2534
3507
|
}
|
|
2535
3508
|
return {
|
|
2536
3509
|
uri: result.data.uri,
|
|
@@ -2566,7 +3539,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2566
3539
|
try {
|
|
2567
3540
|
const result = await this.agent.com.atproto.repo.listRecords({
|
|
2568
3541
|
repo: this.repoDid,
|
|
2569
|
-
collection:
|
|
3542
|
+
collection: HYPERCERT_COLLECTIONS.CLAIM,
|
|
2570
3543
|
limit: params?.limit,
|
|
2571
3544
|
cursor: params?.cursor,
|
|
2572
3545
|
});
|
|
@@ -2704,7 +3677,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2704
3677
|
}
|
|
2705
3678
|
// Build location record according to app.certified.location lexicon
|
|
2706
3679
|
const locationRecord = {
|
|
2707
|
-
$type:
|
|
3680
|
+
$type: HYPERCERT_COLLECTIONS.LOCATION,
|
|
2708
3681
|
lpVersion: "1.0",
|
|
2709
3682
|
srs: location.srs,
|
|
2710
3683
|
locationType,
|
|
@@ -2713,13 +3686,13 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2713
3686
|
name: location.name,
|
|
2714
3687
|
description: location.description,
|
|
2715
3688
|
};
|
|
2716
|
-
const validation =
|
|
2717
|
-
if (!validation.
|
|
2718
|
-
throw new ValidationError(`Invalid location record: ${validation.error}`);
|
|
3689
|
+
const validation = lexicon.validate(locationRecord, HYPERCERT_COLLECTIONS.LOCATION, "main", false);
|
|
3690
|
+
if (!validation.success) {
|
|
3691
|
+
throw new ValidationError(`Invalid location record: ${validation.error?.message}`);
|
|
2719
3692
|
}
|
|
2720
3693
|
const result = await this.agent.com.atproto.repo.createRecord({
|
|
2721
3694
|
repo: this.repoDid,
|
|
2722
|
-
collection:
|
|
3695
|
+
collection: HYPERCERT_COLLECTIONS.LOCATION,
|
|
2723
3696
|
record: locationRecord,
|
|
2724
3697
|
});
|
|
2725
3698
|
if (!result.success) {
|
|
@@ -2798,7 +3771,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2798
3771
|
try {
|
|
2799
3772
|
const createdAt = new Date().toISOString();
|
|
2800
3773
|
const contributionRecord = {
|
|
2801
|
-
$type:
|
|
3774
|
+
$type: HYPERCERT_COLLECTIONS.CONTRIBUTION,
|
|
2802
3775
|
contributors: params.contributors,
|
|
2803
3776
|
role: params.role,
|
|
2804
3777
|
createdAt,
|
|
@@ -2809,13 +3782,13 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2809
3782
|
const hypercert = await this.get(params.hypercertUri);
|
|
2810
3783
|
contributionRecord.hypercert = { uri: hypercert.uri, cid: hypercert.cid };
|
|
2811
3784
|
}
|
|
2812
|
-
const validation =
|
|
2813
|
-
if (!validation.
|
|
2814
|
-
throw new ValidationError(`Invalid contribution record: ${validation.error}`);
|
|
3785
|
+
const validation = lexicon.validate(contributionRecord, HYPERCERT_COLLECTIONS.CONTRIBUTION, "main", false);
|
|
3786
|
+
if (!validation.success) {
|
|
3787
|
+
throw new ValidationError(`Invalid contribution record: ${validation.error?.message}`);
|
|
2815
3788
|
}
|
|
2816
3789
|
const result = await this.agent.com.atproto.repo.createRecord({
|
|
2817
3790
|
repo: this.repoDid,
|
|
2818
|
-
collection:
|
|
3791
|
+
collection: HYPERCERT_COLLECTIONS.CONTRIBUTION,
|
|
2819
3792
|
record: contributionRecord,
|
|
2820
3793
|
});
|
|
2821
3794
|
if (!result.success) {
|
|
@@ -2864,7 +3837,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2864
3837
|
const hypercert = await this.get(params.hypercertUri);
|
|
2865
3838
|
const createdAt = new Date().toISOString();
|
|
2866
3839
|
const measurementRecord = {
|
|
2867
|
-
$type:
|
|
3840
|
+
$type: HYPERCERT_COLLECTIONS.MEASUREMENT,
|
|
2868
3841
|
hypercert: { uri: hypercert.uri, cid: hypercert.cid },
|
|
2869
3842
|
measurers: params.measurers,
|
|
2870
3843
|
metric: params.metric,
|
|
@@ -2873,13 +3846,13 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2873
3846
|
measurementMethodURI: params.methodUri,
|
|
2874
3847
|
evidenceURI: params.evidenceUris,
|
|
2875
3848
|
};
|
|
2876
|
-
const validation =
|
|
2877
|
-
if (!validation.
|
|
2878
|
-
throw new ValidationError(`Invalid measurement record: ${validation.error}`);
|
|
3849
|
+
const validation = lexicon.validate(measurementRecord, HYPERCERT_COLLECTIONS.MEASUREMENT, "main", false);
|
|
3850
|
+
if (!validation.success) {
|
|
3851
|
+
throw new ValidationError(`Invalid measurement record: ${validation.error?.message}`);
|
|
2879
3852
|
}
|
|
2880
3853
|
const result = await this.agent.com.atproto.repo.createRecord({
|
|
2881
3854
|
repo: this.repoDid,
|
|
2882
|
-
collection:
|
|
3855
|
+
collection: HYPERCERT_COLLECTIONS.MEASUREMENT,
|
|
2883
3856
|
record: measurementRecord,
|
|
2884
3857
|
});
|
|
2885
3858
|
if (!result.success) {
|
|
@@ -2920,19 +3893,19 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2920
3893
|
const subject = await this.get(params.subjectUri);
|
|
2921
3894
|
const createdAt = new Date().toISOString();
|
|
2922
3895
|
const evaluationRecord = {
|
|
2923
|
-
$type:
|
|
3896
|
+
$type: HYPERCERT_COLLECTIONS.EVALUATION,
|
|
2924
3897
|
subject: { uri: subject.uri, cid: subject.cid },
|
|
2925
3898
|
evaluators: params.evaluators,
|
|
2926
3899
|
summary: params.summary,
|
|
2927
3900
|
createdAt,
|
|
2928
3901
|
};
|
|
2929
|
-
const validation =
|
|
2930
|
-
if (!validation.
|
|
2931
|
-
throw new ValidationError(`Invalid evaluation record: ${validation.error}`);
|
|
3902
|
+
const validation = lexicon.validate(evaluationRecord, HYPERCERT_COLLECTIONS.EVALUATION, "main", false);
|
|
3903
|
+
if (!validation.success) {
|
|
3904
|
+
throw new ValidationError(`Invalid evaluation record: ${validation.error?.message}`);
|
|
2932
3905
|
}
|
|
2933
3906
|
const result = await this.agent.com.atproto.repo.createRecord({
|
|
2934
3907
|
repo: this.repoDid,
|
|
2935
|
-
collection:
|
|
3908
|
+
collection: HYPERCERT_COLLECTIONS.EVALUATION,
|
|
2936
3909
|
record: evaluationRecord,
|
|
2937
3910
|
});
|
|
2938
3911
|
if (!result.success) {
|
|
@@ -2995,7 +3968,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
2995
3968
|
}
|
|
2996
3969
|
}
|
|
2997
3970
|
const collectionRecord = {
|
|
2998
|
-
$type:
|
|
3971
|
+
$type: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
2999
3972
|
title: params.title,
|
|
3000
3973
|
claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })),
|
|
3001
3974
|
createdAt,
|
|
@@ -3006,13 +3979,13 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3006
3979
|
if (coverPhotoRef) {
|
|
3007
3980
|
collectionRecord.coverPhoto = coverPhotoRef;
|
|
3008
3981
|
}
|
|
3009
|
-
const validation =
|
|
3010
|
-
if (!validation.
|
|
3011
|
-
throw new ValidationError(`Invalid collection record: ${validation.error}`);
|
|
3982
|
+
const validation = lexicon.validate(collectionRecord, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
3983
|
+
if (!validation.success) {
|
|
3984
|
+
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
3012
3985
|
}
|
|
3013
3986
|
const result = await this.agent.com.atproto.repo.createRecord({
|
|
3014
3987
|
repo: this.repoDid,
|
|
3015
|
-
collection:
|
|
3988
|
+
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
3016
3989
|
record: collectionRecord,
|
|
3017
3990
|
});
|
|
3018
3991
|
if (!result.success) {
|
|
@@ -3058,9 +4031,9 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3058
4031
|
throw new NetworkError("Failed to get collection");
|
|
3059
4032
|
}
|
|
3060
4033
|
// Validate with lexicon registry (more lenient - doesn't require $type)
|
|
3061
|
-
const validation =
|
|
3062
|
-
if (!validation.
|
|
3063
|
-
throw new ValidationError(`Invalid collection record format: ${validation.error}`);
|
|
4034
|
+
const validation = lexicon.validate(result.data.value, HYPERCERT_COLLECTIONS.COLLECTION, "#main", false);
|
|
4035
|
+
if (!validation.success) {
|
|
4036
|
+
throw new ValidationError(`Invalid collection record format: ${validation.error?.message}`);
|
|
3064
4037
|
}
|
|
3065
4038
|
return {
|
|
3066
4039
|
uri: result.data.uri,
|
|
@@ -3093,7 +4066,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3093
4066
|
try {
|
|
3094
4067
|
const result = await this.agent.com.atproto.repo.listRecords({
|
|
3095
4068
|
repo: this.repoDid,
|
|
3096
|
-
collection:
|
|
4069
|
+
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
3097
4070
|
limit: params?.limit,
|
|
3098
4071
|
cursor: params?.cursor,
|
|
3099
4072
|
});
|
|
@@ -3857,7 +4830,6 @@ class Repository {
|
|
|
3857
4830
|
* @param session - Authenticated OAuth session
|
|
3858
4831
|
* @param serverUrl - Base URL of the AT Protocol server
|
|
3859
4832
|
* @param repoDid - DID of the repository to operate on
|
|
3860
|
-
* @param lexiconRegistry - Registry for lexicon validation
|
|
3861
4833
|
* @param isSDS - Whether this is a Shared Data Server
|
|
3862
4834
|
* @param logger - Optional logger for debugging
|
|
3863
4835
|
*
|
|
@@ -3867,20 +4839,16 @@ class Repository {
|
|
|
3867
4839
|
*
|
|
3868
4840
|
* @internal
|
|
3869
4841
|
*/
|
|
3870
|
-
constructor(session, serverUrl, repoDid,
|
|
4842
|
+
constructor(session, serverUrl, repoDid, isSDS, logger) {
|
|
3871
4843
|
this.session = session;
|
|
3872
4844
|
this.serverUrl = serverUrl;
|
|
3873
4845
|
this.repoDid = repoDid;
|
|
3874
|
-
this.lexiconRegistry = lexiconRegistry;
|
|
3875
4846
|
this._isSDS = isSDS;
|
|
3876
4847
|
this.logger = logger;
|
|
3877
4848
|
// Create a ConfigurableAgent that routes requests to the specified server URL
|
|
3878
4849
|
// This allows routing to PDS, SDS, or any custom server while maintaining
|
|
3879
4850
|
// the OAuth session's authentication
|
|
3880
4851
|
this.agent = new ConfigurableAgent(session, serverUrl);
|
|
3881
|
-
this.lexiconRegistry.addToAgent(this.agent);
|
|
3882
|
-
// Register hypercert lexicons
|
|
3883
|
-
this.lexiconRegistry.registerMany(lexicon.HYPERCERT_LEXICONS);
|
|
3884
4852
|
}
|
|
3885
4853
|
/**
|
|
3886
4854
|
* The DID (Decentralized Identifier) of this repository.
|
|
@@ -3951,7 +4919,7 @@ class Repository {
|
|
|
3951
4919
|
* ```
|
|
3952
4920
|
*/
|
|
3953
4921
|
repo(did) {
|
|
3954
|
-
return new Repository(this.session, this.serverUrl, did, this.
|
|
4922
|
+
return new Repository(this.session, this.serverUrl, did, this._isSDS, this.logger);
|
|
3955
4923
|
}
|
|
3956
4924
|
/**
|
|
3957
4925
|
* Low-level record operations for CRUD on any AT Protocol record type.
|
|
@@ -3984,7 +4952,7 @@ class Repository {
|
|
|
3984
4952
|
*/
|
|
3985
4953
|
get records() {
|
|
3986
4954
|
if (!this._records) {
|
|
3987
|
-
this._records = new RecordOperationsImpl(this.agent, this.repoDid
|
|
4955
|
+
this._records = new RecordOperationsImpl(this.agent, this.repoDid);
|
|
3988
4956
|
}
|
|
3989
4957
|
return this._records;
|
|
3990
4958
|
}
|
|
@@ -4079,7 +5047,7 @@ class Repository {
|
|
|
4079
5047
|
*/
|
|
4080
5048
|
get hypercerts() {
|
|
4081
5049
|
if (!this._hypercerts) {
|
|
4082
|
-
this._hypercerts = new HypercertOperationsImpl(this.agent, this.repoDid, this.serverUrl, this.
|
|
5050
|
+
this._hypercerts = new HypercertOperationsImpl(this.agent, this.repoDid, this.serverUrl, this.logger);
|
|
4083
5051
|
}
|
|
4084
5052
|
return this._hypercerts;
|
|
4085
5053
|
}
|
|
@@ -4183,9 +5151,34 @@ const OAuthConfigSchema = zod.z.object({
|
|
|
4183
5151
|
redirectUri: zod.z.string().url(),
|
|
4184
5152
|
/**
|
|
4185
5153
|
* OAuth scopes to request, space-separated.
|
|
4186
|
-
*
|
|
5154
|
+
*
|
|
5155
|
+
* Can be a string of space-separated permissions or use the permission system:
|
|
5156
|
+
*
|
|
5157
|
+
* @example Using presets
|
|
5158
|
+
* ```typescript
|
|
5159
|
+
* import { ScopePresets } from '@hypercerts-org/sdk-core';
|
|
5160
|
+
* scope: ScopePresets.EMAIL_AND_PROFILE
|
|
5161
|
+
* ```
|
|
5162
|
+
*
|
|
5163
|
+
* @example Building custom scopes
|
|
5164
|
+
* ```typescript
|
|
5165
|
+
* import { PermissionBuilder, buildScope } from '@hypercerts-org/sdk-core';
|
|
5166
|
+
* scope: buildScope(
|
|
5167
|
+
* new PermissionBuilder()
|
|
5168
|
+
* .accountEmail('read')
|
|
5169
|
+
* .repoWrite('app.bsky.feed.post')
|
|
5170
|
+
* .build()
|
|
5171
|
+
* )
|
|
5172
|
+
* ```
|
|
5173
|
+
*
|
|
5174
|
+
* @example Legacy scopes
|
|
5175
|
+
* ```typescript
|
|
5176
|
+
* scope: "atproto transition:generic"
|
|
5177
|
+
* ```
|
|
5178
|
+
*
|
|
5179
|
+
* @see https://atproto.com/specs/permission for permission details
|
|
4187
5180
|
*/
|
|
4188
|
-
scope: zod.z.string(),
|
|
5181
|
+
scope: zod.z.string().min(1, "OAuth scope is required"),
|
|
4189
5182
|
/**
|
|
4190
5183
|
* URL to your public JWKS (JSON Web Key Set) endpoint.
|
|
4191
5184
|
* Used by the authorization server to verify your client's signatures.
|
|
@@ -4350,8 +5343,6 @@ class ATProtoSDK {
|
|
|
4350
5343
|
this.logger = config.logger;
|
|
4351
5344
|
// Initialize OAuth client
|
|
4352
5345
|
this.oauthClient = new OAuthClient(configWithDefaults);
|
|
4353
|
-
// Initialize lexicon registry
|
|
4354
|
-
this.lexiconRegistry = new LexiconRegistry();
|
|
4355
5346
|
this.logger?.info("ATProto SDK initialized");
|
|
4356
5347
|
}
|
|
4357
5348
|
/**
|
|
@@ -4477,6 +5468,91 @@ class ATProtoSDK {
|
|
|
4477
5468
|
}
|
|
4478
5469
|
return this.oauthClient.revoke(did.trim());
|
|
4479
5470
|
}
|
|
5471
|
+
/**
|
|
5472
|
+
* Gets the account email address from the authenticated session.
|
|
5473
|
+
*
|
|
5474
|
+
* This method retrieves the email address associated with the user's account
|
|
5475
|
+
* by calling the `com.atproto.server.getSession` endpoint. The email will only
|
|
5476
|
+
* be returned if the appropriate OAuth scope was granted during authorization.
|
|
5477
|
+
*
|
|
5478
|
+
* Required OAuth scopes:
|
|
5479
|
+
* - **Granular permissions**: `account:email?action=read` or `account:email`
|
|
5480
|
+
* - **Transitional permissions**: `transition:email`
|
|
5481
|
+
*
|
|
5482
|
+
* @param session - An authenticated OAuth session
|
|
5483
|
+
* @returns A Promise resolving to email info, or `null` if permission not granted
|
|
5484
|
+
* @throws {@link ValidationError} if the session is invalid
|
|
5485
|
+
* @throws {@link NetworkError} if the API request fails
|
|
5486
|
+
*
|
|
5487
|
+
* @example Using granular permissions
|
|
5488
|
+
* ```typescript
|
|
5489
|
+
* import { ScopePresets } from '@hypercerts-org/sdk-core';
|
|
5490
|
+
*
|
|
5491
|
+
* // Authorize with email scope
|
|
5492
|
+
* const authUrl = await sdk.authorize("user.bsky.social", {
|
|
5493
|
+
* scope: ScopePresets.EMAIL_READ
|
|
5494
|
+
* });
|
|
5495
|
+
*
|
|
5496
|
+
* // After callback...
|
|
5497
|
+
* const emailInfo = await sdk.getAccountEmail(session);
|
|
5498
|
+
* if (emailInfo) {
|
|
5499
|
+
* console.log(`Email: ${emailInfo.email}`);
|
|
5500
|
+
* console.log(`Confirmed: ${emailInfo.emailConfirmed}`);
|
|
5501
|
+
* } else {
|
|
5502
|
+
* console.log("Email permission not granted");
|
|
5503
|
+
* }
|
|
5504
|
+
* ```
|
|
5505
|
+
*
|
|
5506
|
+
* @example Using transitional permissions (legacy)
|
|
5507
|
+
* ```typescript
|
|
5508
|
+
* // Authorize with transition:email scope
|
|
5509
|
+
* const authUrl = await sdk.authorize("user.bsky.social", {
|
|
5510
|
+
* scope: "atproto transition:email"
|
|
5511
|
+
* });
|
|
5512
|
+
*
|
|
5513
|
+
* // After callback...
|
|
5514
|
+
* const emailInfo = await sdk.getAccountEmail(session);
|
|
5515
|
+
* ```
|
|
5516
|
+
*/
|
|
5517
|
+
async getAccountEmail(session) {
|
|
5518
|
+
if (!session) {
|
|
5519
|
+
throw new ValidationError("Session is required");
|
|
5520
|
+
}
|
|
5521
|
+
try {
|
|
5522
|
+
// Determine PDS URL from session or config
|
|
5523
|
+
const pdsUrl = this.config.servers?.pds;
|
|
5524
|
+
if (!pdsUrl) {
|
|
5525
|
+
throw new ValidationError("PDS server URL not configured");
|
|
5526
|
+
}
|
|
5527
|
+
// Call com.atproto.server.getSession endpoint using session's fetchHandler
|
|
5528
|
+
// which automatically includes proper authorization with DPoP
|
|
5529
|
+
const response = await session.fetchHandler("/xrpc/com.atproto.server.getSession", {
|
|
5530
|
+
method: "GET",
|
|
5531
|
+
headers: {
|
|
5532
|
+
"Content-Type": "application/json",
|
|
5533
|
+
},
|
|
5534
|
+
});
|
|
5535
|
+
if (!response.ok) {
|
|
5536
|
+
throw new NetworkError(`Failed to get session info: ${response.status} ${response.statusText}`);
|
|
5537
|
+
}
|
|
5538
|
+
const data = (await response.json());
|
|
5539
|
+
// Return null if email not present (permission not granted)
|
|
5540
|
+
if (!data.email) {
|
|
5541
|
+
return null;
|
|
5542
|
+
}
|
|
5543
|
+
return {
|
|
5544
|
+
email: data.email,
|
|
5545
|
+
emailConfirmed: data.emailConfirmed ?? false,
|
|
5546
|
+
};
|
|
5547
|
+
}
|
|
5548
|
+
catch (error) {
|
|
5549
|
+
this.logger?.error("Failed to get account email", { error });
|
|
5550
|
+
if (error instanceof ValidationError || error instanceof NetworkError) {
|
|
5551
|
+
throw error;
|
|
5552
|
+
}
|
|
5553
|
+
throw new NetworkError(`Failed to get account email: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
4480
5556
|
/**
|
|
4481
5557
|
* Creates a repository instance for data operations.
|
|
4482
5558
|
*
|
|
@@ -4548,30 +5624,7 @@ class ATProtoSDK {
|
|
|
4548
5624
|
}
|
|
4549
5625
|
// Get repository DID (default to session DID)
|
|
4550
5626
|
const repoDid = session.did || session.sub;
|
|
4551
|
-
return new Repository(session, serverUrl, repoDid,
|
|
4552
|
-
}
|
|
4553
|
-
/**
|
|
4554
|
-
* Gets the lexicon registry for schema validation.
|
|
4555
|
-
*
|
|
4556
|
-
* The lexicon registry manages AT Protocol lexicon schemas used for
|
|
4557
|
-
* validating record data. You can register custom lexicons to extend
|
|
4558
|
-
* the SDK's capabilities.
|
|
4559
|
-
*
|
|
4560
|
-
* @returns The {@link LexiconRegistry} instance
|
|
4561
|
-
*
|
|
4562
|
-
* @example
|
|
4563
|
-
* ```typescript
|
|
4564
|
-
* const registry = sdk.getLexiconRegistry();
|
|
4565
|
-
*
|
|
4566
|
-
* // Register custom lexicons
|
|
4567
|
-
* registry.register(myCustomLexicons);
|
|
4568
|
-
*
|
|
4569
|
-
* // Check if a lexicon is registered
|
|
4570
|
-
* const hasLexicon = registry.has("org.example.myRecord");
|
|
4571
|
-
* ```
|
|
4572
|
-
*/
|
|
4573
|
-
getLexiconRegistry() {
|
|
4574
|
-
return this.lexiconRegistry;
|
|
5627
|
+
return new Repository(session, serverUrl, repoDid, isSDS, this.logger);
|
|
4575
5628
|
}
|
|
4576
5629
|
/**
|
|
4577
5630
|
* The configured PDS (Personal Data Server) URL.
|
|
@@ -4727,6 +5780,18 @@ const CollaboratorSchema = zod.z.object({
|
|
|
4727
5780
|
revokedAt: zod.z.string().optional(),
|
|
4728
5781
|
});
|
|
4729
5782
|
|
|
5783
|
+
Object.defineProperty(exports, "AppCertifiedBadgeAward", {
|
|
5784
|
+
enumerable: true,
|
|
5785
|
+
get: function () { return lexicon.AppCertifiedBadgeAward; }
|
|
5786
|
+
});
|
|
5787
|
+
Object.defineProperty(exports, "AppCertifiedBadgeDefinition", {
|
|
5788
|
+
enumerable: true,
|
|
5789
|
+
get: function () { return lexicon.AppCertifiedBadgeDefinition; }
|
|
5790
|
+
});
|
|
5791
|
+
Object.defineProperty(exports, "AppCertifiedBadgeResponse", {
|
|
5792
|
+
enumerable: true,
|
|
5793
|
+
get: function () { return lexicon.AppCertifiedBadgeResponse; }
|
|
5794
|
+
});
|
|
4730
5795
|
Object.defineProperty(exports, "AppCertifiedLocation", {
|
|
4731
5796
|
enumerable: true,
|
|
4732
5797
|
get: function () { return lexicon.AppCertifiedLocation; }
|
|
@@ -4735,17 +5800,29 @@ Object.defineProperty(exports, "ComAtprotoRepoStrongRef", {
|
|
|
4735
5800
|
enumerable: true,
|
|
4736
5801
|
get: function () { return lexicon.ComAtprotoRepoStrongRef; }
|
|
4737
5802
|
});
|
|
4738
|
-
Object.defineProperty(exports, "
|
|
5803
|
+
Object.defineProperty(exports, "HYPERCERTS_NSIDS", {
|
|
4739
5804
|
enumerable: true,
|
|
4740
|
-
get: function () { return lexicon.
|
|
5805
|
+
get: function () { return lexicon.HYPERCERTS_NSIDS; }
|
|
4741
5806
|
});
|
|
4742
|
-
Object.defineProperty(exports, "
|
|
5807
|
+
Object.defineProperty(exports, "HYPERCERTS_NSIDS_BY_TYPE", {
|
|
4743
5808
|
enumerable: true,
|
|
4744
|
-
get: function () { return lexicon.
|
|
5809
|
+
get: function () { return lexicon.HYPERCERTS_NSIDS_BY_TYPE; }
|
|
4745
5810
|
});
|
|
4746
|
-
Object.defineProperty(exports, "
|
|
5811
|
+
Object.defineProperty(exports, "HYPERCERTS_SCHEMAS", {
|
|
4747
5812
|
enumerable: true,
|
|
4748
|
-
get: function () { return lexicon.
|
|
5813
|
+
get: function () { return lexicon.HYPERCERTS_SCHEMAS; }
|
|
5814
|
+
});
|
|
5815
|
+
Object.defineProperty(exports, "HYPERCERTS_SCHEMA_DICT", {
|
|
5816
|
+
enumerable: true,
|
|
5817
|
+
get: function () { return lexicon.HYPERCERTS_SCHEMA_DICT; }
|
|
5818
|
+
});
|
|
5819
|
+
Object.defineProperty(exports, "OrgHypercertsClaimActivity", {
|
|
5820
|
+
enumerable: true,
|
|
5821
|
+
get: function () { return lexicon.OrgHypercertsClaimActivity; }
|
|
5822
|
+
});
|
|
5823
|
+
Object.defineProperty(exports, "OrgHypercertsClaimCollection", {
|
|
5824
|
+
enumerable: true,
|
|
5825
|
+
get: function () { return lexicon.OrgHypercertsClaimCollection; }
|
|
4749
5826
|
});
|
|
4750
5827
|
Object.defineProperty(exports, "OrgHypercertsClaimContribution", {
|
|
4751
5828
|
enumerable: true,
|
|
@@ -4763,52 +5840,67 @@ Object.defineProperty(exports, "OrgHypercertsClaimMeasurement", {
|
|
|
4763
5840
|
enumerable: true,
|
|
4764
5841
|
get: function () { return lexicon.OrgHypercertsClaimMeasurement; }
|
|
4765
5842
|
});
|
|
4766
|
-
Object.defineProperty(exports, "
|
|
4767
|
-
enumerable: true,
|
|
4768
|
-
get: function () { return lexicon.OrgHypercertsClaimRights; }
|
|
4769
|
-
});
|
|
4770
|
-
Object.defineProperty(exports, "OrgHypercertsCollection", {
|
|
4771
|
-
enumerable: true,
|
|
4772
|
-
get: function () { return lexicon.OrgHypercertsCollection; }
|
|
4773
|
-
});
|
|
4774
|
-
Object.defineProperty(exports, "ids", {
|
|
5843
|
+
Object.defineProperty(exports, "OrgHypercertsClaimProject", {
|
|
4775
5844
|
enumerable: true,
|
|
4776
|
-
get: function () { return lexicon.
|
|
5845
|
+
get: function () { return lexicon.OrgHypercertsClaimProject; }
|
|
4777
5846
|
});
|
|
4778
|
-
Object.defineProperty(exports, "
|
|
4779
|
-
enumerable: true,
|
|
4780
|
-
get: function () { return lexicon.lexicons; }
|
|
4781
|
-
});
|
|
4782
|
-
Object.defineProperty(exports, "schemaDict", {
|
|
5847
|
+
Object.defineProperty(exports, "OrgHypercertsClaimRights", {
|
|
4783
5848
|
enumerable: true,
|
|
4784
|
-
get: function () { return lexicon.
|
|
5849
|
+
get: function () { return lexicon.OrgHypercertsClaimRights; }
|
|
4785
5850
|
});
|
|
4786
|
-
Object.defineProperty(exports, "
|
|
5851
|
+
Object.defineProperty(exports, "OrgHypercertsFundingReceipt", {
|
|
4787
5852
|
enumerable: true,
|
|
4788
|
-
get: function () { return lexicon.
|
|
5853
|
+
get: function () { return lexicon.OrgHypercertsFundingReceipt; }
|
|
4789
5854
|
});
|
|
4790
5855
|
Object.defineProperty(exports, "validate", {
|
|
4791
5856
|
enumerable: true,
|
|
4792
5857
|
get: function () { return lexicon.validate; }
|
|
4793
5858
|
});
|
|
5859
|
+
exports.ATPROTO_SCOPE = ATPROTO_SCOPE;
|
|
4794
5860
|
exports.ATProtoSDK = ATProtoSDK;
|
|
4795
5861
|
exports.ATProtoSDKConfigSchema = ATProtoSDKConfigSchema;
|
|
4796
5862
|
exports.ATProtoSDKError = ATProtoSDKError;
|
|
5863
|
+
exports.AccountActionSchema = AccountActionSchema;
|
|
5864
|
+
exports.AccountAttrSchema = AccountAttrSchema;
|
|
5865
|
+
exports.AccountPermissionSchema = AccountPermissionSchema;
|
|
4797
5866
|
exports.AuthenticationError = AuthenticationError;
|
|
5867
|
+
exports.BlobPermissionSchema = BlobPermissionSchema;
|
|
4798
5868
|
exports.CollaboratorPermissionsSchema = CollaboratorPermissionsSchema;
|
|
4799
5869
|
exports.CollaboratorSchema = CollaboratorSchema;
|
|
4800
5870
|
exports.ConfigurableAgent = ConfigurableAgent;
|
|
5871
|
+
exports.HYPERCERT_COLLECTIONS = HYPERCERT_COLLECTIONS;
|
|
5872
|
+
exports.HYPERCERT_LEXICONS = HYPERCERT_LEXICONS;
|
|
5873
|
+
exports.IdentityAttrSchema = IdentityAttrSchema;
|
|
5874
|
+
exports.IdentityPermissionSchema = IdentityPermissionSchema;
|
|
4801
5875
|
exports.InMemorySessionStore = InMemorySessionStore;
|
|
4802
5876
|
exports.InMemoryStateStore = InMemoryStateStore;
|
|
4803
|
-
exports.
|
|
5877
|
+
exports.IncludePermissionSchema = IncludePermissionSchema;
|
|
5878
|
+
exports.MimeTypeSchema = MimeTypeSchema;
|
|
4804
5879
|
exports.NetworkError = NetworkError;
|
|
5880
|
+
exports.NsidSchema = NsidSchema;
|
|
4805
5881
|
exports.OAuthConfigSchema = OAuthConfigSchema;
|
|
4806
5882
|
exports.OrganizationSchema = OrganizationSchema;
|
|
5883
|
+
exports.PermissionBuilder = PermissionBuilder;
|
|
5884
|
+
exports.PermissionSchema = PermissionSchema;
|
|
5885
|
+
exports.RepoActionSchema = RepoActionSchema;
|
|
5886
|
+
exports.RepoPermissionSchema = RepoPermissionSchema;
|
|
4807
5887
|
exports.Repository = Repository;
|
|
5888
|
+
exports.RpcPermissionSchema = RpcPermissionSchema;
|
|
4808
5889
|
exports.SDSRequiredError = SDSRequiredError;
|
|
5890
|
+
exports.ScopePresets = ScopePresets;
|
|
4809
5891
|
exports.ServerConfigSchema = ServerConfigSchema;
|
|
4810
5892
|
exports.SessionExpiredError = SessionExpiredError;
|
|
5893
|
+
exports.TRANSITION_SCOPES = TRANSITION_SCOPES;
|
|
4811
5894
|
exports.TimeoutConfigSchema = TimeoutConfigSchema;
|
|
5895
|
+
exports.TransitionScopeSchema = TransitionScopeSchema;
|
|
4812
5896
|
exports.ValidationError = ValidationError;
|
|
5897
|
+
exports.buildScope = buildScope;
|
|
4813
5898
|
exports.createATProtoSDK = createATProtoSDK;
|
|
5899
|
+
exports.hasAllPermissions = hasAllPermissions;
|
|
5900
|
+
exports.hasAnyPermission = hasAnyPermission;
|
|
5901
|
+
exports.hasPermission = hasPermission;
|
|
5902
|
+
exports.mergeScopes = mergeScopes;
|
|
5903
|
+
exports.parseScope = parseScope;
|
|
5904
|
+
exports.removePermissions = removePermissions;
|
|
5905
|
+
exports.validateScope = validateScope;
|
|
4814
5906
|
//# sourceMappingURL=index.cjs.map
|