@opentdf/sdk 0.13.0-beta.119 → 0.13.0-beta.123

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.
Files changed (57) hide show
  1. package/README.md +60 -10
  2. package/dist/cjs/src/access/access-rpc.js +6 -5
  3. package/dist/cjs/src/access.js +18 -5
  4. package/dist/cjs/src/auth/interceptors.js +186 -0
  5. package/dist/cjs/src/auth/oidc.js +5 -3
  6. package/dist/cjs/src/index.js +6 -2
  7. package/dist/cjs/src/opentdf.js +40 -32
  8. package/dist/cjs/src/platform.js +3 -46
  9. package/dist/cjs/src/policy/api.js +9 -5
  10. package/dist/cjs/src/policy/discovery.js +10 -9
  11. package/dist/cjs/tdf3/src/client/index.js +35 -17
  12. package/dist/cjs/tdf3/src/tdf.js +8 -7
  13. package/dist/types/src/access/access-rpc.d.ts +3 -3
  14. package/dist/types/src/access/access-rpc.d.ts.map +1 -1
  15. package/dist/types/src/access.d.ts +3 -3
  16. package/dist/types/src/access.d.ts.map +1 -1
  17. package/dist/types/src/auth/interceptors.d.ts +99 -0
  18. package/dist/types/src/auth/interceptors.d.ts.map +1 -0
  19. package/dist/types/src/auth/oidc.d.ts +1 -1
  20. package/dist/types/src/auth/oidc.d.ts.map +1 -1
  21. package/dist/types/src/index.d.ts +1 -0
  22. package/dist/types/src/index.d.ts.map +1 -1
  23. package/dist/types/src/opentdf.d.ts +18 -15
  24. package/dist/types/src/opentdf.d.ts.map +1 -1
  25. package/dist/types/src/platform.d.ts +6 -3
  26. package/dist/types/src/platform.d.ts.map +1 -1
  27. package/dist/types/src/policy/api.d.ts +3 -3
  28. package/dist/types/src/policy/api.d.ts.map +1 -1
  29. package/dist/types/src/policy/discovery.d.ts +5 -5
  30. package/dist/types/src/policy/discovery.d.ts.map +1 -1
  31. package/dist/types/tdf3/src/client/index.d.ts +10 -1
  32. package/dist/types/tdf3/src/client/index.d.ts.map +1 -1
  33. package/dist/types/tdf3/src/tdf.d.ts +5 -2
  34. package/dist/types/tdf3/src/tdf.d.ts.map +1 -1
  35. package/dist/web/src/access/access-rpc.js +6 -5
  36. package/dist/web/src/access.js +18 -5
  37. package/dist/web/src/auth/interceptors.js +142 -0
  38. package/dist/web/src/auth/oidc.js +5 -3
  39. package/dist/web/src/index.js +2 -1
  40. package/dist/web/src/opentdf.js +40 -32
  41. package/dist/web/src/platform.js +3 -46
  42. package/dist/web/src/policy/api.js +9 -5
  43. package/dist/web/src/policy/discovery.js +10 -9
  44. package/dist/web/tdf3/src/client/index.js +35 -17
  45. package/dist/web/tdf3/src/tdf.js +8 -7
  46. package/package.json +1 -1
  47. package/src/access/access-rpc.ts +5 -5
  48. package/src/access.ts +29 -13
  49. package/src/auth/interceptors.ts +197 -0
  50. package/src/auth/oidc.ts +5 -3
  51. package/src/index.ts +10 -0
  52. package/src/opentdf.ts +54 -34
  53. package/src/platform.ts +8 -52
  54. package/src/policy/api.ts +8 -5
  55. package/src/policy/discovery.ts +9 -9
  56. package/tdf3/src/client/index.ts +46 -17
  57. package/tdf3/src/tdf.ts +14 -11
package/src/access.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AuthProvider } from './auth/auth.js';
1
+ import { type AuthConfig, resolveAuthConfig } from './auth/interceptors.js';
2
2
  import { RewrapResponse } from './platform/kas/kas_pb.js';
3
3
  import { getPlatformUrlFromKasEndpoint, validateSecureUrl } from './utils.js';
4
4
  import { base64 } from './encodings/index.js';
