@terrascale/sdk 0.0.2 → 0.0.4

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 (84) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +193 -56
  3. package/package.json +14 -5
  4. package/sdk-public-v1-route-manifest.json +81 -0
  5. package/sdk-v1-route-manifest.json +8996 -1697
  6. package/src/database-codec.js +21 -26
  7. package/src/database-types.js +11 -11
  8. package/src/database-view.js +6 -6
  9. package/src/http.js +13 -48
  10. package/src/ids.js +22 -0
  11. package/src/index.js +77 -2
  12. package/src/local/better-auth.js +17 -6
  13. package/src/local/environment.js +0 -1
  14. package/src/local/index.js +2 -2
  15. package/src/local/legacy-client.js +5 -5
  16. package/src/local/router.js +12 -12
  17. package/src/local/store.js +29 -28
  18. package/src/local/test-environment.js +3 -2
  19. package/src/postgres.js +5 -4
  20. package/src/react/core.js +2 -2
  21. package/src/ts-auth-ids.js +122 -0
  22. package/src/ts-auth.js +75 -77
  23. package/src/v1/backups.js +111 -0
  24. package/src/v1/catalog.js +210 -0
  25. package/src/v1/changes.js +225 -0
  26. package/src/v1/database-schema.js +45 -0
  27. package/src/v1/decode.js +20 -8
  28. package/src/v1/errors.js +23 -3
  29. package/src/v1/ids.js +9 -59
  30. package/src/v1/imports.js +72 -0
  31. package/src/v1/indexes.js +76 -0
  32. package/src/v1/json.js +4 -3
  33. package/src/v1/management-decode.js +24 -0
  34. package/src/v1/management-routes.js +67 -0
  35. package/src/v1/management-validation.js +25 -0
  36. package/src/v1/routes.js +36 -11
  37. package/src/v1/saved-queries.js +58 -0
  38. package/src/v1/shape-crypto.js +125 -0
  39. package/src/v1/shape-manifest.js +31 -0
  40. package/src/v1/shape-reader.js +212 -0
  41. package/src/v1/shape-subscription.js +208 -0
  42. package/src/v1/shapes.js +15 -10
  43. package/src/v1/sql.js +98 -0
  44. package/src/v1/system.js +80 -0
  45. package/src/v1/terrabase-v1.js +192 -33
  46. package/src/v1/transport.js +80 -34
  47. package/src/v1/usage.js +101 -0
  48. package/types/database-codec.d.ts +1 -6
  49. package/types/database-types.d.ts +11 -11
  50. package/types/http.d.ts +2 -8
  51. package/types/ids.d.ts +10 -0
  52. package/types/index.d.ts +70 -1
  53. package/types/local/environment.d.ts +1 -1
  54. package/types/local/store.d.ts +10 -0
  55. package/types/local/test-environment.d.ts +2 -2
  56. package/types/postgres.d.ts +4 -4
  57. package/types/ts-auth-ids.d.ts +14 -0
  58. package/types/ts-auth.d.ts +99 -101
  59. package/types/v1/backups.d.ts +128 -0
  60. package/types/v1/catalog.d.ts +92 -0
  61. package/types/v1/changes.d.ts +53 -0
  62. package/types/v1/database-schema.d.ts +82 -0
  63. package/types/v1/decode.d.ts +17 -4
  64. package/types/v1/errors.d.ts +16 -1
  65. package/types/v1/ids.d.ts +4 -16
  66. package/types/v1/imports.d.ts +132 -0
  67. package/types/v1/indexes.d.ts +75 -0
  68. package/types/v1/management-decode.d.ts +15 -0
  69. package/types/v1/management-routes.d.ts +3 -0
  70. package/types/v1/management-validation.d.ts +8 -0
  71. package/types/v1/routes.d.ts +5 -2
  72. package/types/v1/saved-queries.d.ts +33 -0
  73. package/types/v1/shape-crypto.d.ts +39 -0
  74. package/types/v1/shape-manifest.d.ts +28 -0
  75. package/types/v1/shape-reader.d.ts +45 -0
  76. package/types/v1/shape-subscription.d.ts +35 -0
  77. package/types/v1/shapes.d.ts +14 -0
  78. package/types/v1/sql.d.ts +47 -0
  79. package/types/v1/system.d.ts +85 -0
  80. package/types/v1/terrabase-v1.d.ts +116 -7
  81. package/types/v1/transport.d.ts +27 -10
  82. package/types/v1/usage.d.ts +171 -0
  83. package/src/ts-auth-discovery.js +0 -207
  84. package/types/ts-auth-discovery.d.ts +0 -19
