diff --git a/README.md b/README.md index 1a61270..f169355 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ direct libSQL authority also support Node 24 serverless runtimes. Install the current immutable release directly from GitHub: ```sh -bun add --global github:hraness/oh#v0.2.0 +bun add --global github:hraness/oh#v0.2.1 oh --help ``` @@ -100,7 +100,7 @@ For a project dependency, pin the same immutable release in `package.json`: ```json { "dependencies": { - "@hraness/oh": "github:hraness/oh#v0.2.0" + "@hraness/oh": "github:hraness/oh#v0.2.1" } } ``` @@ -162,6 +162,7 @@ import { createClient } from "@libsql/client"; import { bootstrapOhLibSqlAuthorityV1, createOhLibSqlStoreAuthorityV1, + openExistingOhLibSqlStoreAuthorityV1, } from "@hraness/oh/libsql"; import { OH_WORKING_STORE_PROFILE_V1 } from "@hraness/oh/store"; @@ -186,6 +187,21 @@ const authority = await createOhLibSqlStoreAuthorityV1(runtimeClient, { const store = authority.store; console.log(await store.head()); + +// A separately held purge worker can fail closed unless the exact binding +// already exists. Opening performs reads only; it cannot create a space. +const purgeClient = createClient({ + authToken: process.env.OH_PURGE_TOKEN!, + url: process.env.OH_DATABASE_URL!, +}); +const existing = await openExistingOhLibSqlStoreAuthorityV1(purgeClient, { + profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "tenant:example/thread:research", + spaceId: "thread:research", +}); +await existing.host.purgeWorkingSpace({}); +await existing.store.close(); +purgeClient.close(); ``` The working profile disables operation replication. Dependency-closure export @@ -451,7 +467,7 @@ keep remote sync explicit. You can also give an agent this prompt: ```text -Install hraness/oh and its Oh Agent Skill from the immutable v0.2.0 tag at +Install hraness/oh and its Oh Agent Skill from the immutable v0.2.1 tag at https://github.com/hraness/oh. Verify the CLI with `oh --help` and `oh version`. Do not create or modify an Oh database until I name its path and ask you to. ``` diff --git a/dist/cli.d.ts b/dist/cli.d.ts index dd13ae4..f491c88 100644 --- a/dist/cli.d.ts +++ b/dist/cli.d.ts @@ -1,4 +1,4 @@ #!/usr/bin/env bun -export declare const OH_PACKAGE_VERSION: "0.2.0"; +export declare const OH_PACKAGE_VERSION: "0.2.1"; export declare function runOhCli(arguments_: readonly string[]): Promise; //# sourceMappingURL=cli.d.ts.map \ No newline at end of file diff --git a/dist/cli.js b/dist/cli.js index 5193eba..599832a 100755 --- a/dist/cli.js +++ b/dist/cli.js @@ -3072,7 +3072,7 @@ class Oh { // src/cli.ts import { readFile } from "fs/promises"; -var OH_PACKAGE_VERSION = "0.2.0"; +var OH_PACKAGE_VERSION = "0.2.1"; var KNOWN_OPTIONS = new Set([ "actor", "after", diff --git a/dist/libsql.d.ts b/dist/libsql.d.ts index d98458f..832f57b 100644 --- a/dist/libsql.d.ts +++ b/dist/libsql.d.ts @@ -39,4 +39,10 @@ export declare function bootstrapOhLibSqlAuthorityV1(client: OhLibSqlClientV1): }>>; /** Opens a direct libSQL/Turso authority; this is not operation-log sync. */ export declare function createOhLibSqlStoreAuthorityV1(client: OhLibSqlClientV1, options?: OhLibSqlStoreAuthorityOptionsV1): Promise; +/** + * Opens an already-bound direct libSQL/Turso authority without creating or + * updating data. This seam is for separately held read or purge custody that + * must fail closed instead of acquiring space-creation authority. + */ +export declare function openExistingOhLibSqlStoreAuthorityV1(client: OhLibSqlClientV1, options?: OhLibSqlStoreAuthorityOptionsV1): Promise; //# sourceMappingURL=libsql.d.ts.map \ No newline at end of file diff --git a/dist/libsql.d.ts.map b/dist/libsql.d.ts.map index 7865758..a7e3433 100644 --- a/dist/libsql.d.ts.map +++ b/dist/libsql.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"libsql.d.ts","sourceRoot":"","sources":["../src/libsql.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAMrB,OAAO,EAuBL,KAAK,kBAAkB,EAGvB,KAAK,gBAAgB,EAGtB,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,IAAI,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAC1G,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAC/F,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AAEH,iEAAiE;AACjE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CACH,UAAU,EAAE,SAAS,mBAAmB,EAAE,EAC1C,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,GACnC,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC,CAAC;IACxC,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,OAAO,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC7E;AAED,MAAM,MAAM,+BAA+B,GAAG,QAAQ,CAAC;IACrD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;EASpC,CAAC;AAuVH,oFAAoF;AACpF,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,QAAQ,CAAC;IAAE,YAAY,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,CAAC,CAAC;IAAC,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC,CAsBxE;AAu+BD,6EAA6E;AAC7E,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,kBAAkB,CAAC,CAsC7B"} \ No newline at end of file +{"version":3,"file":"libsql.d.ts","sourceRoot":"","sources":["../src/libsql.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAMrB,OAAO,EAuBL,KAAK,kBAAkB,EAGvB,KAAK,gBAAgB,EAGtB,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,IAAI,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAC1G,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAC/F,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AAEH,iEAAiE;AACjE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CACH,UAAU,EAAE,SAAS,mBAAmB,EAAE,EAC1C,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,GACnC,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC,CAAC;IACxC,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,OAAO,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC7E;AAED,MAAM,MAAM,+BAA+B,GAAG,QAAQ,CAAC;IACrD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;EASpC,CAAC;AAuVH,oFAAoF;AACpF,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,QAAQ,CAAC;IAAE,YAAY,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,CAAC,CAAC;IAAC,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC,CAsBxE;AAijCD,6EAA6E;AAC7E,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,kBAAkB,CAAC,CAS7B;AAED;;;;GAIG;AACH,wBAAsB,oCAAoC,CACxD,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,kBAAkB,CAAC,CAS7B"} \ No newline at end of file diff --git a/dist/libsql.js b/dist/libsql.js index e21dee0..a38bbb1 100644 --- a/dist/libsql.js +++ b/dist/libsql.js @@ -1367,6 +1367,38 @@ async function initializeSpace(client, binding) { throw new OhProfileError("The remote space is already bound to a different realm or profile."); } } +async function requireExistingSpace(client, binding) { + const results = await client.batch([ + { sql: BINDING_ROW_SELECT, args: [binding.spaceId] }, + { + sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, + records_sha256, sequence, contract_id FROM oh_authority_spaces WHERE space_id = ?`, + args: [binding.spaceId] + }, + { sql: PURGE_ROW_SELECT, args: [binding.spaceId] } + ], "read"); + if (results.length !== 3) { + throw new OhIntegrityError("The remote authority returned an incomplete existing-space proof."); + } + const [bindingResult, spaceResult, purgeResult] = results; + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + throw new OhPurgedSpaceError(parsePurgeReceiptRow(purgeRow, binding.spaceId, binding.bindingSha256)); + } + const bindingRow = bindingResult.rows[0]; + const spaceRow = spaceResult.rows[0]; + if (bindingRow === undefined || spaceRow === undefined) { + throw new OhIntegrityError("The requested remote Oh space does not already exist."); + } + const persisted = parseBindingRow(bindingRow, binding.spaceId); + if (canonicalJson(persisted) !== canonicalJson(binding)) { + throw new OhProfileError("The remote space is bound to a different realm or profile."); + } + if (rowValue(spaceRow, "contract_id", 5) !== OH_CONTRACT_MANIFEST_V1.contractId) { + throw new OhIntegrityError("The existing remote space uses a different Oh contract."); + } + parseHeadRow(spaceRow); +} class OhLibSqlStoreV1 { binding; @@ -2427,20 +2459,8 @@ class OhLibSqlStoreV1 { this.#client.close?.(); } } -async function createOhLibSqlStoreAuthorityV1(client, options = {}) { - const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); - if (profile === null) - throw new TypeError("Invalid libSQL store profile."); - const spaceId = options.spaceId ?? "default"; - const binding = createOhStoreBindingV1({ - profile, - realmId: options.realmId ?? `realm:${spaceId}`, - spaceId, - v: 1 - }); - await verifyAuthoritySchema(client); - await initializeSpace(client, binding); - const authority = new OhLibSqlStoreV1(client, binding, options.closeClient ?? false); +function bindOhLibSqlStoreAuthorityV1(client, binding, profile, closeClient) { + const authority = new OhLibSqlStoreV1(client, binding, closeClient); const store = Object.freeze({ binding, changesSince: (from, changeOptions) => authority.changesSince(from, changeOptions), @@ -2466,7 +2486,38 @@ async function createOhLibSqlStoreAuthorityV1(client, options = {}) { }); return Object.freeze({ host, store }); } +async function createOhLibSqlStoreAuthorityV1(client, options = {}) { + const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); + if (profile === null) + throw new TypeError("Invalid libSQL store profile."); + const spaceId = options.spaceId ?? "default"; + const binding = createOhStoreBindingV1({ + profile, + realmId: options.realmId ?? `realm:${spaceId}`, + spaceId, + v: 1 + }); + await verifyAuthoritySchema(client); + await initializeSpace(client, binding); + return bindOhLibSqlStoreAuthorityV1(client, binding, profile, options.closeClient ?? false); +} +async function openExistingOhLibSqlStoreAuthorityV1(client, options = {}) { + const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); + if (profile === null) + throw new TypeError("Invalid libSQL store profile."); + const spaceId = options.spaceId ?? "default"; + const binding = createOhStoreBindingV1({ + profile, + realmId: options.realmId ?? `realm:${spaceId}`, + spaceId, + v: 1 + }); + await verifyAuthoritySchema(client); + await requireExistingSpace(client, binding); + return bindOhLibSqlStoreAuthorityV1(client, binding, profile, options.closeClient ?? false); +} export { + openExistingOhLibSqlStoreAuthorityV1, createOhLibSqlStoreAuthorityV1, bootstrapOhLibSqlAuthorityV1, OH_LIBSQL_STORE_LIMITS_V1 diff --git a/package.json b/package.json index 7fb0051..f2cfbd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hraness/oh", - "version": "0.2.0", + "version": "0.2.1", "description": "open-source tools for agentic research", "type": "module", "license": "MIT", diff --git a/site/package.json b/site/package.json index da6e344..1a4f4e5 100644 --- a/site/package.json +++ b/site/package.json @@ -1,6 +1,6 @@ { "name": "oh-site", - "version": "0.2.0", + "version": "0.2.1", "private": true, "packageManager": "bun@1.3.14", "engines": { diff --git a/site/public/spec/v1/store.md b/site/public/spec/v1/store.md index 85aa95e..f5607dc 100644 --- a/site/public/spec/v1/store.md +++ b/site/public/spec/v1/store.md @@ -69,6 +69,13 @@ Run it in a deployment or migration step with a short-lived schema credential. installed schema and contract before reading or creating a bound data space, so a runtime token does not need schema-change permission. +`openExistingOhLibSqlStoreAuthorityV1` is the least-privilege open for a +separately held reader or purge worker. It verifies the same exact schema, +contract, space head, and binding using reads only. It rejects a missing, +purged, or differently bound space and never inserts, updates, or deletes data +during open. A provider credential can therefore omit space and binding +creation while retaining only the data actions required by its later task. + Runtime open verifies the exact installed table, index, and trigger set, not only a schema marker. Every operation, binding, and purge receipt read parses its canonical JSON and cross-checks each duplicated SQL column. Current reads diff --git a/skills/oh/SKILL.md b/skills/oh/SKILL.md index 7dd821f..fbb6008 100644 --- a/skills/oh/SKILL.md +++ b/skills/oh/SKILL.md @@ -30,7 +30,7 @@ oh --help oh version ``` -The supported CLI is `@hraness/oh@0.2.0` from the immutable `v0.2.0` GitHub +The supported CLI is `@hraness/oh@0.2.1` from the immutable `v0.2.1` GitHub tag. It requires Bun 1.3.14 or newer. The versioned contract is published at . diff --git a/spec/v1/store.md b/spec/v1/store.md index 85aa95e..f5607dc 100644 --- a/spec/v1/store.md +++ b/spec/v1/store.md @@ -69,6 +69,13 @@ Run it in a deployment or migration step with a short-lived schema credential. installed schema and contract before reading or creating a bound data space, so a runtime token does not need schema-change permission. +`openExistingOhLibSqlStoreAuthorityV1` is the least-privilege open for a +separately held reader or purge worker. It verifies the same exact schema, +contract, space head, and binding using reads only. It rejects a missing, +purged, or differently bound space and never inserts, updates, or deletes data +during open. A provider credential can therefore omit space and binding +creation while retaining only the data actions required by its later task. + Runtime open verifies the exact installed table, index, and trigger set, not only a schema marker. Every operation, binding, and purge receipt read parses its canonical JSON and cross-checks each duplicated SQL column. Current reads diff --git a/src/cli.ts b/src/cli.ts index 280f2d2..fdaba5f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,7 +9,7 @@ import { Oh } from "./sdk"; import { OH_SQLITE_SCHEMA_VERSION } from "./sqlite/migrations"; import { createOhSyncBundleV1, parseOhSyncBundleV1 } from "./sync"; -export const OH_PACKAGE_VERSION = "0.2.0" as const; +export const OH_PACKAGE_VERSION = "0.2.1" as const; type ParsedArguments = { options: Map; positionals: string[] }; type ValidatedInvocation = Readonly<{ diff --git a/src/libsql.test.ts b/src/libsql.test.ts index 512f4cf..f274db1 100644 --- a/src/libsql.test.ts +++ b/src/libsql.test.ts @@ -7,6 +7,7 @@ import { bootstrapOhLibSqlAuthorityV1, createOhLibSqlStoreAuthorityV1, OH_LIBSQL_STORE_LIMITS_V1, + openExistingOhLibSqlStoreAuthorityV1, type OhLibSqlClientV1, type OhLibSqlResultV1, type OhLibSqlStatementV1, @@ -114,6 +115,68 @@ describe("direct libSQL Oh authority", () => { schemaClient.close(); }); + test("opens existing purge custody without acquiring space-creation authority", async () => { + const provider = await bootstrappedClient(); + const created = await createOhLibSqlStoreAuthorityV1(provider, { + profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:existing-only", + spaceId: "existing-only", + }); + await created.store.commit({ + actorId: "host.existing-only", + changes: [{ kind: "put", record: entity("entity:private", "Private"), v: 1 }], + expectedHead: await created.store.head(), + operationId: "op_existing_only", + }); + await created.store.close(); + + const observed: string[] = []; + const forbidCreation = (statement: OhLibSqlStatementV1 | string): void => { + const sql = typeof statement === "string" ? statement : statement.sql; + observed.push(sql); + if (/\bINSERT\s+INTO\s+oh_authority_(?:spaces|bindings)\b/iu.test(sql)) { + throw new Error("existing-only credential cannot create a space or binding"); + } + }; + const existingOnlyClient: OhLibSqlClientV1 = { + execute: async (statement) => { + forbidCreation(statement); + return await provider.execute(statement); + }, + batch: async (statements, mode) => { + statements.forEach(forbidCreation); + return await provider.batch(statements, mode); + }, + }; + + await expect(openExistingOhLibSqlStoreAuthorityV1(existingOnlyClient, { + profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:missing-existing-only", + spaceId: "missing-existing-only", + })).rejects.toThrow(OhIntegrityError); + await expect(openExistingOhLibSqlStoreAuthorityV1(existingOnlyClient, { + profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:different", + spaceId: "existing-only", + })).rejects.toThrow(OhProfileError); + + observed.length = 0; + const authority = await openExistingOhLibSqlStoreAuthorityV1(existingOnlyClient, { + profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:existing-only", + spaceId: "existing-only", + }); + expect((await authority.store.head()).sequence).toBe(1); + expect(observed.every((sql) => /^\s*SELECT\b/iu.test(sql))).toBe(true); + await authority.host.purgeWorkingSpace({ purgedAt: "2026-08-30T01:00:00.000Z" }); + expect(observed.some((sql) => /\bINSERT\s+INTO\s+oh_authority_purges\b/iu.test(sql))) + .toBe(true); + expect(observed.some((sql) => + /\bINSERT\s+INTO\s+oh_authority_(?:spaces|bindings)\b/iu.test(sql))).toBe(false); + await authority.store.close(); + provider.close(); + }); + test("is an async authoritative store rather than an operation-sync cache", async () => { const client = await bootstrappedClient(); const authority = await createOhLibSqlStoreAuthorityV1(client, { diff --git a/src/libsql.ts b/src/libsql.ts index 32e9b31..4f4eb0a 100644 --- a/src/libsql.ts +++ b/src/libsql.ts @@ -502,6 +502,42 @@ async function initializeSpace( } } +async function requireExistingSpace( + client: OhLibSqlClientV1, + binding: OhStoreBindingV1, +): Promise { + const results = await client.batch([ + { sql: BINDING_ROW_SELECT, args: [binding.spaceId] }, + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, + records_sha256, sequence, contract_id FROM oh_authority_spaces WHERE space_id = ?`, + args: [binding.spaceId] }, + { sql: PURGE_ROW_SELECT, args: [binding.spaceId] }, + ], "read"); + if (results.length !== 3) { + throw new OhIntegrityError("The remote authority returned an incomplete existing-space proof."); + } + const [bindingResult, spaceResult, purgeResult] = results as + [OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1]; + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + throw new OhPurgedSpaceError(parsePurgeReceiptRow( + purgeRow, binding.spaceId, binding.bindingSha256)); + } + const bindingRow = bindingResult.rows[0]; + const spaceRow = spaceResult.rows[0]; + if (bindingRow === undefined || spaceRow === undefined) { + throw new OhIntegrityError("The requested remote Oh space does not already exist."); + } + const persisted = parseBindingRow(bindingRow, binding.spaceId); + if (canonicalJson(persisted) !== canonicalJson(binding)) { + throw new OhProfileError("The remote space is bound to a different realm or profile."); + } + if (rowValue(spaceRow, "contract_id", 5) !== OH_CONTRACT_MANIFEST_V1.contractId) { + throw new OhIntegrityError("The existing remote space uses a different Oh contract."); + } + parseHeadRow(spaceRow); +} + class OhLibSqlStoreV1 implements OhStoreV1 { readonly binding: OhStoreBindingV1; readonly #client: OhLibSqlClientV1; @@ -1443,19 +1479,13 @@ class OhLibSqlStoreV1 implements OhStoreV1 { } } -/** Opens a direct libSQL/Turso authority; this is not operation-log sync. */ -export async function createOhLibSqlStoreAuthorityV1( +function bindOhLibSqlStoreAuthorityV1( client: OhLibSqlClientV1, - options: OhLibSqlStoreAuthorityOptionsV1 = {}, -): Promise { - const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); - if (profile === null) throw new TypeError("Invalid libSQL store profile."); - const spaceId = options.spaceId ?? "default"; - const binding = createOhStoreBindingV1({ profile, - realmId: options.realmId ?? `realm:${spaceId}`, spaceId, v: 1 }); - await verifyAuthoritySchema(client); - await initializeSpace(client, binding); - const authority = new OhLibSqlStoreV1(client, binding, options.closeClient ?? false); + binding: OhStoreBindingV1, + profile: OhStoreProfileV1, + closeClient: boolean, +): OhStoreAuthorityV1 { + const authority = new OhLibSqlStoreV1(client, binding, closeClient); const store: OhStoreV1 = Object.freeze({ binding, changesSince: (from: OhHeadRefV1, changeOptions?: Readonly<{ limit?: number; through?: OhHeadRefV1 }>) => @@ -1486,3 +1516,37 @@ export async function createOhLibSqlStoreAuthorityV1( }); return Object.freeze({ host, store }); } + +/** Opens a direct libSQL/Turso authority; this is not operation-log sync. */ +export async function createOhLibSqlStoreAuthorityV1( + client: OhLibSqlClientV1, + options: OhLibSqlStoreAuthorityOptionsV1 = {}, +): Promise { + const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); + if (profile === null) throw new TypeError("Invalid libSQL store profile."); + const spaceId = options.spaceId ?? "default"; + const binding = createOhStoreBindingV1({ profile, + realmId: options.realmId ?? `realm:${spaceId}`, spaceId, v: 1 }); + await verifyAuthoritySchema(client); + await initializeSpace(client, binding); + return bindOhLibSqlStoreAuthorityV1(client, binding, profile, options.closeClient ?? false); +} + +/** + * Opens an already-bound direct libSQL/Turso authority without creating or + * updating data. This seam is for separately held read or purge custody that + * must fail closed instead of acquiring space-creation authority. + */ +export async function openExistingOhLibSqlStoreAuthorityV1( + client: OhLibSqlClientV1, + options: OhLibSqlStoreAuthorityOptionsV1 = {}, +): Promise { + const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); + if (profile === null) throw new TypeError("Invalid libSQL store profile."); + const spaceId = options.spaceId ?? "default"; + const binding = createOhStoreBindingV1({ profile, + realmId: options.realmId ?? `realm:${spaceId}`, spaceId, v: 1 }); + await verifyAuthoritySchema(client); + await requireExistingSpace(client, binding); + return bindOhLibSqlStoreAuthorityV1(client, binding, profile, options.closeClient ?? false); +} diff --git a/tests/node-portable.mjs b/tests/node-portable.mjs index 37b975d..3330b8e 100644 --- a/tests/node-portable.mjs +++ b/tests/node-portable.mjs @@ -11,6 +11,7 @@ assert.equal(typeof store.createOhStoreBindingV1, "function"); assert.equal(typeof store.OhRecordCodecRegistry, "function"); assert.equal(typeof store.OhSemanticBundleIngressV1, "function"); assert.equal(typeof libsql.createOhLibSqlStoreAuthorityV1, "function"); +assert.equal(typeof libsql.openExistingOhLibSqlStoreAuthorityV1, "function"); assert.equal(typeof memory.createOhMemoryAgentV1, "function"); assert.equal(store.OH_WORKING_STORE_PROFILE_V1.profileKind, "working"); assert.equal(store.OH_WORKING_STORE_PROFILE_V1.capabilities.operationReplication, false); diff --git a/tests/public-surface.test.ts b/tests/public-surface.test.ts index 4fee5e3..cad53e9 100644 --- a/tests/public-surface.test.ts +++ b/tests/public-surface.test.ts @@ -152,7 +152,7 @@ describe("public identity and documentation", () => { ]); expect(readme.startsWith(`# ${tagline}\n`)).toBe(true); expect(packageJson.name).toBe("@hraness/oh"); - expect(packageJson.version).toBe("0.2.0"); + expect(packageJson.version).toBe("0.2.1"); expect(packageJson.description).toBe(tagline); expect(packageJson.homepage).toBe("https://oh.computer"); expect(packageJson.license).toBe("MIT");