Workspaces
Storage connections
client.storageConnections manages the storage connections a new workspace can keep its files in, instead of Nace's managed storage. See Use your own storage for the concept, and the API reference for every field.
- A storage connection is your own S3 bucket or Azure Blob container. A workspace created with one keeps its files there, under its own folder, and never falls back to Nace's managed storage.
- Who can do what: any member of your account can list, read and use a connection. Only its creator, or an owner or admin, can change it; anyone else gets
PermissionDeniedError, codeaccess_denied. A connection'scan_managesays whether your key can. - Cost: managing storage connections costs nothing and works at any balance.
- Availability: where a deployment doesn't offer storage connections, every method rejects with
NotFoundError, codestorage_connection_unavailable.
A connection's status is one of:
| State | What you can do |
|---|---|
"draft" | Set it up, save credentials and validate it. It can't hold a workspace yet. |
"ready" | Give it to a new workspace. Validated: Nace can write, read and delete in the destination. |
"degraded" | A later check failed. Fix the access in your cloud and validate it again. |
"revoked" | Nace stopped using it, and workspaces in it stop reading and writing. Validate one you revoked with reactivate: true to restore it. |
A connection goes through these steps before a workspace can use it:
- Create a draft with the bucket or container (
storageConnections.create). - Read what to create in your cloud: an IAM role for S3, a service principal's access for Azure (
storageConnections.setup). Optionally save your AWS account and role name, or your Azure subscription and resource group, first, so the setup is filled in (storageConnections.setSetup). - Create the role or service principal in your cloud, then save it (
storageConnections.setCredentials). - Validate it and wait for the job (
storageConnections.validate,storageConnections.waitForValidation). A new IAM role or Azure role assignment can take a few minutes to apply, so a fresh one may fail at first: validate again. - Create a workspace with the connection's
idasstorage_connection_id(workspaces.create).
Only a connection whose status is "ready", with credentials_pending, validation_running and reactivation_requires_approval all false, can be given to a new workspace.
Every method that works on one connection takes its connectionId first; one that isn't your account's rejects with NotFoundError, code storage_connection_unavailable.
- Errors: a change by someone who isn't the connection's creator, or an owner or admin, rejects with
PermissionDeniedError(access_denied). Credentials the cloud refuses reject withUnprocessableEntityError(storage_credentials_refused).deleteon a connection a workspace uses, andvalidateon one Nace revoked, reject withConflictError. AconnectionIdthat is empty or holds/,?or#, or alimitoutside 1 to 200, throws aNaceErrorbefore any request. - Retries:
createcarries anIdempotency-Key, minted per call whenidempotency_keyis omitted, so a retried create returns the same connection.validateanddeletearen't retried unless you passretryinoptions: a retry after the first call went through starts a second validation, or rejects withNotFoundErrorfor the deleted connection. Every other method is retried. - Secrets: an Azure
client_secretis sent once and never returned. Debug logs show it as***, and a refusal never quotes it.
List storage connections
client.storageConnections.list(params?, options?) returns APIPromise<StorageConnectionPage>, one page of your account's storage connections. params is a StorageConnectionListParams.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | 1 to 200. Default: 50. |
cursor | string | null | No | The previous page's next_cursor. Keep going until it's null. |
const page = await client.storageConnections.list({ limit: 20 });
for (const connection of page.items) {
console.log(connection.id, connection.name, connection.status);
}Iterate over storage connections
client.storageConnections.iter(options?) returns AsyncGenerator<StorageConnection>, following every page, 200 a page.
for await (const connection of client.storageConnections.iter()) {
console.log(connection.id, connection.destination.type);
}Create a storage connection
client.storageConnections.create(params, options?) returns APIPromise<StorageConnection>: a draft. params is a CreateStorageConnectionParams.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The name people see: 1 to 200 characters. |
destination | StorageDestination | Yes | An S3Destination or an AzureBlobDestination. It can't be changed later. |
idempotency_key | string | No | Sent as the Idempotency-Key header, up to 200 characters: the same key returns the same connection. Minted per call when omitted. |
S3Destination field | Type | Description |
|---|---|---|
type | "s3" | An S3 bucket. |
bucket | string | Your S3 bucket. |
region | string | The AWS region the bucket is in, such as us-east-1. |
prefix | string | A base prefix in the bucket, without a leading or trailing slash. Each workspace gets its own folder beneath it. Default: "". |
AzureBlobDestination field | Type | Description |
|---|---|---|
type | "azure_blob" | An Azure Blob container. |
account | string | The Azure storage account name. |
container | string | Your Blob container. |
prefix | string | A base prefix in the container, without a leading or trailing slash. Each workspace gets its own folder beneath it. Default: "". |
const connection = await client.storageConnections.create({
name: "Audit evidence",
destination: { type: "s3", bucket: "acme-audit-evidence", region: "us-east-1", prefix: "nace" },
});
console.log(connection.id, connection.external_id);Read a storage connection
client.storageConnections.get(connectionId, options?) returns APIPromise<StorageConnection>.
| Parameter | Type | Required | Description |
|---|---|---|---|
connectionId | string | Yes | The connection. |
const connection = await client.storageConnections.get(connectionId);
console.log(connection.status, connection.validated_at);Read the setup
client.storageConnections.setup(connectionId, options?) returns APIPromise<StorageConnectionSetup>. It says what to create in your cloud so Nace can reach the destination, generated from the setup options you saved.
| Parameter | Type | Required | Description |
|---|---|---|---|
connectionId | string | Yes | The connection. |
For S3 it has an IAM role trust_policy carrying the connection's external_id, its permissions_policy, a cloudformation_template that creates the role, the role_arn that role will have, and a launch_url that opens the stack in the AWS console once your AWS account id and role name are saved. For Azure it has azure_cli, the az commands that give your service principal access to the container.
const setup = await client.storageConnections.setup(connectionId);
console.log(setup.trust_policy, setup.permissions_policy);Save the setup options
client.storageConnections.setSetup(connectionId, setup, options?) returns APIPromise<StorageConnectionSetup>, generated from what you saved. These are the non-secret inputs only.
| Parameter | Type | Required | Description |
|---|---|---|---|
connectionId | string | Yes | The connection. |
setup | StorageSetupOptions | Yes | An S3SetupOptions or an AzureSetupOptions. Its type must match the connection's destination. |
S3SetupOptions field | Type | Description |
|---|---|---|
type | "s3" | For an S3 destination. |
access_method | "auto" | "manual" | "auto" for the CloudFormation stack, "manual" to write the role yourself. Default: "auto". |
aws_account_id | string | null | The AWS account the role is created in: 12 digits. |
role_name | string | null | The name of the role the stack creates. |
kms_key_arn | string | null | A KMS key the role may use; the bucket's settings decide encryption. |
AzureSetupOptions field | Type | Description |
|---|---|---|
type | "azure_blob" | For an Azure Blob destination. |
subscription_id | string | null | The subscription the storage account is in. |
resource_group | string | null | The resource group the storage account is in. |
tenant_id | string | null | Your Microsoft Entra tenant. |
client_id | string | null | The application (client) id to give access to. |
const setup = await client.storageConnections.setSetup(connectionId, {
type: "s3",
aws_account_id: "444455556666",
role_name: "nace-workspaces",
});
console.log(setup.launch_url, setup.role_arn);Save the credentials
client.storageConnections.setCredentials(connectionId, credential, options?) returns APIPromise<StorageConnection>, with credentials_pending until a validation passes. The credentials are write-only: they are never returned.
| Parameter | Type | Required | Description |
|---|---|---|---|
connectionId | string | Yes | The connection. |
credential | StorageCredential | Yes | An AwsRoleCredential for S3, or an AzureServicePrincipalCredential for Azure. |
AwsRoleCredential field | Type | Description |
|---|---|---|
type | "aws_role" | An IAM role. |
role_arn | string | Your IAM role that trusts Nace's roles with the connection's external_id. |
AzureServicePrincipalCredential field | Type | Description |
|---|---|---|
type | "azure_service_principal" | A Microsoft Entra application. |
tenant_id | string | Your Microsoft Entra tenant. |
client_id | string | The application (client) id, with Storage Blob Data Contributor on the container. |
client_secret | string | The application's secret. Never returned. |
Credentials the service refuses reject with UnprocessableEntityError, code storage_credentials_refused.
await client.storageConnections.setCredentials(connectionId, {
type: "azure_service_principal",
tenant_id: process.env.AZURE_TENANT_ID ?? "",
client_id: process.env.AZURE_CLIENT_ID ?? "",
client_secret: process.env.AZURE_CLIENT_SECRET ?? "",
});Validate a storage connection
client.storageConnections.validate(connectionId, params?, options?) returns APIPromise<DocumentJob>. The job, of kind storage_validation, checks Nace can write, read and delete in the destination. Once it succeeds, the connection is ready. params is a ValidateStorageConnectionParams.
| Parameter | Type | Required | Description |
|---|---|---|---|
connectionId | string | Yes | The connection. |
reactivate | boolean | No | Restore a connection you revoked once the validation passes. Default: false. |
A connection that can't be validated now, for example because Nace revoked it, rejects with ConflictError.
client.storageConnections.validation(connectionId, options?) returns APIPromise<DocumentJob>: the connection's latest validation job. While it is queued or running, the connection has validation_running. A failed job's error says what Nace couldn't do.
| Parameter | Type | Required | Description |
|---|---|---|---|
connectionId | string | Yes | The connection. |
Wait for the validation
client.storageConnections.waitForValidation(connectionId, options?) returns Promise<DocumentJob>. It reads the latest validation job with storageConnections.validation until its status is one of TERMINAL_STATUSES, and returns it. A job that ends failed is returned, not thrown: read its error.
| Parameter | Type | Required | Description |
|---|---|---|---|
connectionId | string | Yes | The connection. |
options is a WaitForValidationOptions: a RequestOptions plus pollIntervalMs and timeoutMs. Each read takes the request options. pollIntervalMs is the milliseconds between reads, default 2000. timeoutMs is how long to wait for the job to settle, in milliseconds: the whole wait, not one read, default 180000 (3 minutes).
When the time runs out it throws JobTimeoutError, whose job is the job as last read and timeoutMs the wait budget. The job keeps going: wait again later. Aborting signal throws APIUserAbortError.
| Constant | Value | Description |
|---|---|---|
DEFAULT_STORAGE_VALIDATION_POLL_MS | 2000 | The default pollIntervalMs of storageConnections.waitForValidation. |
DEFAULT_STORAGE_VALIDATION_TIMEOUT_MS | 180000 | The default timeoutMs of storageConnections.waitForValidation. |
await client.storageConnections.validate(connectionId);
const job = await client.storageConnections.waitForValidation(connectionId);
console.log(job.status, job.error);Revoke a storage connection
client.storageConnections.revoke(connectionId, options?) returns APIPromise<StorageConnection>, with status "revoked". Nace stops using it: workspaces in it stop reading and writing until it is validated again with reactivate: true. Their files stay in your bucket.
| Parameter | Type | Required | Description |
|---|---|---|---|
connectionId | string | Yes | The connection. |
const revoked = await client.storageConnections.revoke(connectionId);
console.log(revoked.status, revoked.revoked_reason);Delete a storage connection
client.storageConnections.delete(connectionId, options?) returns Promise<void>. Nothing in the bucket is touched. A connection a workspace uses rejects with ConflictError. It isn't retried unless you pass retry in options.
| Parameter | Type | Required | Description |
|---|---|---|---|
connectionId | string | Yes | The connection. |
await client.storageConnections.delete(connectionId);Create a workspace in your storage
Pass a ready connection's id as storage_connection_id. See Create a workspace.
const connection = await client.storageConnections.get(connectionId);
if (connection.status === "ready" && !connection.credentials_pending && !connection.validation_running) {
const workspace = await client.workspaces.create({ name: "fy25-audit", storage_connection_id: connection.id });
console.log(workspace.storage?.location);
}