/** * Object Store helpers. Wraps `@nats-io/obj` with the v0.4 `obj://` URI codec * used by `messages.materializePart` to offload >1 MB Part payloads. * * The SDK never opens its own ObjectStore — the caller (adapter) passes an * already-opened handle. Bucket naming is sesh-ops's concern. */ import type { ObjectStore } from "@nats-io/obj"; import { ObjectURLError } from "./errors.ts"; export const OBJ_URI_SCHEME = "obj" as const; /** * obj:// URI shape: `obj://///` * - scope-kind ∈ { "task" | "session" | "machine" } (mirrors sesh-ops/scope) * - scope-id — bucket-name-safe sesh-ops scope id (8 or 32 hex typically) * - task-id — ULID * - artifact-id — ULID OR a part-id when used for Part offload */ export interface ObjectURLParts { scopeKind: "task" | "session" | "machine"; scopeId: string; taskId: string; artifactId: string; } const SCOPE_ID_RE = /^[A-Za-z0-9_-]+$/; const SEG_RE = /^[A-Za-z0-9_.-]+$/; export function objectURL(p: ObjectURLParts): string { if (!SCOPE_ID_RE.test(p.scopeId)) { throw new ObjectURLError(`invalid scopeId for obj:// URL: ${p.scopeId}`); } if (!SEG_RE.test(p.taskId)) { throw new ObjectURLError(`invalid taskId for obj:// URL: ${p.taskId}`); } if (!SEG_RE.test(p.artifactId)) { throw new ObjectURLError(`invalid artifactId for obj:// URL: ${p.artifactId}`); } return `${OBJ_URI_SCHEME}://${p.scopeKind}/${p.scopeId}/${p.taskId}/${p.artifactId}`; } export function parseObjectURL(url: string): ObjectURLParts { let u: URL; try { u = new URL(url); } catch (e) { throw new ObjectURLError(`invalid URL: ${url}`, e); } if (u.protocol !== `${OBJ_URI_SCHEME}:`) { throw new ObjectURLError(`expected ${OBJ_URI_SCHEME}:// scheme, got ${u.protocol}`); } const scopeKind = u.hostname; if (scopeKind !== "task" && scopeKind !== "session" && scopeKind !== "machine") { throw new ObjectURLError(`invalid scope kind: ${scopeKind}`); } const segments = u.pathname.split("/").filter(Boolean); if (segments.length !== 3) { throw new ObjectURLError(`expected 3 path segments, got ${segments.length}: ${url}`); } const scopeId = segments[0] as string; const taskId = segments[1] as string; const artifactId = segments[2] as string; return { scopeKind, scopeId, taskId, artifactId }; } /** * Compose the Object Store key for an obj:// URL. The store itself is * per-scope; the key is `/`. Bucket choice is the * caller's concern — the SDK doesn't open or name buckets. */ export function objectKey(p: Pick): string { return `${p.taskId}/${p.artifactId}`; } export async function putObject( os: ObjectStore, key: string, bytes: Uint8Array, ): Promise { await os.putBlob({ name: key }, bytes); } export async function getObject( os: ObjectStore, key: string, ): Promise { return await os.getBlob(key); }