@@ -37,19 +37,28 @@ export type RewrapRequest = {
37
37
  export async function fetchWrappedKey(
38
38
  url: string,
39
39
  signedRequestToken: string,
40
- authProvider: AuthProvider,
40
+ auth: AuthConfig,
41
41
  fulfillableObligationFQNs: string[]
42
42
  ): Promise<RewrapResponse> {
43
43
  const platformUrl = getPlatformUrlFromKasEndpoint(url);
44
+ const { interceptors, authProvider } = resolveAuthConfig(auth);
45
+
46
+ const rpcCall = () =>
47
+ fetchWrappedKeysRpc(
48
+ platformUrl,
49
+ signedRequestToken,
50
+ { interceptors },
51
+ rewrapAdditionalContextHeader(fulfillableObligationFQNs)
52
+ );
53
+
54
+ // When no AuthProvider is available, skip the legacy fallback so the real
55
+ // RPC error propagates instead of being masked by tryPromisesUntilFirstSuccess.
56
+ if (!authProvider) {
57
+ return await rpcCall();
58
+ }
44
59
 
45
60
  return await tryPromisesUntilFirstSuccess(
46
- () =>
47
- fetchWrappedKeysRpc(
48
- platformUrl,
49
- signedRequestToken,
50
- authProvider,
51
- rewrapAdditionalContextHeader(fulfillableObligationFQNs)
52
- ),
61
+ rpcCall,
53
62
  // We intentionally do not provide the rewrap additional context to legacy requests destined for older platforms.
54
63
  // Platforms new enough to have knowledge of obligations will be handling RPC requests successfully.
55
64
  () =>
@@ -164,11 +173,18 @@ export type KasPublicKeyInfo = {
164
173
  */
165
174
  export async function fetchKeyAccessServers(
166
175
  platformUrl: string,
167
- authProvider: AuthProvider
176
+ auth: AuthConfig
168
177
  ): Promise<OriginAllowList> {
169
- return await tryPromisesUntilFirstSuccess(
170
- () => fetchKeyAccessServersRpc(platformUrl, authProvider),
171
- () => fetchKeyAccessServersLegacy(platformUrl, authProvider)
178
+ const { interceptors, authProvider } = resolveAuthConfig(auth);
179
+
180
+ const rpcCall = () => fetchKeyAccessServersRpc(platformUrl, { interceptors });
181
+
182
+ if (!authProvider) {
183
+ return await rpcCall();
184
+ }
185
+
186
+ return await tryPromisesUntilFirstSuccess(rpcCall, () =>
187
+ fetchKeyAccessServersLegacy(platformUrl, authProvider)
172
188
  );
173
189
  }
174
190
 
@@ -0,0 +1,197 @@
1
+ import { type Interceptor } from '@connectrpc/connect';
2
+ export type { Interceptor } from '@connectrpc/connect';
3
+ import { type CryptoService, type KeyPair } from '../../tdf3/src/crypto/declarations.js';
4
+ import * as DefaultCryptoService from '../../tdf3/src/crypto/index.js';
5
+ import DPoP from './dpop.js';
6
+ import { type AuthProvider } from './auth.js';
7
+ import { base64 } from '../encodings/index.js';
8
+
9
+ /**
10
+ * A function that returns a valid access token string.
11
+ * Called per-request; implementations should handle caching/refresh internally.
12
+ */
13
+ export type TokenProvider = () => Promise<string>;
14
+
15
+ /**
16
+ * Options for creating a DPoP-aware auth interceptor.
17
+ */
18
+ export type DPoPInterceptorOptions = {
19
+ /** Function that returns a valid access token (may cache/refresh internally). */
20
+ tokenProvider: TokenProvider;
21
+ /** DPoP signing key pair. If omitted, one is generated automatically. */
22
+ dpopKeys?: KeyPair | Promise<KeyPair>;
23
+ /** CryptoService for signing. Defaults to DefaultCryptoService. */
24
+ cryptoService?: CryptoService;
25
+ };
26
+
27
+ /**
28
+ * A DPoP interceptor that also exposes the resolved signing key pair.
29
+ * TDF encrypt/decrypt needs these keys for request body signing (reqSignature).
30
+ */
31
+ export type DPoPInterceptor = Interceptor & {
32
+ /** The resolved DPoP key pair, for use in TDF request token signing. */
33
+ readonly dpopKeys: Promise<KeyPair>;
34
+ };
35
+
36
+ /**
37
+ * Creates a simple bearer-token interceptor.
38
+ * Calls `tokenProvider()` per-request and sets the `Authorization` header.
39
+ *
40
+ * @param tokenProvider Function returning a valid access token.
41
+ * @returns A Connect RPC Interceptor.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const opentdf = new OpenTDF({
46
+ * interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())],
47
+ * platformUrl: '/api',
48
+ * });
49
+ * ```
50
+ */
51
+ export function authTokenInterceptor(tokenProvider: TokenProvider): Interceptor {
52
+ return (next) => async (req) => {
53
+ const token = await tokenProvider();
54
+ req.header.set('Authorization', `Bearer ${token}`);
55
+ return next(req);
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Creates a DPoP-aware auth interceptor.
61
+ * Per-request: gets token, generates DPoP proof JWT, sets Authorization + DPoP + X-VirtruPubKey headers.
62
+ * Exposes `dpopKeys` for TDF request body signing.
63
+ *
64
+ * @param options DPoP interceptor configuration.
65
+ * @returns A DPoP interceptor with an exposed `dpopKeys` promise.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const dpopInterceptor = authTokenDPoPInterceptor({
70
+ * tokenProvider: () => myAuth.getAccessToken(),
71
+ * });
72
+ * const opentdf = new OpenTDF({
73
+ * interceptors: [dpopInterceptor],
74
+ * dpopKeys: dpopInterceptor.dpopKeys,
75
+ * platformUrl: '/api',
76
+ * });
77
+ * ```
78
+ */
79
+ export function authTokenDPoPInterceptor(options: DPoPInterceptorOptions): DPoPInterceptor {
80
+ const cryptoService = options.cryptoService ?? DefaultCryptoService;
81
+ const dpopKeysPromise: Promise<KeyPair> = options.dpopKeys
82
+ ? Promise.resolve(options.dpopKeys)
83
+ : cryptoService.generateSigningKeyPair();
84
+
85
+ const interceptor: Interceptor = (next) => async (req) => {
86
+ const [token, keys] = await Promise.all([options.tokenProvider(), dpopKeysPromise]);
87
+
88
+ const url = new URL(req.url);
89
+ const httpUri = `${url.origin}${url.pathname}`;
90
+
91
+ // Generate DPoP proof JWT for this request
92
+ const dpopProof = await DPoP(keys, cryptoService, httpUri, 'POST');
93
+
94
+ // Export public key PEM for X-VirtruPubKey header
95
+ const publicKeyPem = await cryptoService.exportPublicKeyPem(keys.publicKey);
96
+
97
+ req.header.set('Authorization', `Bearer ${token}`);
98
+ req.header.set('DPoP', dpopProof);
99
+ req.header.set('X-VirtruPubKey', base64.encode(publicKeyPem));
100
+
101
+ return next(req);
102
+ };
103
+
104
+ // Attach dpopKeys to the interceptor function
105
+ const dpopInterceptor = interceptor as DPoPInterceptor;
106
+ Object.defineProperty(dpopInterceptor, 'dpopKeys', {
107
+ value: dpopKeysPromise,
108
+ writable: false,
109
+ enumerable: true,
110
+ });
111
+
112
+ return dpopInterceptor;
113
+ }
114
+
115
+ /**
116
+ * Creates an interceptor that bridges an existing AuthProvider to the Interceptor pattern.
117
+ * Use this for backwards compatibility when migrating from AuthProvider to interceptors.
118
+ *
119
+ * @param authProvider The legacy AuthProvider to bridge.
120
+ * @returns A Connect RPC Interceptor.
121
+ */
122
+ export function authProviderInterceptor(authProvider: AuthProvider): Interceptor {
123
+ return (next) => async (req) => {
124
+ const url = new URL(req.url);
125
+ const pathOnly = url.pathname;
126
+ // Signs only the path of the url in the request
127
+ let token;
128
+ try {
129
+ token = await authProvider.withCreds({
130
+ url: pathOnly,
131
+ method: 'POST',
132
+ // Start with any headers Connect already has
133
+ headers: {
134
+ ...Object.fromEntries(req.header.entries()),
135
+ 'Content-Type': 'application/json',
136
+ },
137
+ });
138
+ } catch (err) {
139
+ const msg = err instanceof Error ? err.message : String(err);
140
+ if (msg.includes('public key') || msg.includes('updateClientPublicKey')) {
141
+ throw new Error(
142
+ 'PlatformClient: DPoP key binding is not complete. ' +
143
+ 'If you are using OpenTDF with PlatformClient, create OpenTDF first and ' +
144
+ '`await client.ready` before constructing PlatformClient. ' +
145
+ `Original error: ${msg}`
146
+ );
147
+ }
148
+ throw err;
149
+ }
150
+
151
+ Object.entries(token.headers).forEach(([key, value]) => {
152
+ req.header.set(key, value);
153
+ });
154
+
155
+ return await next(req);
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Auth configuration: either a legacy AuthProvider or an object with interceptors.
161
+ */
162
+ export type AuthConfig = AuthProvider | { interceptors: Interceptor[] };
163
+
164
+ /**
165
+ * Type guard for AuthConfig with interceptors.
166
+ */
167
+ export function isInterceptorConfig(auth: AuthConfig): auth is { interceptors: Interceptor[] } {
168
+ return 'interceptors' in auth && Array.isArray((auth as { interceptors: unknown }).interceptors);
169
+ }
170
+
171
+ /**
172
+ * Resolves an AuthConfig into interceptors for use with PlatformClient.
173
+ * If the config is an AuthProvider, it is bridged via authProviderInterceptor.
174
+ */
175
+ export function resolveInterceptors(auth: AuthConfig): Interceptor[] {
176
+ if (isInterceptorConfig(auth)) {
177
+ return auth.interceptors;
178
+ }
179
+ return [authProviderInterceptor(auth)];
180
+ }
181
+
182
+ /**
183
+ * Resolves an AuthConfig into both interceptors and an optional AuthProvider.
184
+ * The AuthProvider is available for legacy code paths that need withCreds().
185
+ */
186
+ export function resolveAuthConfig(auth: AuthConfig): {
187
+ interceptors: Interceptor[];
188
+ authProvider?: AuthProvider;
189
+ } {
190
+ if (isInterceptorConfig(auth)) {
191
+ return { interceptors: auth.interceptors };
192
+ }
193
+ return {
194
+ interceptors: [authProviderInterceptor(auth)],
195
+ authProvider: auth,
196
+ };
197
+ }
package/src/auth/oidc.ts CHANGED
@@ -11,7 +11,7 @@ import { type CryptoService, type KeyPair } from '../../tdf3/src/crypto/declarat
11
11
  export type CommonCredentials = {
12
12
  /** The OIDC client ID used for token issuance and exchange flows */
13
13
  clientId: string;
14
- /** The endpoint of the OIDC IdP to authenticate against, ex. 'https://virtru.com/auth' */
14
+ /** The endpoint of the OIDC IdP to authenticate against, ex. 'https://keycloak.opentdf.local/auth' */
15
15
  oidcOrigin: string;
16
16
  oidcTokenEndpoint?: string;
17
17
  oidcUserInfoEndpoint?: string;
@@ -176,6 +176,8 @@ export class AccessToken {
176
176
  }
177
177
  // Export opaque public key to PEM format for header
178
178
  const publicKeyPem = await this.cryptoService.exportPublicKeyPem(this.signingKey.publicKey);
179
+ // TODO: Rename to X-OpenTDF-PubKey; requires coordinated change with
180
+ // platform Keycloak mapper (lib/fixtures/keycloak.go `client.publickey`).
179
181
  headers['X-VirtruPubKey'] = base64.encode(publicKeyPem);
180
182
  headers.DPoP = await dpopFn(this.signingKey, this.cryptoService, url, 'POST');
181
183
  }
@@ -300,9 +302,9 @@ export class AccessToken {
300
302
  }
301
303
 
302
304
  async withCreds(httpReq: HttpRequest): Promise<HttpRequest> {
303
- if (!this.signingKey) {
305
+ if (this.config.dpopEnabled && !this.signingKey) {
304
306
  throw new ConfigurationError(
305
- 'Client public key was not set via `updateClientPublicKey` or passed in via constructor, cannot fetch OIDC token with valid Virtru claims'
307
+ 'Client public key was not set via `updateClientPublicKey` or passed in via constructor; required when DPoP is enabled'
306
308
  );
307
309
  }
308
310
  const accessToken = (this.currentAccessToken ??= await this.get());
package/src/index.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  export { type AuthProvider, type HttpMethod, HttpRequest, withHeaders } from './auth/auth.js';
2
2
  export * as AuthProviders from './auth/providers.js';
3
+ export {
4
+ authTokenInterceptor,
5
+ authTokenDPoPInterceptor,
6
+ authProviderInterceptor,
7
+ type AuthConfig,
8
+ type DPoPInterceptor,
9
+ type DPoPInterceptorOptions,
10
+ type Interceptor,
11
+ type TokenProvider,
12
+ } from './auth/interceptors.js';
3
13
  export { attributeFQNsAsValues } from './policy/api.js';
4
14
  export {
5
15
  listAttributes,
package/src/opentdf.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type AuthProvider } from './auth/providers.js';
2
+ import { type Interceptor } from '@connectrpc/connect';
2
3
  import { ConfigurationError, InvalidFileError } from './errors.js';
3
4
  export { Client as TDF3Client } from '../tdf3/src/client/index.js';
4
5
  import { Chunker, fromSource, sourceToStream, type Source } from './seekable.js';
@@ -164,8 +165,17 @@ export type OpenTDFOptions = {
164
165
  /** Platform URL. */
165
166
  platformUrl?: string;
166
167
 
167
- /** Auth provider for connections to the policy service and KASes. */
168
- authProvider: AuthProvider;
168
+ /**
169
+ * Connect RPC interceptors for authentication. Preferred over authProvider.
170
+ * Use `authTokenInterceptor()` or `authTokenDPoPInterceptor()` to create interceptors.
171
+ */
172
+ interceptors?: Interceptor[];
173
+
174
+ /**
175
+ * Auth provider for connections to the policy service and KASes.
176
+ * @deprecated since 0.14.0. Use `interceptors` with `authTokenInterceptor()` or `authTokenDPoPInterceptor()` instead.
177
+ */
178
+ authProvider?: AuthProvider;
169
179
 
170
180
  /** Default settings for 'encrypt' type requests. */
171
181
  defaultCreateOptions?: Omit<CreateOptions, 'source'>;
@@ -236,18 +246,10 @@ export type TDFReader = {
236
246
  * It also requires a platform URL to be set, which is used to fetch key access servers and policies.
237
247
  * @example
238
248
  * ```
239
- * import { type Chunker, OpenTDF } from '@opentdf/sdk';
240
- *
241
- * const oidcCredentials: RefreshTokenCredentials = {
242
- * clientId: keycloakClientId,
243
- * exchange: 'refresh',
244
- * refreshToken: refreshToken,
245
- * oidcOrigin: keycloakUrl,
246
- * };
247
- * const authProvider = await AuthProviders.refreshAuthProvider(oidcCredentials);
249
+ * import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk';
248
250
  *
249
251
  * const client = new OpenTDF({
250
- * authProvider,
252
+ * interceptors: [authTokenInterceptor(() => `${myAuth.token.accessToken}`)],
251
253
  * platformUrl: 'https://platform.example.com',
252
254
  * });
253
255
  *
@@ -264,8 +266,10 @@ export class OpenTDF {
264
266
  readonly platformUrl: string;
265
267
  /** The policy service endpoint */
266
268
  readonly policyEndpoint: string;
267
- /** The auth provider for the OpenTDF instance. */
268
- readonly authProvider: AuthProvider;
269
+ /** The auth provider for the OpenTDF instance (deprecated, use interceptors). */
270
+ readonly authProvider?: AuthProvider;
271
+ /** Connect RPC interceptors for authentication. */
272
+ readonly interceptors?: Interceptor[];
269
273
  /** If DPoP is enabled for this instance. */
270
274
  readonly dpopEnabled: boolean;
271
275
  /** Default options for creating TDF objects. */
@@ -283,6 +287,7 @@ export class OpenTDF {
283
287
 
284
288
  constructor({
285
289
  authProvider,
290
+ interceptors,
286
291
  dpopKeys,
287
292
  defaultCreateOptions,
288
293
  defaultReadOptions,
@@ -291,7 +296,11 @@ export class OpenTDF {
291
296
  platformUrl,
292
297
  cryptoService,
293
298
  }: OpenTDFOptions) {
299
+ if (!authProvider && !interceptors?.length) {
300
+ throw new ConfigurationError('Either authProvider or interceptors must be provided.');
301
+ }
294
302
  this.authProvider = authProvider;
303
+ this.interceptors = interceptors;
295
304
  this.defaultCreateOptions = defaultCreateOptions || {};
296
305
  this.defaultReadOptions = defaultReadOptions || {};
297
306
  this.dpopEnabled = !disableDPoP;
@@ -308,6 +317,7 @@ export class OpenTDF {
308
317
  this.dpopKeys = dpopKeys ?? this.cryptoService.generateSigningKeyPair();
309
318
  this.tdf3Client = new TDF3Client({
310
319
  authProvider,
320
+ interceptors,
311
321
  dpopEnabled: this.dpopEnabled,
312
322
  dpopKeys: this.dpopEnabled ? this.dpopKeys : undefined,
313
323
  kasEndpoint: this.platformUrl || 'https://disallow.all.invalid',
@@ -315,21 +325,31 @@ export class OpenTDF {
315
325
  policyEndpoint,
316
326
  cryptoService: this.cryptoService,
317
327
  });
318
- // Eagerly bind DPoP keys to the auth provider so PlatformClient
319
- // can make gRPC calls without waiting for a TDF operation first.
320
- // Note: TDF3Client.createSessionKeys() also calls updateClientPublicKey
321
- // with the same keys, but the duplicate call is benign —
322
- // refreshTokenClaimsWithClientPubkeyIfNeeded short-circuits when
323
- // the signing key hasn't changed.
324
- this.ready = this.dpopEnabled
325
- ? this.dpopKeys.then((keys) => authProvider.updateClientPublicKey(keys))
326
- : Promise.resolve();
327
- // Prevent unhandled rejection if caller doesn't await ready.
328
- // The error will still surface via TDF3Client's own key binding
329
- // when encrypt/decrypt is called.
330
- this.ready.catch((err) => {
331
- console.warn('OpenTDF: DPoP key binding failed during initialization:', err);
332
- });
328
+
329
+ if (interceptors?.length && !authProvider) {
330
+ // Interceptor path: no updateClientPublicKey needed.
331
+ // DPoP key binding is handled by the interceptor itself.
332
+ this.ready = Promise.resolve();
333
+ } else if (authProvider) {
334
+ // Legacy AuthProvider path: eagerly bind DPoP keys to the auth provider
335
+ // so PlatformClient can make gRPC calls without waiting for a TDF
336
+ // operation first.
337
+ // Note: TDF3Client.createSessionKeys() also calls updateClientPublicKey
338
+ // with the same keys, but the duplicate call is benign —
339
+ // refreshTokenClaimsWithClientPubkeyIfNeeded short-circuits when
340
+ // the signing key hasn't changed.
341
+ this.ready = this.dpopEnabled
342
+ ? this.dpopKeys.then((keys) => authProvider.updateClientPublicKey(keys))
343
+ : Promise.resolve();
344
+ // Prevent unhandled rejection if caller doesn't await ready.
345
+ // The error will still surface via TDF3Client's own key binding
346
+ // when encrypt/decrypt is called.
347
+ this.ready.catch((err) => {
348
+ console.warn('OpenTDF: DPoP key binding failed during initialization:', err);
349
+ });
350
+ } else {
351
+ this.ready = Promise.resolve();
352
+ }
333
353
  }
334
354
 
335
355
  /** Creates a new TDF stream. */
@@ -485,9 +505,9 @@ class ZTDFReader {
485
505
 
486
506
  const dpopKeys = await this.client.dpopKeys;
487
507
 
488
- const { authProvider, cryptoService } = this.client;
489
- if (!authProvider) {
490
- throw new ConfigurationError('authProvider is required');
508
+ const { auth, cryptoService } = this.client;
509
+ if (!auth) {
510
+ throw new ConfigurationError('authProvider or interceptors are required');
491
511
  }
492
512
 
493
513
  let allowList: OriginAllowList | undefined;
@@ -498,14 +518,14 @@ class ZTDFReader {
498
518
  this.opts.ignoreAllowlist
499
519
  );
500
520
  } else if (this.opts.platformUrl) {
501
- allowList = await fetchKeyAccessServers(this.opts.platformUrl, authProvider);
521
+ allowList = await fetchKeyAccessServers(this.opts.platformUrl, auth);
502
522
  }
503
523
 
504
524
  const overview = await this.overview;
505
525
  const oldStream = await decryptStreamFrom(
506
526
  {
507
527
  allowList,
508
- authProvider,
528
+ auth,
509
529
  chunker: this.source,
510
530
  concurrencyLimit: 1,
511
531
  cryptoService,
package/src/platform.ts CHANGED
@@ -3,7 +3,8 @@ export * as platformConnectWeb from '@connectrpc/connect-web';
3
3
  export * as platformConnect from '@connectrpc/connect';
4
4
 
5
5
  import { createConnectTransport } from '@connectrpc/connect-web';
6
- import { AuthProvider } from '../tdf3/index.js';
6
+ import type { AuthProvider } from '../tdf3/index.js';
7
+ import { authProviderInterceptor } from './auth/interceptors.js';
7
8
 
8
9
  import { Client, createClient, Interceptor } from '@connectrpc/connect';
9
10
  import { WellKnownService } from './platform/wellknownconfiguration/wellknown_configuration_pb.js';
@@ -44,9 +45,12 @@ export interface PlatformServicesV2 {
44
45
  }
45
46
 
46
47
  export interface PlatformClientOptions {
47
- /** Optional authentication provider for generating auth interceptor. */
48
+ /**
49
+ * Authentication provider for generating auth interceptor.
50
+ * @deprecated since 0.14.0. Use `interceptors` with `authTokenInterceptor()` or `authTokenDPoPInterceptor()` instead.
51
+ */
48
52
  authProvider?: AuthProvider;
49
- /** Array of custom interceptors to apply to rpc requests. */
53
+ /** Array of interceptors to apply to rpc requests. Preferred over authProvider. */
50
54
  interceptors?: Interceptor[];
51
55
  /** Base URL of the platform API. */
52
56
  platformUrl: string;
@@ -85,8 +89,7 @@ export class PlatformClient {
85
89
  const interceptors: Interceptor[] = [];
86
90
 
87
91
  if (options.authProvider) {
88
- const authInterceptor = createAuthInterceptor(options.authProvider);
89
- interceptors.push(authInterceptor);
92
+ interceptors.push(authProviderInterceptor(options.authProvider));
90
93
  }
91
94
 
92
95
  if (options.interceptors?.length) {
@@ -120,50 +123,3 @@ export class PlatformClient {
120
123
  };
121
124
  }
122
125
  }
123
-
124
- /**
125
- * Creates an interceptor that adds authentication headers to outgoing requests.
126
- *
127
- * This function uses the provided `AuthProvider` to generate authentication credentials
128
- * for each request. The `AuthProvider` is expected to implement a `withCreds` method
129
- * that returns an object containing authentication headers. These headers are then
130
- * added to the request before it is sent to the server.
131
- *
132
- */
133
- function createAuthInterceptor(authProvider: AuthProvider): Interceptor {
134
- const authInterceptor: Interceptor = (next) => async (req) => {
135
- const url = new URL(req.url);
136
- const pathOnly = url.pathname;
137
- // Signs only the path of the url in the request
138
- let token;
139
- try {
140
- token = await authProvider.withCreds({
141
- url: pathOnly,
142
- method: 'POST',
143
- // Start with any headers Connect already has
144
- headers: {
145
- ...Object.fromEntries(req.header.entries()),
146
- 'Content-Type': 'application/json',
147
- },
148
- });
149
- } catch (err) {
150
- const msg = err instanceof Error ? err.message : String(err);
151
- if (msg.includes('public key') || msg.includes('updateClientPublicKey')) {
152
- throw new Error(
153
- 'PlatformClient: DPoP key binding is not complete. ' +
154
- 'If you are using OpenTDF with PlatformClient, create OpenTDF first and ' +
155
- '`await client.ready` before constructing PlatformClient. ' +
156
- `Original error: ${msg}`
157
- );
158
- }
159
- throw err;
160
- }
161
-
162
- Object.entries(token.headers).forEach(([key, value]) => {
163
- req.header.set(key, value);
164
- });
165
-
166
- return await next(req);
167
- };
168
- return authInterceptor;
169
- }
package/src/policy/api.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NetworkError } from '../errors.js';
2
- import { AuthProvider } from '../auth/auth.js';
2
+ import { type AuthConfig, resolveInterceptors } from '../auth/interceptors.js';
3
3
  import { extractRpcErrorMessage, getPlatformUrlFromKasEndpoint } from '../utils.js';
4
4
  import { PlatformClient } from '../platform.js';
5
5
  import { Value } from './attributes.js';
@@ -12,11 +12,11 @@ import { ValueSchema } from '../platform/policy/objects_pb.js';
12
12
  // TODO KAS: go over web-sdk and remove policyEndpoint that is only defined to be used here
13
13
  export async function attributeFQNsAsValues(
14
14
  platformUrl: string,
15
- authProvider: AuthProvider,
15
+ auth: AuthConfig,
16
16
  ...fqns: string[]
17
17
  ): Promise<Value[]> {
18
18
  platformUrl = getPlatformUrlFromKasEndpoint(platformUrl);
19
- const platform = new PlatformClient({ authProvider, platformUrl });
19
+ const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
20
20
 
21
21
  let response: GetAttributeValuesByFqnsResponse;
22
22
  try {
@@ -52,7 +52,7 @@ export async function attributeFQNsAsValues(
52
52
  // Get root certificates from a namespace
53
53
  export async function getRootCertsFromNamespace(
54
54
  platformUrl: string,
55
- authProvider?: AuthProvider,
55
+ auth?: AuthConfig,
56
56
  namespaceId?: string,
57
57
  fqn?: string
58
58
  ): Promise<Certificate[]> {
@@ -63,7 +63,10 @@ export async function getRootCertsFromNamespace(
63
63
  throw new Error('Either namespaceId or fqn must be provided');
64
64
  }
65
65
 
66
- const platform = new PlatformClient({ authProvider, platformUrl });
66
+ const platform = new PlatformClient({
67
+ ...(auth ? { interceptors: resolveInterceptors(auth) } : {}),
68
+ platformUrl,
69
+ });
67
70
 
68
71
  let response: GetNamespaceResponse;
69
72
  try {
@@ -1,6 +1,6 @@
1
1
  import { ConnectError, Code } from '@connectrpc/connect';
2
2
  import { AttributeNotFoundError, ConfigurationError, NetworkError } from '../errors.js';
3
- import { type AuthProvider } from '../auth/auth.js';
3
+ import { type AuthConfig, resolveInterceptors } from '../auth/interceptors.js';
4
4
  import { extractRpcErrorMessage, validateSecureUrl } from '../utils.js';
5
5
  import { PlatformClient } from '../platform.js';
6
6
  import type { Attribute } from '../platform/policy/objects_pb.js';
@@ -49,13 +49,13 @@ const ATTRIBUTE_FQN_RE = /^https?:\/\/[a-zA-Z0-9._~%-]+\/attr\/[a-zA-Z0-9._~%-]+
49
49
  */
50
50
  export async function listAttributes(
51
51
  platformUrl: string,
52
- authProvider: AuthProvider,
52
+ auth: AuthConfig,
53
53
  namespace?: string
54
54
  ): Promise<Attribute[]> {
55
55
  if (!validateSecureUrl(platformUrl)) {
56
56
  throw new ConfigurationError('platformUrl must use HTTPS protocol');
57
57
  }
58
- const platform = new PlatformClient({ authProvider, platformUrl });
58
+ const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
59
59
  const result: Attribute[] = [];
60
60
  let nextOffset = 0;
61
61
 
@@ -106,7 +106,7 @@ export async function listAttributes(
106
106
  */
107
107
  export async function validateAttributes(
108
108
  platformUrl: string,
109
- authProvider: AuthProvider,
109
+ auth: AuthConfig,
110
110
  fqns: string[]
111
111
  ): Promise<void> {
112
112
  if (!fqns || fqns.length === 0) {
@@ -129,7 +129,7 @@ export async function validateAttributes(
129
129
  }
130
130
  }
131
131
 
132
- const platform = new PlatformClient({ authProvider, platformUrl });
132
+ const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
133
133
  let resp;
134
134
  try {
135
135
  resp = await platform.v1.attributes.getAttributeValuesByFqns({ fqns });
@@ -159,7 +159,7 @@ export async function validateAttributes(
159
159
  */
160
160
  export async function attributeExists(
161
161
  platformUrl: string,
162
- authProvider: AuthProvider,
162
+ auth: AuthConfig,
163
163
  attributeFqn: string
164
164
  ): Promise<boolean> {
165
165
  if (!validateSecureUrl(platformUrl)) {
@@ -170,7 +170,7 @@ export async function attributeExists(
170
170
  throw new ConfigurationError('invalid attribute FQN format');
171
171
  }
172
172
 
173
- const platform = new PlatformClient({ authProvider, platformUrl });
173
+ const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
174
174
  try {
175
175
  await platform.v1.attributes.getAttribute({
176
176
  identifier: { case: 'fqn', value: attributeFqn },
@@ -199,7 +199,7 @@ export async function attributeExists(
199
199
  */
200
200
  export async function attributeValueExists(
201
201
  platformUrl: string,
202
- authProvider: AuthProvider,
202
+ auth: AuthConfig,
203
203
  valueFqn: string
204
204
  ): Promise<boolean> {
205
205
  if (!validateSecureUrl(platformUrl)) {
@@ -210,7 +210,7 @@ export async function attributeValueExists(
210
210
  throw new ConfigurationError('invalid attribute value FQN format');
211
211
  }
212
212
 
213
- const platform = new PlatformClient({ authProvider, platformUrl });
213
+ const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
214
214
  let resp;
215
215
  try {
216
216
  resp = await platform.v1.attributes.getAttributeValuesByFqns({ fqns: [valueFqn] });