@terella/data-plane
v0.1.0
Published
Portable S3-compatible data-plane SDK for Bun and Cloudflare Workers.
Downloads
90
Maintainers
Readme
@terella/data-plane
Portable S3-compatible data-plane SDK for Bun and Cloudflare Workers. The SDK
uses only fetch and Web Crypto APIs, signs requests with AWS Signature V4,
and provides typed access to shared Terella data contracts.
Installation
bun add @terella/data-planeThe package is also compatible with npm-compatible package managers and can be bundled for Cloudflare Workers.
Using the package directly from Git
The SDK is maintained as the packages/terella-sdk/data-plane submodule of Terella and
has its own repository. Add it directly as a Git dependency:
bun add git+https://github.com/terella-project/terella-data-plane.gitThis adds a dependency similar to:
{
"dependencies": {
"@terella/data-plane": "git+https://github.com/terella-project/terella-data-plane.git"
}
}Pin a branch, tag, or commit when reproducibility is important:
{
"dependencies": {
"@terella/data-plane": "git+https://github.com/terella-project/terella-data-plane.git#main"
}
}To work on the package through the Terella repository, initialize submodules:
git clone --recurse-submodules https://github.com/terella-project/terella.git
# or, from an existing checkout:
git submodule update --init packages/terella-sdk/data-planeConfiguration
Create a client with an endpoint, bucket, and S3 credentials:
import { createDataPlaneClient } from "@terella/data-plane";
const dataPlane = createDataPlaneClient({
endpoint: "https://account-id.r2.cloudflarestorage.com",
bucket: "terella-shared",
accessKey: env.S3_ACCESS_KEY,
secretKey: env.S3_SECRET_KEY,
region: "auto", // default
});The endpoint may include the bucket as its first path segment. Both forms are equivalent:
{ endpoint: "https://account-id.r2.cloudflarestorage.com/terella-shared", accessKey, secretKey }
{ endpoint: "https://account-id.r2.cloudflarestorage.com", bucket: "terella-shared", accessKey, secretKey }For existing deployments, configuration can be read from environment-style values without exposing credentials in logs:
import { createDataPlaneClient, s3ConfigurationFromEnv } from "@terella/data-plane";
const config = s3ConfigurationFromEnv({
S3_API_URL: process.env.S3_API_URL,
S3_BUCKET: process.env.S3_BUCKET,
ACCESS_KEY: process.env.ACCESS_KEY,
SECRET_ACCESS_KEY: process.env.SECRET_ACCESS_KEY,
S3_REGION: process.env.S3_REGION,
});
const dataPlane = config ? createDataPlaneClient(config) : null;Supported environment names are:
| Purpose | Preferred names | Compatibility names |
| --- | --- | --- |
| Endpoint | DATA_PLANE_S3_ENDPOINT | S3_API_URL |
| Bucket | DATA_PLANE_S3_BUCKET | S3_BUCKET |
| Access key | DATA_PLANE_S3_ACCESS_KEY | S3_ACCESS_KEY, ACCESS_KEY |
| Secret key | DATA_PLANE_S3_SECRET_KEY | S3_SECRET_KEY, SECRET_ACCESS_KEY |
| Region | DATA_PLANE_S3_REGION | S3_REGION |
s3ConfigurationFromEnv returns null when the endpoint or credentials are
incomplete. It does not print or validate live credentials until a client is
created.
Ion steering exports
Publish a validated steering export. The SDK writes the immutable object and then advances the current pointer:
const contentId = await dataPlane.ion.steering.v1.publish(steeringExport);
const current = await dataPlane.ion.steering.v1.getCurrent();
const historical = await dataPlane.ion.steering.v1.getByContentId(contentId);publish validates the ion.steering.v1 contract. Invalid payloads throw a
DataPlaneError. Use parseIonSteeringExport or
safeParseIonSteeringExport when validation is needed before publication.
Waypoint entries may include title, narrative, and rationale. These fields
are optional so previously published ion.steering.v1 objects remain valid.
During the Helios-to-Ion migration window, reads try ion/steering/v1 first
and then fall back to helios/steering/v1; publishes write both prefixes so
older consumers continue to work. Legacy payloads are normalized to the Ion
schema when returned by the client.
Geodynamo project contexts
Project contexts are isolated by project segment:
const contentId = await dataPlane.geodynamo.projectContext.v1.publish(
"midi-vibe",
projectContext,
);
const current = await dataPlane.geodynamo.projectContext.v1.getCurrent("midi-vibe");Project identifiers must be a single safe object-key segment. The context must
use the geodynamo.project-context.v1 schema and include at least:
{
schema: "geodynamo.project-context.v1",
generatedAt: "2026-07-10T12:00:00.000Z",
scope: "terella-factory-cycle-only",
source: "geodynamo",
repo: "owner/midi-vibe",
projectName: "midi-vibe",
context: "Prioritized factory-cycle direction..."
}External integration records
Integration records are addressed by provider, scope, and record ID:
await dataPlane.integrations.v1.publish(
"github",
"owner",
"midi-vibe",
{
schema: "terella.integration-record.v1",
provider: "github",
scope: "owner",
record: "midi-vibe",
updatedAt: new Date().toISOString(),
status: "active",
},
);
const record = await dataPlane.integrations.v1.getCurrent(
"github",
"owner",
"midi-vibe",
);Object layout and content addressing
Published values are stored as immutable JSON objects, with a mutable pointer identifying the current content:
ion/steering/v1/objects/<contentId>.json
ion/steering/v1/current.json
# Compatibility copy while Helios consumers are being migrated.
helios/steering/v1/objects/<contentId>.json
helios/steering/v1/current.json
geodynamo/contexts/v1/<project>/objects/<contentId>.json
geodynamo/contexts/v1/<project>/current.json
integrations/v1/<provider>/<scope>/<record>/objects/<contentId>.json
integrations/v1/<provider>/<scope>/<record>/current.jsonThe SDK computes SHA-256 content IDs from canonical JSON for project contexts
and integration records. Ion steering exports retain their contract-defined
contentId.
Low-level client and helpers
Use S3Client for arbitrary JSON objects within the configured bucket:
import { S3Client, objectKey } from "@terella/data-plane";
const s3 = new S3Client(config);
const key = objectKey("reports", "v1", "latest.json");
await s3.putJson(key, { generatedAt: new Date().toISOString() });
const report = await s3.getJson<{ generatedAt: string }>(key);objectKey rejects empty, traversal, slash-containing, and control-character
segments. This prevents callers from escaping a domain prefix or accidentally
overwriting another domain's pointer.
The package also exports canonicalJson, contentId, parseS3Configuration,
awsSignatureV4, and the shared contract types and schema constants.
Errors and optional mirroring
All SDK failures use DataPlaneError. Error messages redact authorization
headers, signatures, secret-bearing URLs, and secret-key-like values. A missing
object is returned as null by getJson, getCurrent, and
getByContentId.
Applications that treat shared storage as an additive mirror should catch
DataPlaneError around reads and writes. Native Ion R2 publication and
Geodynamo Pages output should remain the canonical paths when shared S3 is
unavailable.
Testing and verification
Run the package checks from this directory:
bun run check
bun test
bun run buildThe live S3 test is read-only and runs only when endpoint, bucket, access-key,
and secret-key environment variables are configured. It never prints object
payloads or credentials. Unit tests use a fake fetch implementation and do
not require S3 access.
