@terrascale/sdk 0.0.1 → 0.0.3

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 (43) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +46 -17
  3. package/package.json +5 -2
  4. package/sdk-v1-route-manifest.json +7593 -1175
  5. package/src/database-codec.js +21 -26
  6. package/src/database-types.js +11 -11
  7. package/src/database-view.js +6 -6
  8. package/src/http.js +26 -5
  9. package/src/ids.js +22 -0
  10. package/src/index.js +2 -1
  11. package/src/local/better-auth.js +17 -6
  12. package/src/local/index.js +2 -2
  13. package/src/local/legacy-client.js +5 -5
  14. package/src/local/router.js +12 -12
  15. package/src/local/store.js +29 -28
  16. package/src/local/test-environment.js +3 -2
  17. package/src/postgres.js +5 -4
  18. package/src/react/core.js +2 -2
  19. package/src/ts-auth-discovery.js +17 -0
  20. package/src/ts-auth-ids.js +122 -0
  21. package/src/ts-auth.js +75 -72
  22. package/src/v1/decode.js +30 -10
  23. package/src/v1/ids.js +10 -62
  24. package/src/v1/routes.js +5 -5
  25. package/src/v1/shapes.js +6 -7
  26. package/src/v1/terrabase-v1.js +24 -16
  27. package/src/v1/transport.js +6 -4
  28. package/types/database-codec.d.ts +1 -6
  29. package/types/database-types.d.ts +11 -11
  30. package/types/http.d.ts +1 -0
  31. package/types/ids.d.ts +10 -0
  32. package/types/index.d.ts +2 -1
  33. package/types/local/store.d.ts +10 -0
  34. package/types/local/test-environment.d.ts +2 -2
  35. package/types/postgres.d.ts +4 -4
  36. package/types/ts-auth-discovery.d.ts +8 -0
  37. package/types/ts-auth-ids.d.ts +14 -0
  38. package/types/ts-auth.d.ts +99 -99
  39. package/types/v1/decode.d.ts +27 -11
  40. package/types/v1/ids.d.ts +5 -17
  41. package/types/v1/shapes.d.ts +2 -2
  42. package/types/v1/terrabase-v1.d.ts +3 -3
  43. package/types/v1/transport.d.ts +2 -2
@@ -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,7 +1,11 @@
1
1
  import { resolveAccessToken, resolveTsAuthRuntimeConfig } from "./config.js";
2
2
  import { err, ok, sanitizeDiagnostic } from "./result.js";
3
- import { TsAuthEndpointDiscovery } from "./ts-auth-discovery.js";
3
+ import {
4
+ regionalIssuerOrigin,
5
+ TsAuthEndpointDiscovery,
6
+ } from "./ts-auth-discovery.js";
4
7
  import { parseDatabaseJson } from "./database-codec.js";
8
+ import { validateTsAuthIds } from "./ts-auth-ids.js";
5
9
  /** @import { ResolvedTsAuthRuntimeConfig, TsAuthRuntimeConfig, FetchLike } from "./config.js" */
6
10
  /** @import { TerraScaleError, TerraScaleErrorCategory, TerraScaleResult, TerraScaleValidationIssue } from "./result.js" */
7
11
 
@@ -18,6 +22,7 @@ import { parseDatabaseJson } from "./database-codec.js";
18
22
  * readonly formBody?: URLSearchParams | string | undefined;
19
23
  * readonly userinfoAccessToken?: string | undefined;
20
24
  * readonly resourceIntrospectionBasic?: string | undefined;
25
+ * readonly issuer?: string | undefined; // Behind the Atlas proxy a regional issuer is called on its own host.
21
26
  * readonly signal?: AbortSignal | undefined;
22
27
  * }} TsAuthRequest
23
28
  */
