diff --git a/code_samples/js_auth_note.mdx b/code_samples/js_auth_note.mdx new file mode 100644 index 00000000..bee547b4 --- /dev/null +++ b/code_samples/js_auth_note.mdx @@ -0,0 +1,5 @@ +:::info JavaScript examples use client credentials for simplicity +The JavaScript SDK is designed for **browser applications**. The examples on this page use `clientCredentialsTokenProvider` because it's self-contained and easy to follow, but it requires a client secret and **must not be used in browser code**. + +In production browser apps, complete an OIDC login flow to obtain a refresh token, then use [`refreshTokenProvider()`](/sdks/authentication#refresh-token). See the [Authentication Decision Guide](/guides/authentication-guide) for help choosing the right method. +::: diff --git a/code_samples/policy_code/create_attribute.mdx b/code_samples/policy_code/create_attribute.mdx index 844b049b..f809e107 100644 --- a/code_samples/policy_code/create_attribute.mdx +++ b/code_samples/policy_code/create_attribute.mdx @@ -73,11 +73,15 @@ import CreateAttributeExample from '@site/code_samples/java/create-attribute.mdx ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum } from '@opentdf/sdk/platform/policy/objects_pb.js'; const platform = new PlatformClient({ - authProvider, + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_namespace.mdx b/code_samples/policy_code/create_namespace.mdx index 954b9345..b6d3c28c 100644 --- a/code_samples/policy_code/create_namespace.mdx +++ b/code_samples/policy_code/create_namespace.mdx @@ -54,10 +54,14 @@ import CreateNamespaceExample from '@site/code_samples/java/create-namespace.mdx ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_condition_set.mdx b/code_samples/policy_code/create_subject_condition_set.mdx index e560520a..1da03c26 100644 --- a/code_samples/policy_code/create_subject_condition_set.mdx +++ b/code_samples/policy_code/create_subject_condition_set.mdx @@ -76,6 +76,7 @@ import CreateSubjectConditionSetExample from '@site/code_samples/java/create-sub ```typescript import { create } from '@bufbuild/protobuf'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { ConditionBooleanTypeEnum, @@ -86,7 +87,10 @@ import { } from '@opentdf/sdk/platform/policy/subjectmapping/subject_mapping_pb.js'; const platform = new PlatformClient({ - authProvider, + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_mapping.mdx b/code_samples/policy_code/create_subject_mapping.mdx index 17e81459..e4742821 100644 --- a/code_samples/policy_code/create_subject_mapping.mdx +++ b/code_samples/policy_code/create_subject_mapping.mdx @@ -63,10 +63,14 @@ import CreateSubjectMappingExample from '@site/code_samples/java/create-subject- ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_attributes.mdx b/code_samples/policy_code/list_attributes.mdx index a224bb43..157897e1 100644 --- a/code_samples/policy_code/list_attributes.mdx +++ b/code_samples/policy_code/list_attributes.mdx @@ -58,10 +58,14 @@ import ListAttributesExample from '@site/code_samples/java/list-attributes.mdx'; ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_namespaces.mdx b/code_samples/policy_code/list_namespaces.mdx index f5dc0bb1..39a6dd6c 100644 --- a/code_samples/policy_code/list_namespaces.mdx +++ b/code_samples/policy_code/list_namespaces.mdx @@ -53,10 +53,14 @@ import ListNamespacesExample from '@site/code_samples/java/list-namespaces.mdx'; ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_subject_mapping.mdx b/code_samples/policy_code/list_subject_mapping.mdx index a0ea3326..8f749add 100644 --- a/code_samples/policy_code/list_subject_mapping.mdx +++ b/code_samples/policy_code/list_subject_mapping.mdx @@ -56,10 +56,14 @@ import ListSubjectMappingsExample from '@site/code_samples/java/list-subject-map ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/docs/guides/authentication-guide.mdx b/docs/guides/authentication-guide.mdx index 95ed9c5e..a9331d25 100644 --- a/docs/guides/authentication-guide.mdx +++ b/docs/guides/authentication-guide.mdx @@ -61,8 +61,9 @@ flowchart TD | Scenario | Recommended Method | Notes | |----------|-------------------|-------| -| Backend service / microservice | [Client Credentials](/sdks/authentication#client-credentials) | Most straightforward. SDK auto-refreshes tokens on expiry. | -| Web application (browser) | [OIDC Login Flow](/sdks/authentication#refresh-token) | JS SDK only. Your app completes an OIDC login to get an access token and optional refresh token. Never expose client secrets in browser code. | +| Backend service / microservice | [Client Credentials](/sdks/authentication#client-credentials) | Most straightforward. Use `clientCredentialsTokenProvider()` — it caches tokens and auto-refreshes on expiry. | +| Web application (browser) with refresh token | [Refresh Token](/sdks/authentication#refresh-token) | JS SDK only. Use `refreshTokenProvider()` with a refresh token from your OIDC login flow. Handles automatic renewal. | +| Web application (browser) with access token only | [Access Token](/sdks/authentication#access-token) | JS SDK only. Use `authTokenInterceptor()` with a function that returns your access token. Use when your IdP doesn't issue refresh tokens. | | CLI tool (interactive user) | `otdfctl auth login` | Opens a browser for OIDC login. See [CLI auth docs](/components/cli/auth/login). | | CI/CD pipeline / automated job | [Client Credentials](/sdks/authentication#client-credentials) | Use a dedicated service account. Rotate secrets regularly. | | Federated identity / SAML | [Token Exchange](/sdks/authentication#token-exchange) | Exchange an existing token for one the platform accepts. | @@ -73,7 +74,7 @@ flowchart TD ### Client Credentials (Backend Services) -The simplest path. Register a client in your IdP, then pass the client ID and secret to the SDK. The SDK handles token acquisition and refresh automatically. +The simplest path. Register a client in your IdP, then use `clientCredentialsTokenProvider()` (JS) or pass the client ID and secret directly (Go/Java). The SDK handles token acquisition, caching, and refresh automatically. **What you need:** A client ID and client secret from your IdP. @@ -81,11 +82,12 @@ See [SDK code examples](/sdks/authentication#client-credentials) for Go, Java, a ### OIDC Login Flow (Browser Apps) -For browser-based applications, your app completes an [OIDC Authorization Code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) to obtain an access token and (optionally) a refresh token from the IdP. The IdP returns both tokens after a successful `/token` call with a valid authorization code — though the refresh token is only issued if the IdP is configured to provide one. You then pass the refresh token to the JS SDK, which uses it to maintain a valid access token. +For browser-based applications, your app completes an [OIDC Authorization Code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) to obtain an access token and (optionally) a refresh token from the IdP. -**What you need:** An access token and optional refresh token from a completed OIDC login flow. +- **If you have a refresh token**, use [`refreshTokenProvider()`](/sdks/authentication#refresh-token) — it handles token exchange and automatic renewal. +- **If you only have an access token** (because your IdP doesn't issue refresh tokens or the grant is disabled for security reasons), use [`authTokenInterceptor()`](/sdks/authentication#access-token) with a function that returns your token. -See [SDK code examples](/sdks/authentication#refresh-token) (JavaScript only). +See SDK code examples: [Refresh Token](/sdks/authentication#refresh-token) | [Access Token](/sdks/authentication#access-token) (JavaScript only). ### Token Exchange (Federated Identity) @@ -165,9 +167,9 @@ OpenTDF does not prescribe a specific deployment strategy. This checklist covers ### "My token expired and I'm getting errors" -- **Client credentials:** The SDK refreshes automatically — if you're seeing expiry errors, check that your IdP is reachable. +- **Client credentials:** The built-in `clientCredentialsTokenProvider()` (JS) and Go/Java SDKs refresh automatically — if you're seeing expiry errors, check that your IdP is reachable. - **OAuth token source (Go):** `WithOAuthAccessTokenSource` does not auto-refresh. Your `TokenSource` implementation must handle refresh. -- **Refresh token (JS):** Ensure the refresh token itself hasn't expired. Long-lived refresh tokens can be configured in your IdP. +- **Refresh token (JS):** `refreshTokenProvider()` handles rotation automatically, but ensure the initial refresh token itself hasn't expired. Long-lived refresh tokens can be configured in your IdP. ### "DPoP-related errors" diff --git a/docs/sdks/authentication.mdx b/docs/sdks/authentication.mdx index e1a3b61b..0fa58b76 100644 --- a/docs/sdks/authentication.mdx +++ b/docs/sdks/authentication.mdx @@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem'; The OpenTDF SDKs authenticate with an [OIDC](https://openid.net/developers/how-connect-works/)-compatible identity provider (IdP) to obtain access tokens for the platform. The platform itself is a **resource server**, not an identity provider — you bring your own IdP (Keycloak is the reference implementation). :::tip Not sure which method to use? -See the [Authentication Decision Guide](/guides/authentication-guide) to choose the right approach for your use case. +The JavaScript SDK is designed for **browser applications**. If your app already has an access token, use [Access Token](#access-token). If your OIDC flow provides a refresh token, use [Refresh Token](#refresh-token) for automatic renewal. For backend scripts and testing, see [Client Credentials](#client-credentials). See the [Authentication Decision Guide](/guides/authentication-guide) for a full comparison. ::: ## Client Credentials @@ -48,30 +48,26 @@ SDK sdk = new SDKBuilder() ```typescript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; - -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'my-client-id', - clientSecret: 'my-client-secret', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', - exchange: 'client', -}); +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; const client = new OpenTDF({ - authProvider, + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); -await client.ready; ``` -:::warning Confidential clients only -`clientSecretAuthProvider` should be used only in Node.js or server-side environments. Never expose client secrets in browser code. +:::warning Server-side only — not for browsers +`clientCredentialsTokenProvider` requires a client secret and is intended for server-side scripts, CLI tools, and CI/CD pipelines. **Never expose client secrets in browser code.** The JavaScript SDK is designed for browser applications — in production, use [`refreshTokenProvider()`](#refresh-token) with a token from your OIDC login flow instead. ::: -**Token refresh:** The SDK automatically obtains a new token when the current one expires. No manual refresh handling is needed. +**Token lifecycle:** The built-in token providers automatically cache tokens and refresh them when they expire (with a 30-second buffer). You can also write your own `TokenProvider` — any `() => Promise` function works. ## Token Exchange @@ -110,20 +106,16 @@ The Java SDK wraps the JWT as a `BearerAccessToken` and performs an RFC 8693 tok ```typescript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; - -const authProvider = await AuthProviders.externalAuthProvider({ - clientId: 'my-client-id', - externalJwt: 'eyJhbGciOi...', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', - exchange: 'external', -}); +import { authTokenInterceptor, externalJwtTokenProvider, OpenTDF } from '@opentdf/sdk'; const client = new OpenTDF({ - authProvider, + interceptors: [authTokenInterceptor(externalJwtTokenProvider({ + clientId: 'my-client-id', + externalJwt: 'eyJhbGciOi...', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); -await client.ready; ``` @@ -138,22 +130,46 @@ Refresh token authentication is currently available in the JavaScript SDK only. ::: ```typescript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, refreshTokenProvider, OpenTDF } from '@opentdf/sdk'; -const authProvider = await AuthProviders.refreshAuthProvider({ - clientId: 'my-client-id', - refreshToken: 'refresh-token-from-login-flow', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', - exchange: 'refresh', +// Use a refresh token obtained from a prior OIDC login flow. +// The provider automatically exchanges it for access tokens and handles rotation. +const client = new OpenTDF({ + interceptors: [authTokenInterceptor(refreshTokenProvider({ + clientId: 'my-app', + refreshToken: 'refresh-token-from-login-flow', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', }); +``` +## Access Token + +Use an **access token** when your application already has a valid token from its own authentication system — for example, from an OIDC library like `oidc-client-ts`, an auth context provider, or a cookie. This is common when the IdP doesn't issue refresh tokens or when your security policy restricts their use. + +:::info JavaScript SDK only +Access token authentication is currently available in the JavaScript SDK only. Go and Java SDKs handle token lifecycle differently — see [Client Credentials](#client-credentials) for backend use cases, or [Custom Token Source](#custom-token-source) for advanced integrations. +::: + +```typescript +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; + +// Pass any () => Promise function that returns a valid access token. +// Example: read from your OIDC library's user manager. const client = new OpenTDF({ - authProvider, + interceptors: [authTokenInterceptor(async () => { + const user = await userManager.getUser(); + return user?.access_token ?? ''; + })], platformUrl: 'http://localhost:8080', }); -await client.ready; ``` +:::warning You manage token lifecycle +Unlike the built-in token providers (`clientCredentialsTokenProvider`, `refreshTokenProvider`), a custom function does not automatically cache or refresh tokens. If your tokens are short-lived, ensure your function returns a fresh token when called — `authTokenInterceptor` calls it on every request. +::: + ## Certificate Exchange (mTLS) Use **certificate-based authentication** in environments that require [mutual TLS (mTLS)](https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/) with client certificates. @@ -206,9 +222,7 @@ Java's TLS configuration is layered on top of another auth method (typically cli -The JavaScript SDK relies on the runtime's TLS configuration (browser or Node.js). Configure client certificates at the environment level rather than in the SDK. - -For Node.js, use the `NODE_EXTRA_CA_CERTS` environment variable or configure the `https` agent. +In browsers, client certificates are managed by the operating system and browser — the user is prompted to select a certificate when the server requests one. No SDK configuration is needed. @@ -265,27 +279,28 @@ SDK sdk = new SDKBuilder() -Implement the `AuthProvider` interface: +Write a custom interceptor for full control over request authentication. This is useful when your app already has its own auth system (e.g., an OIDC library like `oidc-client-ts`, Auth0, or a custom token store): ```typescript -import type { AuthProvider } from '@opentdf/sdk'; - -const customProvider: AuthProvider = { - async updateClientPublicKey(signingKey) { - // Store the signing key for DPoP, if applicable - }, - async withCreds(httpReq) { - // Add authentication headers to the request - httpReq.headers = { - ...httpReq.headers, - Authorization: `Bearer ${await getMyToken()}`, - }; - return httpReq; - }, +import { type Interceptor } from '@connectrpc/connect'; +import { OpenTDF } from '@opentdf/sdk'; + +// Replace this with however your app obtains tokens — +// e.g., from an OIDC library, auth context, or token store. +async function getMyToken(): Promise { + // Example: read from your OIDC library's user manager + // const user = await userManager.getUser(); + // return user?.access_token ?? ''; + throw new Error('Implement getMyToken() for your auth system'); +} + +const myAuthInterceptor: Interceptor = (next) => async (req) => { + req.header.set('Authorization', `Bearer ${await getMyToken()}`); + return next(req); }; const client = new OpenTDF({ - authProvider: customProvider, + interceptors: [myAuthInterceptor], platformUrl: 'http://localhost:8080', }); ``` @@ -301,8 +316,91 @@ const client = new OpenTDF({ |-----|-----------------|---------------| | **Go** | DPoP key auto-generated | `sdk.WithSessionSignerRSA(key)` to provide your own RSA key | | **Java** | Always on (RSA, auto-generated) | `SDKBuilder.srtSigner(signer)` for custom signing | -| **JavaScript** | On by default | `disableDPoP: true` to opt out, `dpopKeys` for custom keys | +| **JavaScript** | Off by default with interceptors | Use `authTokenDPoPInterceptor()` to enable | + + + + +DPoP is enabled by default with an auto-generated key. To provide your own RSA key: + +```go +import "crypto/rsa" + +// Provide your own RSA key for DPoP signing +client, err := sdk.New("http://localhost:8080", + sdk.WithClientCredentials("my-client-id", "my-client-secret", nil), + sdk.WithSessionSignerRSA(myRSAPrivateKey), // *rsa.PrivateKey +) +``` + + + + +DPoP is always on with an auto-generated RSA key. To provide a custom signer: + +```java +SDK sdk = new SDKBuilder() + .platformEndpoint("http://localhost:8080") + .clientSecret("my-client-id", "my-client-secret") + .srtSigner(customSigner) // custom DPoP signer + .build(); +``` + + + + +DPoP is off by default with interceptors. Use `authTokenDPoPInterceptor()` to enable it: + +```typescript +import { authTokenDPoPInterceptor, OpenTDF } from '@opentdf/sdk'; + +// Use any TokenProvider: refreshTokenProvider(), clientCredentialsTokenProvider(), +// externalJwtTokenProvider(), or a custom () => Promise function. +const dpopInterceptor = authTokenDPoPInterceptor({ + tokenProvider: getAccessToken, // your token provider here +}); + +const client = new OpenTDF({ + interceptors: [dpopInterceptor], + dpopKeys: dpopInterceptor.dpopKeys, + platformUrl: 'http://localhost:8080', +}); +``` + + + :::warning Only disable DPoP if your IdP does not support it. DPoP is recommended for production deployments as it provides protection against token exfiltration. ::: + +## Legacy: AuthProvider + +:::info +The `AuthProvider` interface is deprecated as of the interceptor-based auth introduced in `@opentdf/sdk`. Existing code using `AuthProvider` continues to work. For new code, use interceptors as shown above. +::: + +The legacy `AuthProvider` pattern managed token lifecycle internally: + + + + +```typescript +import { AuthProviders, OpenTDF } from '@opentdf/sdk'; + +const authProvider = await AuthProviders.clientSecretAuthProvider({ + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + exchange: 'client', +}); + +const client = new OpenTDF({ + authProvider, + platformUrl: 'http://localhost:8080', +}); +await client.ready; +``` + + + diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 77580c55..ab0ecffc 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -5,6 +5,7 @@ title: Authorization import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # Making Authorization Decisions @@ -27,6 +28,8 @@ OpenTDF's authorization system provides two primary methods for access control: All authorization calls require proper authentication. Here's how to set up the SDK client: + + @@ -94,10 +97,22 @@ public class AuthorizationSetup { ```typescript +// Option 1: Using interceptors (recommended for new code) +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; +import { PlatformClient } from '@opentdf/sdk/platform'; + +const platformClient = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); + +// Option 2: Legacy AuthProvider (deprecated — use interceptors for new code) import { AuthProviders, OpenTDF } from '@opentdf/sdk'; -import { platformConnect, PlatformClient } from '@opentdf/sdk/platform'; -// Option 1: Using AuthProvider (recommended when also using TDF client) const authProvider = await AuthProviders.clientSecretAuthProvider({ clientId: 'opentdf', clientSecret: 'secret', @@ -106,21 +121,9 @@ const authProvider = await AuthProviders.clientSecretAuthProvider({ const client = new OpenTDF({ authProvider, platformUrl: 'http://localhost:8080' }); await client.ready; -const platformClient = new PlatformClient({ - authProvider, - platformUrl: 'http://localhost:8080', -}); - -// Option 2: Using a raw interceptor (when you manage tokens yourself) -const accessToken = 'your-access-token-here'; -const interceptor: platformConnect.Interceptor = (next) => async (req) => { - req.header.set('Authorization', `Bearer ${accessToken}`); - return next(req); -}; - const platformClient2 = new PlatformClient({ + authProvider, platformUrl: 'http://localhost:8080', - interceptors: [interceptor], }); ``` diff --git a/docs/sdks/discovery.mdx b/docs/sdks/discovery.mdx index d7280667..0468b9c9 100644 --- a/docs/sdks/discovery.mdx +++ b/docs/sdks/discovery.mdx @@ -5,6 +5,7 @@ title: Discovery import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # Discovery @@ -12,6 +13,8 @@ Before encrypting data with `CreateTDF`, it helps to verify that the attributes The Go, Java, and JavaScript SDKs provide five methods to discover and validate attributes without manual platform queries. + + --- ## ListAttributes @@ -65,9 +68,14 @@ List attrs = sdk.listAttributes("opentdf.io"); ```ts -import { listAttributes } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, listAttributes } from '@opentdf/sdk'; + +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', +}))] }; -const attrs = await listAttributes(platformUrl, authProvider); +const attrs = await listAttributes(platformUrl, auth); for (const a of attrs) { console.log(a.fqn, a.rule); @@ -80,7 +88,7 @@ for (const a of attrs) { To filter by namespace: ```ts -const attrs = await listAttributes(platformUrl, authProvider, 'opentdf.io'); +const attrs = await listAttributes(platformUrl, auth, 'opentdf.io'); ``` @@ -127,9 +135,14 @@ if (!exists) { ```ts -import { attributeExists } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, attributeExists } from '@opentdf/sdk'; + +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', +}))] }; -const exists = await attributeExists(platformUrl, authProvider, 'https://opentdf.io/attr/department'); +const exists = await attributeExists(platformUrl, auth, 'https://opentdf.io/attr/department'); if (!exists) { console.log('attribute does not exist — create it before use'); } @@ -177,9 +190,14 @@ if (!exists) { ```ts -import { attributeValueExists } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, attributeValueExists } from '@opentdf/sdk'; -const exists = await attributeValueExists(platformUrl, authProvider, 'https://opentdf.io/attr/department/value/finance'); +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', +}))] }; + +const exists = await attributeValueExists(platformUrl, auth, 'https://opentdf.io/attr/department/value/finance'); if (!exists) { console.log('value does not exist on this attribute'); } @@ -245,7 +263,12 @@ sdk.createTDF(inputStream, outputStream, config); ```ts -import { validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; + +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', +}))] }; const fqns = [ 'https://opentdf.io/attr/department/value/marketing', @@ -253,7 +276,7 @@ const fqns = [ ]; try { - await validateAttributes(platformUrl, authProvider, fqns); + await validateAttributes(platformUrl, auth, fqns); } catch (e) { if (e instanceof AttributeNotFoundError) { console.error('attribute validation failed:', e.message); @@ -356,7 +379,7 @@ The JavaScript SDK does not expose a `getEntityAttributes` convenience function ```ts import { PlatformClient } from '@opentdf/sdk/platform'; -const platform = new PlatformClient({ authProvider, platformUrl }); +const platform = new PlatformClient({ ...auth, platformUrl }); // By email address const resp = await platform.v2.authorization.getEntitlements({ @@ -495,20 +518,20 @@ try (SDK sdk = new SDKBuilder() ```ts -import { OpenTDF, AuthProviders, listAttributes, attributeExists, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF, listAttributes, attributeExists, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'opentdf', - clientSecret: 'secret', +// Define auth once — reuse for standalone functions and the OpenTDF client +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', oidcOrigin: oidcEndpoint, -}); +}))] }; // 1. See what's available on the platform -const attrs = await listAttributes(platformUrl, authProvider); +const attrs = await listAttributes(platformUrl, auth); console.log(`platform has ${attrs.length} active attributes`); // 2. Check a specific attribute exists before using it -const exists = await attributeExists(platformUrl, authProvider, 'https://opentdf.io/attr/department'); +const exists = await attributeExists(platformUrl, auth, 'https://opentdf.io/attr/department'); if (!exists) { console.error('attribute missing — create it first'); process.exit(1); @@ -517,7 +540,7 @@ if (!exists) { // 3. Validate the specific values before encrypting const required = ['https://opentdf.io/attr/department/value/marketing']; try { - await validateAttributes(platformUrl, authProvider, required); + await validateAttributes(platformUrl, auth, required); } catch (e) { if (e instanceof AttributeNotFoundError) { console.error('required attributes missing — create them first:', e.message); @@ -527,7 +550,7 @@ try { } // 4. Encrypt with confidence -const client = new OpenTDF({ authProvider, platformUrl }); +const client = new OpenTDF({ ...auth, platformUrl }); const ciphertext = await client.createTDF({ source: { type: 'stream', location: dataStream }, attributes: required, diff --git a/docs/sdks/platform-client.mdx b/docs/sdks/platform-client.mdx index c902101a..45f31aa3 100644 --- a/docs/sdks/platform-client.mdx +++ b/docs/sdks/platform-client.mdx @@ -5,6 +5,7 @@ title: Architecture import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # Architecture @@ -24,6 +25,8 @@ This is the same pattern used by cloud provider SDKs — you instantiate a typed ## Initializing the SDK client + + @@ -32,7 +35,7 @@ import ( "github.com/opentdf/platform/sdk" // Plus the service-specific package for each call, e.g.: "github.com/opentdf/platform/protocol/go/policy/namespaces" - "github.com/opentdf/platform/protocol/go/authorization" + authorization "github.com/opentdf/platform/protocol/go/authorization/v2" ) client, err := sdk.New("http://localhost:8080", @@ -42,6 +45,13 @@ if err != nil { log.Fatal(err) } defer client.Close() + +// Platform services are accessed directly on the client, e.g.: +resp, err := client.Namespaces.ListNamespaces(ctx, &namespaces.ListNamespacesRequest{}) + +decision, err := client.AuthorizationV2.GetDecision(ctx, &authorization.GetDecisionRequest{ + // ... +}) ``` @@ -62,24 +72,19 @@ SDK sdk = SDKBuilder.newBuilder() ```typescript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'client-id', - clientSecret: 'client-secret', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', -}); - -// OpenTDF eagerly binds DPoP keys to the auth provider. -// Await ready before creating PlatformClient. -const client = new OpenTDF({ authProvider, platformUrl: 'http://localhost:8080' }); -await client.ready; - -const platform = new PlatformClient({ - authProvider, - platformUrl: 'http://localhost:8080', -}); +const auth = { + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'client-id', + clientSecret: 'client-secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], +}; + +const client = new OpenTDF({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); ``` diff --git a/docs/sdks/policy.mdx b/docs/sdks/policy.mdx index d65c2ec8..892a9c51 100644 --- a/docs/sdks/policy.mdx +++ b/docs/sdks/policy.mdx @@ -12,11 +12,14 @@ import ListAttributes from '../../code_samples/policy_code/list_attributes.mdx' import CreateConditionSet from '../../code_samples/policy_code/create_subject_condition_set.mdx' import CreateSubjectMapping from '../../code_samples/policy_code/create_subject_mapping.mdx' import ListSubjectMapping from '../../code_samples/policy_code/list_subject_mapping.mdx' +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # Managing Policy Policy in OpenTDF is the set of rules that govern who can access data and under what conditions. It is made up of **namespaces**, **attributes**, **subject mappings**, and **subject condition sets**. The SDK provides CRUD access to these policy rules through remote gRPC calls powered by the [platform service client](/sdks/platform-client). + + --- ## Namespaces @@ -126,10 +129,16 @@ public class GetNamespaceExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); // Look up by UUID let resp = await platform.v1.namespace.getNamespace({ id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' }); @@ -234,10 +243,16 @@ public class UpdateNamespaceExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.namespace.updateNamespace({ id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', @@ -322,10 +337,16 @@ public class DeactivateNamespaceExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); await platform.v1.namespace.deactivateNamespace({ id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' }); console.log('Namespace deactivated.'); @@ -435,10 +456,16 @@ public class GetAttributeExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); // Look up by FQN const resp = await platform.v1.attributes.getAttribute({ @@ -536,10 +563,16 @@ public class UpdateAttributeExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.updateAttribute({ id: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -624,10 +657,16 @@ public class DeactivateAttributeExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.deactivateAttribute({ id: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -716,10 +755,16 @@ public class CreateAttributeValueExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.createAttributeValue({ attributeId: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -807,10 +852,16 @@ public class ListAttributeValuesExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.listAttributeValues({ attributeId: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -898,10 +949,16 @@ public class GetAttributeValueExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); // Look up by FQN const resp = await platform.v1.attributes.getAttributeValue({ @@ -1002,10 +1059,16 @@ public class GetAttributeValuesByFqnsExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.getAttributeValuesByFqns({ fqns: [ @@ -1104,10 +1167,16 @@ public class UpdateAttributeValueExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.updateAttributeValue({ id: 'v1b2c3d4-0000-0000-0000-000000000001', @@ -1192,10 +1261,16 @@ public class DeactivateAttributeValueExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.deactivateAttributeValue({ id: 'v1b2c3d4-0000-0000-0000-000000000001', @@ -1289,10 +1364,16 @@ public class ListSubjectConditionSetsExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.listSubjectConditionSets({}); for (const scs of resp.subjectConditionSets) { @@ -1380,10 +1461,16 @@ public class GetSubjectConditionSetExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.getSubjectConditionSet({ id: 'a0b1c2d3-0000-0000-0000-000000000099', @@ -1504,10 +1591,16 @@ public class UpdateSubjectConditionSetExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.updateSubjectConditionSet({ id: 'a0b1c2d3-0000-0000-0000-000000000099', @@ -1606,10 +1699,16 @@ public class DeleteSubjectConditionSetExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.deleteSubjectConditionSet({ id: 'a0b1c2d3-0000-0000-0000-000000000099', @@ -1689,10 +1788,16 @@ public class DeleteAllUnmappedSubjectConditionSetsExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.deleteAllUnmappedSubjectConditionSets({}); console.log(`Deleted ${resp.subjectConditionSets.length} unmapped subject condition sets.`); @@ -1786,10 +1891,16 @@ public class GetSubjectMappingExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.getSubjectMapping({ id: '890b26db-4ee4-447f-ae8a-2862d922eeef', @@ -1891,10 +2002,16 @@ public class UpdateSubjectMappingExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.updateSubjectMapping({ id: '890b26db-4ee4-447f-ae8a-2862d922eeef', @@ -1978,10 +2095,16 @@ public class DeleteSubjectMappingExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.deleteSubjectMapping({ id: '890b26db-4ee4-447f-ae8a-2862d922eeef', @@ -2082,10 +2205,16 @@ public class MatchSubjectMappingsExample { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.matchSubjectMappings({ subjectProperties: [ @@ -2128,10 +2257,14 @@ if err != nil { ```typescript +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 7763ecde..1643e304 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -12,7 +12,7 @@ This guide covers the **JavaScript/TypeScript SDK** implementation. For other la ## Prerequisites - [Node.js LTS](https://nodejs.org/en/about/previous-releases) -- Your OpenTDF platform running locally (from Getting Started guide) +- Your OpenTDF platform running locally (from the [Getting Started guide](/quickstart)) :::warning Platform Must Be Running Before you begin, **make sure your OpenTDF platform is running!** @@ -34,7 +34,7 @@ See the [Managing the Platform](/getting-started/managing-platform) guide for de ## Step 1: Create a New Project {#step-1-js} -Create a new directory and initialize a Node.js project: +The JavaScript SDK is designed for **browser-based applications** — React, Angular, Vue, or any web app that runs in the browser. This quickstart uses Node.js so you can get a working project running quickly without scaffolding a full web app, but in production you'd integrate the SDK into your frontend build. ```bash mkdir opentdf-quickstart @@ -65,12 +65,16 @@ Expected output: ## Step 3: Create Your Application {#step-3-js} +:::note Local development only +This quickstart uses `clientCredentialsTokenProvider` with the pre-configured Keycloak client (`opentdf`/`secret`) so you can focus on learning the SDK. In production browser apps, you would complete an OIDC login flow and pass the resulting refresh token to [`refreshTokenProvider()`](/sdks/authentication#refresh-token) instead — the JavaScript SDK is designed for browser use. See [Authentication](/sdks/authentication) for all options. +::: + ### JavaScript Implementation Code Create a file named `index.mjs`: ```javascript title="index.mjs" -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; async function main() { console.log('🚀 Starting OpenTDF SDK Quickstart...'); @@ -79,16 +83,17 @@ async function main() { const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; console.log(`📡 Connecting to platform: ${platformUrl}`); - // Create an auth provider with client credentials - console.log('🔐 Initializing auth provider with client credentials...'); - const authProvider = await AuthProviders.clientSecretAuthProvider({ + // For this quickstart we use client credentials (server-side only). + // In a browser app, use refreshTokenProvider() with your OIDC login flow instead. + const tokenProvider = clientCredentialsTokenProvider({ clientId: 'opentdf', clientSecret: 'secret', oidcOrigin, }); // Create a new OpenTDF client - const client = new OpenTDF({ authProvider, platformUrl }); + console.log('🔐 Initializing client with auth interceptor...'); + const client = new OpenTDF({ interceptors: [authTokenInterceptor(tokenProvider)], platformUrl }); console.log('✅ SDK client initialized successfully'); // Encrypt data @@ -140,7 +145,7 @@ NODE_TLS_REJECT_UNAUTHORIZED=0 node index.mjs ```console 🚀 Starting OpenTDF SDK Quickstart... 📡 Connecting to platform: https://platform.opentdf.local:8443 -🔐 Initializing auth provider with client credentials... +🔐 Initializing client with auth interceptor... ✅ SDK client initialized successfully 📝 Encrypting sensitive data... @@ -185,21 +190,22 @@ For additional policy management examples including managing attributes, namespa Let's work with the "department" attribute and add a "marketing" value. If you completed the [Quickstart setup guide](/quickstart), the attribute may already exist with finance and engineering values. If not, we'll create it: ```javascript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, attributeExists, attributeValueExists, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum } from '@opentdf/sdk/platform/policy/objects_pb.js'; const platformUrl = 'https://platform.opentdf.local:8443'; -const authProvider = await AuthProviders.clientSecretAuthProvider({ +const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; + +// Client credentials for this quickstart only — use refreshTokenProvider() in browser apps +const interceptors = [authTokenInterceptor(clientCredentialsTokenProvider({ clientId: 'opentdf', clientSecret: 'secret', - oidcOrigin: 'https://keycloak.opentdf.local:9443/auth/realms/opentdf', -}); - -// Create the OpenTDF client and wait for DPoP key binding. -const client = new OpenTDF({ authProvider, platformUrl }); -await client.ready; -const platform = new PlatformClient({ authProvider, platformUrl }); + oidcOrigin, +}))]; +const auth = { interceptors }; +const client = new OpenTDF({ ...auth, platformUrl }); +const platform = new PlatformClient({ ...auth, platformUrl }); // First, ensure the namespace exists let namespaceId; @@ -222,46 +228,36 @@ try { } } -// Get or create the department attribute -const listResp = await platform.v1.attributes.listAttributes({}); -let attribute = listResp.attributes.find( - (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId -); - -if (!attribute) { - const attrResp = await platform.v1.attributes.createAttribute({ +// Create the department attribute if it doesn't exist +const deptFqn = 'https://opentdf.io/attr/department'; +if (!(await attributeExists(platformUrl, auth, deptFqn))) { + await platform.v1.attributes.createAttribute({ namespaceId, name: 'department', rule: AttributeRuleTypeEnum.ANY_OF, values: ['marketing'], }); - attribute = attrResp.attribute; - console.log(`✅ Created attribute: ${attribute?.name}`); + console.log('✅ Created department attribute with marketing value'); } else { - console.log(`✅ Found existing attribute: ${attribute.name}`); + console.log('✅ Using existing department attribute'); } -// Check if "marketing" value already exists -const targetValue = 'marketing'; -const valueExists = attribute?.values?.some((v) => v.value === targetValue); +// Ensure the marketing value exists on the attribute +const marketingFqn = `${deptFqn}/value/marketing`; +const listResp = await platform.v1.attributes.listAttributes({}); +const attribute = listResp.attributes.find( + (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId +); -if (!valueExists) { +if (!(await attributeValueExists(platformUrl, auth, marketingFqn))) { await platform.v1.attributes.createAttributeValue({ attributeId: attribute?.id, - value: targetValue, + value: 'marketing', }); - console.log(`✅ Added '${targetValue}' value to department attribute`); -} else { - console.log(`✅ Attribute 'department' already has '${targetValue}' value`); + console.log('✅ Added marketing value to department attribute'); } -console.log(`Full attribute FQN: https://opentdf.io/attr/department/value/${targetValue}`); - -// Re-fetch the attribute to get updated values with IDs -const refreshedList = await platform.v1.attributes.listAttributes({}); -attribute = refreshedList.attributes.find( - (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId -); +console.log(`Full attribute FQN: ${marketingFqn}`); ``` :::warning @@ -303,28 +299,23 @@ import { } from '@opentdf/sdk/platform/policy/objects_pb.js'; // Get the attribute value ID for "marketing" -const marketingValue = attribute?.values?.find((v) => v.value === targetValue); +const marketingValue = attribute?.values?.find((v) => v.value === 'marketing'); const attributeValueId = marketingValue?.id; -// Create a subject condition set that matches your identity +// Subject condition sets use a nested structure: sets → groups → conditions. +// This one matches any entity whose .clientId claim includes 'opentdf'. const scsResp = await platform.v1.subjectMapping.createSubjectConditionSet({ subjectConditionSet: { - subjectSets: [ - { - conditionGroups: [ - { - booleanOperator: ConditionBooleanTypeEnum.AND, - conditions: [ - { - subjectExternalSelectorValue: '.clientId', - operator: SubjectMappingOperatorEnum.IN, - subjectExternalValues: ['opentdf'], - }, - ], - }, - ], - }, - ], + subjectSets: [{ + conditionGroups: [{ + booleanOperator: ConditionBooleanTypeEnum.AND, + conditions: [{ + subjectExternalSelectorValue: '.clientId', + operator: SubjectMappingOperatorEnum.IN, + subjectExternalValues: ['opentdf'], + }], + }], + }], }, }); @@ -396,7 +387,7 @@ For reference, here's a complete example showing all the pieces together: ```javascript title="index.mjs" import fs from 'node:fs'; -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, attributeExists, attributeValueExists, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum, @@ -408,17 +399,15 @@ async function main() { const platformUrl = 'https://platform.opentdf.local:8443'; const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; - // Create auth provider - const authProvider = await AuthProviders.clientSecretAuthProvider({ + // Client credentials for this quickstart only — use refreshTokenProvider() in browser apps + const interceptors = [authTokenInterceptor(clientCredentialsTokenProvider({ clientId: 'opentdf', clientSecret: 'secret', oidcOrigin, - }); - - // Create the OpenTDF client and wait for DPoP key binding. - const client = new OpenTDF({ authProvider, platformUrl }); - await client.ready; - const platform = new PlatformClient({ authProvider, platformUrl }); + }))]; + const auth = { interceptors }; + const client = new OpenTDF({ ...auth, platformUrl }); + const platform = new PlatformClient({ ...auth, platformUrl }); // 1. Create namespace (or use existing) let namespaceId; @@ -430,7 +419,6 @@ async function main() { console.log(`✅ Created namespace: ${namespaceId}`); } catch (err) { if (err.message?.includes('already_exists')) { - // Namespace exists — fetch it directly by FQN const getNsResp = await platform.v1.namespace.getNamespace({ identifier: { case: 'fqn', value: 'https://opentdf.io' }, }); @@ -441,36 +429,32 @@ async function main() { } } - // 2. Create attribute with marketing value (or use existing) - let attribute; - try { - const attrResp = await platform.v1.attributes.createAttribute({ + // 2. Create the department attribute if it doesn't exist + const deptFqn = 'https://opentdf.io/attr/department'; + if (!(await attributeExists(platformUrl, auth, deptFqn))) { + await platform.v1.attributes.createAttribute({ namespaceId, name: 'department', rule: AttributeRuleTypeEnum.ANY_OF, values: ['marketing'], }); - attribute = attrResp.attribute; - console.log(`✅ Created attribute: ${attribute?.name}`); - } catch (err) { - if (err.message?.includes('already_exists')) { - const listResp = await platform.v1.attributes.listAttributes({}); - attribute = listResp.attributes.find( - (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId - ); - console.log(`✅ Using existing attribute: ${attribute?.name}`); - - // Ensure the 'marketing' value exists on the attribute - if (attribute && !attribute.values?.some((v) => v.value === 'marketing')) { - await platform.v1.attributes.createAttributeValue({ - attributeId: attribute.id, - value: 'marketing', - }); - console.log(`✅ Added 'marketing' value to department attribute`); - } - } else { - throw err; - } + console.log('✅ Created department attribute with marketing value'); + } else { + console.log('✅ Using existing department attribute'); + } + + // Ensure the marketing value exists on the attribute + const marketingFqn = `${deptFqn}/value/marketing`; + if (!(await attributeValueExists(platformUrl, auth, marketingFqn))) { + const listResp = await platform.v1.attributes.listAttributes({}); + const attribute = listResp.attributes.find( + (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId + ); + await platform.v1.attributes.createAttributeValue({ + attributeId: attribute?.id, + value: 'marketing', + }); + console.log('✅ Added marketing value to department attribute'); } // 3. Encrypt data with the marketing attribute @@ -493,35 +477,30 @@ async function main() { console.log('✅ TDF saved to encrypted.tdf'); // 5. Grant yourself access to the marketing attribute - // Re-fetch to get value IDs - const refreshedList = await platform.v1.attributes.listAttributes({}); - attribute = refreshedList.attributes.find( + // Fetch the attribute to get the marketing value ID for the subject mapping + const attrList = await platform.v1.attributes.listAttributes({}); + const deptAttr = attrList.attributes.find( (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId ); - - const marketingValue = attribute?.values?.find((v) => v.value === 'marketing'); + const marketingValue = deptAttr?.values?.find((v) => v.value === 'marketing'); if (!marketingValue?.id) { throw new Error('Marketing value not found in department attribute'); } + // Subject condition sets use a nested structure: sets → groups → conditions. + // This one matches any entity whose .clientId claim includes 'opentdf'. const scsResp = await platform.v1.subjectMapping.createSubjectConditionSet({ subjectConditionSet: { - subjectSets: [ - { - conditionGroups: [ - { - booleanOperator: ConditionBooleanTypeEnum.AND, - conditions: [ - { - subjectExternalSelectorValue: '.clientId', - operator: SubjectMappingOperatorEnum.IN, - subjectExternalValues: ['opentdf'], - }, - ], - }, - ], - }, - ], + subjectSets: [{ + conditionGroups: [{ + booleanOperator: ConditionBooleanTypeEnum.AND, + conditions: [{ + subjectExternalSelectorValue: '.clientId', + operator: SubjectMappingOperatorEnum.IN, + subjectExternalValues: ['opentdf'], + }], + }], + }], }, }); diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index 500bca27..adb45075 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -8,6 +8,7 @@ import TabItem from '@theme/TabItem'; import EncryptOptions from '../../code_samples/tdf/encrypt_options.mdx' import DecryptOptions from '../../code_samples/tdf/decrypt_options.mdx' import AssertionExamples from '../../code_samples/tdf/assertion_examples.mdx' +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # TDF @@ -31,6 +32,8 @@ This page covers the core TDF operations: ## Quick Start + + Initialize a client and run an end-to-end encrypt/decrypt in one block. @@ -120,16 +123,17 @@ try (FileChannel ch = FileChannel.open(tmp, StandardOpenOption.READ)) { ```typescript -import { OpenTDF, AuthProviders } from '@opentdf/sdk'; - -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'client-id', - clientSecret: 'client-secret', - oidcOrigin: 'https://platform.example.com/auth/realms/opentdf', +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; + +const client = new OpenTDF({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'client-id', + clientSecret: 'client-secret', + oidcOrigin: 'https://platform.example.com/auth/realms/opentdf', + }))], + platformUrl: 'https://platform.example.com', }); -const client = new OpenTDF({ authProvider, platformUrl: 'https://platform.example.com' }); - // Encrypt const enc = new TextEncoder(); const tdfStream = await client.createTDF({ @@ -183,11 +187,33 @@ async createTDF(options: CreateTDFOptions): Promise **Parameters** -| Parameter | Required | Description | -|-----------|----------|-------------| -| Output destination | Required | Where the encrypted TDF bytes are written. | -| Plaintext source | Required | The data to encrypt. See the signature tab for your language for the expected type. | -| Configuration | Required* | Encryption options. A KAS endpoint must be specified unless autoconfigure is enabled. See [Encrypt Options](#encrypt-options). | + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `writer` | `io.Writer` | Where the encrypted TDF bytes are written. | +| `reader` | `io.ReadSeeker` | The plaintext data to encrypt. | +| `opts` | `...TDFOption` | Encryption options. At minimum, a KAS endpoint must be specified (e.g., `sdk.WithKasInformation()`). See [Encrypt Options](#encrypt-options). | + + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `plaintext` | `InputStream` | The plaintext data to encrypt. | +| `out` | `OutputStream` | Where the encrypted TDF bytes are written. | +| `config` | `Config.TDFConfig` | Encryption options. At minimum, a KAS endpoint must be specified. See [Encrypt Options](#encrypt-options). | + + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `options` | `CreateTDFOptions` | A single options object containing the source, KAS endpoint, attributes, and other settings. See [Encrypt Options](#encrypt-options). | + + + **Returns** @@ -333,10 +359,31 @@ open(options: ReadOptions): TDFReader **Parameters** -| Parameter | Required | Description | -|-----------|----------|-------------| -| Encrypted TDF source | Required | The TDF to open. See the signature tab for your language for the expected type. For JavaScript, `source` accepts a `buffer` (`Uint8Array`) or `stream` (`ReadableStream`). | -| Configuration | Optional | Decryption options. Defaults allow any KAS endpoint. See [Decrypt Options](#decrypt-options). | + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `reader` | `io.ReadSeeker` | The encrypted TDF to open. | +| `opts` | `...TDFReaderOption` | Optional decryption settings. See [Decrypt Options](#decrypt-options). | + + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `channel` | `SeekableByteChannel` | The encrypted TDF to open. | +| `config` | `Config.TDFReaderConfig` | Decryption settings. Use `Config.newTDFReaderConfig()` for defaults. See [Decrypt Options](#decrypt-options). | + + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `options` | `ReadOptions` | A single options object. `source` accepts a `buffer` (`Uint8Array`) or `stream` (`ReadableStream`). See [Decrypt Options](#decrypt-options). | + + + **Returns** @@ -964,9 +1011,15 @@ for _, fqn := range obligations.FQNs { ```typescript -import { OpenTDF } from '@opentdf/sdk'; - -const client = new OpenTDF({ authProvider, platformUrl: 'http://localhost:8080' }); +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; + +const client = new OpenTDF({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const reader = client.open({ source: ciphertextSource }); const obligations = await reader.obligations(); diff --git a/docs/sdks/troubleshooting.mdx b/docs/sdks/troubleshooting.mdx index e4ad2d96..cdee13dc 100644 --- a/docs/sdks/troubleshooting.mdx +++ b/docs/sdks/troubleshooting.mdx @@ -5,6 +5,7 @@ title: Troubleshooting import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # SDK Troubleshooting @@ -41,18 +42,19 @@ curl https:/// otdfctl auth client-credentials ``` -2. **Implement token refresh in your application**: The JavaScript SDK's `refreshAuthProvider` handles automatic token renewal: +2. **Use a built-in token provider**: The SDK's token providers handle caching and auto-refresh automatically: ```js - import { AuthProviders } from '@opentdf/sdk'; - - const authProvider = AuthProviders.refreshAuthProvider({ - clientId: 'your-client-id', - exchange: 'client', - clientSecret: 'your-client-secret', - oidcOrigin: 'https://', // Keycloak: append /realms/; adjust path for your IdP - }); + import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; + + const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))] }; + const client = new OpenTDF({ ...auth, platformUrl }); ``` + + 3. **Re-instantiate the SDK**: For Go and Java, create a new SDK instance with fresh credentials when a token error occurs. ## Certificate Errors (SSL/TLS) @@ -320,7 +322,7 @@ exit status 1 - **Go**: `client.ValidateAttributes(ctx, fqns)` - **Java**: `sdk.validateAttributes(fqns)` -- **JavaScript**: `await validateAttributes(platformUrl, authProvider, fqns)` +- **JavaScript**: `await validateAttributes(platformUrl, auth, fqns)` **Solution**: Create the resource first using otdfctl or the SDK policy functions. For attributes, see the [SDK Create Attribute function](/sdks/policy#create-attribute). @@ -358,7 +360,12 @@ If you can't resolve an issue using this guide, here are the next steps: 2. **Search existing GitHub Discussions** — many common questions are already answered in the [opentdf/platform discussions](https://github.com/opentdf/platform/discussions). -3. **Open a GitHub issue** with: +3. **Open a GitHub issue** on the relevant repo: + - [opentdf/platform](https://github.com/opentdf/platform/issues) — Go SDK and platform + - [opentdf/java-sdk](https://github.com/opentdf/java-sdk/issues) — Java SDK + - [opentdf/web-sdk](https://github.com/opentdf/web-sdk/issues) — JavaScript/TypeScript SDK + + Include: - SDK language and version - Full error message (including stack trace if available) - Minimal code that reproduces the issue