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