@tinycloud/sdk-services 2.4.0-beta.1 → 2.4.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@ export { g as ErrorCode, h as ErrorCodes, i as FetchRequestInit, j as FetchRespo
3
3
  import { z } from 'zod';
4
4
  import { IKVService } from './kv/index.cjs';
5
5
  export { DEFAULT_SIGNED_READ_URL_EXPIRY_MS, IPrefixedKVService, KVAction, KVActionType, KVBatchPutItem, KVBatchPutOptions, KVBatchPutResponse, KVCreateSignedReadUrlOptions, KVDeleteOptions, KVGetOptions, KVHeadOptions, KVListOptions, KVListResponse, KVPutOptions, KVResponse, KVResponseHeaders, KVService, KVServiceConfig, KVSignedReadUrlResponse, PrefixedKVService } from './kv/index.cjs';
6
- export { BatchOptions, BatchResponse, DatabaseHandle, ExecuteOptions, ExecuteResponse, IDatabaseHandle, ISQLService, QueryOptions, QueryResponse, SQLAction, SQLActionType, SQLService, SQLServiceConfig, SqlStatement, SqlValue } from './sql/index.cjs';
6
+ export { BatchOptions, BatchResponse, DatabaseHandle, ExecuteOptions, ExecuteResponse, IDatabaseHandle, ISQLMigrations, ISQLService, QueryOptions, QueryResponse, SQLAction, SQLActionType, SQLMigrations, SQLService, SQLServiceConfig, SqlMigration, SqlMigrationApplyOptions, SqlMigrationApplyResponse, SqlStatement, SqlValue } from './sql/index.cjs';
7
7
  import { IEncryptionService, DecryptCapabilityProof } from './encryption/index.cjs';
8
8
  export { BuildCanonicalDecryptRequestInput, BuildDecryptFactsInput, BuildDecryptInvocationInput, BuiltDecryptInvocation, CanonicalDecryptRequest, CanonicalJson, DECRYPT_ACTION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, DEFAULT_ENCRYPTION_ALG, DEFAULT_KEY_VERSION, DecryptEnvelopeOptions, DecryptInvocationFact, DecryptInvocationSigner, DecryptRequestBody, DecryptResponseBody, DecryptTransport, DiscoverNetworkInput, DiscoveredNetwork, DiscoverySource, ENCRYPTION_NETWORK_URN_PREFIX, ENCRYPTION_SERVICE, ENCRYPTION_SERVICE_SHORT, ENVELOPE_VERSION, EncryptToNetworkInput, EncryptToNetworkOptions, EncryptToNetworkResult, EncryptionCrypto, EncryptionError, EncryptionErrorInput, EncryptionService, EncryptionServiceConfig, InlineEncryptedEnvelope, Json, NETWORK_NAME_PATTERN, NetworkDescriptor, NetworkIdError, NodeDescriptorFetcher, ParsedNetworkId, RandomReceiverKeyInput, ReceiverKeyPair, ReceiverKeySigner, SignedReceiverKeyInput, VerifyDecryptResponseInput, WellKnownDescriptorFetcher, base64Decode, base64Encode, buildCanonicalDecryptRequest, buildDecryptAttenuation, buildDecryptFacts, buildDecryptInvocation, buildNetworkId, canonicalHashHex, canonicalSignedResponse, canonicalize as canonicalizeEncryptionJson, checkDecryptInvocationInput, decryptEnvelopeWithKey, deriveSignedReceiverKey, discoverNetwork, encryptToNetwork, encryptionError, ensureNetworkUsableForDecrypt, generateRandomReceiverKey, hexDecode, hexEncode, isNetworkId, networkDiscoveryKey, openWrappedKey, parseNetworkId, utf8Decode, utf8Encode, validateEnvelope, verifyDecryptResponse } from './encryption/index.cjs';
9
9
 
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { g as ErrorCode, h as ErrorCodes, i as FetchRequestInit, j as FetchRespo
3
3
  import { z } from 'zod';
4
4
  import { IKVService } from './kv/index.js';
5
5
  export { DEFAULT_SIGNED_READ_URL_EXPIRY_MS, IPrefixedKVService, KVAction, KVActionType, KVBatchPutItem, KVBatchPutOptions, KVBatchPutResponse, KVCreateSignedReadUrlOptions, KVDeleteOptions, KVGetOptions, KVHeadOptions, KVListOptions, KVListResponse, KVPutOptions, KVResponse, KVResponseHeaders, KVService, KVServiceConfig, KVSignedReadUrlResponse, PrefixedKVService } from './kv/index.js';
6
- export { BatchOptions, BatchResponse, DatabaseHandle, ExecuteOptions, ExecuteResponse, IDatabaseHandle, ISQLService, QueryOptions, QueryResponse, SQLAction, SQLActionType, SQLService, SQLServiceConfig, SqlStatement, SqlValue } from './sql/index.js';
6
+ export { BatchOptions, BatchResponse, DatabaseHandle, ExecuteOptions, ExecuteResponse, IDatabaseHandle, ISQLMigrations, ISQLService, QueryOptions, QueryResponse, SQLAction, SQLActionType, SQLMigrations, SQLService, SQLServiceConfig, SqlMigration, SqlMigrationApplyOptions, SqlMigrationApplyResponse, SqlStatement, SqlValue } from './sql/index.js';
7
7
  import { IEncryptionService, DecryptCapabilityProof } from './encryption/index.js';
8
8
  export { BuildCanonicalDecryptRequestInput, BuildDecryptFactsInput, BuildDecryptInvocationInput, BuiltDecryptInvocation, CanonicalDecryptRequest, CanonicalJson, DECRYPT_ACTION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, DEFAULT_ENCRYPTION_ALG, DEFAULT_KEY_VERSION, DecryptEnvelopeOptions, DecryptInvocationFact, DecryptInvocationSigner, DecryptRequestBody, DecryptResponseBody, DecryptTransport, DiscoverNetworkInput, DiscoveredNetwork, DiscoverySource, ENCRYPTION_NETWORK_URN_PREFIX, ENCRYPTION_SERVICE, ENCRYPTION_SERVICE_SHORT, ENVELOPE_VERSION, EncryptToNetworkInput, EncryptToNetworkOptions, EncryptToNetworkResult, EncryptionCrypto, EncryptionError, EncryptionErrorInput, EncryptionService, EncryptionServiceConfig, InlineEncryptedEnvelope, Json, NETWORK_NAME_PATTERN, NetworkDescriptor, NetworkIdError, NodeDescriptorFetcher, ParsedNetworkId, RandomReceiverKeyInput, ReceiverKeyPair, ReceiverKeySigner, SignedReceiverKeyInput, VerifyDecryptResponseInput, WellKnownDescriptorFetcher, base64Decode, base64Encode, buildCanonicalDecryptRequest, buildDecryptAttenuation, buildDecryptFacts, buildDecryptInvocation, buildNetworkId, canonicalHashHex, canonicalSignedResponse, canonicalize as canonicalizeEncryptionJson, checkDecryptInvocationInput, decryptEnvelopeWithKey, deriveSignedReceiverKey, discoverNetwork, encryptToNetwork, encryptionError, ensureNetworkUsableForDecrypt, generateRandomReceiverKey, hexDecode, hexEncode, isNetworkId, networkDiscoveryKey, openWrappedKey, parseNetworkId, utf8Decode, utf8Encode, validateEnvelope, verifyDecryptResponse } from './encryption/index.js';
9
9
 
package/dist/index.js CHANGED
@@ -983,6 +983,31 @@ var KVService = class extends BaseService {
983
983
  }
984
984
  return void 0;
985
985
  }
986
+ /**
987
+ * Classify a KV 404 by reading the response body once.
988
+ *
989
+ * The server returns 404 both for a genuinely missing key AND for an
990
+ * un-hosted space (body "Space not found"). Previously get/head/delete
991
+ * collapsed every 404 to KV_NOT_FOUND before reading the body, so an
992
+ * un-hosted-space read was indistinguishable from a missing key. We now
993
+ * preserve status + the "Space not found" body for the un-hosted case (so the
994
+ * CLI/SDK can normalize it to SPACE_NOT_HOSTED, matching put/list/sql), and
995
+ * fall through to KV_NOT_FOUND for a real missing key.
996
+ */
997
+ async classifyNotFound(response, key) {
998
+ const errorText = await response.text();
999
+ if (/space not found/i.test(errorText)) {
1000
+ return err(
1001
+ serviceError(
1002
+ ErrorCodes.KV_NOT_FOUND,
1003
+ `KV ${response.status} - ${errorText}`,
1004
+ "kv",
1005
+ { meta: { status: response.status, statusText: response.statusText } }
1006
+ )
1007
+ );
1008
+ }
1009
+ return err(serviceError(ErrorCodes.KV_NOT_FOUND, `Key not found: ${key}`, "kv"));
1010
+ }
986
1011
  /**
987
1012
  * Get the full path with optional prefix.
988
1013
  *
@@ -1229,13 +1254,7 @@ var KVService = class extends BaseService {
1229
1254
  }));
1230
1255
  }
1231
1256
  if (response.status === 404) {
1232
- return err(
1233
- serviceError(
1234
- ErrorCodes.KV_NOT_FOUND,
1235
- `Key not found: ${key}`,
1236
- "kv"
1237
- )
1238
- );
1257
+ return this.classifyNotFound(response, key);
1239
1258
  }
1240
1259
  const errorText = await response.text();
1241
1260
  return err(
@@ -1496,13 +1515,7 @@ var KVService = class extends BaseService {
1496
1515
  }));
1497
1516
  }
1498
1517
  if (response.status === 404) {
1499
- return err(
1500
- serviceError(
1501
- ErrorCodes.KV_NOT_FOUND,
1502
- `Key not found: ${key}`,
1503
- "kv"
1504
- )
1505
- );
1518
+ return this.classifyNotFound(response, key);
1506
1519
  }
1507
1520
  const errorText = await response.text();
1508
1521
  return err(
@@ -1547,13 +1560,7 @@ var KVService = class extends BaseService {
1547
1560
  }));
1548
1561
  }
1549
1562
  if (response.status === 404) {
1550
- return err(
1551
- serviceError(
1552
- ErrorCodes.KV_NOT_FOUND,
1553
- `Key not found: ${key}`,
1554
- "kv"
1555
- )
1556
- );
1563
+ return this.classifyNotFound(response, key);
1557
1564
  }
1558
1565
  const errorText = await response.text();
1559
1566
  return err(
@@ -1680,11 +1687,59 @@ var KVService = class extends BaseService {
1680
1687
  */
1681
1688
  KVService.serviceName = "kv";
1682
1689
 
1690
+ // src/responseErrors.ts
1691
+ function parseServiceErrorBody(errorText) {
1692
+ try {
1693
+ return JSON.parse(errorText);
1694
+ } catch {
1695
+ return {};
1696
+ }
1697
+ }
1698
+ function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
1699
+ if (parsed.message) {
1700
+ return compactMessage(parsed.message);
1701
+ }
1702
+ if (looksLikeHtml(errorText)) {
1703
+ if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
1704
+ return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
1705
+ }
1706
+ return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
1707
+ }
1708
+ return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
1709
+ }
1710
+ function responseErrorMeta(status, statusText, errorText) {
1711
+ const meta = { status, statusText };
1712
+ const snippet = compactMessage(errorText);
1713
+ if (snippet) {
1714
+ meta.responseSnippet = snippet.slice(0, 300);
1715
+ }
1716
+ return meta;
1717
+ }
1718
+ function looksLikeHtml(text) {
1719
+ return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
1720
+ }
1721
+ function compactMessage(message) {
1722
+ const compact = message.replace(/\s+/g, " ").trim();
1723
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
1724
+ }
1725
+
1726
+ // src/sql/SQLMigrations.ts
1727
+ var SQLMigrations = class {
1728
+ constructor(service, dbName) {
1729
+ this.service = service;
1730
+ this.dbName = dbName;
1731
+ }
1732
+ apply(options) {
1733
+ return this.service.applyMigrationsOnDb(this.dbName, options);
1734
+ }
1735
+ };
1736
+
1683
1737
  // src/sql/DatabaseHandle.ts
