@terrascale/sdk 0.0.3 → 0.0.5

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +206 -67
  3. package/package.json +10 -4
  4. package/sdk-public-v1-route-manifest.json +85 -0
  5. package/sdk-v1-route-manifest.json +896 -15
  6. package/src/http.js +3 -48
  7. package/src/index.js +80 -1
  8. package/src/local/environment.js +0 -1
  9. package/src/ts-auth.js +1 -5
  10. package/src/v1/backups.js +111 -0
  11. package/src/v1/catalog.js +240 -0
  12. package/src/v1/changes.js +225 -0
  13. package/src/v1/database-schema.js +45 -0
  14. package/src/v1/decode.js +6 -5
  15. package/src/v1/errors.js +23 -3
  16. package/src/v1/export.js +68 -0
  17. package/src/v1/ids.js +5 -2
  18. package/src/v1/imports.js +72 -0
  19. package/src/v1/indexes.js +76 -0
  20. package/src/v1/json.js +4 -3
  21. package/src/v1/management-decode.js +24 -0
  22. package/src/v1/management-routes.js +72 -0
  23. package/src/v1/management-validation.js +25 -0
  24. package/src/v1/routes.js +32 -7
  25. package/src/v1/saved-queries.js +58 -0
  26. package/src/v1/shape-crypto.js +125 -0
  27. package/src/v1/shape-manifest.js +31 -0
  28. package/src/v1/shape-reader.js +214 -0
  29. package/src/v1/shape-subscription.js +208 -0
  30. package/src/v1/shapes.js +11 -5
  31. package/src/v1/sql.js +98 -0
  32. package/src/v1/system.js +80 -0
  33. package/src/v1/terrabase-v1.js +231 -30
  34. package/src/v1/transport.js +117 -33
  35. package/src/v1/usage.js +101 -0
  36. package/types/http.d.ts +2 -8
  37. package/types/index.d.ts +73 -0
  38. package/types/local/environment.d.ts +1 -1
  39. package/types/ts-auth.d.ts +0 -2
  40. package/types/v1/backups.d.ts +128 -0
  41. package/types/v1/catalog.d.ts +104 -0
  42. package/types/v1/changes.d.ts +53 -0
  43. package/types/v1/database-schema.d.ts +82 -0
  44. package/types/v1/decode.d.ts +20 -4
  45. package/types/v1/errors.d.ts +16 -1
  46. package/types/v1/export.d.ts +26 -0
  47. package/types/v1/ids.d.ts +2 -2
  48. package/types/v1/imports.d.ts +132 -0
  49. package/types/v1/indexes.d.ts +75 -0
  50. package/types/v1/management-decode.d.ts +15 -0
  51. package/types/v1/management-routes.d.ts +3 -0
  52. package/types/v1/management-validation.d.ts +8 -0
  53. package/types/v1/routes.d.ts +5 -2
  54. package/types/v1/saved-queries.d.ts +33 -0
  55. package/types/v1/shape-crypto.d.ts +39 -0
  56. package/types/v1/shape-manifest.d.ts +28 -0
  57. package/types/v1/shape-reader.d.ts +45 -0
  58. package/types/v1/shape-subscription.d.ts +35 -0
  59. package/types/v1/shapes.d.ts +16 -2
  60. package/types/v1/sql.d.ts +47 -0
  61. package/types/v1/system.d.ts +85 -0
  62. package/types/v1/terrabase-v1.d.ts +140 -6
  63. package/types/v1/transport.d.ts +33 -10
  64. package/types/v1/usage.d.ts +171 -0
  65. package/src/ts-auth-discovery.js +0 -207
  66. package/types/ts-auth-discovery.d.ts +0 -19
@@ -16,19 +16,20 @@ export const maximumRequestBytes = 4 * 1024 * 1024;
16
16
  // Query budgets count stored JSON before wrappers and serializer escaping.
17
17
  const defaultMaximumResponseBytes = 64 * 1024 * 1024;
18
18
  // Validate header-safe credential syntax only; the producer validates JWT claims/signatures.
