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