@@ -1,3 +1,5 @@
1
+ import { requireId as requireDatabaseId } from "./ids.js";
2
+ export { requireId as requireDatabaseId } from "./ids.js";
1
3
  /**
2
4
  * @import {
3
5
  * DatabaseAuthority, DatabaseCommit, DatabaseCommitReceipt, DatabaseCondition, DatabaseEvent,
@@ -20,18 +22,6 @@ const maximumBodyBytes = 4 * 1024 * 1024;
20
22
  const maximumValueBytes = 1024 * 1024;
21
23
  const scalarTypes = new Set(["bool", "i64", "utf8", "bytes"]);
22
24
 
23
- /**
24
- * @param {unknown} value
25
- * @param {string} [name]
26
- * @returns {string}
27
- */
28
- export function requireDatabaseUuid(value, name = "UUID") {
29
- if (typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test(value)) {
30
- throw new TypeError(`${name} must be a canonical lowercase UUID.`);
31
- }
32
- return value;
33
- }
34
-
35
25
  /**
36
26
  * @param {unknown} value
37
27
  * @param {string} [name]
@@ -295,7 +285,7 @@ function observedToWire(entries) {
295
285
  let previous = "";
296
286
  return array(entries).map(raw => {
297
287
  const entry = object(raw, ["replica", "incarnation", "sequence"]);
298
- const replica = requireDatabaseUuid(entry.replica, "replica");
288
+ const replica = requireDatabaseId(entry.replica, "replica");
299
289
  const incarnation = hex(entry.incarnation, 16, "incarnation");
300
290
  const order = replica + incarnation;
301
291
  if (order <= previous) throw new TypeError("Observed origins must be strictly increasing.");
@@ -311,7 +301,7 @@ function observedToWire(entries) {
311
301
  function observedFromWire(raw) {
312
302
  const entries = array(raw).map(value => {
313
303
  const entry = object(value, ["replica", "incarnation", "sequence"]);
314
- return { replica: requireDatabaseUuid(entry.replica, "replica"), incarnation: hex(entry.incarnation, 16, "incarnation"), sequence: decimal(entry.sequence, "sequence", true) };
304
+ return { replica: requireDatabaseId(entry.replica, "replica"), incarnation: hex(entry.incarnation, 16, "incarnation"), sequence: decimal(entry.sequence, "sequence", true) };
315
305
  });
316
306
  observedToWire(entries);
317
307
  return entries;
@@ -324,8 +314,8 @@ function observedFromWire(raw) {
324
314
  function eventFromWire(raw) {
325
315
  const entry = object(raw, ["database", "shard", "replica", "incarnation", "sequence"]);
326
316
  return {
327
- database: requireDatabaseUuid(entry.database, "database"), shard: decimal(entry.shard, "shard"),
328
- replica: requireDatabaseUuid(entry.replica, "replica"), incarnation: hex(entry.incarnation, 16, "incarnation"),
317
+ database: requireDatabaseId(entry.database, "database"), shard: decimal(entry.shard, "shard"),
318
+ replica: requireDatabaseId(entry.replica, "replica"), incarnation: hex(entry.incarnation, 16, "incarnation"),
329
319
  sequence: decimal(entry.sequence, "sequence", true),
330
320
  };
331
321
  }
@@ -430,7 +420,7 @@ function rowsFromWire(raw) {
430
420
  let previous = "";
431
421
  return array(raw, 128).map(rawRow => {
432
422
  const entry = object(rawRow, ["document", "point"]);
433
- const document = requireDatabaseUuid(entry.document, "document");
423
+ const document = requireDatabaseId(entry.document, "document");
434
424
  if (document <= previous) throw new TypeError("Query documents must be strictly increasing.");
435
425
  previous = document;
436
426
  return { document, point: decodeDatabasePoint(entry.point) };
@@ -486,7 +476,7 @@ export function decodeDatabaseSubscription(raw) {
486
476
  previous = sequence;
487
477
  return { sequence, effects: array(group.effects, 128).map(rawEffect => {
488
478
  const effect = object(rawEffect, ["document", "point"]);
489
- return { document: requireDatabaseUuid(effect.document, "document"), point: effect.point === null ? null : decodeDatabasePoint(effect.point) };
479
+ return { document: requireDatabaseId(effect.document, "document"), point: effect.point === null ? null : decodeDatabasePoint(effect.point) };
490
480
  }) };
491
481
  });
492
482
  return { version: 1, phase: "poll", groups, poll_cursor: cursor(entry.poll_cursor) };
@@ -501,7 +491,7 @@ export function encodeDatabaseAuthority(authority) {
501
491
  object(authority, ["mode", "generation", "writer"]);
502
492
  if (authority.mode !== "single_writer") throw new TypeError("Unsupported authority mode.");
503
493
  const writer = object(authority.writer, ["replica", "incarnation"]);
504
- return { mode: "single_writer", generation: requireDatabaseU64(authority.generation, "authority generation", true).toString(), writer: { replica: requireDatabaseUuid(writer.replica, "writer"), incarnation: hex(writer.incarnation, 16, "incarnation") } };
494
+ return { mode: "single_writer", generation: requireDatabaseU64(authority.generation, "authority generation", true).toString(), writer: { replica: requireDatabaseId(writer.replica, "writer"), incarnation: hex(writer.incarnation, 16, "incarnation") } };
505
495
  }
506
496
 
507
497
  /**
@@ -514,7 +504,7 @@ export function decodeDatabaseAuthority(raw) {
514
504
  const entry = object(raw, ["mode", "generation", "writer"]);
515
505
  if (entry.mode !== "single_writer") throw new TypeError("Unsupported authority mode.");
516
506
  const writer = object(entry.writer, ["replica", "incarnation"]);
517
- return { mode: "single_writer", generation: decimal(entry.generation, "authority generation", true), writer: { replica: requireDatabaseUuid(writer.replica, "writer"), incarnation: hex(writer.incarnation, 16, "incarnation") } };
507
+ return { mode: "single_writer", generation: decimal(entry.generation, "authority generation", true), writer: { replica: requireDatabaseId(writer.replica, "writer"), incarnation: hex(writer.incarnation, 16, "incarnation") } };
518
508
  }
519
509
 
520
510
  /**
@@ -569,7 +559,7 @@ function writeToWire(write, authority) {
569
559
  : (object(write.value, ["kind", "fields"]), { kind: "live", fields: fieldsToWire(write.value.fields, 1, { nodes: 0 }) });
570
560
  if (write.value.kind !== "live" && write.value.kind !== "tombstone") throw new TypeError("Unsupported write kind.");
571
561
  if (value.kind === "live") validateNativeDocument(/** @type {readonly Json[]} */ (value.fields));