19
- // Retain C# keys while the published TS-589 JWT contract awaits a producer release.
19
+ // Retain C# keys; current project routes also authenticate project access JWTs.
20
20
  const tokenPattern = /^(?!tbk1\.)(?:[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|tb_[A-Za-z0-9_-]{43})$/u;
21
21
  /**
22
22
  * Resolver input contains no credential. A successful URL is cached for this client/database.
23
- * @typedef {{ readonly project: string; readonly database: string; readonly signal?: AbortSignal | undefined }} V1EndpointContext
23
+ * @typedef {{ readonly project: string | undefined; readonly database?: string | undefined; readonly signal?: AbortSignal | undefined }} V1EndpointContext
24
24
  */
25
25
  /** @typedef {(context: V1EndpointContext) => string | Promise<string>} V1EndpointResolver */
26
+ /** A fresh access token for each authenticated request, including stream reconnects. @typedef {() => string | Promise<string>} V1TokenProvider */
26
27
  /**
27
28
  * Project-scoped C# Documents API client. Supply exactly one of baseUrl or endpointResolver.
28
29
  * URLs must use HTTPS or loopback HTTP. Mutations are never automatically replayed.
29
30
  * @typedef {object} TerraBaseV1ClientConfig
30
31
  * @property {string} project
31
- * @property {string} token A ts-auth project access JWT (or a legacy C# tb_ key); held privately.
32
+ * @property {string | V1TokenProvider} token An access JWT (or a legacy C# tb_ key), or a provider resolved per authenticated request; held privately. Browser callers must use end-user tokens, never project credentials.
32
33
  * @property {string | undefined} [baseUrl]
33
34
  * @property {V1EndpointResolver | undefined} [endpointResolver]
34
35
  * @property {FetchLike | undefined} [fetch]
@@ -37,11 +38,12 @@ const tokenPattern = /^(?!tbk1\.)(?:[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-
37
38
 
38
39
  /**
39
40
  * @typedef {object} V1CallOptions
40
- * @property {string} [body]
41
+ * @property {string | Uint8Array} [body]
41
42
  * @property {string} [contentType]
42
43
  * @property {Readonly<Record<string, string>>} [headers] Contract headers only (If-Match, If-None-Match).
43
44
  * @property {URLSearchParams | undefined} [query]
44
45
  * @property {AbortSignal | undefined} [signal]
46
+ * @property {"no-store" | undefined} [cache] Authenticated shape epoch keys must bypass caches.
45
47
  */
46
48
 
47
49
  /**
@@ -116,6 +118,13 @@ function canonicalBaseUrl(raw) {
116
118
  return `${url.origin}${url.pathname.replace(/\/+$/u, "")}`;
117
119
  }
118
120
 
121
+ /** @param {unknown} token @returns {string} */
122
+ function requireToken(token) {
123
+ if (typeof token !== "string" || token.trim() !== token || !tokenPattern.test(token))
124
+ throw new TypeError("token must be a compact access JWT or a legacy C# tb_ API key.");
125
+ return token;
126
+ }
127
+
119
128
  /** A response body that failed in transit after its headers arrived. */
120
129
  export class BodyTransportError extends Error {
121
130
  /** @param {unknown} cause */
@@ -127,30 +136,30 @@ export class BodyTransportError extends Error {
127
136
 
128
137
  /** Internal transport bound to one project credential. */
129
138
  export class V1Transport {
130
- /** @type {string} */
139
+ /** @type {string | undefined} */
131
140
  #project;
132
141
  /** @type {V1EndpointResolver} */
133
142
  #resolver;
134
143
  /** @type {Map<string, { promise: Promise<string>; timer: ReturnType<typeof setTimeout>; waiters: number }>} */
135
144
  #bases = new Map();
136
- /** @type {string} */
145
+ /** @type {string | V1TokenProvider | undefined} */
137
146
  #token;
138
147
  /** @type {FetchLike} */
139
148
  #fetch;
140
149
  /** @type {number} */
141
150
  #maxResponseBytes;
142
151
 
143
- /** @param {TerraBaseV1ClientConfig} config */
152
+ /** @param {TerraBaseV1ClientConfig | (import("./system.js").TerraBaseSystemClientConfig & { project?: undefined; endpointResolver?: undefined })} config */
144
153
  constructor(config) {
145
154
  if (typeof config !== "object" || config === null) throw new TypeError("TerraBase v1 client config is required.");
146
- this.#project = requireProject(config.project);
155
+ this.#project = config.project === undefined ? undefined : requireProject(config.project);
147
156
  if ((config.baseUrl === undefined) === (config.endpointResolver === undefined))
148
157
  throw new TypeError("Supply exactly one of baseUrl or endpointResolver.");
149
158
  if (config.endpointResolver !== undefined && typeof config.endpointResolver !== "function")
150
159
  throw new TypeError("endpointResolver must be a function.");
151
160
  this.#resolver = config.endpointResolver ?? staticBaseUrlResolver(/** @type {string} */ (config.baseUrl));
152
- if (typeof config.token !== "string" || !tokenPattern.test(config.token))
153
- throw new TypeError("token must be a compact access JWT or a legacy C# tb_ API key.");
161
+ if ((config.project !== undefined || config.token !== undefined) && typeof config.token !== "function")
162
+ requireToken(config.token);
154
163
  if ("autoIdempotencyKey" in config || "maxRetries" in config || "resolver" in config)
155
164
  throw new TypeError("Unsupported client configuration member.");
156
165
  const fetchHandler = config.fetch ?? globalThis.fetch?.bind(globalThis);
@@ -163,6 +172,35 @@ export class V1Transport {
163
172
  this.#maxResponseBytes = maxResponseBytes;
164
173
  }
165
174
 
175
+ /** Internal credential lifetime metadata; never exposes the bearer value. */
176
+ get hasTokenProvider() { return typeof this.#token === "function"; }
177
+
178
+ /** @param {AbortSignal | undefined} signal @returns {Promise<string>} */
179
+ async #accessToken(signal) {
180
+ signal?.throwIfAborted();
181
+ const provider = this.#token;
182
+ if (typeof provider !== "function") return requireToken(provider);
183
+ /** @type {() => void} */
184
+ let abort = () => {};
185
+ try {
186
+ const token = await new Promise((resolve, reject) => {
187
+ abort = () => reject(signal?.reason);
188
+ signal?.addEventListener("abort", abort, { once: true });
189
+ void Promise.resolve().then(() => {
190
+ signal?.throwIfAborted();
191
+ return provider();
192
+ }).then(resolve, () => {
193
+ // Provider errors may contain credentials; do not retain their message or cause.
194
+ reject(new Error("TerraBase token provider failed."));
195
+ });
196
+ });
197
+ signal?.throwIfAborted();
198
+ return requireToken(token);
199
+ } finally {
200
+ signal?.removeEventListener("abort", abort);
201
+ }
202
+ }
203
+
166
204
  /**
167
205
  * @param {V1Operation} operation
168
206
  * @param {Readonly<Record<string, string>>} params
@@ -171,8 +209,9 @@ export class V1Transport {
171
209
  */
172
210
  async url(operation, params, signal) {
173
211
  signal?.throwIfAborted();
174
- const database = requireName("database", params.database);
175
- let base = this.#bases.get(database);
212
+ const database = v1Route(operation).pathTemplate.includes("{database}") ? requireName("database", params.database) : undefined;
213
+ const session = database ?? "";
214
+ let base = this.#bases.get(session);
176
215
  if (base === undefined) {
177
216
  const controller = new AbortController();
178
217
  const timer = setTimeout(() => controller.abort(new DOMException(
@@ -182,13 +221,13 @@ export class V1Transport {
182
221
  const promise = new Promise((resolve, reject) => {
183
222
  controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true });
184
223
  void Promise.resolve().then(() => this.#resolver(Object.freeze({
185
- project: this.#project, database, signal: controller.signal,
224
+ project: this.#project, ...(database === undefined ? {} : { database }), signal: controller.signal,
186
225
  }))).then(canonicalBaseUrl).then(resolve, reject).finally(() => clearTimeout(timer));
187
226
  });
188
227
  base = { promise, timer, waiters: 0 };
189
- this.#bases.set(database, base);
228
+ this.#bases.set(session, base);
190
229
  // A failed resolution is not a session; a later operation may resolve again.
191
- void promise.catch(() => { if (this.#bases.get(database) === base) this.#bases.delete(database); });
230
+ void promise.catch(() => { if (this.#bases.get(session) === base) this.#bases.delete(session); });
192
231
  }
193
232
  base.waiters++;
194
233
  if (typeof base.timer === "object") base.timer.ref();
@@ -206,7 +245,8 @@ export class V1Transport {
206
245
  });
207
246
  });
208
247
  signal?.throwIfAborted();
209
- return `${resolved}${v1RoutePath(v1Route(operation), { ...params, project: this.#project })}`;
248
+ if (v1Route(operation).pathTemplate.includes("{project}") && this.#project === undefined) throw new TypeError("This operation requires a project client.");
249
+ return `${resolved}${v1RoutePath(v1Route(operation), { ...params, ...(this.#project === undefined ? {} : { project: this.#project }) })}`;
210
250
  } finally {
211
251
  // Keep Node alive only while callers are waiting for this resolution.
212
252
  if (--base.waiters === 0 && typeof base.timer === "object") base.timer.unref();
@@ -217,9 +257,11 @@ export class V1Transport {
217
257
  * Reads a response body as UTF-8 text within the response byte bound. A
218
258
  * connection failure while reading is a BodyTransportError.
219
259
  * @param {Response} response
260
+ * @param {AbortSignal | undefined} [signal]
220
261
  * @returns {Promise<string>}
221
262
  */
222
- async readText(response) {
263
+ async readText(response, signal) {
264
+ signal?.throwIfAborted();
223
265
  const reader = response.body?.getReader();
224
266
  if (reader === undefined) return "";
225
267
  /** @type {Uint8Array[]} */
@@ -227,13 +269,16 @@ export class V1Transport {
227
269
  let size = 0;
228
270
  try {
229
271
  while (true) {
272
+ signal?.throwIfAborted();
230
273
  /** @type {ReadableStreamReadResult<Uint8Array>} */
231
274
  let part;
232
275
  try {
233
276
  part = await reader.read();
234
277
  } catch (error) {
278
+ signal?.throwIfAborted();
235
279
  throw new BodyTransportError(error);
236
280
  }
281
+ signal?.throwIfAborted();
237
282
  if (part.done) break;
238
283
  size += part.value.byteLength;
239
284
  if (size > this.#maxResponseBytes)
@@ -252,16 +297,17 @@ export class V1Transport {
252
297
  bytes.set(part, offset);
253
298
  offset += part.byteLength;
254
299
  }
255
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
300
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
256
301
  }
257
302
 
258
303
  /**
259
304
  * Parses a JSON response body exactly.
260
305
  * @param {Response} response
306
+ * @param {AbortSignal | undefined} [signal]
261
307
  * @returns {Promise<JsonValue | undefined>}
262
308
  */
263
- async readJson(response) {
264
- return V1Transport.#decodeJson(response, await this.readText(response));
309
+ async readJson(response, signal) {
310
+ return V1Transport.#decodeJson(response, await this.readText(response, signal));
265
311
  }
266
312
 
267
313
  /**
@@ -280,13 +326,15 @@ export class V1Transport {
280
326
  /**
281
327
  * Converts a non-success response to a TerraBaseV1Error.
282
328
  * @param {Response} response
329
+ * @param {AbortSignal | undefined} [signal]
283
330
  * @returns {Promise<TerraBaseV1Error>}
284
331
  */
285
- async error(response) {
332
+ async error(response, signal) {
286
333
  let text = "";
287
334
  try {
288
- text = await this.readText(response);
335
+ text = await this.readText(response, signal);
289
336
  } catch {
337
+ signal?.throwIfAborted();
290
338
  text = "";
291
339
  }
292
340
  return parseV1Error(response.status, response.headers, text);
@@ -300,27 +348,63 @@ export class V1Transport {
300
348
  * @returns {Promise<V1CallResult>}
301
349
  */
302
350
  async call(operation, params, options = {}) {
351
+ const response = await this.#send(operation, params, options);
352
+ if (response.status === 307) {
353
+ await response.body?.cancel();
354
+ return { status: response.status, headers: response.headers, value: undefined };
355
+ }
356
+ return { status: response.status, headers: response.headers, value: await this.readJson(response, options.signal) };
357
+ }
358
+
359
+ /**
360
+ * @param {"streamDatabaseChanges" | "exportDatabase"} operation
361
+ * @param {Readonly<Record<string, string>>} params
362
+ * @param {V1CallOptions} [options]
363
+ * @returns {Promise<Response>}
364
+ */
365
+ async openStream(operation, params, options = {}) {
366
+ const response = await this.#send(operation, params, options);
367
+ const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
368
+ const expected = v1Route(operation).responseMediaType;
369
+ if (mediaType !== expected || response.body === null) {
370
+ void response.body?.cancel().catch(() => undefined);
371
+ throw new TypeError(`TerraBase ${operation} requires a ${expected} response body.`);
372
+ }
373
+ return response;
374
+ }
375
+
376
+ /** @param {V1Operation} operation @param {Readonly<Record<string, string>>} params @param {V1CallOptions} options @returns {Promise<Response>} */
377
+ async #send(operation, params, options) {
303
378
  const route = v1Route(operation);
304
- if (options.body !== undefined && new TextEncoder().encode(options.body).byteLength > maximumRequestBytes)
379
+ const bytes = typeof options.body === "string" ? new TextEncoder().encode(options.body) : options.body;
380
+ if (bytes !== undefined && bytes.byteLength > maximumRequestBytes)
305
381
  throw new TypeError(`TerraBase request bodies are limited to ${maximumRequestBytes} bytes.`);
306
382
  let url = await this.url(operation, params, options.signal);
307
383
  if (options.query !== undefined && options.query.size > 0) url += `?${options.query}`;
308
384
  const headers = new Headers(options.headers);
309
- headers.set("authorization", `Bearer ${this.#token}`);
310
- headers.set("accept", route.responseMediaType);
385
+ if (route.permission !== "anonymous" && this.#token !== undefined)
386
+ headers.set("authorization", `Bearer ${await this.#accessToken(options.signal)}`);
387
+ if (route.responseMediaType !== "") headers.set("accept", route.responseMediaType);
311
388
  if (options.body !== undefined) headers.set("content-type", options.contentType ?? "application/json");
312
- /** @type {RequestInit} */
389
+ /** @type {RequestInit & { duplex?: "half" }} */
313
390
  const init = { method: route.method, headers, redirect: "manual" };
314
- if (options.body !== undefined) init.body = options.body;
391
+ if (options.cache !== undefined) init.cache = options.cache;
392
+ if (route.permission.endsWith(":write") && typeof process !== "undefined" && process.release?.name === "node") {
393
+ // Node fetch replays buffered or absent bodies on 421; use a one-shot stream, including DELETE.
394
+ if (operation === "uploadImportSource" && bytes !== undefined) headers.set("content-length", String(bytes.byteLength));
395
+ init.body = new ReadableStream({ start(controller) { if (bytes !== undefined && bytes.byteLength > 0) controller.enqueue(bytes); controller.close(); } });
396
+ init.duplex = "half";
397
+ } else if (options.body !== undefined) init.body = typeof options.body === "string" ? options.body : options.body.slice().buffer;
315
398
  if (options.signal !== undefined) init.signal = options.signal;
399
+ options.signal?.throwIfAborted();
316
400
  const response = await this.#fetch(url, init);
401
+ if (options.signal?.aborted) {
402
+ void response.body?.cancel().catch(() => undefined);
403
+ options.signal.throwIfAborted();
404
+ }
317
405
  if (response.type === "opaqueredirect")
318
406
  throw new TypeError("Fresh shape bootstrap requires server fetch; obtain a handle and snapshot offset with collection.shape() on your server.");
319
- if (!route.successStatuses.includes(response.status)) throw await this.error(response);
320
- if (response.status === 307) {
321
- await response.body?.cancel();
322
- return { status: response.status, headers: response.headers, value: undefined };
323
- }
324
- return { status: response.status, headers: response.headers, value: await this.readJson(response) };
407
+ if (!route.successStatuses.includes(response.status)) throw await this.error(response, options.signal);
408
+ return response;
325
409
  }
326
410
  }
@@ -0,0 +1,101 @@
1
+ import { requireId } from "../ids.js";
2
+ import { requireName } from "./ids.js";
3
+ import { dictionary, flag, list, nullable, numeric, record, text } from "./management-decode.js";
4
+ import { members } from "./management-validation.js";
5
+ /** @import { V1Transport } from "./transport.js" */
6
+ /** @import { V1RequestOptions } from "./terrabase-v1.js" */
7
+ const period = record({ from: text, to: text, granularity: text, reads: numeric, writes: numeric });
8
+ const series = list(record({ start: text, reads: numeric, writes: numeric }));
9
+ const databaseUsage = record({ databaseId: requireId, storageBytes: numeric, logicalBytes: numeric, collections: numeric,
10
+ documents: numeric, reads: numeric, writes: numeric, period, series });
11
+ const projectUsage = record({ projectId: requireId, databases: numeric, storageBytes: numeric, logicalBytes: numeric,
12
+ documents: numeric, reads: numeric, writes: numeric, period, series, items: list(databaseUsage) });
13
+ const bandwidth = record({ bytesSent: numeric, bytesReceived: numeric });
14
+ const directedBandwidth = list(record({ sourceRegion: text, destinationRegion: text, kind: text, bytesSent: numeric, bytesReceived: numeric }));
15
+ const latency = record({ p50Ms: nullable(numeric), p95Ms: nullable(numeric), p99Ms: nullable(numeric) });
16
+ const analytics = record({ projectId: requireId, range: text, from: text, to: text, bucketSeconds: numeric,
17
+ items: list(record({ databaseId: requireId, databaseName: text, region: text,
18
+ series: list(record({ start: text, requests: dictionary(numeric), operations: dictionary(numeric), errors: numeric,
19
+ latency, storageBytes: nullable(numeric), clientBandwidth: bandwidth, replicationBandwidth: bandwidth })) })) });
20
+ const replication = record({ projectId: requireId, range: text, from: text, to: text, bucketSeconds: numeric,
21
+ items: list(record({ databaseId: requireId, databaseName: text, region: text, status: text, replicatedOps: numeric,
22
+ lagMs: nullable(numeric), divergence: nullable(flag), convergenceMs: nullable(numeric), lastApplied: nullable(text),
23
+ bandwidth: directedBandwidth, series: list(record({ start: text, replicatedOps: numeric, bandwidth: directedBandwidth })) })) });
24
+ /** @typedef {ReturnType<typeof databaseUsage>} V1DatabaseUsage */
25
+ /** @typedef {ReturnType<typeof projectUsage>} V1ProjectUsage */
26
+ /** @typedef {ReturnType<typeof analytics>} V1Analytics */
27
+ /** @typedef {ReturnType<typeof replication>} V1Replication */
28
+ /** @typedef {{ readonly granularity?: "hour" | "day"; readonly from?: string; readonly to?: string }} V1UsageRequest */
29
+ /** @typedef {{ readonly range?: "1h" | "24h" | "7d" | "30d"; readonly database?: string; readonly region?: string }} V1AnalyticsRequest */
30
+ /** @param {V1UsageRequest} request */
31
+ function usageQuery(request) {
32
+ members(request, ["granularity", "from", "to"]);
33
+ const query = new URLSearchParams();
34
+ if (request.granularity !== undefined) {
35
+ if (!["hour", "day"].includes(request.granularity)) throw new TypeError("granularity must be hour or day.");
36
+ query.set("granularity", request.granularity);
37
+ }
38
+ for (const name of /** @type {const} */ (["from", "to"])) {
39
+ const value = request[name];
40
+ if (value === undefined) continue;
41
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:Z|[+-]\d{2}:\d{2})$/u.test(value) || !Number.isFinite(Date.parse(value)))
42
+ throw new TypeError("from and to must be RFC 3339 instants.");
43
+ query.set(name, value);
44
+ }
45
+ return query;
46
+ }
47
+ /** @param {V1AnalyticsRequest} request @param {boolean} project */
48
+ function analyticsQuery(request, project) {
49
+ members(request, project ? ["range", "database", "region"] : ["range", "region"]);
50
+ const query = new URLSearchParams();
51
+ if (request.range !== undefined) {
52
+ if (!["1h", "24h", "7d", "30d"].includes(request.range)) throw new TypeError("Invalid analytics range.");
53
+ query.set("range", request.range);
54
+ }
55
+ if (request.database !== undefined) query.set("database", requireId(request.database));
56
+ if (request.region !== undefined) {
57
+ if (typeof request.region !== "string" || !/^[a-z][a-z0-9-]{0,62}$/u.test(request.region)) throw new TypeError("Invalid region.");
58
+ query.set("region", request.region);
59
+ }
60
+ return query;
61
+ }
62
+
63
+ /** Project and database usage retain the producer's separate response envelopes. */
64
+ export class V1ProjectUsageClient {
65
+ /** @type {V1Transport} */
66
+ #transport;
67
+ /** @param {V1Transport} transport */
68
+ constructor(transport) { this.#transport = transport; Object.freeze(this); }
69
+ /** @param {V1UsageRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1ProjectUsage>} */
70
+ async usage(request = {}, options = {}) {
71
+ return projectUsage((await this.#transport.call("getProjectUsage", {}, { query: usageQuery(request), signal: options.signal })).value);
72
+ }
73
+ /** @param {V1AnalyticsRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1Analytics>} */
74
+ async analytics(request = {}, options = {}) {
75
+ return analytics((await this.#transport.call("getProjectAnalytics", {}, { query: analyticsQuery(request, true), signal: options.signal })).value);
76
+ }
77
+ /** @param {V1AnalyticsRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1Replication>} */
78
+ async replication(request = {}, options = {}) {
79
+ return replication((await this.#transport.call("getProjectReplication", {}, { query: analyticsQuery(request, true), signal: options.signal })).value);
80
+ }
81
+ }
82
+ export class V1DatabaseUsageClient {
83
+ /** @type {V1Transport} */
84
+ #transport;
85
+ /** @type {{database: string}} */
86
+ #params;
87
+ /** @param {V1Transport} transport @param {string} database */
88
+ constructor(transport, database) { this.#transport = transport; this.#params = { database: requireName("database", database) }; Object.freeze(this); }
89
+ /** @param {V1UsageRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1DatabaseUsage>} */
90
+ async usage(request = {}, options = {}) {
91
+ return databaseUsage((await this.#transport.call("getDatabaseUsage", this.#params, { query: usageQuery(request), signal: options.signal })).value);
92
+ }
93
+ /** @param {Omit<V1AnalyticsRequest, "database">} [request] @param {V1RequestOptions} [options] @returns {Promise<V1Analytics>} */
94
+ async analytics(request = {}, options = {}) {
95
+ return analytics((await this.#transport.call("getDatabaseAnalytics", this.#params, { query: analyticsQuery(request, false), signal: options.signal })).value);
96
+ }
97
+ /** @param {Omit<V1AnalyticsRequest, "database">} [request] @param {V1RequestOptions} [options] @returns {Promise<V1Replication>} */
98
+ async replication(request = {}, options = {}) {
99
+ return replication((await this.#transport.call("getDatabaseReplication", this.#params, { query: analyticsQuery(request, false), signal: options.signal })).value);
100
+ }
101
+ }
package/types/http.d.ts CHANGED
@@ -1,12 +1,7 @@
1
1
  /** Private transport for the named hosted identity and OIDC operations. */
2
2
  export class TsAuthHttp {
3
- /**
4
- * @param {TsAuthRuntimeConfig} config
5
- * @param {{ readonly authReverseProxy?: boolean }} [options]
6
- */
7
- constructor(config: TsAuthRuntimeConfig, options?: {
8
- readonly authReverseProxy?: boolean;
9
- });
3
+ /** @param {TsAuthRuntimeConfig} config */
4
+ constructor(config: TsAuthRuntimeConfig);
10
5
  /**
11
6
  * @readonly
12
7
  * @type {ResolvedTsAuthRuntimeConfig}
@@ -39,7 +34,6 @@ export type TsAuthRequest<TBody> = {
39
34
  readonly formBody?: URLSearchParams | string | undefined;
40
35
  readonly userinfoAccessToken?: string | undefined;
41
36
  readonly resourceIntrospectionBasic?: string | undefined;
42
- readonly issuer?: string | undefined;
43
37
  readonly signal?: AbortSignal | undefined;
44
38
  };
45
39
  import type { ResolvedTsAuthRuntimeConfig } from "./config.js";
package/types/index.d.ts CHANGED
@@ -1,8 +1,18 @@
1
1
  export { v1Filter } from "./v1/filter.js";
2
+ export { V1ShapeSubscription } from "./v1/shape-subscription.js";
2
3
  export { ExactDecimal } from "./v1/json.js";
3
4
  export { staticBaseUrlResolver } from "./v1/transport.js";
5
+ export { V1Databases } from "./v1/catalog.js";
6
+ export { V1SavedQueries } from "./v1/saved-queries.js";
7
+ export { V1DatabaseSchemaClient } from "./v1/database-schema.js";
8
+ export { V1Indexes } from "./v1/indexes.js";
9
+ export { V1Imports } from "./v1/imports.js";
10
+ export type V1ShapeSubscribeOptions = import("./v1/shape-subscription.js").V1ShapeSubscribeOptions;
11
+ export type V1ShapeDocument = import("./v1/shape-subscription.js").V1ShapeDocument;
12
+ export type V1SealedShapePosition = import("./v1/shape-reader.js").V1SealedShapePosition;
4
13
  export type EntityId = import("./ids.js").EntityId;
5
14
  export type TerraBaseV1ClientConfig = import("./v1/transport.js").TerraBaseV1ClientConfig;
15
+ export type V1TokenProvider = import("./v1/transport.js").V1TokenProvider;
6
16
  export type V1RequestOptions = import("./v1/terrabase-v1.js").V1RequestOptions;
7
17
  export type V1ReadOptions = import("./v1/terrabase-v1.js").V1ReadOptions;
8
18
  export type V1WriteOptions = import("./v1/terrabase-v1.js").V1WriteOptions;
@@ -11,6 +21,7 @@ export type V1ConditionalWriteOptions = import("./v1/terrabase-v1.js").V1Conditi
11
21
  export type V1PatchOp = import("./v1/terrabase-v1.js").V1PatchOp;
12
22
  export type V1Patch = import("./v1/terrabase-v1.js").V1Patch;
13
23
  export type V1SortKey = import("./v1/terrabase-v1.js").V1SortKey;
24
+ export type V1NearSortKey = import("./v1/terrabase-v1.js").V1NearSortKey;
14
25
  export type V1Query = import("./v1/terrabase-v1.js").V1Query;
15
26
  export type V1Count = import("./v1/terrabase-v1.js").V1Count;
16
27
  export type V1QueryPage = import("./v1/terrabase-v1.js").V1QueryPage;
@@ -31,6 +42,9 @@ export type V1JsonInputObject = import("./v1/json.js").JsonInputObject;
31
42
  export type V1ShapeRequest = import("./v1/shapes.js").V1ShapeRequest;
32
43
  export type V1ShapeChange = import("./v1/shapes.js").V1ShapeChange;
33
44
  export type V1ShapeOp = import("./v1/shapes.js").V1ShapeOp;
45
+ export type V1SavedQuery = import("./v1/saved-queries.js").V1SavedQuery;
46
+ export type V1SavedQueryRequest = import("./v1/saved-queries.js").V1SavedQueryRequest;
47
+ export type V1SavedQueryPage = import("./v1/saved-queries.js").V1SavedQueryPage;
34
48
  export type TerraBaseClientConfig = import("./v1/transport.js").TerraBaseV1ClientConfig;
35
49
  export type V1EndpointContext = import("./v1/transport.js").V1EndpointContext;
36
50
  export type V1EndpointResolver = import("./v1/transport.js").V1EndpointResolver;
@@ -38,7 +52,66 @@ export type V1CollectionInfo = import("./v1/terrabase-v1.js").V1CollectionInfo;
38
52
  export type V1CollectionPage = import("./v1/terrabase-v1.js").V1CollectionPage;
39
53
  export type V1CollectionListOptions = import("./v1/terrabase-v1.js").V1CollectionListOptions;
40
54
  export type V1ShapePage = import("./v1/shapes.js").V1ShapePage;
55
+ export type V1SqlRow = import("./v1/sql.js").V1SqlRow;
56
+ export type V1SqlResult = import("./v1/sql.js").V1SqlResult;
57
+ export type V1SqlIndexSuggestion = import("./v1/sql.js").V1SqlIndexSuggestion;
58
+ export type V1SqlCheck = import("./v1/sql.js").V1SqlCheck;
59
+ export type V1ChangeStreamRequest = import("./v1/changes.js").V1ChangeStreamRequest;
60
+ export type V1DatabaseChange = import("./v1/changes.js").V1DatabaseChange;
61
+ export type V1DatabaseChangeCursor = import("./v1/changes.js").V1DatabaseChangeCursor;
62
+ export type V1ExportOptions = import("./v1/export.js").V1ExportOptions;
63
+ export type V1ExportedDocument = import("./v1/export.js").V1ExportedDocument;
64
+ export type V1ManagedDatabase = import("./v1/catalog.js").V1ManagedDatabase;
65
+ export type V1DatabaseCreateRequest = import("./v1/catalog.js").V1DatabaseCreateRequest;
66
+ export type V1DatabaseUpdateRequest = import("./v1/catalog.js").V1DatabaseUpdateRequest;
67
+ export type V1DatabaseBranchRequest = import("./v1/catalog.js").V1DatabaseBranchRequest;
68
+ export type V1DatabaseListRequest = import("./v1/catalog.js").V1DatabaseListRequest;
69
+ export type V1DatabasePage = import("./v1/catalog.js").V1DatabasePage;
70
+ export type V1CollectionSchema = import("./v1/catalog.js").V1CollectionSchema;
71
+ export type V1CollectionTtl = import("./v1/catalog.js").V1CollectionTtl;
72
+ export type V1InferredSchema = import("./v1/database-schema.js").V1InferredSchema;
73
+ export type V1DatabaseSchema = import("./v1/database-schema.js").V1DatabaseSchema;
74
+ export type V1UniqueSchema = import("./v1/database-schema.js").V1UniqueSchema;
75
+ export type V1UniqueSchemaRequest = import("./v1/database-schema.js").V1UniqueSchemaRequest;
76
+ export type V1QueryShapes = import("./v1/database-schema.js").V1QueryShapes;
77
+ export type V1Index = import("./v1/indexes.js").V1Index;
78
+ export type V1IndexList = import("./v1/indexes.js").V1IndexList;
79
+ export type V1AutoIndexPolicy = import("./v1/indexes.js").V1AutoIndexPolicy;
80
+ export type V1IndexRecommendations = import("./v1/indexes.js").V1IndexRecommendations;
81
+ export type V1AppliedIndexRecommendation = import("./v1/indexes.js").V1AppliedIndexRecommendation;
82
+ export type V1CreateIndexRequest = import("./v1/indexes.js").V1CreateIndexRequest;
83
+ export type V1DatabaseUsage = import("./v1/usage.js").V1DatabaseUsage;
84
+ export type V1ProjectUsage = import("./v1/usage.js").V1ProjectUsage;
85
+ export type V1Analytics = import("./v1/usage.js").V1Analytics;
86
+ export type V1Replication = import("./v1/usage.js").V1Replication;
87
+ export type V1UsageRequest = import("./v1/usage.js").V1UsageRequest;
88
+ export type V1AnalyticsRequest = import("./v1/usage.js").V1AnalyticsRequest;
89
+ export type V1Backup = import("./v1/backups.js").V1Backup;
90
+ export type V1BackupPage = import("./v1/backups.js").V1BackupPage;
91
+ export type V1BackupPolicy = import("./v1/backups.js").V1BackupPolicy;
92
+ export type V1RestoreWindow = import("./v1/backups.js").V1RestoreWindow;
93
+ export type V1PointInTimePlan = import("./v1/backups.js").V1PointInTimePlan;
94
+ export type V1RestoredDatabase = import("./v1/backups.js").V1RestoredDatabase;
95
+ export type V1BackupDestination = import("./v1/backups.js").V1BackupDestination;
96
+ export type V1BackupPolicyRequest = import("./v1/backups.js").V1BackupPolicyRequest;
97
+ export type V1PointInTimeRequest = import("./v1/backups.js").V1PointInTimeRequest;
98
+ export type V1PointInTimeRestoreRequest = import("./v1/backups.js").V1PointInTimeRestoreRequest;
99
+ export type V1BackupDestinationRequest = import("./v1/backups.js").V1BackupDestinationRequest;
100
+ export type V1ImportJob = import("./v1/imports.js").V1ImportJob;
101
+ export type V1ImportOptions = import("./v1/imports.js").V1ImportOptions;
102
+ export type V1CreateImportRequest = import("./v1/imports.js").V1CreateImportRequest;
103
+ export type TerraBaseSystemClientConfig = import("./v1/system.js").TerraBaseSystemClientConfig;
104
+ export type V1ProbeStatus = import("./v1/system.js").V1ProbeStatus;
105
+ export type V1ReplicationHealth = import("./v1/system.js").V1ReplicationHealth;
106
+ export type V1PlatformDatabases = import("./v1/system.js").V1PlatformDatabases;
107
+ export type V1ShapeManifest = import("./v1/shape-manifest.js").V1ShapeManifest;
108
+ export type V1ShapeManifestRequest = import("./v1/shape-manifest.js").V1ShapeManifestRequest;
109
+ export type V1ShapeKeyRequest = import("./v1/shape-reader.js").V1ShapeKeyRequest;
110
+ export type V1ShapeKey = import("./v1/shape-reader.js").V1ShapeKey;
41
111
  export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient, createTerraBaseV1Client, TerraBaseV1Client, V1Database, V1Collection, V1Collections, V1Transaction } from "./v1/terrabase-v1.js";
42
112
  export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
43
113
  export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
44
114
  export { generateId, isValidId, requireId } from "./ids.js";
115
+ export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
116
+ export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
117
+ export { createTerraBaseSystemClient, V1Health, V1Platform } from "./v1/system.js";
@@ -3,7 +3,7 @@
3
3
  * @returns {void}
4
4
  */
5
5
  export function assertLocalSqliteEnvironment(environment?: Readonly<Record<string, string | undefined>>): void;
6
- export const localSqlitePresenceIndicators: readonly ["CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "FORGEJO_ACTIONS", "GITEA_ACTIONS", "TF_BUILD", "BUILDKITE", "CIRCLECI", "JENKINS_URL", "TEAMCITY_VERSION", "VERCEL", "NETLIFY", "CF_PAGES", "RENDER", "FLY_APP_NAME", "K_SERVICE", "CLOUD_RUN_JOB", "KUBERNETES_SERVICE_HOST", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV", "ECS_CONTAINER_METADATA_URI", "ECS_CONTAINER_METADATA_URI_V4", "WEBSITE_INSTANCE_ID", "WEBSITE_SITE_NAME", "FUNCTIONS_WORKER_RUNTIME", "DYNO", "HEROKU_APP_NAME", "RAILWAY_ENVIRONMENT", "RAILWAY_SERVICE_ID", "NOMAD_ALLOC_ID", "TERRASCALE_DEPLOYMENT", "TERRASCALE_QUALIFICATION"];
6
+ export const localSqlitePresenceIndicators: readonly ["CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "FORGEJO_ACTIONS", "GITEA_ACTIONS", "TF_BUILD", "BUILDKITE", "CIRCLECI", "JENKINS_URL", "TEAMCITY_VERSION", "VERCEL", "NETLIFY", "CF_PAGES", "RENDER", "K_SERVICE", "CLOUD_RUN_JOB", "KUBERNETES_SERVICE_HOST", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV", "ECS_CONTAINER_METADATA_URI", "ECS_CONTAINER_METADATA_URI_V4", "WEBSITE_INSTANCE_ID", "WEBSITE_SITE_NAME", "FUNCTIONS_WORKER_RUNTIME", "DYNO", "HEROKU_APP_NAME", "RAILWAY_ENVIRONMENT", "RAILWAY_SERVICE_ID", "NOMAD_ALLOC_ID", "TERRASCALE_DEPLOYMENT", "TERRASCALE_QUALIFICATION"];
7
7
  /**
8
8
  * Explicit opt-in for a hosted development environment.
9
9
  *
@@ -38,7 +38,6 @@ export function verifyTsAuthWebhookWithTolerance(signingSecret: string, timestam
38
38
  * @typedef {Omit<TsAuthRuntimeConfig, "apiBaseUrl"> & {
39
39
  * readonly apiBaseUrl: string; // The ts-auth deployment's public origin; there is no default.
40
40
  * readonly instanceSlug: string; // The instance the `sk_` key belongs to; it prefixes every route.
41
- * readonly useReverseProxy?: boolean; // Route public auth calls through the reverse proxy instead of regional discovery.
42
41
  * }} TsAuthClientConfig
43
42
  */
44
43
  /**
@@ -1335,7 +1334,6 @@ export type TsAuthRuntimeConfig = import("./config.js").TsAuthRuntimeConfig;
1335
1334
  export type TsAuthClientConfig = Omit<TsAuthRuntimeConfig, "apiBaseUrl"> & {
1336
1335
  readonly apiBaseUrl: string;
1337
1336
  readonly instanceSlug: string;
1338
- readonly useReverseProxy?: boolean;
1339
1337
  };
1340
1338
  export type TsAuthRequestOptions = {
1341
1339
  readonly signal?: AbortSignal | undefined;