From d0389dc16be0b05faab5aba872bb2b73137563ed Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Sat, 14 Mar 2026 15:56:17 -0700 Subject: [PATCH 1/7] feat(docs): add JavaScript/TypeScript SDK quickstart Replace the placeholder JS/TS quickstart with a full guide mirroring the Go quickstart structure: prerequisites, project setup, basic encrypt/decrypt, ABAC policy management, and a complete reference implementation. Update the SDK quickstart index card from "coming soon" to active link. Co-Authored-By: Claude Opus 4.6 --- docs/sdks/quickstart/index.mdx | 2 +- docs/sdks/quickstart/javascript.mdx | 537 +++++++++++++++++++++++++++- 2 files changed, 530 insertions(+), 9 deletions(-) diff --git a/docs/sdks/quickstart/index.mdx b/docs/sdks/quickstart/index.mdx index d44add9b..a9a114a3 100644 --- a/docs/sdks/quickstart/index.mdx +++ b/docs/sdks/quickstart/index.mdx @@ -76,7 +76,7 @@ OpenTDF provides native SDKs in three languages. Choose your language to get sta
- Under development - coming soon + Get Started with JavaScript
diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index a015d478..6526ac6f 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -9,18 +9,539 @@ title: JavaScript/TypeScript This guide covers the **JavaScript/TypeScript SDK** implementation. For other languages or general information, see the [SDK Quickstart](/sdks/quickstart) page. ::: -:::info Coming Soon -The JavaScript/TypeScript SDK quickstart guide is currently under development. +## Prerequisites -In the meantime, you can: -- Review the [SDK overview](/sdks) -- Check out the [web-sdk repository](https://github.com/opentdf/web-sdk) on GitHub -- Explore the [Go](/sdks/quickstart/go) or [Java](/sdks/quickstart/java) quickstart guides for similar concepts -- See the [Getting Started](/quickstart) guide to set up your OpenTDF platform +- Node.js 18 or later +- Your OpenTDF platform running locally (from Getting Started guide) -Check back soon for a complete JavaScript/TypeScript quickstart guide! +:::warning Platform Must Be Running +Before you begin, **make sure your OpenTDF platform is running!** + +Verify it's running: +```bash +curl -k https://platform.opentdf.local:8443/healthz +``` + +Should return: `{"status":"SERVING"}` + +If not running, start it: +```bash +cd ~/.opentdf/platform && docker compose up -d +``` + +See the [Managing the Platform](/getting-started/managing-platform) guide for details. +::: + +## Step 1: Create a New Project {#step-1-js} + +Create a new directory and initialize a Node.js project: + +```bash +mkdir opentdf-quickstart +cd opentdf-quickstart +npm init -y +``` + +Enable ES modules by adding `"type": "module"` to your `package.json`: + +```json title="package.json" +{ + "name": "opentdf-quickstart", + "version": "1.0.0", + "type": "module" +} +``` + +## Step 2: Install the SDK {#step-2-js} + +```bash +npm install @opentdf/sdk +``` + +Expected output: +> ```console +> added XX packages in Xs +> ``` + +## Step 3: Create Your Application {#step-3-js} + +### JavaScript Implementation Code + +Create a file named `index.mjs`: + +```javascript title="index.mjs" +import { AuthProviders, OpenTDF } from '@opentdf/sdk'; + +async function main() { + console.log('šŸš€ Starting OpenTDF SDK Quickstart...'); + + const platformUrl = 'https://platform.opentdf.local:8443'; + 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({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin, + }); + + // Create a new OpenTDF client + const client = new OpenTDF({ authProvider, platformUrl }); + console.log('āœ… SDK client initialized successfully'); + + // Encrypt data + console.log('\nšŸ“ Encrypting sensitive data...'); + const sensitiveData = 'Hello from the OpenTDF JS SDK! This data is encrypted.'; + const encoder = new TextEncoder(); + + console.log('šŸ”’ Creating TDF...'); + const tdfStream = await client.createTDF({ + source: { type: 'buffer', location: encoder.encode(sensitiveData) }, + // defaultKASEndpoint specifies the Key Access Service (KAS) endpoint + // KAS manages encryption keys and enforces access policies + defaultKASEndpoint: `${platformUrl}/kas`, + }); + + const encrypted = new Uint8Array(await new Response(tdfStream).arrayBuffer()); + console.log('āœ… Data successfully encrypted'); + console.log(`šŸ“Š Encrypted TDF size: ${encrypted.length} bytes`); + + // Decrypt data + console.log('\nšŸ”“ Decrypting TDF...'); + const decryptedStream = await client.read({ + source: { type: 'buffer', location: encrypted }, + }); + + const decryptedText = await new Response(decryptedStream).text(); + console.log('āœ… Data successfully decrypted'); + console.log(`šŸ“¤ Decrypted content:\n\n${decryptedText}\n`); + + console.log('\nšŸŽ‰ Quickstart complete!'); +} + +main().catch(console.error); +``` + +## Step 4: Run Your Application {#step-4-js} + +:::warning Development Only +`NODE_TLS_REJECT_UNAUTHORIZED=0` disables TLS certificate verification to allow connections to the platform's self-signed certificate. **Never use this in production.** +::: + +```bash +NODE_TLS_REJECT_UNAUTHORIZED=0 node index.mjs +``` + +
+Expected output + +```console +šŸš€ Starting OpenTDF SDK Quickstart... +šŸ“” Connecting to platform: https://platform.opentdf.local:8443 +šŸ” Initializing auth provider with client credentials... +āœ… SDK client initialized successfully + +šŸ“ Encrypting sensitive data... +šŸ”’ Creating TDF... +āœ… Data successfully encrypted +šŸ“Š Encrypted TDF size: 1234 bytes + +šŸ”“ Decrypting TDF... +āœ… Data successfully decrypted +šŸ“¤ Decrypted content: + +Hello from the OpenTDF JS SDK! This data is encrypted. + +šŸŽ‰ Quickstart complete! +``` + +
+ +--- + +## Step 5: Add ABAC Features {#step-5-js} + +:::tip JavaScript-Specific Examples +The following examples are specific to the **JavaScript/TypeScript SDK**. +::: + +Now that you have basic encryption working, you can add attribute-based access control to your application. + +If you want to follow along, you can add the code to your quickstart file as we go. Otherwise, a [complete version is available below](#complete-example-create-attribute-encrypt-grant-access-and-decrypt). + +:::info More ABAC Examples +For additional policy management examples including managing attributes, namespaces, subject mappings, and more, see the [Policy SDK Guide](/sdks/policy). +::: + +### Create a New Attribute Value + +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 { 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({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin: 'https://keycloak.opentdf.local:9443/auth/realms/opentdf', +}); + +// Create the OpenTDF client first — it automatically generates DPoP keys +// and configures them on the auth provider, which PlatformClient also needs +const client = new OpenTDF({ authProvider, platformUrl }); +const platform = new PlatformClient({ authProvider, platformUrl }); + +// First, ensure the namespace exists +let namespaceId; +try { + const nsResp = await platform.v1.namespace.createNamespace({ + name: 'opentdf.io', + }); + namespaceId = nsResp.namespace?.id; + console.log(`āœ… Created namespace: ${namespaceId}`); +} catch (err) { + if (err.message?.includes('already_exists')) { + // Namespace exists, fetch it + const listNsResp = await platform.v1.namespace.listNamespaces({}); + const ns = listNsResp.namespaces.find((n) => n.name === 'opentdf.io'); + namespaceId = ns?.id; + console.log(`āœ… Using existing namespace: ${namespaceId}`); + } else { + throw err; + } +} + +// 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({ + namespaceId, + name: 'department', + rule: AttributeRuleTypeEnum.ANY_OF, + values: ['marketing'], + }); + attribute = attrResp.attribute; + console.log(`āœ… Created attribute: ${attribute?.name}`); +} else { + console.log(`āœ… Found existing attribute: ${attribute.name}`); +} + +// Check if "marketing" value already exists +const targetValue = 'marketing'; +const valueExists = attribute?.values?.some((v) => v.value === targetValue); + +if (!valueExists) { + await platform.v1.attributes.createAttributeValue({ + attributeId: attribute?.id, + value: targetValue, + }); + console.log(`āœ… Added '${targetValue}' value to department attribute`); +} else { + console.log(`āœ… Attribute 'department' already has '${targetValue}' value`); +} + +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 +); +``` + +:::warning +If you get a [resource not found error](/sdks/troubleshooting#resource-not-found), you may need to create the "department" attribute, along with the namespace. +::: + +### Add Attributes for Access Control + +Now that you've created the attribute, update your `createTDF` call to include the attribute for access control: + +```javascript +const tdfStream = await client.createTDF({ + source: { type: 'buffer', location: encoder.encode(sensitiveData) }, + // defaultKASEndpoint specifies the Key Access Service (KAS) endpoint + // KAS manages encryption keys and enforces access policies + defaultKASEndpoint: `${platformUrl}/kas`, + attributes: ['https://opentdf.io/attr/department/value/marketing'], +}); +``` + +:::tip +Only users with the `department/marketing` entitlement will be able to decrypt this TDF. If you try to decrypt before granting access to the attribute, you will see a [permission denied error](/sdks/troubleshooting#permission-denied--insufficient-entitlements). Try it now! +::: + +### Grant Yourself Access to the Attribute + +To decrypt the TDF you just created, you need to grant yourself the `marketing` entitlement by creating a subject mapping. This connects your identity to the attribute value, giving you permission to access data encrypted with it. + +:::tip About the `read` Action +The subject mapping below uses the `read` action, which specifies what you can _do_ with this attribute value. The `read` action is one of the standard actions used for TDF decryption. Other standard actions include `create`, `update`, and `delete`, and you can define custom actions for your specific use cases. + +Learn more about actions in the [Actions documentation](/components/policy/actions). ::: +```javascript +import { + ConditionBooleanTypeEnum, + SubjectMappingOperatorEnum, +} 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 attributeValueId = marketingValue?.id; + +// Create a subject condition set that matches your identity +const scsResp = await platform.v1.subjectMapping.createSubjectConditionSet({ + subjectConditionSet: { + subjectSets: [ + { + conditionGroups: [ + { + booleanOperator: ConditionBooleanTypeEnum.AND, + conditions: [ + { + subjectExternalSelectorValue: '.clientId', + operator: SubjectMappingOperatorEnum.IN, + subjectExternalValues: ['opentdf'], + }, + ], + }, + ], + }, + ], + }, +}); + +// Create the subject mapping to grant yourself the entitlement +await platform.v1.subjectMapping.createSubjectMapping({ + attributeValueId, + actions: [{ name: 'read' }], + existingSubjectConditionSetId: scsResp.subjectConditionSet?.id, +}); + +console.log('āœ… Granted yourself access to department/marketing'); +``` + +Now you can decrypt the TDF you encrypted with the `marketing` attribute. šŸŽ‰ + + + +### Save TDF to a File + +In production applications, you'll often need to persist encrypted TDFs to disk for storage, transmission, or archival. This allows you to: + +- **Separate encryption from distribution**: Encrypt data once, then share the TDF file through your preferred channels (email, S3, SFTP, etc.) +- **Enable offline access**: Recipients can decrypt TDFs without needing to re-fetch data from your application +- **Archive encrypted data**: Store TDFs in backup systems or long-term storage with their access policies intact + +```javascript +import fs from 'node:fs'; + +// After encryption +fs.writeFileSync('encrypted.tdf', encrypted); + +// Later, load from file +const tdfData = fs.readFileSync('encrypted.tdf'); +const decryptedStream = await client.read({ + source: { type: 'buffer', location: new Uint8Array(tdfData) }, +}); +``` + +### Handle Large Files with Streaming + +For large files, use streams instead of loading the entire file into memory: + +```javascript +import fs from 'node:fs'; + +// Encrypt a large file using a stream source +const inputStream = fs.createReadStream('large-file.pdf'); +const tdfStream = await client.createTDF({ + source: { type: 'stream', location: inputStream }, + // defaultKASEndpoint specifies the Key Access Service (KAS) endpoint + // KAS manages encryption keys and enforces access policies + defaultKASEndpoint: `${platformUrl}/kas`, +}); + +// Pipe encrypted output to a file +const outputStream = fs.createWriteStream('large-file.pdf.tdf'); +for await (const chunk of tdfStream) { + outputStream.write(chunk); +} +outputStream.end(); +``` + +## Step 6: Complete Reference Implementation {#step-6-js} + +For reference, here's a complete example showing all the pieces together: + +
+Create Attribute, Encrypt, Grant Access, Decrypt, Save File + +```javascript title="index.mjs" +import fs from 'node:fs'; +import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { PlatformClient } from '@opentdf/sdk/platform'; +import { + AttributeRuleTypeEnum, + ConditionBooleanTypeEnum, + SubjectMappingOperatorEnum, +} from '@opentdf/sdk/platform/policy/objects_pb.js'; + +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({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin, + }); + + // Create the OpenTDF client first — it automatically generates DPoP keys + // and configures them on the auth provider, which PlatformClient also needs + const client = new OpenTDF({ authProvider, platformUrl }); + const platform = new PlatformClient({ authProvider, platformUrl }); + + // 1. Create namespace (or use existing) + let namespaceId; + try { + const nsResp = await platform.v1.namespace.createNamespace({ + name: 'opentdf.io', + }); + namespaceId = nsResp.namespace?.id; + console.log(`āœ… Created namespace: ${namespaceId}`); + } catch (err) { + if (err.message?.includes('already_exists')) { + const listResp = await platform.v1.namespace.listNamespaces({}); + const ns = listResp.namespaces.find((n) => n.name === 'opentdf.io'); + namespaceId = ns?.id; + console.log(`āœ… Using existing namespace: ${namespaceId}`); + } else { + throw err; + } + } + + // 2. Create attribute with marketing value (or use existing) + let attribute; + try { + const attrResp = 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}`); + } else { + throw err; + } + } + + // 3. Encrypt data with the marketing attribute + const plaintext = 'Sensitive marketing campaign data'; + const encoder = new TextEncoder(); + + // defaultKASEndpoint specifies the Key Access Service (KAS) endpoint + // KAS manages encryption keys and enforces access policies + const tdfStream = await client.createTDF({ + source: { type: 'buffer', location: encoder.encode(plaintext) }, + defaultKASEndpoint: `${platformUrl}/kas`, + attributes: ['https://opentdf.io/attr/department/value/marketing'], + }); + + const encrypted = new Uint8Array(await new Response(tdfStream).arrayBuffer()); + console.log('āœ… Data encrypted with marketing attribute'); + + // 4. Save TDF to file + fs.writeFileSync('encrypted.tdf', encrypted); + 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( + (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId + ); + + const marketingValue = attribute?.values?.find((v) => v.value === 'marketing'); + if (!marketingValue?.id) { + throw new Error('Marketing value not found in department attribute'); + } + + const scsResp = await platform.v1.subjectMapping.createSubjectConditionSet({ + subjectConditionSet: { + subjectSets: [ + { + conditionGroups: [ + { + booleanOperator: ConditionBooleanTypeEnum.AND, + conditions: [ + { + subjectExternalSelectorValue: '.clientId', + operator: SubjectMappingOperatorEnum.IN, + subjectExternalValues: ['opentdf'], + }, + ], + }, + ], + }, + ], + }, + }); + + try { + await platform.v1.subjectMapping.createSubjectMapping({ + attributeValueId: marketingValue.id, + actions: [{ name: 'read' }], + existingSubjectConditionSetId: scsResp.subjectConditionSet?.id, + }); + console.log('āœ… Granted yourself access to department/marketing'); + } catch (err) { + if (err.message?.includes('already_exists')) { + console.log('āœ… Subject mapping already exists for department/marketing'); + } else { + throw err; + } + } + + // 6. Load TDF from file + const tdfData = fs.readFileSync('encrypted.tdf'); + console.log('āœ… TDF loaded from encrypted.tdf'); + + // 7. Decrypt the data + const decryptedStream = await client.read({ + source: { type: 'buffer', location: new Uint8Array(tdfData) }, + }); + + const decryptedText = await new Response(decryptedStream).text(); + console.log('āœ… Data successfully decrypted'); + console.log(`šŸ“¤ Decrypted content: ${decryptedText}`); +} + +main().catch(console.error); +``` + +
+ ## Troubleshooting Having issues? See the **[SDK Troubleshooting](/sdks/troubleshooting)** guide for solutions to common problems. From 98d62603d189716209371c0de9a5de570332ba3e Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Sat, 14 Mar 2026 16:27:41 -0700 Subject: [PATCH 2/7] chore(deps): update vendored OpenAPI specs from platform main Co-Authored-By: Claude Opus 4.6 --- .../authorization/authorization.openapi.yaml | 377 +++++++++++++++++- .../v2/authorization.openapi.yaml | 218 ++++++++++ specs/policy/actions/actions.openapi.yaml | 44 ++ .../policy/attributes/attributes.openapi.yaml | 4 + specs/policy/objects.openapi.yaml | 4 + .../obligations/obligations.openapi.yaml | 4 + .../registered_resources.openapi.yaml | 4 + .../resource_mapping.openapi.yaml | 4 + .../subject_mapping.openapi.yaml | 4 + specs/policy/unsafe/unsafe.openapi.yaml | 4 + 10 files changed, 663 insertions(+), 4 deletions(-) diff --git a/specs/authorization/authorization.openapi.yaml b/specs/authorization/authorization.openapi.yaml index 582d5392..1938f339 100644 --- a/specs/authorization/authorization.openapi.yaml +++ b/specs/authorization/authorization.openapi.yaml @@ -60,7 +60,37 @@ paths: schema: type: string title: name - - name: decisionRequests.actions.metadata.createdAt.seconds + - name: decisionRequests.actions.namespace.id + in: query + description: generated uuid in database + schema: + type: string + title: id + description: generated uuid in database + - name: decisionRequests.actions.namespace.name + in: query + description: |- + used to partition Attribute Definitions, support by namespace AuthN and + enable federation + schema: + type: string + title: name + description: |- + used to partition Attribute Definitions, support by namespace AuthN and + enable federation + - name: decisionRequests.actions.namespace.fqn + in: query + schema: + type: string + title: fqn + - name: decisionRequests.actions.namespace.active.value + in: query + description: The bool value. + schema: + type: boolean + title: value + description: The bool value. + - name: decisionRequests.actions.namespace.metadata.createdAt.seconds in: query description: |- Represents seconds of UTC time since Unix epoch @@ -76,7 +106,7 @@ paths: Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive. - - name: decisionRequests.actions.metadata.createdAt.nanos + - name: decisionRequests.actions.namespace.metadata.createdAt.nanos in: query description: |- Non-negative fractions of a second at nanosecond resolution. Negative @@ -92,16 +122,128 @@ paths: second values with fractions must still have non-negative nanos values that count forward in time. Must be from 0 to 999,999,999 inclusive. - - name: decisionRequests.actions.metadata.labels.key + - name: decisionRequests.actions.namespace.metadata.labels.key in: query schema: type: string title: key - - name: decisionRequests.actions.metadata.labels.value + - name: decisionRequests.actions.namespace.metadata.labels.value in: query schema: type: string title: value + - name: decisionRequests.actions.namespace.grants.id + in: query + schema: + type: string + title: id + - name: decisionRequests.actions.namespace.grants.uri + in: query + description: Address of a KAS instance + schema: + type: string + title: uri + description: |+ + Address of a KAS instance + URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: + ``` + this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') + ``` + + - name: decisionRequests.actions.namespace.grants.publicKey.remote + in: query + description: kas public key url - optional since can also be retrieved via public key + schema: + type: string + title: remote + description: |+ + kas public key url - optional since can also be retrieved via public key + URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: + ``` + this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') + ``` + + - name: decisionRequests.actions.namespace.grants.publicKey.cached.keys.pem + in: query + description: x509 ASN.1 content in PEM envelope, usually + schema: + type: string + title: pem + maxLength: 8192 + minLength: 1 + description: x509 ASN.1 content in PEM envelope, usually + - name: decisionRequests.actions.namespace.grants.publicKey.cached.keys.kid + in: query + description: A unique string identifier for this key + schema: + type: string + title: kid + maxLength: 32 + minLength: 1 + description: A unique string identifier for this key + - name: decisionRequests.actions.namespace.grants.publicKey.cached.keys.alg + in: query + description: |- + A known algorithm type with any additional parameters encoded. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. + schema: + not: + enum: + - 0 + title: alg + description: |- + A known algorithm type with any additional parameters encoded. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. + $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' + - name: decisionRequests.actions.namespace.grants.sourceType + in: query + description: 'The source of the KAS: (INTERNAL, EXTERNAL)' + schema: + title: source_type + description: 'The source of the KAS: (INTERNAL, EXTERNAL)' + $ref: '#/components/schemas/policy.SourceType' + - name: decisionRequests.actions.namespace.grants.kasKeys.kasUri + in: query + description: The URL of the Key Access Server + schema: + type: string + title: kas_uri + description: The URL of the Key Access Server + - name: decisionRequests.actions.namespace.grants.kasKeys.publicKey.algorithm + in: query + schema: + title: algorithm + $ref: '#/components/schemas/policy.Algorithm' + - name: decisionRequests.actions.namespace.grants.kasKeys.publicKey.kid + in: query + schema: + type: string + title: kid + - name: decisionRequests.actions.namespace.grants.kasKeys.publicKey.pem + in: query + schema: + type: string + title: pem + - name: decisionRequests.actions.namespace.grants.kasKeys.kasId + in: query + description: The ID of the Key Access Server + schema: + type: string + title: kas_id + description: The ID of the Key Access Server + - name: decisionRequests.actions.namespace.grants.name + in: query + description: |- + Optional + Unique name of the KAS instance + schema: + type: string + title: name + description: |- + Optional + Unique name of the KAS instance - name: decisionRequests.tokens.id in: query description: ephemeral id for tracking between request and response @@ -189,6 +331,38 @@ components: - STANDARD_ACTION_UNSPECIFIED - STANDARD_ACTION_DECRYPT - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + description: Supported key algorithms. + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. authorization.DecisionRequest: type: object properties: @@ -660,6 +834,15 @@ components: additionalProperties: true additionalProperties: true description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. + google.protobuf.BoolValue: + type: boolean + description: |- + Wrapper message for `bool`. + + The JSON representation for `BoolValue` is JSON `true` and `false`. + + Not recommended for use in new APIs, but still useful for legacy APIs and + has no plan to be removed. google.protobuf.Timestamp: type: string examples: @@ -783,12 +966,198 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' title: Action additionalProperties: false description: An action an entity can take + policy.KasPublicKey: + type: object + properties: + pem: + type: string + title: pem + maxLength: 8192 + minLength: 1 + description: x509 ASN.1 content in PEM envelope, usually + kid: + type: string + title: kid + maxLength: 32 + minLength: 1 + description: A unique string identifier for this key + alg: + not: + enum: + - 0 + title: alg + description: |- + A known algorithm type with any additional parameters encoded. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. + $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' + title: KasPublicKey + additionalProperties: false + description: |- + Deprecated + A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeySet: + type: object + properties: + keys: + type: array + items: + $ref: '#/components/schemas/policy.KasPublicKey' + title: keys + title: KasPublicKeySet + additionalProperties: false + description: |- + Deprecated + A list of known KAS public keys + policy.KeyAccessServer: + type: object + properties: + id: + type: string + title: id + uri: + type: string + title: uri + description: |+ + Address of a KAS instance + URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: + ``` + this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') + ``` + + publicKey: + title: public_key + description: 'Deprecated: KAS can have multiple key pairs' + $ref: '#/components/schemas/policy.PublicKey' + sourceType: + title: source_type + description: 'The source of the KAS: (INTERNAL, EXTERNAL)' + $ref: '#/components/schemas/policy.SourceType' + kasKeys: + type: array + items: + $ref: '#/components/schemas/policy.SimpleKasKey' + title: kas_keys + description: Kas keys associated with this KAS + name: + type: string + title: name + description: |- + Optional + Unique name of the KAS instance + metadata: + title: metadata + description: Common metadata + $ref: '#/components/schemas/common.Metadata' + title: KeyAccessServer + additionalProperties: false + description: Key Access Server Registry + policy.Namespace: + type: object + properties: + id: + type: string + title: id + description: generated uuid in database + name: + type: string + title: name + description: |- + used to partition Attribute Definitions, support by namespace AuthN and + enable federation + fqn: + type: string + title: fqn + active: + title: active + description: active by default until explicitly deactivated + $ref: '#/components/schemas/google.protobuf.BoolValue' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + grants: + type: array + items: + $ref: '#/components/schemas/policy.KeyAccessServer' + title: grants + description: Deprecated KAS grants for the namespace. Use kas_keys instead. + kasKeys: + type: array + items: + $ref: '#/components/schemas/policy.SimpleKasKey' + title: kas_keys + description: Keys for the namespace + title: Namespace + additionalProperties: false + policy.PublicKey: + type: object + oneOf: + - properties: + cached: + title: cached + description: public key with additional information. Current preferred version + $ref: '#/components/schemas/policy.KasPublicKeySet' + title: cached + required: + - cached + - properties: + remote: + type: string + title: remote + description: |+ + kas public key url - optional since can also be retrieved via public key + URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: + ``` + this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') + ``` + + title: remote + required: + - remote + title: PublicKey + additionalProperties: false + description: Deprecated + policy.SimpleKasKey: + type: object + properties: + kasUri: + type: string + title: kas_uri + description: The URL of the Key Access Server + publicKey: + title: public_key + description: The public key of the Key that belongs to the KAS + $ref: '#/components/schemas/policy.SimpleKasPublicKey' + kasId: + type: string + title: kas_id + description: The ID of the Key Access Server + title: SimpleKasKey + additionalProperties: false + policy.SimpleKasPublicKey: + type: object + properties: + algorithm: + title: algorithm + $ref: '#/components/schemas/policy.Algorithm' + kid: + type: string + title: kid + pem: + type: string + title: pem + title: SimpleKasPublicKey + additionalProperties: false connect-protocol-version: type: number title: Connect-Protocol-Version diff --git a/specs/authorization/v2/authorization.openapi.yaml b/specs/authorization/v2/authorization.openapi.yaml index e284ea2a..0f4f92d5 100644 --- a/specs/authorization/v2/authorization.openapi.yaml +++ b/specs/authorization/v2/authorization.openapi.yaml @@ -165,6 +165,38 @@ components: - STANDARD_ACTION_UNSPECIFIED - STANDARD_ACTION_DECRYPT - STANDARD_ACTION_TRANSMIT + policy.Algorithm: + type: string + title: Algorithm + enum: + - ALGORITHM_UNSPECIFIED + - ALGORITHM_RSA_2048 + - ALGORITHM_RSA_4096 + - ALGORITHM_EC_P256 + - ALGORITHM_EC_P384 + - ALGORITHM_EC_P521 + description: Supported key algorithms. + policy.KasPublicKeyAlgEnum: + type: string + title: KasPublicKeyAlgEnum + enum: + - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048 + - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 + - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + policy.SourceType: + type: string + title: SourceType + enum: + - SOURCE_TYPE_UNSPECIFIED + - SOURCE_TYPE_INTERNAL + - SOURCE_TYPE_EXTERNAL + description: |- + Describes whether this kas is managed by the organization or if they imported + the kas information from an external party. These two modes are necessary in order + to encrypt a tdf dek with an external parties kas public key. authorization.v2.EntityEntitlements: type: object properties: @@ -772,12 +804,198 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' title: Action additionalProperties: false description: An action an entity can take + policy.KasPublicKey: + type: object + properties: + pem: + type: string + title: pem + maxLength: 8192 + minLength: 1 + description: x509 ASN.1 content in PEM envelope, usually + kid: + type: string + title: kid + maxLength: 32 + minLength: 1 + description: A unique string identifier for this key + alg: + not: + enum: + - 0 + title: alg + description: |- + A known algorithm type with any additional parameters encoded. + To start, these may be `rsa:2048` for RSA-based wrapping and + `ec:secp256r1` for EC-based wrapping, but more formats may be added as needed. + $ref: '#/components/schemas/policy.KasPublicKeyAlgEnum' + title: KasPublicKey + additionalProperties: false + description: |- + Deprecated + A KAS public key and some associated metadata for further identifcation + policy.KasPublicKeySet: + type: object + properties: + keys: + type: array + items: + $ref: '#/components/schemas/policy.KasPublicKey' + title: keys + title: KasPublicKeySet + additionalProperties: false + description: |- + Deprecated + A list of known KAS public keys + policy.KeyAccessServer: + type: object + properties: + id: + type: string + title: id + uri: + type: string + title: uri + description: |+ + Address of a KAS instance + URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: + ``` + this.matches('^https?://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(/.*)?$') + ``` + + publicKey: + title: public_key + description: 'Deprecated: KAS can have multiple key pairs' + $ref: '#/components/schemas/policy.PublicKey' + sourceType: + title: source_type + description: 'The source of the KAS: (INTERNAL, EXTERNAL)' + $ref: '#/components/schemas/policy.SourceType' + kasKeys: + type: array + items: + $ref: '#/components/schemas/policy.SimpleKasKey' + title: kas_keys + description: Kas keys associated with this KAS + name: + type: string + title: name + description: |- + Optional + Unique name of the KAS instance + metadata: + title: metadata + description: Common metadata + $ref: '#/components/schemas/common.Metadata' + title: KeyAccessServer + additionalProperties: false + description: Key Access Server Registry + policy.Namespace: + type: object + properties: + id: + type: string + title: id + description: generated uuid in database + name: + type: string + title: name + description: |- + used to partition Attribute Definitions, support by namespace AuthN and + enable federation + fqn: + type: string + title: fqn + active: + title: active + description: active by default until explicitly deactivated + $ref: '#/components/schemas/google.protobuf.BoolValue' + metadata: + title: metadata + $ref: '#/components/schemas/common.Metadata' + grants: + type: array + items: + $ref: '#/components/schemas/policy.KeyAccessServer' + title: grants + description: Deprecated KAS grants for the namespace. Use kas_keys instead. + kasKeys: + type: array + items: + $ref: '#/components/schemas/policy.SimpleKasKey' + title: kas_keys + description: Keys for the namespace + title: Namespace + additionalProperties: false + policy.PublicKey: + type: object + oneOf: + - properties: + cached: + title: cached + description: public key with additional information. Current preferred version + $ref: '#/components/schemas/policy.KasPublicKeySet' + title: cached + required: + - cached + - properties: + remote: + type: string + title: remote + description: |+ + kas public key url - optional since can also be retrieved via public key + URI must be a valid URL (e.g., 'https://demo.com/') followed by additional segments. Each segment must start and end with an alphanumeric character, can contain hyphens, alphanumeric characters, and slashes.: + ``` + this.matches('^https://[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*(/.*)?$') + ``` + + title: remote + required: + - remote + title: PublicKey + additionalProperties: false + description: Deprecated + policy.SimpleKasKey: + type: object + properties: + kasUri: + type: string + title: kas_uri + description: The URL of the Key Access Server + publicKey: + title: public_key + description: The public key of the Key that belongs to the KAS + $ref: '#/components/schemas/policy.SimpleKasPublicKey' + kasId: + type: string + title: kas_id + description: The ID of the Key Access Server + title: SimpleKasKey + additionalProperties: false + policy.SimpleKasPublicKey: + type: object + properties: + algorithm: + title: algorithm + $ref: '#/components/schemas/policy.Algorithm' + kid: + type: string + title: kid + pem: + type: string + title: pem + title: SimpleKasPublicKey + additionalProperties: false connect-protocol-version: type: number title: Connect-Protocol-Version diff --git a/specs/policy/actions/actions.openapi.yaml b/specs/policy/actions/actions.openapi.yaml index a3efaf59..c7189a2b 100644 --- a/specs/policy/actions/actions.openapi.yaml +++ b/specs/policy/actions/actions.openapi.yaml @@ -435,6 +435,10 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' @@ -1053,6 +1057,17 @@ components: this.matches('^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$') ``` + namespaceId: + type: string + title: namespace_id + format: uuid + description: ID of the namespace. Required if namespace_fqn is not provided. + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri + description: FQN of the namespace. Required if namespace_id is not provided. metadata: title: metadata description: Optional @@ -1116,8 +1131,26 @@ components: title: name required: - name + properties: + namespaceId: + type: string + title: namespace_id + format: uuid + description: ID of the namespace. Required when identifier is set to name. + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri + description: FQN of the namespace. Required when identifier is set to name. title: GetActionRequest additionalProperties: false + description: |+ + Either namespace_id or namespace_fqn must be provided when getting an action by name: + ``` + !has(this.name) || this.namespace_id != '' || this.namespace_fqn != '' + ``` + policy.actions.GetActionResponse: type: object properties: @@ -1135,6 +1168,17 @@ components: policy.actions.ListActionsRequest: type: object properties: + namespaceId: + type: string + title: namespace_id + format: uuid + description: ID of the namespace. Required if namespace_fqn is not provided. + namespaceFqn: + type: string + title: namespace_fqn + minLength: 1 + format: uri + description: FQN of the namespace. Required if namespace_id is not provided. pagination: title: pagination description: Optional diff --git a/specs/policy/attributes/attributes.openapi.yaml b/specs/policy/attributes/attributes.openapi.yaml index faf39ff3..e73ac435 100644 --- a/specs/policy/attributes/attributes.openapi.yaml +++ b/specs/policy/attributes/attributes.openapi.yaml @@ -956,6 +956,10 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' diff --git a/specs/policy/objects.openapi.yaml b/specs/policy/objects.openapi.yaml index 361920e1..8f6c22f5 100644 --- a/specs/policy/objects.openapi.yaml +++ b/specs/policy/objects.openapi.yaml @@ -248,6 +248,10 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' diff --git a/specs/policy/obligations/obligations.openapi.yaml b/specs/policy/obligations/obligations.openapi.yaml index a0bc8c8c..550d41c0 100644 --- a/specs/policy/obligations/obligations.openapi.yaml +++ b/specs/policy/obligations/obligations.openapi.yaml @@ -784,6 +784,10 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' diff --git a/specs/policy/registeredresources/registered_resources.openapi.yaml b/specs/policy/registeredresources/registered_resources.openapi.yaml index a11f21fb..a1e390ba 100644 --- a/specs/policy/registeredresources/registered_resources.openapi.yaml +++ b/specs/policy/registeredresources/registered_resources.openapi.yaml @@ -645,6 +645,10 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' diff --git a/specs/policy/resourcemapping/resource_mapping.openapi.yaml b/specs/policy/resourcemapping/resource_mapping.openapi.yaml index 86b0d952..22016ce1 100644 --- a/specs/policy/resourcemapping/resource_mapping.openapi.yaml +++ b/specs/policy/resourcemapping/resource_mapping.openapi.yaml @@ -645,6 +645,10 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' diff --git a/specs/policy/subjectmapping/subject_mapping.openapi.yaml b/specs/policy/subjectmapping/subject_mapping.openapi.yaml index f8dcc5ba..7b73d00e 100644 --- a/specs/policy/subjectmapping/subject_mapping.openapi.yaml +++ b/specs/policy/subjectmapping/subject_mapping.openapi.yaml @@ -681,6 +681,10 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' diff --git a/specs/policy/unsafe/unsafe.openapi.yaml b/specs/policy/unsafe/unsafe.openapi.yaml index bf346f58..40d42bad 100644 --- a/specs/policy/unsafe/unsafe.openapi.yaml +++ b/specs/policy/unsafe/unsafe.openapi.yaml @@ -614,6 +614,10 @@ components: name: type: string title: name + namespace: + title: namespace + description: Namespace context for this action + $ref: '#/components/schemas/policy.Namespace' metadata: title: metadata $ref: '#/components/schemas/common.Metadata' From 5cadc164c4654de21bb963c1f845e2196cf374ea Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 16 Mar 2026 11:33:47 -0700 Subject: [PATCH 3/7] fix(docs): address code review on JS quickstart examples - Replace listNamespaces + filter with getNamespace by FQN for direct lookup when namespace already exists - Add missing 'marketing' value creation in Step 6 complete example when attribute exists but value doesn't Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/quickstart/javascript.mdx | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 6526ac6f..ed381b4f 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -206,10 +206,11 @@ try { console.log(`āœ… Created namespace: ${namespaceId}`); } catch (err) { if (err.message?.includes('already_exists')) { - // Namespace exists, fetch it - const listNsResp = await platform.v1.namespace.listNamespaces({}); - const ns = listNsResp.namespaces.find((n) => n.name === 'opentdf.io'); - namespaceId = ns?.id; + // Namespace exists — fetch it directly by FQN + const getNsResp = await platform.v1.namespace.getNamespace({ + identifier: { case: 'fqn', value: 'https://opentdf.io' }, + }); + namespaceId = getNsResp.namespace?.id; console.log(`āœ… Using existing namespace: ${namespaceId}`); } else { throw err; @@ -424,9 +425,11 @@ async function main() { console.log(`āœ… Created namespace: ${namespaceId}`); } catch (err) { if (err.message?.includes('already_exists')) { - const listResp = await platform.v1.namespace.listNamespaces({}); - const ns = listResp.namespaces.find((n) => n.name === 'opentdf.io'); - namespaceId = ns?.id; + // Namespace exists — fetch it directly by FQN + const getNsResp = await platform.v1.namespace.getNamespace({ + identifier: { case: 'fqn', value: 'https://opentdf.io' }, + }); + namespaceId = getNsResp.namespace?.id; console.log(`āœ… Using existing namespace: ${namespaceId}`); } else { throw err; @@ -451,6 +454,15 @@ async function main() { (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; } From b4d90d64e5bf73ec7e357b59c9735f0ec32239eb Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 16 Mar 2026 11:37:19 -0700 Subject: [PATCH 4/7] fix(docs): explain why ES modules are needed in JS quickstart Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/quickstart/javascript.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index ed381b4f..c1e997d8 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -42,7 +42,7 @@ cd opentdf-quickstart npm init -y ``` -Enable ES modules by adding `"type": "module"` to your `package.json`: +The OpenTDF SDK uses ES module (`import`/`export`) syntax. Enable ES modules by adding `"type": "module"` to your `package.json`: ```json title="package.json" { From 95b1b3edea63be81b36961e60b3354831d876142 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 16 Mar 2026 11:43:43 -0700 Subject: [PATCH 5/7] fix(docs): document expected BaseKey warning in JS quickstart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK logs a NetworkError about missing BaseKey in WellKnownConfiguration during encryption. This is harmless — it falls back to the legacy KAS public key endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/quickstart/javascript.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index c1e997d8..c471f896 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -145,6 +145,7 @@ NODE_TLS_REJECT_UNAUTHORIZED=0 node index.mjs šŸ“ Encrypting sensitive data... šŸ”’ Creating TDF... +NetworkError: ... is missing BaseKey in WellKnownConfiguration ← expected, see note below āœ… Data successfully encrypted šŸ“Š Encrypted TDF size: 1234 bytes @@ -159,6 +160,10 @@ Hello from the OpenTDF JS SDK! This data is encrypted. +:::note BaseKey warning is expected +You may see a `NetworkError: ... is missing BaseKey in WellKnownConfiguration` message during encryption. This is harmless — the SDK first tries to fetch the KAS public key from the platform's well-known configuration, and when that isn't populated, it falls back to the legacy KAS public key endpoint. Encryption and decryption will still succeed. +::: + --- ## Step 5: Add ABAC Features {#step-5-js} From c89bbc9326fb975561e897ac97d5aedb0dab90c3 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 16 Mar 2026 12:24:36 -0700 Subject: [PATCH 6/7] fix(docs): add DPoP key binding before PlatformClient usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlatformClient requires DPoP keys bound to the auth provider before making API calls. OpenTDF generates these keys but only binds them lazily during encrypt/decrypt — so explicit binding is needed when PlatformClient is used before any TDF operation. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/platform-client.mdx | 6 +++++ docs/sdks/policy.mdx | 42 ++++++++++++++--------------- docs/sdks/quickstart/javascript.mdx | 12 ++++++--- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/docs/sdks/platform-client.mdx b/docs/sdks/platform-client.mdx index 96001e9f..0de6c0bf 100644 --- a/docs/sdks/platform-client.mdx +++ b/docs/sdks/platform-client.mdx @@ -62,9 +62,15 @@ SDK sdk = SDKBuilder.newBuilder() ```typescript +import { OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; // See the Auth Providers guide for authProvider setup. +// OpenTDF generates DPoP keys needed for authenticated platform calls. +// Bind them to the auth provider before creating PlatformClient. +const client = new OpenTDF({ authProvider, platformUrl: 'http://localhost:8080' }); +await authProvider.updateClientPublicKey(await client.dpopKeys); + const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080', diff --git a/docs/sdks/policy.mdx b/docs/sdks/policy.mdx index 6332b53d..3bcd6e25 100644 --- a/docs/sdks/policy.mdx +++ b/docs/sdks/policy.mdx @@ -126,7 +126,7 @@ public class GetNamespaceExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -234,7 +234,7 @@ public class UpdateNamespaceExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -322,7 +322,7 @@ public class DeactivateNamespaceExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -435,7 +435,7 @@ public class GetAttributeExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -536,7 +536,7 @@ public class UpdateAttributeExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -624,7 +624,7 @@ public class DeactivateAttributeExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -716,7 +716,7 @@ public class CreateAttributeValueExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -807,7 +807,7 @@ public class ListAttributeValuesExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -898,7 +898,7 @@ public class GetAttributeValueExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1002,7 +1002,7 @@ public class GetAttributeValuesByFqnsExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1104,7 +1104,7 @@ public class UpdateAttributeValueExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1192,7 +1192,7 @@ public class DeactivateAttributeValueExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1289,7 +1289,7 @@ public class ListSubjectConditionSetsExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1380,7 +1380,7 @@ public class GetSubjectConditionSetExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1504,7 +1504,7 @@ public class UpdateSubjectConditionSetExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1606,7 +1606,7 @@ public class DeleteSubjectConditionSetExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1689,7 +1689,7 @@ public class DeleteAllUnmappedSubjectConditionSetsExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1786,7 +1786,7 @@ public class GetSubjectMappingExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1891,7 +1891,7 @@ public class UpdateSubjectMappingExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -1978,7 +1978,7 @@ public class DeleteSubjectMappingExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); @@ -2082,7 +2082,7 @@ public class MatchSubjectMappingsExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See the Auth Providers guide for authProvider setup. +// See /sdks/platform-client for full setup including DPoP key binding. const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index c471f896..bfe39a40 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -196,9 +196,11 @@ const authProvider = await AuthProviders.clientSecretAuthProvider({ oidcOrigin: 'https://keycloak.opentdf.local:9443/auth/realms/opentdf', }); -// Create the OpenTDF client first — it automatically generates DPoP keys -// and configures them on the auth provider, which PlatformClient also needs +// Create the OpenTDF client — this generates DPoP keys but doesn't +// bind them to the auth provider until the first encrypt/decrypt call. +// We need to do that explicitly so PlatformClient can authenticate. const client = new OpenTDF({ authProvider, platformUrl }); +await authProvider.updateClientPublicKey(await client.dpopKeys); const platform = new PlatformClient({ authProvider, platformUrl }); // First, ensure the namespace exists @@ -415,9 +417,11 @@ async function main() { oidcOrigin, }); - // Create the OpenTDF client first — it automatically generates DPoP keys - // and configures them on the auth provider, which PlatformClient also needs + // Create the OpenTDF client — this generates DPoP keys but doesn't + // bind them to the auth provider until the first encrypt/decrypt call. + // We need to do that explicitly so PlatformClient can authenticate. const client = new OpenTDF({ authProvider, platformUrl }); + await authProvider.updateClientPublicKey(await client.dpopKeys); const platform = new PlatformClient({ authProvider, platformUrl }); // 1. Create namespace (or use existing) From 7928750c9ef02ca95c1cd75a04b094adf70f6849 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 17 Mar 2026 08:27:33 -0700 Subject: [PATCH 7/7] fix(docs): address PR review feedback on JS quickstart - Change Node.js prerequisite to "Node.js LTS" with link - Clarify SDK supports both ESM and CommonJS Co-Authored-By: Claude Opus 4.6 --- docs/sdks/quickstart/javascript.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 5fa8566e..7763ecde 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -11,7 +11,7 @@ This guide covers the **JavaScript/TypeScript SDK** implementation. For other la ## Prerequisites -- Node.js 18 or later +- [Node.js LTS](https://nodejs.org/en/about/previous-releases) - Your OpenTDF platform running locally (from Getting Started guide) :::warning Platform Must Be Running @@ -42,7 +42,7 @@ cd opentdf-quickstart npm init -y ``` -The OpenTDF SDK uses ES module (`import`/`export`) syntax. Enable ES modules by adding `"type": "module"` to your `package.json`: +The SDK supports both ESM and CommonJS. In this guide we'll use `import` syntax. Ensure your environment supports ES modules (e.g., via `"type": "module"` in `package.json` or a bundler): ```json title="package.json" {