@@ -85,10 +90,17 @@ export class TsAuthHttp {
85
90
  options.body !== undefined
86
91
  ? JSON.stringify(options.body)
87
92
  : options.formBody?.toString();
88
- const endpoints = await withCancellation(
89
- this.#discovery?.endpoints(signal) ?? Promise.resolve(undefined),
90
- signal,
91
- );
93
+ const issuerOrigin =
94
+ this.#discovery === undefined || options.issuer === undefined
95
+ ? undefined
96
+ : regionalIssuerOrigin(options.issuer);
97
+ const endpoints =
98
+ issuerOrigin === undefined
99
+ ? await withCancellation(
100
+ this.#discovery?.endpoints(signal) ?? Promise.resolve(undefined),
101
+ signal,
102
+ )
103
+ : [issuerOrigin];
92
104
  let endpointIndex = 0;
93
105
  let attempt = 0;
94
106
  let refreshed = false;
@@ -216,6 +228,15 @@ export class TsAuthHttp {
216
228
  response.status,
217
229
  response.headers,
218
230
  );
231
+ try {
232
+ validateTsAuthIds(parsed.value, options.routeName);
233
+ } catch {
234
+ return err(options.routeName, {
235
+ kind: "invalid-response", code: "invalid_response",
236
+ message: "ts-auth returned a noncanonical entity ID.", status: response.status,
237
+ requestId: requestIdFrom(response.headers),
238
+ }, response.status, response.headers);
239
+ }
219
240
  return ok(
220
241
  options.routeName,
221
242
  response.status,
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
@@ -19,7 +19,8 @@ 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 { generateId, isValidId, requireId } from "./ids.js";
23
+ /** @typedef {import("./ids.js").EntityId} EntityId */
23
24
  export { ExactDecimal } from "./v1/json.js";
24
25
 
25
26
  /** @typedef {import("./v1/transport.js").TerraBaseV1ClientConfig} TerraBaseV1ClientConfig */
@@ -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
  /**
@@ -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
@@ -1,8 +1,8 @@
1
+ import { generateId } from "../ids.js";
1
2
  import {
2
3
  createHash,
3
4
  createHmac,
4
5
  randomBytes,
5
- randomUUID,
6
6
  timingSafeEqual,
7
7
  } from "node:crypto";
8
8
  import {
@@ -18,7 +18,7 @@ import {
18
18
  encodeDatabaseValue,
19
19
  parseDatabaseJson,
20
20
  requireDatabaseU64,
21
- requireDatabaseUuid,
21
+ requireDatabaseId,
22
22
  } from "../database-codec.js";
23
23
  import { matchesLocalBearerAuthorization } from "./authentication.js";
24
24
  import { jsonResponse, problemResponse } from "./context.js";
@@ -101,8 +101,8 @@ export class TerraScaleLocalRouter {
101
101
  constructor(store, scope, options) {
102
102
  this.#store = store;
103
103
  this.#scope = Object.freeze({
104
- tenant: requireDatabaseUuid(scope.tenant),
105
- database: requireDatabaseUuid(scope.database),
104
+ tenant: requireDatabaseId(scope.tenant),
105
+ database: requireDatabaseId(scope.database),
106
106
  shard: requireDatabaseU64(scope.shard),
107
107
  });
108
108
  this.#scopeKey = `${scope.tenant}/${scope.database}/${scope.shard}`;
@@ -112,7 +112,7 @@ export class TerraScaleLocalRouter {
112
112
  const writer = /** @type {{ readonly replica: string; readonly incarnation: string } | undefined} */ (
113
113
  store.getMeta(`native-writer:${this.#scopeKey}`)
114
114
  ) ?? {
115
- replica: randomUUID(),
115
+ replica: generateId(),
116
116
  incarnation: randomBytes(16).toString("hex"),
117
117
  };
118
118
  this.#writer = Object.freeze(writer);
@@ -131,7 +131,7 @@ export class TerraScaleLocalRouter {
131
131
  * @returns {void}
132
132
  */
133
133
  bindCollection(binding) {
134
- requireDatabaseUuid(binding.collection);
134
+ requireDatabaseId(binding.collection);
135
135
  requireDatabaseU64(binding.schema_generation, "schema generation", true);
136
136
  if (!/^[0-9a-f]{64}$/u.test(binding.schema_digest))
137
137
  throw new TypeError("Schema digest must be lowercase SHA256.");
@@ -170,8 +170,8 @@ export class TerraScaleLocalRouter {
170
170
  suffix,
171
171
  );
172
172
  if (match === null) return problemResponse(404, "unknown_route");
173
- const collection = requireDatabaseUuid(match[1]);
174
- const document = requireDatabaseUuid(match[2]);
173
+ const collection = requireDatabaseId(match[1]);
174
+ const document = requireDatabaseId(match[2]);
175
175
  return await this.#store.exclusive(() => {
176
176
  this.#binding(collection);
177
177
  return jsonResponse(this.#point(collection, document));
@@ -246,7 +246,7 @@ export class TerraScaleLocalRouter {
246
246
  suffix,
247
247
  );
248
248
  if (match === null) return problemResponse(404, "unknown_route");
249
- const collection = requireDatabaseUuid(match[1]);
249
+ const collection = requireDatabaseId(match[1]);
250
250
  return await this.#store.exclusive(() => {
251
251
  this.#binding(collection);
252
252
  return this.#query(collection, match[2] === "subscriptions", raw);
@@ -599,7 +599,7 @@ export class TerraScaleLocalRouter {
599
599
  let id;
600
600
  let offset = 0;
601
601
  if (cursorValue === undefined) {
602
- id = randomUUID();
602
+ id = generateId();
603
603
  const rows = /** @type {readonly { readonly scope: string; readonly id: string; readonly value: WirePoint }[]} */ (
604
604
  this.#store.listNamespace("native-points")
605
605
  )
@@ -812,10 +812,10 @@ function decodeCommit(raw) {
812
812
  if (!Array.isArray(write.observed))
813
813
  throw new TypeError("Invalid observed.");
814
814
  return {
815
- collection: requireDatabaseUuid(write.collection),
815
+ collection: requireDatabaseId(write.collection),
816
816
  schema_generation: decimal(write.schema_generation),
817
817
  schema_digest: text(write.schema_digest),
818
- document: requireDatabaseUuid(write.document),
818
+ document: requireDatabaseId(write.document),
819
819
  condition: /** @type {DatabaseCondition} */ (parsedCondition),
820
820
  observed: write.observed.map((rawObserved) => {
821
821
  const observed = object(rawObserved);