@tinycloud/sdk-services 2.4.0-beta.2 → 2.4.0

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.cjs CHANGED
@@ -52,6 +52,7 @@ __export(index_exports, {
52
52
  RetryPolicySchema: () => RetryPolicySchema,
53
53
  SECRET_NAME_RE: () => SECRET_NAME_RE,
54
54
  SQLAction: () => SQLAction,
55
+ SQLMigrations: () => SQLMigrations,
55
56
  SQLService: () => SQLService,
56
57
  SecretsService: () => SecretsService,
57
58
  ServiceContext: () => ServiceContext,
@@ -63,6 +64,7 @@ __export(index_exports, {
63
64
  ServiceSessionSchema: () => ServiceSessionSchema,
64
65
  TelemetryEvents: () => TelemetryEvents,
65
66
  TelemetrySpanEventSchema: () => TelemetrySpanEventSchema,
67
+ TinyCloudDebugLogger: () => TinyCloudDebugLogger,
66
68
  TinyCloudQuota: () => TinyCloudQuota,
67
69
  VaultHeaders: () => VaultHeaders,
68
70
  VaultPublicSpaceKVActions: () => VaultPublicSpaceKVActions,
@@ -82,21 +84,26 @@ __export(index_exports, {
82
84
  canonicalizeEncryptionJson: () => canonicalize,
83
85
  canonicalizeSecretScope: () => canonicalizeSecretScope,
84
86
  checkDecryptInvocationInput: () => checkDecryptInvocationInput,
87
+ clearTinyCloudDebugLogs: () => clearTinyCloudDebugLogs,
85
88
  createKVResponseSchema: () => createKVResponseSchema,
86
89
  createResultSchema: () => createResultSchema,
87
90
  createVaultCrypto: () => createVaultCrypto,
88
91
  decryptEnvelopeWithKey: () => decryptEnvelopeWithKey,
89
92
  defaultRetryPolicy: () => defaultRetryPolicy,
90
93
  deriveSignedReceiverKey: () => deriveSignedReceiverKey,
94
+ disableTinyCloudDebug: () => disableTinyCloudDebug,
91
95
  discoverNetwork: () => discoverNetwork,
96
+ enableTinyCloudDebug: () => enableTinyCloudDebug,
92
97
  encryptToNetwork: () => encryptToNetwork,
93
98
  encryptionError: () => encryptionError,
94
99
  ensureNetworkUsableForDecrypt: () => ensureNetworkUsableForDecrypt,
95
100
  err: () => err,
96
101
  errorResult: () => errorResult,
97
102
  generateRandomReceiverKey: () => generateRandomReceiverKey,
103
+ getTinyCloudDebugLogs: () => getTinyCloudDebugLogs,
98
104
  hexDecode: () => hexDecode,
99
105
  hexEncode: () => hexEncode2,
106
+ installTinyCloudDebugGlobals: () => installTinyCloudDebugGlobals,
100
107
  isNetworkId: () => isNetworkId,
101
108
  networkDiscoveryKey: () => networkDiscoveryKey,
102
109
  networkError: () => networkError,
@@ -112,6 +119,7 @@ __export(index_exports, {
112
119
  storageLimitReachedError: () => storageLimitReachedError,
113
120
  storageQuotaExceededError: () => storageQuotaExceededError,
114
121
  timeoutError: () => timeoutError,
122
+ tinyCloudDebugLogger: () => tinyCloudDebugLogger,
115
123
  utf8Decode: () => utf8Decode,
116
124
  utf8Encode: () => utf8Encode,
117
125
  validateEnvelope: () => validateEnvelope,
@@ -197,6 +205,202 @@ function serviceError(code, message, service, options) {
197
205
  };
198
206
  }
199
207
 
208
+ // src/debug.ts
209
+ var DEBUG_FLAG = "TinyCloud_debug";
210
+ var MAX_EVENTS = 1e3;
211
+ function getGlobal() {
212
+ return globalThis;
213
+ }
214
+ function nowMs() {
215
+ const performanceNow = globalThis.performance?.now?.bind(globalThis.performance);
216
+ return typeof performanceNow === "function" ? performanceNow() : Date.now();
217
+ }
218
+ function isTrue(value) {
219
+ return value === true || value === "true" || value === "1";
220
+ }
221
+ function getProcessDebugFlag() {
222
+ const processLike = globalThis.process;
223
+ return processLike?.env?.[DEBUG_FLAG];
224
+ }
225
+ function isBrowserWindow() {
226
+ const global = getGlobal();
227
+ return global.window === globalThis;
228
+ }
229
+ function getLocalStorage() {
230
+ if (!isBrowserWindow()) {
231
+ return void 0;
232
+ }
233
+ try {
234
+ return globalThis.localStorage;
235
+ } catch {
236
+ return void 0;
237
+ }
238
+ }
239
+ function getStoredDebugFlag() {
240
+ const storage = getLocalStorage();
241
+ if (!storage) {
242
+ return void 0;
243
+ }
244
+ try {
245
+ return storage.getItem(DEBUG_FLAG);
246
+ } catch {
247
+ return void 0;
248
+ }
249
+ }
250
+ function setStoredDebugFlag(enabled) {
251
+ const storage = getLocalStorage();
252
+ if (!storage) {
253
+ return;
254
+ }
255
+ try {
256
+ if (enabled) {
257
+ storage.setItem(DEBUG_FLAG, "true");
258
+ } else {
259
+ storage.removeItem(DEBUG_FLAG);
260
+ }
261
+ } catch {
262
+ }
263
+ }
264
+ function shouldStartEnabled() {
265
+ const global = getGlobal();
266
+ return isTrue(global[DEBUG_FLAG]) || isTrue(getStoredDebugFlag()) || isTrue(getProcessDebugFlag());
267
+ }
268
+ var TinyCloudDebugLogger = class {
269
+ constructor() {
270
+ this.enabled = shouldStartEnabled();
271
+ this.sequence = 0;
272
+ this.events = [];
273
+ this.maxEvents = MAX_EVENTS;
274
+ }
275
+ isEnabled() {
276
+ return this.enabled || isTrue(getGlobal()[DEBUG_FLAG]) || isTrue(getProcessDebugFlag());
277
+ }
278
+ enable(options = {}) {
279
+ this.enabled = true;
280
+ getGlobal()[DEBUG_FLAG] = true;
281
+ if (options.persist !== false) {
282
+ setStoredDebugFlag(true);
283
+ }
284
+ this.log("debug.enabled", { persisted: options.persist !== false });
285
+ }
286
+ disable(options = {}) {
287
+ this.log("debug.disabled", { persisted: options.persist !== false });
288
+ this.enabled = false;
289
+ getGlobal()[DEBUG_FLAG] = false;
290
+ if (options.persist !== false) {
291
+ setStoredDebugFlag(false);
292
+ }
293
+ }
294
+ clear() {
295
+ this.events = [];
296
+ }
297
+ getLogs() {
298
+ return [...this.events];
299
+ }
300
+ log(event, data, message) {
301
+ if (!this.isEnabled()) {
302
+ return void 0;
303
+ }
304
+ return this.record({
305
+ event,
306
+ data,
307
+ message,
308
+ level: "debug",
309
+ timestamp: Date.now(),
310
+ timestampIso: (/* @__PURE__ */ new Date()).toISOString()
311
+ });
312
+ }
313
+ startTimer(event, data) {
314
+ if (!this.isEnabled()) {
315
+ return { stop: () => void 0 };
316
+ }
317
+ const startedAt = nowMs();
318
+ this.log(`${event}.start`, data);
319
+ return {
320
+ stop: (finishData) => {
321
+ const endedAt = nowMs();
322
+ return this.record({
323
+ event: `${event}.end`,
324
+ data: finishData,
325
+ durationMs: endedAt - startedAt,
326
+ startedAt,
327
+ endedAt,
328
+ level: "debug",
329
+ timestamp: Date.now(),
330
+ timestampIso: (/* @__PURE__ */ new Date()).toISOString()
331
+ });
332
+ }
333
+ };
334
+ }
335
+ async timeAsync(event, operation, data) {
336
+ if (!this.isEnabled()) {
337
+ return operation();
338
+ }
339
+ const timer = this.startTimer(event, data);
340
+ try {
341
+ const result = await operation();
342
+ timer.stop({ ok: true });
343
+ return result;
344
+ } catch (error) {
345
+ timer.stop({ ok: false, error });
346
+ throw error;
347
+ }
348
+ }
349
+ time(event, operation, data) {
350
+ if (!this.isEnabled()) {
351
+ return operation();
352
+ }
353
+ const timer = this.startTimer(event, data);
354
+ try {
355
+ const result = operation();
356
+ timer.stop({ ok: true });
357
+ return result;
358
+ } catch (error) {
359
+ timer.stop({ ok: false, error });
360
+ throw error;
361
+ }
362
+ }
363
+ record(event) {
364
+ const debugEvent = {
365
+ ...event,
366
+ sequence: ++this.sequence
367
+ };
368
+ this.events.push(debugEvent);
369
+ if (this.events.length > this.maxEvents) {
370
+ this.events.splice(0, this.events.length - this.maxEvents);
371
+ }
372
+ try {
373
+ globalThis.console?.debug?.("[TinyCloud]", debugEvent.event, debugEvent);
374
+ } catch {
375
+ }
376
+ return debugEvent;
377
+ }
378
+ };
379
+ var tinyCloudDebugLogger = new TinyCloudDebugLogger();
380
+ function enableTinyCloudDebug(options) {
381
+ tinyCloudDebugLogger.enable(options);
382
+ return tinyCloudDebugLogger;
383
+ }
384
+ function disableTinyCloudDebug(options) {
385
+ tinyCloudDebugLogger.disable(options);
386
+ return tinyCloudDebugLogger;
387
+ }
388
+ function getTinyCloudDebugLogs() {
389
+ return tinyCloudDebugLogger.getLogs();
390
+ }
391
+ function clearTinyCloudDebugLogs() {
392
+ tinyCloudDebugLogger.clear();
393
+ }
394
+ function installTinyCloudDebugGlobals() {
395
+ const global = getGlobal();
396
+ global.TinyCloudDebug = tinyCloudDebugLogger;
397
+ global.enableTinyCloudDebug = enableTinyCloudDebug;
398
+ global.disableTinyCloudDebug = disableTinyCloudDebug;
399
+ global.getTinyCloudDebugLogs = getTinyCloudDebugLogs;
400
+ global.clearTinyCloudDebugLogs = clearTinyCloudDebugLogs;
401
+ }
402
+ installTinyCloudDebugGlobals();
403
+
200
404
  // src/types.schema.ts
201
405
  var import_zod = require("zod");
202
406
  var ServiceErrorSchema = import_zod.z.object({
@@ -438,9 +642,9 @@ var ServiceContext = class {
438
642
  this._services = /* @__PURE__ */ new Map();
439
643
  this._eventHandlers = /* @__PURE__ */ new Map();
440
644
  this._abortController = new AbortController();
441
- this._invoke = config.invoke;
442
- this._invokeAny = config.invokeAny;
443
- this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
645
+ this._invoke = this.wrapInvoke(config.invoke);
646
+ this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
647
+ this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
444
648
  this._hosts = config.hosts;
445
649
  this._session = config.session ?? null;
446
650
  this._retryPolicy = {
@@ -543,6 +747,7 @@ var ServiceContext = class {
543
747
  * @param data - Event data
544
748
  */
545
749
  emit(event, data) {
750
+ tinyCloudDebugLogger.log(event, data);
546
751
  if (this._telemetryEnabled && this._telemetryHandler) {
547
752
  try {
548
753
  this._telemetryHandler(event, data);
@@ -632,6 +837,77 @@ var ServiceContext = class {
632
837
  get retryPolicy() {
633
838
  return this._retryPolicy;
634
839
  }
840
+ wrapInvoke(invoke) {
841
+ return (session, service, path, action, facts) => {
842
+ if (!tinyCloudDebugLogger.isEnabled()) {
843
+ return invoke(session, service, path, action, facts);
844
+ }
845
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
846
+ service,
847
+ path,
848
+ action,
849
+ facts
850
+ });
851
+ try {
852
+ const result = invoke(session, service, path, action, facts);
853
+ timer.stop({ ok: true, service, path, action });
854
+ return result;
855
+ } catch (error) {
856
+ timer.stop({ ok: false, service, path, action, error });
857
+ throw error;
858
+ }
859
+ };
860
+ }
861
+ wrapInvokeAny(invokeAny) {
862
+ return (session, entries, facts) => {
863
+ if (!tinyCloudDebugLogger.isEnabled()) {
864
+ return invokeAny(session, entries, facts);
865
+ }
866
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
867
+ entries,
868
+ facts
869
+ });
870
+ try {
871
+ const result = invokeAny(session, entries, facts);
872
+ timer.stop({ ok: true, entries });
873
+ return result;
874
+ } catch (error) {
875
+ timer.stop({ ok: false, entries, error });
876
+ throw error;
877
+ }
878
+ };
879
+ }
880
+ wrapFetch(fetchFn) {
881
+ return async (url, init) => {
882
+ if (!tinyCloudDebugLogger.isEnabled()) {
883
+ return fetchFn(url, init);
884
+ }
885
+ const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", {
886
+ url,
887
+ method: init?.method ?? "GET",
888
+ init
889
+ });
890
+ try {
891
+ const response = await fetchFn(url, init);
892
+ timer.stop({
893
+ ok: response.ok,
894
+ url,
895
+ method: init?.method ?? "GET",
896
+ status: response.status,
897
+ statusText: response.statusText
898
+ });
899
+ return response;
900
+ } catch (error) {
901
+ timer.stop({
902
+ ok: false,
903
+ url,
904
+ method: init?.method ?? "GET",
905
+ error
906
+ });
907
+ throw error;
908
+ }
909
+ };
910
+ }
635
911
  };
636
912
 
637
913
  // src/errors.ts
@@ -1817,11 +2093,59 @@ var KVService = class extends BaseService {
1817
2093
  */
1818
2094
  KVService.serviceName = "kv";
1819
2095
 
2096
+ // src/responseErrors.ts
2097
+ function parseServiceErrorBody(errorText) {
2098
+ try {
2099
+ return JSON.parse(errorText);
2100
+ } catch {
2101
+ return {};
2102
+ }
2103
+ }
2104
+ function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
2105
+ if (parsed.message) {
2106
+ return compactMessage(parsed.message);
2107
+ }
2108
+ if (looksLikeHtml(errorText)) {
2109
+ if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
2110
+ return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
2111
+ }
2112
+ return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
2113
+ }
2114
+ return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
2115
+ }
2116
+ function responseErrorMeta(status, statusText, errorText) {
2117
+ const meta = { status, statusText };
2118
+ const snippet = compactMessage(errorText);
2119
+ if (snippet) {
2120
+ meta.responseSnippet = snippet.slice(0, 300);
2121
+ }
2122
+ return meta;
2123
+ }
2124
+ function looksLikeHtml(text) {
2125
+ return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
2126
+ }
2127
+ function compactMessage(message) {
2128
+ const compact = message.replace(/\s+/g, " ").trim();
2129
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
2130
+ }
2131
+
2132
+ // src/sql/SQLMigrations.ts
2133
+ var SQLMigrations = class {
2134
+ constructor(service, dbName) {
2135
+ this.service = service;
2136
+ this.dbName = dbName;
2137
+ }
2138
+ apply(options) {
2139
+ return this.service.applyMigrationsOnDb(this.dbName, options);
2140
+ }
2141
+ };
2142
+
1820
2143
  // src/sql/DatabaseHandle.ts
1821
2144
  var DatabaseHandle = class {
1822
2145
  constructor(service, name) {
1823
2146
  this.service = service;
1824
2147
  this.name = name;
2148
+ this.migrations = new SQLMigrations(service, name);
1825
2149
  }
1826
2150
  async query(sql, params, options) {
1827
2151
  return this.service.queryOnDb(this.name, sql, params, options);
@@ -1844,6 +2168,7 @@ var DatabaseHandle = class {
1844
2168
  var SQLAction = {
1845
2169
  READ: "tinycloud.sql/read",
1846
2170
  WRITE: "tinycloud.sql/write",
2171
+ SCHEMA: "tinycloud.sql/schema",
1847
2172
  ADMIN: "tinycloud.sql/admin",
1848
2173
  SELECT: "tinycloud.sql/select",
1849
2174
  INSERT: "tinycloud.sql/insert",
@@ -1855,9 +2180,21 @@ var SQLAction = {
1855
2180
  };
1856
2181
 
1857
2182
  // src/sql/SQLService.ts
2183
+ var DDL_TOKENS = /* @__PURE__ */ new Set(["alter", "create", "drop"]);
2184
+ var MIGRATIONS_TABLE = "__tinycloud_sql_migrations";
2185
+ var MIGRATIONS_SCHEMA = `CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
2186
+ key TEXT PRIMARY KEY,
2187
+ namespace TEXT NOT NULL,
2188
+ id TEXT NOT NULL,
2189
+ applied_at TEXT NOT NULL,
2190
+ statement_count INTEGER NOT NULL
2191
+ )`;
2192
+ var MIGRATIONS_META_NAMESPACE = "tinycloud.sql.migrations";
2193
+ var MIGRATIONS_META_ID = "000_metadata";
1858
2194
  var SQLService = class extends BaseService {
1859
2195
  constructor(config = {}) {
1860
2196
  super();
2197
+ this.migrationLocks = /* @__PURE__ */ new Map();
1861
2198
  this._config = config;
1862
2199
  }
1863
2200
  get config() {
@@ -1893,6 +2230,24 @@ var SQLService = class extends BaseService {
1893
2230
  async batch(statements, options) {
1894
2231
  return this.batchOnDb(this.defaultDbName, statements, options);
1895
2232
  }
2233
+ async applyMigrationsOnDb(dbName, options) {
2234
+ const validationError = validateMigrationOptions(options);
2235
+ if (validationError) {
2236
+ return err(serviceError(ErrorCodes.INVALID_INPUT, validationError, "sql"));
2237
+ }
2238
+ const lockKey = `${dbName}\0${options.namespace}`;
2239
+ const existing = this.migrationLocks.get(lockKey);
2240
+ if (existing) {
2241
+ return existing;
2242
+ }
2243
+ const promise = this.applyMigrationsOnDbUnlocked(dbName, options);
2244
+ this.migrationLocks.set(lockKey, promise);
2245
+ try {
2246
+ return await promise;
2247
+ } finally {
2248
+ this.migrationLocks.delete(lockKey);
2249
+ }
2250
+ }
1896
2251
  // === Internal methods called by DatabaseHandle ===
1897
2252
  async queryOnDb(dbName, sql, params, options) {
1898
2253
  return this.withTelemetry("query", dbName, async () => {
@@ -1930,9 +2285,15 @@ var SQLService = class extends BaseService {
1930
2285
  if (options?.schema) {
1931
2286
  body.schema = options.schema;
1932
2287
  }
2288
+ const actions = [
2289
+ this.actionForSql(sql, SQLAction.WRITE),
2290
+ ...(options?.schema ?? []).map(
2291
+ (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
2292
+ )
2293
+ ];
1933
2294
  const response = await this.invokeSQL(
1934
2295
  dbName,
1935
- this.actionForSql(sql, SQLAction.WRITE),
2296
+ this.dedupeActions(actions),
1936
2297
  body,
1937
2298
  options?.signal
1938
2299
  );
@@ -1954,7 +2315,7 @@ var SQLService = class extends BaseService {
1954
2315
  try {
1955
2316
  const response = await this.invokeSQL(
1956
2317
  dbName,
1957
- this.actionForSqlBatch(statements),
2318
+ this.actionsForSqlBatch(statements),
1958
2319
  { action: "batch", statements },
1959
2320
  options?.signal
1960
2321
  );
@@ -2017,10 +2378,83 @@ var SQLService = class extends BaseService {
2017
2378
  }
2018
2379
  });
2019
2380
  }
2381
+ async applyMigrationsOnDbUnlocked(dbName, options) {
2382
+ return this.withTelemetry("migrations.apply", dbName, async () => {
2383
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
2384
+ if (!created.ok) return created;
2385
+ const listed = await this.queryOnDb(
2386
+ dbName,
2387
+ `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
2388
+ [options.namespace],
2389
+ { signal: options.signal }
2390
+ );
2391
+ if (!listed.ok) return listed;
2392
+ const appliedIds = new Set(
2393
+ listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
2394
+ );
2395
+ const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
2396
+ const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
2397
+ const applied = [];
2398
+ for (const migration of pending) {
2399
+ const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
2400
+ if (!result.ok) return result;
2401
+ applied.push(migration.id);
2402
+ }
2403
+ return ok({
2404
+ database: dbName,
2405
+ namespace: options.namespace,
2406
+ status: applied.length > 0 ? "applied" : "already_current",
2407
+ applied,
2408
+ skipped
2409
+ });
2410
+ });
2411
+ }
2412
+ async applyOneMigration(dbName, namespace, migration, signal) {
2413
+ const statements = [
2414
+ ...migration.sql.map(
2415
+ (statement) => typeof statement === "string" ? { sql: statement } : statement
2416
+ ),
2417
+ {
2418
+ sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
2419
+ params: [
2420
+ migrationKey(namespace, migration.id),
2421
+ namespace,
2422
+ migration.id,
2423
+ (/* @__PURE__ */ new Date()).toISOString(),
2424
+ migration.sql.length
2425
+ ]
2426
+ }
2427
+ ];
2428
+ const result = await this.batchOnDb(dbName, statements, { signal });
2429
+ if (!result.ok) return result;
2430
+ return ok(void 0);
2431
+ }
2432
+ async ensureMigrationsTable(dbName, signal) {
2433
+ const result = await this.batchOnDb(
2434
+ dbName,
2435
+ [
2436
+ { sql: MIGRATIONS_SCHEMA },
2437
+ {
2438
+ sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
2439
+ params: [
2440
+ migrationKey(MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID),
2441
+ MIGRATIONS_META_NAMESPACE,
2442
+ MIGRATIONS_META_ID,
2443
+ (/* @__PURE__ */ new Date()).toISOString(),
2444
+ 1
2445
+ ]
2446
+ }
2447
+ ],
2448
+ { signal }
2449
+ );
2450
+ if (!result.ok) return result;
2451
+ return ok(void 0);
2452
+ }
2020
2453
  // === Private helpers ===
2021
- async invokeSQL(dbName, action, body, signal) {
2454
+ async invokeSQL(dbName, actions, body, signal) {
2022
2455
  const session = this.context.session;
2023
- const headers = this.context.invoke(session, "sql", dbName, action);
2456
+ const actionList = Array.isArray(actions) ? actions : [actions];
2457
+ const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
2024
2458
  return this.context.fetch(`${this.host}/invoke`, {
2025
2459
  method: "POST",
2026
2460
  headers: {
@@ -2032,26 +2466,50 @@ var SQLService = class extends BaseService {
2032
2466
  });
2033
2467
  }
2034
2468
  actionForSql(sql, fallback) {
2035
- return firstSqlToken(sql) === "pragma" ? SQLAction.ADMIN : fallback;
2469
+ const token = firstSqlToken(sql);
2470
+ if (token === "pragma") return SQLAction.ADMIN;
2471
+ if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.SCHEMA;
2472
+ return fallback;
2473
+ }
2474
+ actionsForSqlBatch(statements) {
2475
+ return this.dedupeActions(
2476
+ statements.map((statement) => this.actionForSql(statement.sql, SQLAction.WRITE))
2477
+ );
2036
2478
  }
2037
- actionForSqlBatch(statements) {
2038
- return statements.some(
2039
- (statement) => this.actionForSql(statement.sql, SQLAction.WRITE) === SQLAction.ADMIN
2040
- ) ? SQLAction.ADMIN : SQLAction.WRITE;
2479
+ dedupeActions(actions) {
2480
+ return [...new Set(actions)];
2481
+ }
2482
+ invokeSQLAny(session, dbName, actions) {
2483
+ if (!this.context.invokeAny) {
2484
+ throw new Error(
2485
+ `SQL operation requires multiple permissions (${actions.join(", ")}) but this SDK runtime does not support multi-resource invocations`
2486
+ );
2487
+ }
2488
+ return this.context.invokeAny(
2489
+ session,
2490
+ actions.map((action) => ({
2491
+ spaceId: session.spaceId,
2492
+ service: "sql",
2493
+ path: dbName,
2494
+ action
2495
+ }))
2496
+ );
2041
2497
  }
2042
2498
  async handleErrorResponse(response, operation) {
2043
2499
  const errorText = await response.text();
2044
- let errorBody = {};
2045
- try {
2046
- errorBody = JSON.parse(errorText);
2047
- } catch {
2048
- }
2500
+ const errorBody = parseServiceErrorBody(errorText);
2049
2501
  const errorCode = this.mapHttpStatusToErrorCode(
2050
2502
  response.status,
2051
2503
  errorBody.error
2052
2504
  );
2053
- const message = errorBody.message || `SQL ${operation} failed: ${response.status} - ${errorText}`;
2054
- const meta = { status: response.status, statusText: response.statusText };
2505
+ const message = formatServiceResponseError(
2506
+ "SQL",
2507
+ operation,
2508
+ response.status,
2509
+ errorText,
2510
+ errorBody
2511
+ );
2512
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
2055
2513
  if (response.status === 401) {
2056
2514
  const { resource, action } = parseAuthError(errorText);
2057
2515
  if (action) meta.requiredAction = action;
@@ -2111,6 +2569,53 @@ function firstSqlToken(sql) {
2111
2569
  const match = /^[A-Za-z_]+/.exec(sql.slice(index));
2112
2570
  return match?.[0].toLowerCase();
2113
2571
  }
2572
+ function validateMigrationOptions(options) {
2573
+ if (!options || typeof options !== "object") {
2574
+ return "SQL migrations require options";
2575
+ }
2576
+ if (!options.namespace || typeof options.namespace !== "string") {
2577
+ return "SQL migrations require a non-empty namespace";
2578
+ }
2579
+ if (!Array.isArray(options.migrations)) {
2580
+ return "SQL migrations require an ordered migrations array";
2581
+ }
2582
+ const ids = /* @__PURE__ */ new Set();
2583
+ for (const migration of options.migrations) {
2584
+ if (!migration.id || typeof migration.id !== "string") {
2585
+ return "SQL migrations require every migration to have a non-empty id";
2586
+ }
2587
+ if (ids.has(migration.id)) {
2588
+ return `Duplicate SQL migration id: ${migration.id}`;
2589
+ }
2590
+ ids.add(migration.id);
2591
+ if (!Array.isArray(migration.sql)) {
2592
+ return `SQL migration ${migration.id} requires a sql array`;
2593
+ }
2594
+ for (const statement of migration.sql) {
2595
+ if (typeof statement === "string") {
2596
+ if (statement.trim() === "") {
2597
+ return `SQL migration ${migration.id} contains an empty statement`;
2598
+ }
2599
+ continue;
2600
+ }
2601
+ if (!statement || typeof statement.sql !== "string" || statement.sql.trim() === "") {
2602
+ return `SQL migration ${migration.id} contains an invalid statement`;
2603
+ }
2604
+ }
2605
+ }
2606
+ return null;
2607
+ }
2608
+ function migrationKey(namespace, id) {
2609
+ return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
2610
+ }
2611
+ function rowValue(row, index) {
2612
+ if (Array.isArray(row)) return row[index];
2613
+ if (row && typeof row === "object") {
2614
+ const values = Object.values(row);
2615
+ return values[index];
2616
+ }
2617
+ return void 0;
2618
+ }
2114
2619
 
2115
2620
  // src/duckdb/DuckDbDatabaseHandle.ts
2116
2621
  var DuckDbDatabaseHandle = class {
@@ -2420,17 +2925,19 @@ var DuckDbService = class extends BaseService {
2420
2925
  }
2421
2926
  async handleErrorResponse(response, operation) {
2422
2927
  const errorText = await response.text();
2423
- let errorBody = {};
2424
- try {
2425
- errorBody = JSON.parse(errorText);
2426
- } catch {
2427
- }
2928
+ const errorBody = parseServiceErrorBody(errorText);
2428
2929
  const errorCode = this.mapHttpStatusToErrorCode(
2429
2930
  response.status,
2430
2931
  errorBody.error
2431
2932
  );
2432
- const message = errorBody.message || `DuckDB ${operation} failed: ${response.status} - ${errorText}`;
2433
- const meta = { status: response.status, statusText: response.statusText };
2933
+ const message = formatServiceResponseError(
2934
+ "DuckDB",
2935
+ operation,
2936
+ response.status,
2937
+ errorText,
2938
+ errorBody
2939
+ );
2940
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
2434
2941
  if (response.status === 401) {
2435
2942
  const { resource, action } = parseAuthError(errorText);
2436
2943
  if (action) meta.requiredAction = action;
@@ -5779,6 +6286,7 @@ EncryptionService.serviceName = "encryption";
5779
6286
  RetryPolicySchema,
5780
6287
  SECRET_NAME_RE,
5781
6288
  SQLAction,
6289
+ SQLMigrations,
5782
6290
  SQLService,
5783
6291
  SecretsService,
5784
6292
  ServiceContext,
@@ -5790,6 +6298,7 @@ EncryptionService.serviceName = "encryption";
5790
6298
  ServiceSessionSchema,
5791
6299
  TelemetryEvents,
5792
6300
  TelemetrySpanEventSchema,
6301
+ TinyCloudDebugLogger,
5793
6302
  TinyCloudQuota,
5794
6303
  VaultHeaders,
5795
6304
  VaultPublicSpaceKVActions,
@@ -5809,21 +6318,26 @@ EncryptionService.serviceName = "encryption";
5809
6318
  canonicalizeEncryptionJson,
5810
6319
  canonicalizeSecretScope,
5811
6320
  checkDecryptInvocationInput,
6321
+ clearTinyCloudDebugLogs,
5812
6322
  createKVResponseSchema,
5813
6323
  createResultSchema,
5814
6324
  createVaultCrypto,
5815
6325
  decryptEnvelopeWithKey,
5816
6326
  defaultRetryPolicy,
5817
6327
  deriveSignedReceiverKey,
6328
+ disableTinyCloudDebug,
5818
6329
  discoverNetwork,
6330
+ enableTinyCloudDebug,
5819
6331
  encryptToNetwork,
5820
6332
  encryptionError,
5821
6333
  ensureNetworkUsableForDecrypt,
5822
6334
  err,
5823
6335
  errorResult,
5824
6336
  generateRandomReceiverKey,
6337
+ getTinyCloudDebugLogs,
5825
6338
  hexDecode,
5826
6339
  hexEncode,
6340
+ installTinyCloudDebugGlobals,
5827
6341
  isNetworkId,
5828
6342
  networkDiscoveryKey,
5829
6343
  networkError,
@@ -5839,6 +6353,7 @@ EncryptionService.serviceName = "encryption";
5839
6353
  storageLimitReachedError,
5840
6354
  storageQuotaExceededError,
5841
6355
  timeoutError,
6356
+ tinyCloudDebugLogger,
5842
6357
  utf8Decode,
5843
6358
  utf8Encode,
5844
6359
  validateEnvelope,