@@ -3,155 +3,170 @@ import { EdDSASigner } from "iso-signatures/signers/eddsa.js";
33import { Store } from "iso-ucan/store" ;
44import { type } from "arktype" ;
55import { capabilities , kvDriver , verifierResolver } from "../ucan" ;
6- import { Capability } from "iso-ucan/capability" ;
76import { IdResolver , MemoryCache } from "@atproto/identity" ;
87import { verifyJwt } from "@atproto/xrpc-server" ;
9- import { DID } from "iso-ucan/types" ;
108import { Invocation } from "iso-ucan/invocation" ;
9+ import { Delegation } from "iso-ucan/delegation" ;
1110
1211const ActorCreateInput = type ( {
13- adminDids : "string[]" ,
12+ adminDids : "string[]" ,
1413} ) ;
15-
1614const ConnParams = type ( {
17- clientDid : "string" ,
18- serviceAuthToken : "string" ,
19- } ) . or ( type . undefined ) ;
20-
21- type State = { privateKey : string ; adminDids : string [ ] } ;
15+ clientDid : "string" ,
16+ serviceAuthToken : "string" ,
17+ } )
18+ . or ( type . undefined )
19+ . or ( type . null ) ;
20+ type State = {
21+ privateKey : string ;
22+ adminDids : string [ ] ;
23+ } ;
2224type ConnState = { did : string ; clientDid : string } | undefined ;
2325type Vars = {
24- signer : EdDSASigner ;
25- store : Store ;
26- idResolver : IdResolver ;
26+ signer : EdDSASigner ;
27+ store : Store ;
28+ idResolver : IdResolver ;
2729} ;
2830
2931export const operatorAuth = actor ( {
30- state : {
31- privateKey : "" ,
32- adminDids : [ ] as string [ ] ,
33- } ,
34- onCreate : async ( c , rawInput ) => {
35- // This is a singleton actor that can only be created as "main"
36- if ( c . key . length != 1 || c . key [ 0 ] != "main" ) {
37- console . error (
38- 'Cannot create operatorAuth actor with key other than ["main"]:' ,
39- c . key ,
40- ) ;
41- // If this actor is not "main" immediately destroy it
42- c . destroy ( ) ;
43- return ;
44- }
45-
46- // Error if no admin DIDs were specified
47- const input = ActorCreateInput ( rawInput ) ;
48- if ( input instanceof type . errors ) {
49- console . error ( "Invalid creation input to operatorAuth:" , input . summary ) ;
50- c . destroy ( ) ;
51- return ;
52- }
53-
54- // Set the admin IDS
55- c . state . adminDids = input . adminDids ;
56-
57- // Generate a new signing key
58- c . state . privateKey = ( await EdDSASigner . generate ( ) ) . export ( ) ;
59- } ,
60- createVars : async ( c ) => {
61- // This can happen if creation fails, but this lifecycle hook will still run
62- if ( c . aborted ) return undefined as any ;
63-
64- return {
65- signer : await EdDSASigner . import ( c . state . privateKey ) ,
66- store : new Store ( kvDriver ( c . kv ) ) ,
67- idResolver : new IdResolver ( {
68- didCache : new MemoryCache ( ) ,
69- } ) ,
70- } ;
71- } ,
72-
73- createConnState : async ( c , rawParams ?) : Promise < ConnState > => {
74- const { idResolver } = c . vars as Vars ;
75-
76- const params = ConnParams ( rawParams ) ;
77- if ( ! params ) return ;
78-
79- try {
80- // Parse parameters
81- if ( params instanceof type . errors ) {
82- throw new UserError (
83- `Failed to parse connection parameters: ${ params . summary } ` ,
84- ) ;
85- }
86-
87- const jwt = params . serviceAuthToken ;
88-
89- const payload = await verifyJwt (
90- jwt ,
91- null ,
92- null ,
93- async ( did , forceRefresh ) => {
94- const atprotoData = await idResolver . did . resolveAtprotoData (
95- did ,
96- forceRefresh ,
97- ) ;
98- return atprotoData . signingKey ;
99- } ,
100- ) ;
101- const did = payload . iss ;
102-
103- if ( ! c . state . adminDids . includes ( did ) ) {
104- throw new UserError ( "User is not an admin." ) ;
105- }
106-
107- return { did, clientDid : params . clientDid } ;
108- } catch ( e ) {
109- console . warn ( "Auth error" , e ) ;
110- return undefined ;
111- }
112- } ,
113-
114- actions : {
115- /** Get the signing key for the operator actor. */
116- signingKey ( c ) {
117- return c . vars . signer . toString ( ) ;
118- } ,
119- requestEchoDelegation : async ( c ) => {
120- if ( ! c . conn . state ?. did ) throw new UserError ( "Not authenticated" ) ;
121-
122- const delegation = await capabilities . Echo . delegate ( {
123- iss : c . vars . signer ,
124- aud : c . conn . state . clientDid as DID ,
125- store : c . vars . store ,
126- sub : c . vars . signer . did ,
127- pol : [ ] ,
128- exp : Math . round ( Date . now ( ) / 1000 ) + 3600 ,
129- } ) ;
130- await c . vars . store . add ( [ delegation ] ) ;
131-
132- return { delegation : delegation . toString ( ) } ;
133- } ,
134- echo : async ( { vars : { signer, store } } , rawInvocation : Uint8Array ) => {
135- const invocation = await Invocation . from ( {
136- bytes : rawInvocation ,
137- audience : signer . verifiableDid ,
138- resolveProof : store . resolveProof . bind ( store ) ,
139- verifierResolver,
140- } ) ;
141-
142- const args = capabilities . Echo . schema ( invocation . payload . args as any ) ;
143- if ( args instanceof type . errors ) {
144- throw new UserError ( `Could not parse arguments: ${ args . summary } ` ) ;
145- }
146-
147- return { content : args . content } ;
148- } ,
149- } ,
32+ state : {
33+ privateKey : "" ,
34+ adminDids : [ ] as string [ ] ,
35+ } ,
36+
37+ onCreate : async ( c , rawInput ) => {
38+ // This is a singleton actor that can only be created as "main"
39+ if ( c . key . length != 1 || c . key [ 0 ] != "main" ) {
40+ console . error (
41+ 'Cannot create operatorAuth actor with key other than ["main"]:' ,
42+ c . key ,
43+ ) ;
44+ // If this actor is not "main" immediately destroy it
45+ c . destroy ( ) ;
46+ return ;
47+ }
48+
49+ // Parse creation args
50+ const input = ActorCreateInput ( rawInput ) ;
51+ if ( input instanceof type . errors ) {
52+ console . error ( "Invalid creation input to operatorAuth:" , input . summary ) ;
53+ c . destroy ( ) ;
54+ return ;
55+ }
56+
57+ // Initialize state
58+ c . state . adminDids = input . adminDids ;
59+ c . state . privateKey = ( await EdDSASigner . generate ( ) ) . export ( ) ;
60+ } ,
61+
62+ createVars : async ( c ) => {
63+ // This can happen if creation fails, but this lifecycle hook will still run
64+ if ( c . aborted ) return undefined as any ;
65+
66+ return {
67+ signer : await EdDSASigner . import ( c . state . privateKey ) ,
68+ store : new Store ( kvDriver ( c . kv ) ) ,
69+ idResolver : new IdResolver ( {
70+ didCache : new MemoryCache ( ) ,
71+ } ) ,
72+ } ;
73+ } ,
74+
75+ createConnState : async ( c , rawParams ?) : Promise < ConnState > => {
76+ const { idResolver } = c . vars as Vars ;
77+
78+ // Parse the connection params or return an unauthenticated connection.
79+ const params = ConnParams ( rawParams ) ;
80+ if ( ! params ) return ;
81+
82+ try {
83+ if ( params instanceof type . errors ) {
84+ throw new UserError (
85+ `Failed to parse connection parameters: ${ params . summary } ` ,
86+ ) ;
87+ }
88+
89+ // Get the ATProto service auth JWT
90+ const jwt = params . serviceAuthToken ;
91+
92+ // Verify the service auth JWT
93+ const payload = await verifyJwt (
94+ jwt ,
95+ null ,
96+ null ,
97+ async ( did , forceRefresh ) => {
98+ const atprotoData = await idResolver . did . resolveAtprotoData (
99+ did ,
100+ forceRefresh ,
101+ ) ;
102+ return atprotoData . signingKey ;
103+ } ,
104+ ) ;
105+
106+ // Get the authenticated DID
107+ const did = payload . iss ;
108+
109+ // Make sure the user is an admin
110+ if ( ! c . state . adminDids . includes ( did ) ) {
111+ throw new UserError ( "User is not an admin." ) ;
112+ }
113+
114+ // Return authenticated connection
115+ return { did, clientDid : params . clientDid } ;
116+ } catch ( e ) {
117+ throw new UserError ( `Authentication error: ${ e } ` ) ;
118+ }
119+ } ,
120+
121+ actions : {
122+ /** Get the signing key for the operator actor. */
123+ signingKey ( c ) {
124+ return c . vars . signer . toString ( ) ;
125+ } ,
126+
127+ /** Get the admin */
128+ requestAdminDelegations : async ( c ) => {
129+ if ( ! c . conn . state ?. did ) throw new UserError ( "Not authenticated" ) ;
130+
131+ const delegations = [
132+ // Create a delegation that allows all access to the operatorAuth actor
133+ await Delegation . create ( {
134+ iss : c . vars . signer ,
135+ aud : c . conn . state . clientDid ,
136+ sub : c . vars . signer . did ,
137+ pol : [ ] ,
138+ exp : Math . round ( Date . now ( ) / 1000 ) + 3600 , // Last one hour
139+ cmd : "/" ,
140+ } ) ,
141+ ] ;
142+
143+ await c . vars . store . add ( delegations ) ;
144+
145+ return { delegations : delegations . map ( ( x ) => x . toString ( ) ) } ;
146+ } ,
147+
148+ /** Simple test endpoint that demonstrates UCAN auth. */
149+ echo : async ( { vars : { signer, store } } , rawInvocation : Uint8Array ) => {
150+ const invocation = await Invocation . from ( {
151+ bytes : rawInvocation ,
152+ audience : signer . verifiableDid ,
153+ resolveProof : store . resolveProof . bind ( store ) ,
154+ verifierResolver,
155+ } ) ;
156+
157+ const args = capabilities . Echo . schema ( invocation . payload . args as any ) ;
158+ if ( args instanceof type . errors ) {
159+ throw new UserError ( `Could not parse arguments: ${ args . summary } ` ) ;
160+ }
161+
162+ return { content : args . content } ;
163+ } ,
164+ } ,
150165} ) satisfies ActorDefinition <
151- State ,
152- typeof ConnParams . infer ,
153- ConnState ,
154- Vars ,
155- typeof ActorCreateInput . infer ,
156- any
166+ State ,
167+ typeof ConnParams . infer ,
168+ ConnState ,
169+ Vars ,
170+ typeof ActorCreateInput . infer ,
171+ any
157172> ;
0 commit comments