572
- return { collection: requireDatabaseUuid(write.collection, "collection"), document: requireDatabaseUuid(write.document, "document"), schema_generation: requireDatabaseU64(write.schema_generation, "schema generation", true).toString(), schema_digest: hex(write.schema_digest, 32, "schema digest"), condition: conditionToWire(write.condition), observed: observedToWire(write.observed), value };
562
+ return { collection: requireDatabaseId(write.collection, "collection"), document: requireDatabaseId(write.document, "document"), schema_generation: requireDatabaseU64(write.schema_generation, "schema generation", true).toString(), schema_digest: hex(write.schema_digest, 32, "schema digest"), condition: conditionToWire(write.condition), observed: observedToWire(write.observed), value };
573
563
  }
574
564
 
575
565
  class Bytes {
@@ -603,7 +593,12 @@ class Bytes {
603
593
  * @param {string} value
604
594
  * @returns {void}
605
595
  */
606
- uuid(value) { this.hex(value.replaceAll("-", "")); }
596
+ id(value) {
597
+ const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
598
+ let bits = 0n;
599
+ for (const char of requireDatabaseId(value)) bits = (bits << 5n) | BigInt(alphabet.indexOf(char));
600
+ this.hex(bits.toString(16).padStart(32, "0"));
601
+ }
607
602
  /** @returns {Uint8Array} */
608
603
  finish() { const bytes = new Uint8Array(this.length); let offset = 0; for (const chunk of this.chunks) { bytes.set(chunk, offset); offset += chunk.length; } return bytes; }
609
604
  }
@@ -667,22 +662,22 @@ function validateNativeDocument(fields) {
667
662
  function effectBytes(scope, authority, writes) {
668
663
  const bytes = new Bytes();
669
664
  bytes.put(encoder.encode("TB-DOC-EFFECT-v1\0"));
670
- bytes.uuid(requireDatabaseUuid(scope.tenant, "tenant")); bytes.uuid(requireDatabaseUuid(scope.database, "database")); bytes.integer(requireDatabaseU64(scope.shard, "shard"), 8);
665
+ bytes.id(requireDatabaseId(scope.tenant, "tenant")); bytes.id(requireDatabaseId(scope.database, "database")); bytes.integer(requireDatabaseU64(scope.shard, "shard"), 8);
671
666
  const singleWriter = authority.mode === "single_writer";
672
667
  bytes.integer(singleWriter ? 1n : 0n, 1);
673
668
  if (singleWriter) {
674
669
  const writer = object(authority.writer, ["replica", "incarnation"]);
675
- bytes.integer(BigInt(/** @type {string} */ (authority.generation)), 8); bytes.uuid(/** @type {string} */ (writer.replica)); bytes.hex(/** @type {string} */ (writer.incarnation));
670
+ bytes.integer(BigInt(/** @type {string} */ (authority.generation)), 8); bytes.id(/** @type {string} */ (writer.replica)); bytes.hex(/** @type {string} */ (writer.incarnation));
676
671
  }
677
672
  bytes.integer(BigInt(writes.length), 8);
678
673
  for (const write of writes) {
679
- bytes.uuid(/** @type {string} */ (write.collection)); bytes.uuid(/** @type {string} */ (write.document)); bytes.integer(BigInt(/** @type {string} */ (write.schema_generation)), 8); bytes.hex(/** @type {string} */ (write.schema_digest));
674
+ bytes.id(/** @type {string} */ (write.collection)); bytes.id(/** @type {string} */ (write.document)); bytes.integer(BigInt(/** @type {string} */ (write.schema_generation)), 8); bytes.hex(/** @type {string} */ (write.schema_digest));
680
675
  const condition = object(write.condition, ["kind"], ["version"]);
681
676
  bytes.integer(condition.kind === "none" ? 0n : condition.kind === "absent" ? 1n : 2n, 1);
682
677
  if (condition.kind === "version") bytes.hex(/** @type {string} */ (condition.version));
683
678
  const observed = array(write.observed);
684
679
  bytes.integer(BigInt(observed.length), 8);
685
- for (const raw of observed) { const entry = object(raw, ["replica", "incarnation", "sequence"]); bytes.uuid(/** @type {string} */ (entry.replica)); bytes.hex(/** @type {string} */ (entry.incarnation)); bytes.integer(BigInt(/** @type {string} */ (entry.sequence)), 8); }
680
+ for (const raw of observed) { const entry = object(raw, ["replica", "incarnation", "sequence"]); bytes.id(/** @type {string} */ (entry.replica)); bytes.hex(/** @type {string} */ (entry.incarnation)); bytes.integer(BigInt(/** @type {string} */ (entry.sequence)), 8); }
686
681
  const value = object(write.value, ["kind"], ["fields"]);
687
682
  bytes.integer(value.kind === "tombstone" ? 0n : 1n, 1);
688
683
  if (value.kind === "live") {
@@ -1,20 +1,20 @@
1
1
  /**
2
- * The current TerraBase document protocol uses canonical UUID entity IDs.
3
- * @typedef {string} CanonicalUuid
2
+ * Entity identities are canonical uppercase ULIDs, including explicit local development.
3
+ * @typedef {import("./ids.js").EntityId} EntityId
4
4
  */
5
5
 
6
6
  /**
7
7
  * All 64-bit quantities stay bigint in JavaScript and decimal strings on the wire.
8
8
  * @typedef {{
9
- * readonly tenant: CanonicalUuid;
10
- * readonly database: CanonicalUuid;
9
+ * readonly tenant: EntityId;
10
+ * readonly database: EntityId;
11
11
  * readonly shard: bigint;
12
12
  * }} DatabaseScope
13
13
  */
14
14
 
15
15
  /**
16
16
  * @typedef {{
17
- * readonly collection: CanonicalUuid;
17
+ * readonly collection: EntityId;
18
18
  * readonly schema_generation: bigint;
19
19
  * readonly schema_digest: string;
20
20
  * }} CollectionBinding
@@ -39,7 +39,7 @@
39
39
 
40
40
  /**
41
41
  * @typedef {{
42
- * readonly replica: CanonicalUuid;
42
+ * readonly replica: EntityId;
43
43
  * readonly incarnation: string;
44
44
  * readonly sequence: bigint;
45
45
  * }} DatabaseObserved
@@ -47,7 +47,7 @@
47
47
 
48
48
  /**
49
49
  * @typedef {DatabaseObserved & {
50
- * readonly database: CanonicalUuid;
50
+ * readonly database: EntityId;
51
51
  * readonly shard: bigint;
52
52
  * }} DatabaseEvent
53
53
  */
@@ -58,7 +58,7 @@
58
58
  * | {
59
59
  * readonly mode: "single_writer";
60
60
  * readonly generation: bigint;
61
- * readonly writer: { readonly replica: CanonicalUuid; readonly incarnation: string };
61
+ * readonly writer: { readonly replica: EntityId; readonly incarnation: string };
62
62
  * }
63
63
  * )} DatabaseAuthority
64
64
  */
@@ -80,7 +80,7 @@
80
80
 
81
81
  /**
82
82
  * @typedef {CollectionBinding & {
83
- * readonly document: CanonicalUuid;
83
+ * readonly document: EntityId;
84
84
  * readonly condition: DatabaseCondition;
85
85
  * readonly observed: readonly DatabaseObserved[];
86
86
  * readonly value: DatabaseWriteValue;
@@ -135,7 +135,7 @@
135
135
  * }} DatabaseQuery
136
136
  */
137
137
 
138
- /** @typedef {{ readonly document: CanonicalUuid; readonly point: DatabasePoint }} DatabaseQueryRow */
138
+ /** @typedef {{ readonly document: EntityId; readonly point: DatabasePoint }} DatabaseQueryRow */
139
139
 
140
140
  /**
141
141
  * @typedef {{
@@ -202,7 +202,7 @@
202
202
  * )} DatabaseSubscriptionRequest
203
203
  */
204
204
 
205
- /** @typedef {{ readonly document: CanonicalUuid; readonly point: DatabasePoint | null }} DatabaseSubscriptionEffect */
205
+ /** @typedef {{ readonly document: EntityId; readonly point: DatabasePoint | null }} DatabaseSubscriptionEffect */
206
206
 
207
207
  /**
208
208
  * @typedef {{
@@ -1,4 +1,4 @@
1
- import { encodeDatabaseSubscription, requireDatabaseUuid, requireDatabaseU64 } from "./database-codec.js";
1
+ import { encodeDatabaseSubscription, requireDatabaseId, requireDatabaseU64 } from "./database-codec.js";
2
2
  /**
3
3
  * @import { TerraBaseClient } from "./local/legacy-client.js"
4
4
  * @import {
@@ -213,14 +213,14 @@ export class DatabaseSubscriptionView {
213
213
  */
214
214
  constructor(scope, options) {
215
215
  const capturedScope = Object.freeze({
216
- tenant: requireDatabaseUuid(scope.tenant, "tenant"),
217
- database: requireDatabaseUuid(scope.database, "database"),
216
+ tenant: requireDatabaseId(scope.tenant, "tenant"),
217
+ database: requireDatabaseId(scope.database, "database"),
218
218
  shard: requireDatabaseU64(scope.shard, "shard"),
219
219
  });
220
220
  const predicate = predicateCopy(options.predicate);
221
221
  const limit = boundedInteger(options.limit, 128, 128, "Snapshot page size");
222
222
  encodeDatabaseSubscription({ phase: "snapshot", predicate, limit });
223
- this.binding = Object.freeze({ scope: capturedScope, collection: requireDatabaseUuid(options.collection, "collection"), predicate, limit });
223
+ this.binding = Object.freeze({ scope: capturedScope, collection: requireDatabaseId(options.collection, "collection"), predicate, limit });
224
224
  this.#maximumRows = boundedInteger(options.maximumRows, 10_000, 1_000_000, "Maximum rows");
225
225
  }
226
226
 
@@ -258,7 +258,7 @@ export class DatabaseSubscriptionView {
258
258
  if (response.rows.length > this.binding.limit) throw new DatabaseSubscriptionError("page_limit_exceeded");
259
259
  let previous = this.#snapshotRows.at(-1)?.document ?? "";
260
260
  const page = response.rows.map(row => {
261
- const document = requireDatabaseUuid(row.document, "document");
261
+ const document = requireDatabaseId(row.document, "document");
262
262
  if (document <= previous) throw new DatabaseSubscriptionError("snapshot_order_invalid");
263
263
  previous = document;
264
264
  return Object.freeze({ document, point: pointCopy(row.point, this.binding.scope) });
@@ -293,7 +293,7 @@ export class DatabaseSubscriptionView {
293
293
  /** @type {Set<string>} */
294
294
  const documents = new Set();
295
295
  for (const effect of group.effects) {
296
- const document = requireDatabaseUuid(effect.document, "document");
296
+ const document = requireDatabaseId(effect.document, "document");
297
297
  if (documents.has(document)) throw new DatabaseSubscriptionError("duplicate_group_document");
298
298
  documents.add(document);
299
299
  if (effect.point === null) rows.delete(document);
package/src/http.js CHANGED
@@ -1,10 +1,7 @@
1
1
  import { resolveAccessToken, resolveTsAuthRuntimeConfig } from "./config.js";
2
2
  import { err, ok, sanitizeDiagnostic } from "./result.js";
3
- import {
4
- regionalIssuerOrigin,
5
- TsAuthEndpointDiscovery,
6
- } from "./ts-auth-discovery.js";
7
3
  import { parseDatabaseJson } from "./database-codec.js";
4
+ import { validateTsAuthIds } from "./ts-auth-ids.js";
8
5
  /** @import { ResolvedTsAuthRuntimeConfig, TsAuthRuntimeConfig, FetchLike } from "./config.js" */
9
6
  /** @import { TerraScaleError, TerraScaleErrorCategory, TerraScaleResult, TerraScaleValidationIssue } from "./result.js" */
10
7
 
@@ -21,7 +18,6 @@ import { parseDatabaseJson } from "./database-codec.js";
21
18
  * readonly formBody?: URLSearchParams | string | undefined;
22
19
  * readonly userinfoAccessToken?: string | undefined;
23
20
  * readonly resourceIntrospectionBasic?: string | undefined;
24
- * readonly issuer?: string | undefined; // Behind the Atlas proxy a regional issuer is called on its own host.
25
21
  * readonly signal?: AbortSignal | undefined;
26
22
  * }} TsAuthRequest
27
23
  */
@@ -39,25 +35,14 @@ export class TsAuthHttp {
39
35
  #fetch;
40
36
  /** @readonly @type {TsAuthRuntimeConfig["accessToken"]} */
41
37
  #accessToken;
42
- /** @readonly @type {TsAuthEndpointDiscovery | undefined} */
43
- #discovery;
44
-
45
- /**
46
- * @param {TsAuthRuntimeConfig} config
47
- * @param {{ readonly authReverseProxy?: boolean }} [options]
48
- */
49
- constructor(config, options) {
38
+ /** @param {TsAuthRuntimeConfig} config */
39
+ constructor(config) {
50
40
  this.config = Object.freeze(resolveTsAuthRuntimeConfig(config));
51
41
  const fetchHandler = config.fetch ?? globalThis.fetch?.bind(globalThis);
52
42
  if (fetchHandler === undefined)
53
43
  throw new TypeError("TsAuthClient requires a fetch implementation.");
54
44
  this.#fetch = fetchHandler;
55
45
  this.#accessToken = config.accessToken;
56
- this.#discovery =
57
- options?.authReverseProxy !== true &&
58
- this.config.apiBaseUrl === "https://auth.terrascale.tech"
59
- ? new TsAuthEndpointDiscovery(fetchHandler)
60
- : undefined;
61
46
  }
62
47
 
63
48
  /**
@@ -89,18 +74,6 @@ export class TsAuthHttp {
89
74
  options.body !== undefined
90
75
  ? JSON.stringify(options.body)
91
76
  : options.formBody?.toString();
92
- const issuerOrigin =
93
- this.#discovery === undefined || options.issuer === undefined
94
- ? undefined
95
- : regionalIssuerOrigin(options.issuer);
96
- const endpoints =
97
- issuerOrigin === undefined
98
- ? await withCancellation(
99
- this.#discovery?.endpoints(signal) ?? Promise.resolve(undefined),
100
- signal,
101
- )
102
- : [issuerOrigin];
103
- let endpointIndex = 0;
104
77
  let attempt = 0;
105
78
  let refreshed = false;
106
79
  /** @type {string | undefined} */
@@ -162,7 +135,7 @@ export class TsAuthHttp {
162
135
  );
163
136
  const request = new Request(
164
137
  buildUrl(
165
- endpoints?.[endpointIndex] ?? this.config.apiBaseUrl,
138
+ this.config.apiBaseUrl,
166
139
  options.path,
167
140
  options.query,
168
141
  ),
@@ -188,16 +161,6 @@ export class TsAuthHttp {
188
161
  signal.throwIfAborted();
189
162
  }
190
163
  responseReceived = true;
191
- if (
192
- options.method === "GET" &&
193
- endpoints !== undefined &&
194
- endpointIndex < endpoints.length - 1 &&
195
- [502, 503, 504].includes(response.status)
196
- ) {
197
- void response.body?.cancel().catch(() => undefined);
198
- endpointIndex += 1;
199
- continue;
200
- }
201
164
  if (
202
165
  response.type === "opaqueredirect" ||
203
166
  (response.status >= 300 && response.status < 400)
@@ -227,6 +190,15 @@ export class TsAuthHttp {
227
190
  response.status,
228
191
  response.headers,
229
192
  );
193
+ try {
194
+ validateTsAuthIds(parsed.value, options.routeName);
195
+ } catch {
196
+ return err(options.routeName, {
197
+ kind: "invalid-response", code: "invalid_response",
198
+ message: "ts-auth returned a noncanonical entity ID.", status: response.status,
199
+ requestId: requestIdFrom(response.headers),
200
+ }, response.status, response.headers);
201
+ }
230
202
  return ok(
231
203
  options.routeName,
232
204
  response.status,
@@ -283,13 +255,6 @@ export class TsAuthHttp {
283
255
  !signal?.aborted &&
284
256
  options.method === "GET"
285
257
  ) {
286
- if (
287
- endpoints !== undefined &&
288
- endpointIndex < endpoints.length - 1
289
- ) {
290
- endpointIndex += 1;
291
- continue;
292
- }
293
258
  if (attempt < this.config.maxRetryAttempts) {
294
259
  await delayForRetry(
295
260
  retryDelay(undefined, this.config.retryDelayMs, attempt),
package/src/ids.js ADDED
@@ -0,0 +1,22 @@
1
+ import { isValid, monotonicFactory } from "ulid";
2
+
3
+ /** A plain 26-character, uppercase Crockford ULID. @typedef {string} EntityId */
4
+
5
+ const nextId = monotonicFactory();
6
+
7
+ /** @returns {EntityId} Monotonic within a millisecond, including clock rollback. */
8
+ export function generateId() {
9
+ return nextId();
10
+ }
11
+
12
+ /** @param {unknown} value @returns {value is EntityId} */
13
+ export function isValidId(value) {
14
+ // The library also accepts lowercase and 130-bit overflow; the public contract does not.
15
+ return typeof value === "string" && isValid(value) && value === value.toUpperCase() && /^[0-7]/u.test(value);
16
+ }
17
+
18
+ /** @param {unknown} value @param {string} [name] @returns {EntityId} */
19
+ export function requireId(value, name = "Entity ID") {
20
+ if (!isValidId(value)) throw new TypeError(`${name} must be a plain 26-character canonical uppercase Crockford ULID.`);
21
+ return value;
22
+ }
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient } from "./v1/terrabase-v1.js";
2
2
  export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
3
- // TerraBase v1: name-based documents, transactions, queries and shapes.
3
+ // Project-scoped TerraBase API; shape support is retained for the historical release.
4
4
  export {
5
5
  createTerraBaseV1Client,
6
6
  TerraBaseV1Client,
@@ -19,7 +19,12 @@ export {
19
19
  isTerraBaseV1Error,
20
20
  } from "./v1/errors.js";
21
21
  export { v1Filter } from "./v1/filter.js";
22
- export { documentIdFor } from "./v1/ids.js";
22
+ export { V1ShapeSubscription } from "./v1/shape-subscription.js";
23
+ /** @typedef {import("./v1/shape-subscription.js").V1ShapeSubscribeOptions} V1ShapeSubscribeOptions */
24
+ /** @typedef {import("./v1/shape-subscription.js").V1ShapeDocument} V1ShapeDocument */
25
+ /** @typedef {import("./v1/shape-reader.js").V1SealedShapePosition} V1SealedShapePosition */
26
+ export { generateId, isValidId, requireId } from "./ids.js";
27
+ /** @typedef {import("./ids.js").EntityId} EntityId */
23
28
  export { ExactDecimal } from "./v1/json.js";
24
29
 
25
30
  /** @typedef {import("./v1/transport.js").TerraBaseV1ClientConfig} TerraBaseV1ClientConfig */
@@ -53,6 +58,11 @@ export { ExactDecimal } from "./v1/json.js";
53
58
  /** @typedef {import("./v1/shapes.js").V1ShapeOp} V1ShapeOp */
54
59
 
55
60
  export { staticBaseUrlResolver } from "./v1/transport.js";
61
+ export { V1Databases } from "./v1/catalog.js";
62
+ export { V1SavedQueries } from "./v1/saved-queries.js";
63
+ /** @typedef {import("./v1/saved-queries.js").V1SavedQuery} V1SavedQuery */
64
+ /** @typedef {import("./v1/saved-queries.js").V1SavedQueryRequest} V1SavedQueryRequest */
65
+ /** @typedef {import("./v1/saved-queries.js").V1SavedQueryPage} V1SavedQueryPage */
56
66
  /** @typedef {import("./v1/transport.js").TerraBaseV1ClientConfig} TerraBaseClientConfig */
57
67
  /** @typedef {import("./v1/transport.js").V1EndpointContext} V1EndpointContext */
58
68
  /** @typedef {import("./v1/transport.js").V1EndpointResolver} V1EndpointResolver */
@@ -60,3 +70,68 @@ export { staticBaseUrlResolver } from "./v1/transport.js";
60
70
  /** @typedef {import("./v1/terrabase-v1.js").V1CollectionPage} V1CollectionPage */
61
71
  /** @typedef {import("./v1/terrabase-v1.js").V1CollectionListOptions} V1CollectionListOptions */
62
72
  /** @typedef {import("./v1/shapes.js").V1ShapePage} V1ShapePage */
73
+ /** @typedef {import("./v1/sql.js").V1SqlRow} V1SqlRow */
74
+ /** @typedef {import("./v1/sql.js").V1SqlResult} V1SqlResult */
75
+ /** @typedef {import("./v1/sql.js").V1SqlIndexSuggestion} V1SqlIndexSuggestion */
76
+ /** @typedef {import("./v1/sql.js").V1SqlCheck} V1SqlCheck */
77
+ /** @typedef {import("./v1/changes.js").V1ChangeStreamRequest} V1ChangeStreamRequest */
78
+ /** @typedef {import("./v1/changes.js").V1DatabaseChange} V1DatabaseChange */
79
+ /** @typedef {import("./v1/changes.js").V1DatabaseChangeCursor} V1DatabaseChangeCursor */
80
+ /** @typedef {import("./v1/catalog.js").V1ManagedDatabase} V1ManagedDatabase */
81
+ /** @typedef {import("./v1/catalog.js").V1DatabaseCreateRequest} V1DatabaseCreateRequest */
82
+ /** @typedef {import("./v1/catalog.js").V1DatabaseUpdateRequest} V1DatabaseUpdateRequest */
83
+ /** @typedef {import("./v1/catalog.js").V1DatabaseBranchRequest} V1DatabaseBranchRequest */
84
+ /** @typedef {import("./v1/catalog.js").V1DatabaseListRequest} V1DatabaseListRequest */
85
+ /** @typedef {import("./v1/catalog.js").V1DatabasePage} V1DatabasePage */
86
+ /** @typedef {import("./v1/catalog.js").V1CollectionSchema} V1CollectionSchema */
87
+
88
+ export { V1DatabaseSchemaClient } from "./v1/database-schema.js";
89
+ /** @typedef {import("./v1/database-schema.js").V1InferredSchema} V1InferredSchema */
90
+ /** @typedef {import("./v1/database-schema.js").V1DatabaseSchema} V1DatabaseSchema */
91
+ /** @typedef {import("./v1/database-schema.js").V1UniqueSchema} V1UniqueSchema */
92
+ /** @typedef {import("./v1/database-schema.js").V1UniqueSchemaRequest} V1UniqueSchemaRequest */
93
+ /** @typedef {import("./v1/database-schema.js").V1QueryShapes} V1QueryShapes */
94
+
95
+ export { V1Indexes } from "./v1/indexes.js";
96
+ /** @typedef {import("./v1/indexes.js").V1Index} V1Index */
97
+ /** @typedef {import("./v1/indexes.js").V1IndexList} V1IndexList */
98
+ /** @typedef {import("./v1/indexes.js").V1AutoIndexPolicy} V1AutoIndexPolicy */
99
+ /** @typedef {import("./v1/indexes.js").V1IndexRecommendations} V1IndexRecommendations */
100
+ /** @typedef {import("./v1/indexes.js").V1AppliedIndexRecommendation} V1AppliedIndexRecommendation */
101
+ /** @typedef {import("./v1/indexes.js").V1CreateIndexRequest} V1CreateIndexRequest */
102
+
103
+ export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
104
+ /** @typedef {import("./v1/usage.js").V1DatabaseUsage} V1DatabaseUsage */
105
+ /** @typedef {import("./v1/usage.js").V1ProjectUsage} V1ProjectUsage */
106
+ /** @typedef {import("./v1/usage.js").V1Analytics} V1Analytics */
107
+ /** @typedef {import("./v1/usage.js").V1Replication} V1Replication */
108
+ /** @typedef {import("./v1/usage.js").V1UsageRequest} V1UsageRequest */
109
+ /** @typedef {import("./v1/usage.js").V1AnalyticsRequest} V1AnalyticsRequest */
110
+
111
+ export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
112
+ /** @typedef {import("./v1/backups.js").V1Backup} V1Backup */
113
+ /** @typedef {import("./v1/backups.js").V1BackupPage} V1BackupPage */
114
+ /** @typedef {import("./v1/backups.js").V1BackupPolicy} V1BackupPolicy */
115
+ /** @typedef {import("./v1/backups.js").V1RestoreWindow} V1RestoreWindow */
116
+ /** @typedef {import("./v1/backups.js").V1PointInTimePlan} V1PointInTimePlan */
117
+ /** @typedef {import("./v1/backups.js").V1RestoredDatabase} V1RestoredDatabase */
118
+ /** @typedef {import("./v1/backups.js").V1BackupDestination} V1BackupDestination */
119
+ /** @typedef {import("./v1/backups.js").V1BackupPolicyRequest} V1BackupPolicyRequest */
120
+ /** @typedef {import("./v1/backups.js").V1PointInTimeRequest} V1PointInTimeRequest */
121
+ /** @typedef {import("./v1/backups.js").V1PointInTimeRestoreRequest} V1PointInTimeRestoreRequest */
122
+ /** @typedef {import("./v1/backups.js").V1BackupDestinationRequest} V1BackupDestinationRequest */
123
+
124
+ export { V1Imports } from "./v1/imports.js";
125
+ /** @typedef {import("./v1/imports.js").V1ImportJob} V1ImportJob */
126
+ /** @typedef {import("./v1/imports.js").V1ImportOptions} V1ImportOptions */
127
+ /** @typedef {import("./v1/imports.js").V1CreateImportRequest} V1CreateImportRequest */
128
+
129
+ export { createTerraBaseSystemClient, V1Health, V1Platform } from "./v1/system.js";
130
+ /** @typedef {import("./v1/system.js").TerraBaseSystemClientConfig} TerraBaseSystemClientConfig */
131
+ /** @typedef {import("./v1/system.js").V1ProbeStatus} V1ProbeStatus */
132
+ /** @typedef {import("./v1/system.js").V1ReplicationHealth} V1ReplicationHealth */
133
+ /** @typedef {import("./v1/system.js").V1PlatformDatabases} V1PlatformDatabases */
134
+ /** @typedef {import("./v1/shape-manifest.js").V1ShapeManifest} V1ShapeManifest */
135
+ /** @typedef {import("./v1/shape-manifest.js").V1ShapeManifestRequest} V1ShapeManifestRequest */
136
+ /** @typedef {import("./v1/shape-reader.js").V1ShapeKeyRequest} V1ShapeKeyRequest */
137
+ /** @typedef {import("./v1/shape-reader.js").V1ShapeKey} V1ShapeKey */
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { generateId, requireId } from "../ids.js";
2
2
  import { createAdapterFactory } from "@better-auth/core/db/adapter";
3
3
  /** @import { CleanedWhere, CustomAdapter, DBAdapter, JoinConfig } from "@better-auth/core/db/adapter" */
4
4
  /** @import { BetterAuthOptions } from "@better-auth/core" */
@@ -29,6 +29,8 @@ export function createLocalBetterAuthAdapter(store) {
29
29
  * @returns {DBAdapter<BetterAuthOptions>}
30
30
  */
31
31
  function make(options, scoped) {
32
+ if (options.advanced?.database?.generateId === "uuid")
33
+ throw new TypeError("Better Auth entity IDs must be ULIDs; UUID generation is unavailable.");
32
34
  /** @type {ReturnType<typeof createAdapterFactory<BetterAuthOptions>>} */
33
35
  const factory = createAdapterFactory({
34
36
  config: {
@@ -38,7 +40,7 @@ export function createLocalBetterAuthAdapter(store) {
38
40
  supportsArrays: true,
39
41
  supportsBooleans: true,
40
42
  supportsNumericIds: false,
41
- supportsUUIDs: true,
43
+ customIdGenerator: generateId,
42
44
  transaction: async (callback) => {
43
45
  if (scoped)
44
46
  throw new Error("Nested local auth transactions are unavailable.");
@@ -65,7 +67,8 @@ function customAdapter(store, config, scoped) {
65
67
  * @param {string} model
66
68
  * @returns {Row[]}
67
69
  */
68
- const rows = (model) => store.list("local-auth", model);
70
+ const rows = (model) => store.listEntries("local-auth", model)
71
+ .map(entry => ({ .../** @type {Row} */ (entry.value), id: requireId(entry.id) }));
69
72
  /**
70
73
  * @param {string} model
71
74
  * @param {readonly CleanedWhere[]} clauses
@@ -150,13 +153,21 @@ function customAdapter(store, config, scoped) {
150
153
  */
151
154
  const save = (model, input, creating = false) => {
152
155
  const row = jsonRow(input);
153
- const id =
154
- typeof row.id === "string" && row.id.length > 0 ? row.id : randomUUID();
156
+ const id = row.id === undefined ? generateId() : requireId(row.id);
157
+ for (const [logical, table] of Object.entries(config.schema)) {
158
+ if (config.getModelName(logical) !== model) continue;
159
+ for (const [field, schema] of Object.entries(table.fields)) {
160
+ if (schema.references?.field !== "id") continue;
161
+ const value = row[config.getFieldName({ model: logical, field })];
162
+ if (value !== undefined && value !== null) requireId(value, field);
163
+ }
164
+ }
155
165
  const candidate = { ...row, id };
156
166
  if (creating && store.get("local-auth", model, id) !== undefined)
157
167
  throw new LocalAuthConstraintError();
158
168
  ensureUnique(model, candidate);
159
- store.put("local-auth", model, id, candidate);
169
+ delete row.id;
170
+ store.put("local-auth", model, id, row);
160
171
  return candidate;
161
172
  };
162
173
  /**
@@ -14,7 +14,6 @@ export const localSqlitePresenceIndicators = /** @type {const} */ ([
14
14
  "NETLIFY",
15
15
  "CF_PAGES",
16
16
  "RENDER",
17
- "FLY_APP_NAME",
18
17
  "K_SERVICE",
19
18
  "CLOUD_RUN_JOB",
20
19
  "KUBERNETES_SERVICE_HOST",
@@ -1,5 +1,5 @@
1
+ import { generateId } from "../ids.js";
1
2
  import { isAbsolute } from "node:path";
2
- import { randomUUID } from "node:crypto";
3
3
  import { createTerraBaseClient } from "./legacy-client.js";
4
4
  import { normalizeLocalBearerToken } from "./authentication.js";
5
5
  import { assertLocalSqliteEnvironment } from "./environment.js";
@@ -86,7 +86,7 @@ export class TerraScaleSqliteRuntime {
86
86
  const scope =
87
87
  options.scope ??
88
88
  (stored === undefined
89
- ? { tenant: randomUUID(), database: randomUUID(), shard: 0n }
89
+ ? { tenant: generateId(), database: generateId(), shard: 0n }
90
90
  : { ...stored, shard: BigInt(stored.shard) });
91
91
  this.#router = new TerraScaleLocalRouter(this.#store, scope, {
92
92
  authentication,
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  decodeDatabaseCommitReceipt, decodeDatabasePoint, decodeDatabaseQueryPage,
4
4
  decodeDatabaseSubscription, decodeDatabaseStatus, encodeDatabaseStatus, encodeDatabaseCommit, encodeDatabaseQuery,
5
- encodeDatabaseSubscription, encodeDatabaseAuthority, parseDatabaseJson, requireDatabaseU64, requireDatabaseUuid,
5
+ encodeDatabaseSubscription, encodeDatabaseAuthority, parseDatabaseJson, requireDatabaseU64, requireDatabaseId,
6
6
  } from "../database-codec.js";
7
7
  /**
8
8
  * @import {
@@ -61,7 +61,7 @@ function identityAuthority(authority) {
61
61
  * @returns {string}
62
62
  */
63
63
  function scopePath(scope) {
64
- return `/v1/tenants/${requireDatabaseUuid(scope.tenant, "tenant")}/databases/${requireDatabaseUuid(scope.database, "database")}/shards/${requireDatabaseU64(scope.shard, "shard")}`;
64
+ return `/v1/tenants/${requireDatabaseId(scope.tenant, "tenant")}/databases/${requireDatabaseId(scope.database, "database")}/shards/${requireDatabaseU64(scope.shard, "shard")}`;
65
65
  }
66
66
 
67
67
  /**
@@ -195,7 +195,7 @@ export class TerraBaseClient {
195
195
 
196
196
  /** @param {TerraBaseClientConfig} config */
197
197
  constructor(config) {
198
- this.#scope = Object.freeze({ tenant: requireDatabaseUuid(config.tenant, "tenant"), database: requireDatabaseUuid(config.database, "database"), shard: requireDatabaseU64(config.shard, "shard") });
198
+ this.#scope = Object.freeze({ tenant: requireDatabaseId(config.tenant, "tenant"), database: requireDatabaseId(config.database, "database"), shard: requireDatabaseU64(config.shard, "shard") });
199
199
  this.#config = { ...config };
200
200
  this.#fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
201
201
  this.#path = scopePath(this.#scope);
@@ -214,7 +214,7 @@ export class TerraBaseClient {
214
214
  * @returns {Promise<DatabaseResult<DatabasePoint>>}
215
215
  */
216
216
  read(collection, document, options = {}) {
217
- const path = `${this.#collectionPath(collection)}/documents/${requireDatabaseUuid(document, "document")}`;
217
+ const path = `${this.#collectionPath(collection)}/documents/${requireDatabaseId(document, "document")}`;
218
218
  return this.#request(path, "GET", undefined, raw => this.#scopedPoint(decodeDatabasePoint(raw)), options);
219
219
  }
220
220
 
@@ -312,7 +312,7 @@ export class TerraBaseClient {
312
312
  * @param {string} collection
313
313
  * @returns {string}
314
314
  */
315
- #collectionPath(collection) { return `${this.#path}/collections/${requireDatabaseUuid(collection, "collection")}`; }
315
+ #collectionPath(collection) { return `${this.#path}/collections/${requireDatabaseId(collection, "collection")}`; }
316
316
 
317
317
  /**
318
318
  * @param {DatabasePoint} point