1684
1738
  var DatabaseHandle = class {
1685
1739
  constructor(service, name) {
1686
1740
  this.service = service;
1687
1741
  this.name = name;
1742
+ this.migrations = new SQLMigrations(service, name);
1688
1743
  }
1689
1744
  async query(sql, params, options) {
1690
1745
  return this.service.queryOnDb(this.name, sql, params, options);
@@ -1707,6 +1762,7 @@ var DatabaseHandle = class {
1707
1762
  var SQLAction = {
1708
1763
  READ: "tinycloud.sql/read",
1709
1764
  WRITE: "tinycloud.sql/write",
1765
+ DDL: "tinycloud.sql/ddl",
1710
1766
  ADMIN: "tinycloud.sql/admin",
1711
1767
  SELECT: "tinycloud.sql/select",
1712
1768
  INSERT: "tinycloud.sql/insert",
@@ -1718,9 +1774,21 @@ var SQLAction = {
1718
1774
  };
1719
1775
 
1720
1776
  // src/sql/SQLService.ts
1777
+ var DDL_TOKENS = /* @__PURE__ */ new Set(["alter", "create", "drop"]);
1778
+ var MIGRATIONS_TABLE = "__tinycloud_sql_migrations";
1779
+ var MIGRATIONS_SCHEMA = `CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
1780
+ key TEXT PRIMARY KEY,
1781
+ namespace TEXT NOT NULL,
1782
+ id TEXT NOT NULL,
1783
+ applied_at TEXT NOT NULL,
1784
+ statement_count INTEGER NOT NULL
1785
+ )`;
1786
+ var MIGRATIONS_META_NAMESPACE = "tinycloud.sql.migrations";
1787
+ var MIGRATIONS_META_ID = "000_metadata";
1721
1788
  var SQLService = class extends BaseService {
1722
1789
  constructor(config = {}) {
1723
1790
  super();
1791
+ this.migrationLocks = /* @__PURE__ */ new Map();
1724
1792
  this._config = config;
1725
1793
  }
1726
1794
  get config() {
@@ -1756,6 +1824,24 @@ var SQLService = class extends BaseService {
1756
1824
  async batch(statements, options) {
1757
1825
  return this.batchOnDb(this.defaultDbName, statements, options);
1758
1826
  }
1827
+ async applyMigrationsOnDb(dbName, options) {
1828
+ const validationError = validateMigrationOptions(options);
1829
+ if (validationError) {
1830
+ return err(serviceError(ErrorCodes.INVALID_INPUT, validationError, "sql"));
1831
+ }
1832
+ const lockKey = `${dbName}\0${options.namespace}`;
1833
+ const existing = this.migrationLocks.get(lockKey);
1834
+ if (existing) {
1835
+ return existing;
1836
+ }
1837
+ const promise = this.applyMigrationsOnDbUnlocked(dbName, options);
1838
+ this.migrationLocks.set(lockKey, promise);
1839
+ try {
1840
+ return await promise;
1841
+ } finally {
1842
+ this.migrationLocks.delete(lockKey);
1843
+ }
1844
+ }
1759
1845
  // === Internal methods called by DatabaseHandle ===
1760
1846
  async queryOnDb(dbName, sql, params, options) {
1761
1847
  return this.withTelemetry("query", dbName, async () => {
@@ -1793,9 +1879,15 @@ var SQLService = class extends BaseService {
1793
1879
  if (options?.schema) {
1794
1880
  body.schema = options.schema;
1795
1881
  }
1882
+ const actions = [
1883
+ this.actionForSql(sql, SQLAction.WRITE),
1884
+ ...(options?.schema ?? []).map(
1885
+ (statement) => this.actionForSql(statement, SQLAction.DDL)
1886
+ )
1887
+ ];
1796
1888
  const response = await this.invokeSQL(
1797
1889
  dbName,
1798
- this.actionForSql(sql, SQLAction.WRITE),
1890
+ this.dedupeActions(actions),
1799
1891
  body,
1800
1892
  options?.signal
1801
1893
  );
@@ -1817,7 +1909,7 @@ var SQLService = class extends BaseService {
1817
1909
  try {
1818
1910
  const response = await this.invokeSQL(
1819
1911
  dbName,
1820
- this.actionForSqlBatch(statements),
1912
+ this.actionsForSqlBatch(statements),
1821
1913
  { action: "batch", statements },
1822
1914
  options?.signal
1823
1915
  );
@@ -1880,10 +1972,83 @@ var SQLService = class extends BaseService {
1880
1972
  }
1881
1973
  });
1882
1974
  }
1975
+ async applyMigrationsOnDbUnlocked(dbName, options) {
1976
+ return this.withTelemetry("migrations.apply", dbName, async () => {
1977
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
1978
+ if (!created.ok) return created;
1979
+ const listed = await this.queryOnDb(
1980
+ dbName,
1981
+ `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
1982
+ [options.namespace],
1983
+ { signal: options.signal }
1984
+ );
1985
+ if (!listed.ok) return listed;
1986
+ const appliedIds = new Set(
1987
+ listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
1988
+ );
1989
+ const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
1990
+ const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
1991
+ const applied = [];
1992
+ for (const migration of pending) {
1993
+ const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
1994
+ if (!result.ok) return result;
1995
+ applied.push(migration.id);
1996
+ }
1997
+ return ok({
1998
+ database: dbName,
1999
+ namespace: options.namespace,
2000
+ status: applied.length > 0 ? "applied" : "already_current",
2001
+ applied,
2002
+ skipped
2003
+ });
2004
+ });
2005
+ }
2006
+ async applyOneMigration(dbName, namespace, migration, signal) {
2007
+ const statements = [
2008
+ ...migration.sql.map(
2009
+ (statement) => typeof statement === "string" ? { sql: statement } : statement
2010
+ ),
2011
+ {
2012
+ sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
2013
+ params: [
2014
+ migrationKey(namespace, migration.id),
2015
+ namespace,
2016
+ migration.id,
2017
+ (/* @__PURE__ */ new Date()).toISOString(),
2018
+ migration.sql.length
2019
+ ]
2020
+ }
2021
+ ];
2022
+ const result = await this.batchOnDb(dbName, statements, { signal });
2023
+ if (!result.ok) return result;
2024
+ return ok(void 0);
2025
+ }
2026
+ async ensureMigrationsTable(dbName, signal) {
2027
+ const result = await this.batchOnDb(
2028
+ dbName,
2029
+ [
2030
+ { sql: MIGRATIONS_SCHEMA },
2031
+ {
2032
+ sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
2033
+ params: [
2034
+ migrationKey(MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID),
2035
+ MIGRATIONS_META_NAMESPACE,
2036
+ MIGRATIONS_META_ID,
2037
+ (/* @__PURE__ */ new Date()).toISOString(),
2038
+ 1
2039
+ ]
2040
+ }
2041
+ ],
2042
+ { signal }
2043
+ );
2044
+ if (!result.ok) return result;
2045
+ return ok(void 0);
2046
+ }
1883
2047
  // === Private helpers ===
1884
- async invokeSQL(dbName, action, body, signal) {
2048
+ async invokeSQL(dbName, actions, body, signal) {
1885
2049
  const session = this.context.session;
1886
- const headers = this.context.invoke(session, "sql", dbName, action);
2050
+ const actionList = Array.isArray(actions) ? actions : [actions];
2051
+ const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
1887
2052
  return this.context.fetch(`${this.host}/invoke`, {
1888
2053
  method: "POST",
1889
2054
  headers: {
@@ -1895,26 +2060,50 @@ var SQLService = class extends BaseService {
1895
2060
  });
1896
2061
  }
1897
2062
  actionForSql(sql, fallback) {
1898
- return firstSqlToken(sql) === "pragma" ? SQLAction.ADMIN : fallback;
2063
+ const token = firstSqlToken(sql);
2064
+ if (token === "pragma") return SQLAction.ADMIN;
2065
+ if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.DDL;
2066
+ return fallback;
2067
+ }
2068
+ actionsForSqlBatch(statements) {
2069
+ return this.dedupeActions(
2070
+ statements.map((statement) => this.actionForSql(statement.sql, SQLAction.WRITE))
2071
+ );
2072
+ }
2073
+ dedupeActions(actions) {
2074
+ return [...new Set(actions)];
1899
2075
  }
1900
- actionForSqlBatch(statements) {
1901
- return statements.some(
1902
- (statement) => this.actionForSql(statement.sql, SQLAction.WRITE) === SQLAction.ADMIN
1903
- ) ? SQLAction.ADMIN : SQLAction.WRITE;
2076
+ invokeSQLAny(session, dbName, actions) {
2077
+ if (!this.context.invokeAny) {
2078
+ throw new Error(
2079
+ `SQL operation requires multiple permissions (${actions.join(", ")}) but this SDK runtime does not support multi-resource invocations`
2080
+ );
2081
+ }
2082
+ return this.context.invokeAny(
2083
+ session,
2084
+ actions.map((action) => ({
2085
+ spaceId: session.spaceId,
2086
+ service: "sql",
2087
+ path: dbName,
2088
+ action
2089
+ }))
2090
+ );
1904
2091
  }
1905
2092
  async handleErrorResponse(response, operation) {
1906
2093
  const errorText = await response.text();
1907
- let errorBody = {};
1908
- try {
1909
- errorBody = JSON.parse(errorText);
1910
- } catch {
1911
- }
2094
+ const errorBody = parseServiceErrorBody(errorText);
1912
2095
  const errorCode = this.mapHttpStatusToErrorCode(
1913
2096
  response.status,
1914
2097
  errorBody.error
1915
2098
  );
1916
- const message = errorBody.message || `SQL ${operation} failed: ${response.status} - ${errorText}`;
1917
- const meta = { status: response.status, statusText: response.statusText };
2099
+ const message = formatServiceResponseError(
2100
+ "SQL",
2101
+ operation,
2102
+ response.status,
2103
+ errorText,
2104
+ errorBody
2105
+ );
2106
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
1918
2107
  if (response.status === 401) {
1919
2108
  const { resource, action } = parseAuthError(errorText);
1920
2109
  if (action) meta.requiredAction = action;
@@ -1974,6 +2163,53 @@ function firstSqlToken(sql) {
1974
2163
  const match = /^[A-Za-z_]+/.exec(sql.slice(index));
1975
2164
  return match?.[0].toLowerCase();
1976
2165
  }
2166
+ function validateMigrationOptions(options) {
2167
+ if (!options || typeof options !== "object") {
2168
+ return "SQL migrations require options";
2169
+ }
2170
+ if (!options.namespace || typeof options.namespace !== "string") {
2171
+ return "SQL migrations require a non-empty namespace";
2172
+ }
2173
+ if (!Array.isArray(options.migrations)) {
2174
+ return "SQL migrations require an ordered migrations array";
2175
+ }
2176
+ const ids = /* @__PURE__ */ new Set();
2177
+ for (const migration of options.migrations) {
2178
+ if (!migration.id || typeof migration.id !== "string") {
2179
+ return "SQL migrations require every migration to have a non-empty id";
2180
+ }
2181
+ if (ids.has(migration.id)) {
2182
+ return `Duplicate SQL migration id: ${migration.id}`;
2183
+ }
2184
+ ids.add(migration.id);
2185
+ if (!Array.isArray(migration.sql)) {
2186
+ return `SQL migration ${migration.id} requires a sql array`;
2187
+ }
2188
+ for (const statement of migration.sql) {
2189
+ if (typeof statement === "string") {
2190
+ if (statement.trim() === "") {
2191
+ return `SQL migration ${migration.id} contains an empty statement`;
2192
+ }
2193
+ continue;
2194
+ }
2195
+ if (!statement || typeof statement.sql !== "string" || statement.sql.trim() === "") {
2196
+ return `SQL migration ${migration.id} contains an invalid statement`;
2197
+ }
2198
+ }
2199
+ }
2200
+ return null;
2201
+ }
2202
+ function migrationKey(namespace, id) {
2203
+ return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
2204
+ }
2205
+ function rowValue(row, index) {
2206
+ if (Array.isArray(row)) return row[index];
2207
+ if (row && typeof row === "object") {
2208
+ const values = Object.values(row);
2209
+ return values[index];
2210
+ }
2211
+ return void 0;
2212
+ }
1977
2213
 
1978
2214
  // src/duckdb/DuckDbDatabaseHandle.ts
1979
2215
  var DuckDbDatabaseHandle = class {
@@ -2283,17 +2519,19 @@ var DuckDbService = class extends BaseService {
2283
2519
  }
2284
2520
  async handleErrorResponse(response, operation) {
2285
2521
  const errorText = await response.text();
2286
- let errorBody = {};
2287
- try {
2288
- errorBody = JSON.parse(errorText);
2289
- } catch {
2290
- }
2522
+ const errorBody = parseServiceErrorBody(errorText);
2291
2523
  const errorCode = this.mapHttpStatusToErrorCode(
2292
2524
  response.status,
2293
2525
  errorBody.error
2294
2526
  );
2295
- const message = errorBody.message || `DuckDB ${operation} failed: ${response.status} - ${errorText}`;
2296
- const meta = { status: response.status, statusText: response.statusText };
2527
+ const message = formatServiceResponseError(
2528
+ "DuckDB",
2529
+ operation,
2530
+ response.status,
2531
+ errorText,
2532
+ errorBody
2533
+ );
2534
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
2297
2535
  if (response.status === 401) {
2298
2536
  const { resource, action } = parseAuthError(errorText);
2299
2537
  if (action) meta.requiredAction = action;
@@ -5641,6 +5879,7 @@ export {
5641
5879
  RetryPolicySchema,
5642
5880
  SECRET_NAME_RE,
5643
5881
  SQLAction,
5882
+ SQLMigrations,
5644
5883
  SQLService,
5645
5884
  SecretsService,
5646
5885
  ServiceContext,