@@ -415,3 +415,336 @@ export type PermissionInput = z.input<typeof PermissionSchema>;
415415 * Output type for any permission (after transform).
416416 */
417417export type Permission = z . output < typeof PermissionSchema > ;
418+
419+ /**
420+ * Fluent builder for constructing OAuth permission arrays.
421+ *
422+ * This class provides a convenient, type-safe way to build arrays of permissions
423+ * using method chaining.
424+ *
425+ * @example Basic usage
426+ * ```typescript
427+ * const builder = new PermissionBuilder()
428+ * .accountEmail('read')
429+ * .repoWrite('app.bsky.feed.post')
430+ * .blob(['image/*', 'video/*']);
431+ *
432+ * const permissions = builder.build();
433+ * // Returns: ['account:email?action=read', 'repo:app.bsky.feed.post?action=create&action=update', 'blob:image/*,video/*']
434+ * ```
435+ *
436+ * @example With transitional scopes
437+ * ```typescript
438+ * const builder = new PermissionBuilder()
439+ * .transition('email')
440+ * .transition('generic');
441+ *
442+ * const scopes = builder.build();
443+ * // Returns: ['transition:email', 'transition:generic']
444+ * ```
445+ */
446+ export class PermissionBuilder {
447+ private permissions : string [ ] = [ ] ;
448+
449+ /**
450+ * Add a transitional scope.
451+ *
452+ * @param scope - The transitional scope name ('email', 'generic', or 'chat.bsky')
453+ * @returns This builder for chaining
454+ *
455+ * @example
456+ * ```typescript
457+ * builder.transition('email').transition('generic');
458+ * ```
459+ */
460+ transition ( scope : "email" | "generic" | "chat.bsky" ) : this {
461+ const fullScope = `transition:${ scope } ` ;
462+ const validated = TransitionScopeSchema . parse ( fullScope ) ;
463+ this . permissions . push ( validated ) ;
464+ return this ;
465+ }
466+
467+ /**
468+ * Add an account permission.
469+ *
470+ * @param attr - The account attribute ('email' or 'repo')
471+ * @param action - Optional action ('read' or 'manage')
472+ * @returns This builder for chaining
473+ *
474+ * @example
475+ * ```typescript
476+ * builder.accountEmail('read').accountRepo('manage');
477+ * ```
478+ */
479+ account ( attr : z . infer < typeof AccountAttrSchema > , action ?: z . infer < typeof AccountActionSchema > ) : this {
480+ const permission = AccountPermissionSchema . parse ( {
481+ type : "account" ,
482+ attr,
483+ action,
484+ } ) ;
485+ this . permissions . push ( permission ) ;
486+ return this ;
487+ }
488+
489+ /**
490+ * Convenience method for account:email permission.
491+ *
492+ * @param action - Optional action ('read' or 'manage')
493+ * @returns This builder for chaining
494+ *
495+ * @example
496+ * ```typescript
497+ * builder.accountEmail('read');
498+ * ```
499+ */
500+ accountEmail ( action ?: z . infer < typeof AccountActionSchema > ) : this {
501+ return this . account ( "email" , action ) ;
502+ }
503+
504+ /**
505+ * Convenience method for account:repo permission.
506+ *
507+ * @param action - Optional action ('read' or 'manage')
508+ * @returns This builder for chaining
509+ *
510+ * @example
511+ * ```typescript
512+ * builder.accountRepo('manage');
513+ * ```
514+ */
515+ accountRepo ( action ?: z . infer < typeof AccountActionSchema > ) : this {
516+ return this . account ( "repo" , action ) ;
517+ }
518+
519+ /**
520+ * Add a repository permission.
521+ *
522+ * @param collection - The NSID of the collection or '*' for all
523+ * @param actions - Optional array of actions ('create', 'update', 'delete')
524+ * @returns This builder for chaining
525+ *
526+ * @example
527+ * ```typescript
528+ * builder.repo('app.bsky.feed.post', ['create', 'update']);
529+ * ```
530+ */
531+ repo ( collection : string , actions ?: z . infer < typeof RepoActionSchema > [ ] ) : this {
532+ const permission = RepoPermissionSchema . parse ( {
533+ type : "repo" ,
534+ collection,
535+ actions,
536+ } ) ;
537+ this . permissions . push ( permission ) ;
538+ return this ;
539+ }
540+
541+ /**
542+ * Convenience method for repository write permissions (create + update).
543+ *
544+ * @param collection - The NSID of the collection or '*' for all
545+ * @returns This builder for chaining
546+ *
547+ * @example
548+ * ```typescript
549+ * builder.repoWrite('app.bsky.feed.post');
550+ * ```
551+ */
552+ repoWrite ( collection : string ) : this {
553+ return this . repo ( collection , [ "create" , "update" ] ) ;
554+ }
555+
556+ /**
557+ * Convenience method for repository read permission (no actions).
558+ *
559+ * @param collection - The NSID of the collection or '*' for all
560+ * @returns This builder for chaining
561+ *
562+ * @example
563+ * ```typescript
564+ * builder.repoRead('app.bsky.feed.post');
565+ * ```
566+ */
567+ repoRead ( collection : string ) : this {
568+ return this . repo ( collection , [ ] ) ;
569+ }
570+
571+ /**
572+ * Convenience method for full repository permissions (create + update + delete).
573+ *
574+ * @param collection - The NSID of the collection or '*' for all
575+ * @returns This builder for chaining
576+ *
577+ * @example
578+ * ```typescript
579+ * builder.repoFull('app.bsky.feed.post');
580+ * ```
581+ */
582+ repoFull ( collection : string ) : this {
583+ return this . repo ( collection , [ "create" , "update" , "delete" ] ) ;
584+ }
585+
586+ /**
587+ * Add a blob permission.
588+ *
589+ * @param mimeTypes - Array of MIME types or a single MIME type
590+ * @returns This builder for chaining
591+ *
592+ * @example
593+ * ```typescript
594+ * builder.blob(['image/*', 'video/*']);
595+ * builder.blob('image/*');
596+ * ```
597+ */
598+ blob ( mimeTypes : string | string [ ] ) : this {
599+ const types = Array . isArray ( mimeTypes ) ? mimeTypes : [ mimeTypes ] ;
600+ const permission = BlobPermissionSchema . parse ( {
601+ type : "blob" ,
602+ mimeTypes : types ,
603+ } ) ;
604+ this . permissions . push ( permission ) ;
605+ return this ;
606+ }
607+
608+ /**
609+ * Add an RPC permission.
610+ *
611+ * @param lexicon - The NSID of the lexicon or '*' for all
612+ * @param aud - The audience (DID or URL)
613+ * @param inheritAud - Whether to inherit audience
614+ * @returns This builder for chaining
615+ *
616+ * @example
617+ * ```typescript
618+ * builder.rpc('com.atproto.repo.createRecord', 'did:web:api.example.com');
619+ * ```
620+ */
621+ rpc ( lexicon : string , aud : string , inheritAud ?: boolean ) : this {
622+ const permission = RpcPermissionSchema . parse ( {
623+ type : "rpc" ,
624+ lexicon,
625+ aud,
626+ inheritAud,
627+ } ) ;
628+ this . permissions . push ( permission ) ;
629+ return this ;
630+ }
631+
632+ /**
633+ * Add an identity permission.
634+ *
635+ * @param attr - The identity attribute ('handle' or '*')
636+ * @returns This builder for chaining
637+ *
638+ * @example
639+ * ```typescript
640+ * builder.identity('handle');
641+ * ```
642+ */
643+ identity ( attr : z . infer < typeof IdentityAttrSchema > ) : this {
644+ const permission = IdentityPermissionSchema . parse ( {
645+ type : "identity" ,
646+ attr,
647+ } ) ;
648+ this . permissions . push ( permission ) ;
649+ return this ;
650+ }
651+
652+ /**
653+ * Add an include permission.
654+ *
655+ * @param nsid - The NSID of the scope set to include
656+ * @param aud - Optional audience restriction
657+ * @returns This builder for chaining
658+ *
659+ * @example
660+ * ```typescript
661+ * builder.include('com.example.authBasicFeatures');
662+ * ```
663+ */
664+ include ( nsid : string , aud ?: string ) : this {
665+ const permission = IncludePermissionSchema . parse ( {
666+ type : "include" ,
667+ nsid,
668+ aud,
669+ } ) ;
670+ this . permissions . push ( permission ) ;
671+ return this ;
672+ }
673+
674+ /**
675+ * Add a custom permission string directly (bypasses validation).
676+ *
677+ * Use this for testing or special cases where you need to add
678+ * a permission that doesn't fit the standard types.
679+ *
680+ * @param permission - The permission string
681+ * @returns This builder for chaining
682+ *
683+ * @example
684+ * ```typescript
685+ * builder.custom('atproto');
686+ * ```
687+ */
688+ custom ( permission : string ) : this {
689+ this . permissions . push ( permission ) ;
690+ return this ;
691+ }
692+
693+ /**
694+ * Add the base atproto scope.
695+ *
696+ * @returns This builder for chaining
697+ *
698+ * @example
699+ * ```typescript
700+ * builder.atproto();
701+ * ```
702+ */
703+ atproto ( ) : this {
704+ this . permissions . push ( ATPROTO_SCOPE ) ;
705+ return this ;
706+ }
707+
708+ /**
709+ * Build and return the array of permission strings.
710+ *
711+ * @returns Array of permission strings
712+ *
713+ * @example
714+ * ```typescript
715+ * const permissions = builder.build();
716+ * ```
717+ */
718+ build ( ) : string [ ] {
719+ return [ ...this . permissions ] ;
720+ }
721+
722+ /**
723+ * Clear all permissions from the builder.
724+ *
725+ * @returns This builder for chaining
726+ *
727+ * @example
728+ * ```typescript
729+ * builder.clear().accountEmail('read');
730+ * ```
731+ */
732+ clear ( ) : this {
733+ this . permissions = [ ] ;
734+ return this ;
735+ }
736+
737+ /**
738+ * Get the current number of permissions.
739+ *
740+ * @returns The number of permissions
741+ *
742+ * @example
743+ * ```typescript
744+ * const count = builder.count();
745+ * ```
746+ */
747+ count ( ) : number {
748+ return this . permissions . length ;
749+ }
750+ }
0 commit comments