Tenant scope keys
TenantScopeKeyring manages versioned, 32-byte keys inside a tenant and scope.
It coordinates lifecycle state; a customer-owned TenantScopeKeyStore remains
the persistence and destruction boundary. DataPrism does not receive or host the
key material.
import {
MemoryTenantScopeKeyStore,
TenantScopeKeyring,
} from "@dataprism/sdk/tenants";
const keys = new TenantScopeKeyring({
tenantId: "tenant-123",
scopeId: "documents",
store: new MemoryTenantScopeKeyStore(),
});
await keys.create(); // creates version 1
const version1 = await keys.resolve(1);The keyring can be passed directly to the file pipeline as its customer-owned resolver. Each operation requests the exact version recorded by the file:
import { FilePipeline } from "@dataprism/sdk/files";
const pipeline = new FilePipeline({
providers,
standard,
scopeKeyResolver: keys,
});
await pipeline.upload("before.bin", beforeBytes, uploadOptions); // version 1
await keys.rotate();
await pipeline.upload("after.bin", afterBytes, {
...uploadOptions,
scopeKeyVersion: 2,
});The memory adapter is non-durable and intended for tests and examples. Production
applications must implement TenantScopeKeyStore against storage they own. Its
compare-and-create operation must be atomic so concurrent rotations cannot reuse
or skip a version.
Key generators and store adapters may return a Node Buffer or a plain
Uint8Array. The keyring makes independent byte copies before clearing its
temporary material. Store adapters must do the same when retaining a key or
returning one from resolve: use Uint8Array.from(bytes) or another copying
operation, not Buffer.slice(), which shares the original bytes. A resolved
copy belongs to the caller and must be cleared when no longer needed.
Three separate keys
The Prism opening key, tenant scope keys and filename-addressing key have different jobs:
| Key | Purpose | Where it comes from |
|---|---|---|
| Prism opening key | Seals metadata, provider settings and file indexes | The dashboard derives it from an account signature; SDK callers can supply a secret through DataPrismKey.fromSecret. |
| Tenant scope key | Derives encryption keys for file chunks and manifests | TenantScopeKeyring creates random, versioned 32-byte keys in the customer-owned key store. |
| Filename-addressing key | Derives stable, scoped file IDs from filenames | TenantScope.bindPrism creates a separate, stable 32-byte key in the customer-owned prism binding store. |
The public cloudId cannot reproduce any of these secrets. A
TenantPrismBindingStore must retain the addressing key across process
restarts and scope-key rotations. TenantScope.openPrism reads an existing
binding; it does not generate a replacement key if the binding is missing.
The memory binding store, like the memory key store, is for tests and examples.
TenantPrism.standard(key) supplies that addressing key to CloudStandard;
the caller still supplies the separate Prism opening key. When building
CloudStandard directly, provide a stable fileDataIdScopeKey yourself. Do
not replace it with each new tenant scope-key version, or filename lookups
will change. See Scoped file identifiers.
V3 uploads
V3 writes are available through an explicit publicIndex upload option.
They require scoped filename addressing, a customer-held scope key or resolver,
and customer-self-custody-v1. A TenantPrism.filePipeline supplies the scoped
standard and resolver and pins the latest active scope-key version when the
pipeline is built; it does not select V3 automatically.
For a pipeline configured with those keys:
await pipeline.upload("report.pdf", reportBytes, {
...uploadOptions,
publicIndex: {
derivationPolicy: "tenant-scope-key-v1",
custody: "customer-self-custody-v1",
encodingMode: "reed-solomon-dense-cauchy-v1",
},
});An on-chain V3 upload also needs an explicit contentAnchorCommitter, such as
AccountExecution; a configured cloudId without it is rejected before shard
writes. This does not imply relayed-write support or support in every deployed
contract. V3 overwrite: true is unsupported: use a new entry. An offline
pipeline with no cloudId can return the sealed slot for customer-managed
submission.
Each new V3 upload records a randomized content commitment with a fresh 32-byte
nonce and custody.recovery: "none". DataPrism holds no recovery key and cannot
restore a lost or destroyed customer scope key. The commitment is checked on
download; it is not a substitute for encryption or scoped filename addressing.
Without publicIndex, uploads retain the V2 path; existing legacy/V2 indexes
remain readable. Enabling V3 does not migrate those entries. Customer KMS
adapters are not implemented. SDK distribution remains restricted pending the
licence decision.
Lifecycle rules
create()creates version 1 only when the scope does not exist.rotate()creates the next version while preserving older versions for historical reads.resolve(version)is the only keyring method that returns key bytes. Do not log or serialize its return value.destroy()records the irreversibledestroyingstate before asking the store to remove each version. Rotation cannot resume after this point.- Destruction reports
succeededonly when every recorded version is a tombstone. A failed adapter operation reports that recorded key material may remain and can be retried.
Lifecycle results contain the tenant, scope, version, derivation and custody
identifiers, but no key or key fingerprint. observedAt is the caller's local
observation time; it is not an independently established timestamp.
Claim boundary
A successful result describes key material recorded by that store. It does not prove that undisclosed copies, application logs, process memory or backups were removed. JavaScript memory zeroing is also not a hardware-backed destruction guarantee.
Resolver-backed reads, health checks, repair and orphan cleanup use the exact key versions recorded in the sealed index. Lifecycle observations can be retained in a customer-owned evidence journal; their presence does not broaden the destruction claim above.

