@@ -95,11 +95,21 @@ export interface ConnectionOptions {
95
95
96
96
/**
97
97
* Optional mapping of gRPC metadata (HTTP headers) to send with each request to the server.
98
+ * Setting the `Authorization` header is mutually exclusive with the {@link apiKey} option.
98
99
*
99
100
* In order to dynamically set metadata, use {@link Connection.withMetadata}
100
101
*/
101
102
metadata ?: Metadata ;
102
103
104
+ /**
105
+ * API key for Temporal. This becomes the "Authorization" HTTP header with "Bearer " prepended.
106
+ * This is mutually exclusive with the `Authorization` header in {@link ConnectionOptions.metadata}.
107
+ *
108
+ * You may provide a static string or a callback. Also see {@link Connection.withApiKey} or
109
+ * {@link Connection.setApiKey}
110
+ */
111
+ apiKey ?: string | ( ( ) => string ) ;
112
+
103
113
/**
104
114
* Milliseconds to wait until establishing a connection with the server.
105
115
*
@@ -113,7 +123,7 @@ export interface ConnectionOptions {
113
123
}
114
124
115
125
export type ConnectionOptionsWithDefaults = Required <
116
- Omit < ConnectionOptions , 'tls' | 'connectTimeout' | 'callCredentials' >
126
+ Omit < ConnectionOptions , 'tls' | 'connectTimeout' | 'callCredentials' | 'apiKey' >
117
127
> & {
118
128
connectTimeoutMs : number ;
119
129
} ;
@@ -142,9 +152,22 @@ function addDefaults(options: ConnectionOptions): ConnectionOptionsWithDefaults
142
152
* - Convert {@link ConnectionOptions.tls} to {@link grpc.ChannelCredentials}
143
153
* - Add the grpc.ssl_target_name_override GRPC {@link ConnectionOptions.channelArgs | channel arg}
144
154
* - Add default port to address if port not specified
155
+ * - Set `Authorization` header based on {@link ConnectionOptions.apiKey}
145
156
*/
146
157
function normalizeGRPCConfig ( options ?: ConnectionOptions ) : ConnectionOptions {
147
158
const { tls : tlsFromConfig , credentials, callCredentials, ...rest } = options || { } ;
159
+ if ( rest . apiKey ) {
160
+ if ( rest . metadata ?. [ 'Authorization' ] ) {
161
+ throw new TypeError (
162
+ 'Both `apiKey` option and `Authorization` header were provided, but only one makes sense to use at a time.'
163
+ ) ;
164
+ }
165
+ if ( credentials !== undefined ) {
166
+ throw new TypeError (
167
+ 'Both `apiKey` and `credentials` ConnectionOptions were provided, but only one makes sense to use at a time'
168
+ ) ;
169
+ }
170
+ }
148
171
if ( rest . address ) {
149
172
// eslint-disable-next-line prefer-const
150
173
let [ host , port ] = rest . address . split ( ':' , 2 ) ;
@@ -189,6 +212,7 @@ export interface RPCImplOptions {
189
212
callContextStorage : AsyncLocalStorage < CallContext > ;
190
213
interceptors ?: grpc . Interceptor [ ] ;
191
214
staticMetadata : Metadata ;
215
+ apiKeyFnRef : { fn ?: ( ) => string } ;
192
216
}
193
217
194
218
export interface ConnectionCtorOptions {
@@ -209,6 +233,7 @@ export interface ConnectionCtorOptions {
209
233
*/
210
234
readonly healthService : HealthService ;
211
235
readonly callContextStorage : AsyncLocalStorage < CallContext > ;
236
+ readonly apiKeyFnRef : { fn ?: ( ) => string } ;
212
237
}
213
238
214
239
/**
@@ -250,9 +275,20 @@ export class Connection {
250
275
public readonly operatorService : OperatorService ;
251
276
public readonly healthService : HealthService ;
252
277
readonly callContextStorage : AsyncLocalStorage < CallContext > ;
278
+ private readonly apiKeyFnRef : { fn ?: ( ) => string } ;
253
279
254
280
protected static createCtorOptions ( options ?: ConnectionOptions ) : ConnectionCtorOptions {
255
- const optionsWithDefaults = addDefaults ( normalizeGRPCConfig ( options ) ) ;
281
+ const normalizedOptions = normalizeGRPCConfig ( options ) ;
282
+ const apiKeyFnRef : { fn ?: ( ) => string } = { } ;
283
+ if ( normalizedOptions . apiKey ) {
284
+ if ( typeof normalizedOptions . apiKey === 'string' ) {
285
+ const apiKey = normalizedOptions . apiKey ;
286
+ apiKeyFnRef . fn = ( ) => apiKey ;
287
+ } else {
288
+ apiKeyFnRef . fn = normalizedOptions . apiKey ;
289
+ }
290
+ }
291
+ const optionsWithDefaults = addDefaults ( normalizedOptions ) ;
256
292
// Allow overriding this
257
293
optionsWithDefaults . metadata [ 'client-name' ] ??= 'temporal-typescript' ;
258
294
optionsWithDefaults . metadata [ 'client-version' ] ??= pkg . version ;
@@ -270,6 +306,7 @@ export class Connection {
270
306
callContextStorage,
271
307
interceptors : optionsWithDefaults ?. interceptors ,
272
308
staticMetadata : optionsWithDefaults . metadata ,
309
+ apiKeyFnRef,
273
310
} ) ;
274
311
const workflowService = WorkflowService . create ( workflowRpcImpl , false , false ) ;
275
312
const operatorRpcImpl = this . generateRPCImplementation ( {
@@ -278,6 +315,7 @@ export class Connection {
278
315
callContextStorage,
279
316
interceptors : optionsWithDefaults ?. interceptors ,
280
317
staticMetadata : optionsWithDefaults . metadata ,
318
+ apiKeyFnRef,
281
319
} ) ;
282
320
const operatorService = OperatorService . create ( operatorRpcImpl , false , false ) ;
283
321
const healthRpcImpl = this . generateRPCImplementation ( {
@@ -286,6 +324,7 @@ export class Connection {
286
324
callContextStorage,
287
325
interceptors : optionsWithDefaults ?. interceptors ,
288
326
staticMetadata : optionsWithDefaults . metadata ,
327
+ apiKeyFnRef,
289
328
} ) ;
290
329
const healthService = HealthService . create ( healthRpcImpl , false , false ) ;
291
330
@@ -296,6 +335,7 @@ export class Connection {
296
335
operatorService,
297
336
healthService,
298
337
options : optionsWithDefaults ,
338
+ apiKeyFnRef,
299
339
} ;
300
340
}
301
341
@@ -359,13 +399,15 @@ export class Connection {
359
399
operatorService,
360
400
healthService,
361
401
callContextStorage,
402
+ apiKeyFnRef,
362
403
} : ConnectionCtorOptions ) {
363
404
this . options = options ;
364
405
this . client = client ;
365
406
this . workflowService = workflowService ;
366
407
this . operatorService = operatorService ;
367
408
this . healthService = healthService ;
368
409
this . callContextStorage = callContextStorage ;
410
+ this . apiKeyFnRef = apiKeyFnRef ;
369
411
}
370
412
371
413
protected static generateRPCImplementation ( {
@@ -374,10 +416,14 @@ export class Connection {
374
416
callContextStorage,
375
417
interceptors,
376
418
staticMetadata,
419
+ apiKeyFnRef,
377
420
} : RPCImplOptions ) : RPCImpl {
378
421
return ( method : { name : string } , requestData : any , callback : grpc . requestCallback < any > ) => {
379
422
const metadataContainer = new grpc . Metadata ( ) ;
380
423
const { metadata, deadline, abortSignal } = callContextStorage . getStore ( ) ?? { } ;
424
+ if ( apiKeyFnRef . fn ) {
425
+ metadataContainer . set ( 'Authorization' , `Bearer ${ apiKeyFnRef . fn ( ) } ` ) ;
426
+ }
381
427
for ( const [ k , v ] of Object . entries ( staticMetadata ) ) {
382
428
metadataContainer . set ( k , v ) ;
383
429
}
@@ -451,7 +497,52 @@ export class Connection {
451
497
*/
452
498
async withMetadata < ReturnType > ( metadata : Metadata , fn : ( ) => Promise < ReturnType > ) : Promise < ReturnType > {
453
499
const cc = this . callContextStorage . getStore ( ) ;
454
- return await this . callContextStorage . run ( { ...cc , metadata : { ...cc ?. metadata , ...metadata } } , fn ) ;
500
+ return await this . callContextStorage . run (
501
+ {
502
+ ...cc ,
503
+ metadata : { ...cc ?. metadata , ...metadata } ,
504
+ } ,
505
+ fn
506
+ ) ;
507
+ }
508
+
509
+ /**
510
+ * Set the apiKey for any service requests executed in `fn`'s scope (thus changing the `Authorization` header).
511
+ *
512
+ * @returns value returned from `fn`
513
+ *
514
+ * @example
515
+ *
516
+ * ```ts
517
+ * const workflowHandle = await conn.withApiKey('secret', () =>
518
+ * conn.withMetadata({ otherKey: 'set' }, () => client.start(options)))
519
+ * );
520
+ * ```
521
+ */
522
+ async withApiKey < ReturnType > ( apiKey : string , fn : ( ) => Promise < ReturnType > ) : Promise < ReturnType > {
523
+ const cc = this . callContextStorage . getStore ( ) ;
524
+ return await this . callContextStorage . run (
525
+ {
526
+ ...cc ,
527
+ metadata : { ...cc ?. metadata , Authorization : `Bearer ${ apiKey } ` } ,
528
+ } ,
529
+ fn
530
+ ) ;
531
+ }
532
+
533
+ /**
534
+ * Set the {@link ConnectionOptions.apiKey} for all subsequent requests. A static string or a
535
+ * callback function may be provided.
536
+ */
537
+ setApiKey ( apiKey : string | ( ( ) => string ) ) : void {
538
+ if ( typeof apiKey === 'string' ) {
539
+ if ( apiKey === '' ) {
540
+ throw new TypeError ( '`apiKey` must not be an empty string' ) ;
541
+ }
542
+ this . apiKeyFnRef . fn = ( ) => apiKey ;
543
+ } else {
544
+ this . apiKeyFnRef . fn = apiKey ;
545
+ }
455
546
}
456
547
457
548
/**
0 commit comments