terrascale 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +143 -0
  3. package/package.json +159 -0
  4. package/sdk-current-contract.json +27 -0
  5. package/sdk-route-manifest.json +67 -0
  6. package/src/admin.js +9 -0
  7. package/src/better-auth.js +14 -0
  8. package/src/config.js +121 -0
  9. package/src/database-codec.js +845 -0
  10. package/src/database-types.js +237 -0
  11. package/src/database-view.js +422 -0
  12. package/src/database.js +420 -0
  13. package/src/discovery.js +374 -0
  14. package/src/http.js +887 -0
  15. package/src/index.js +79 -0
  16. package/src/local/authentication.js +47 -0
  17. package/src/local/better-auth.js +517 -0
  18. package/src/local/cli.js +51 -0
  19. package/src/local/context.js +23 -0
  20. package/src/local/environment.js +109 -0
  21. package/src/local/index.js +204 -0
  22. package/src/local/router.js +999 -0
  23. package/src/local/server.js +664 -0
  24. package/src/local/store.js +530 -0
  25. package/src/local/test-environment.js +74 -0
  26. package/src/management-contracts.js +72 -0
  27. package/src/management.js +12 -0
  28. package/src/native-origin.js +75 -0
  29. package/src/postgres.js +494 -0
  30. package/src/react/core.js +743 -0
  31. package/src/react/index.js +99 -0
  32. package/src/result.js +251 -0
  33. package/src/schema.js +366 -0
  34. package/src/sql.js +996 -0
  35. package/src/svelte/index.js +129 -0
  36. package/src/tanstack/index.js +511 -0
  37. package/src/ts-auth-discovery.js +190 -0
  38. package/src/ts-auth.js +3497 -0
  39. package/types/admin.d.ts +6 -0
  40. package/types/better-auth.d.ts +8 -0
  41. package/types/config.d.ts +58 -0
  42. package/types/database-codec.d.ts +111 -0
  43. package/types/database-types.d.ts +213 -0
  44. package/types/database-view.d.ts +183 -0
  45. package/types/database.d.ts +98 -0
  46. package/types/discovery.d.ts +114 -0
  47. package/types/http.d.ts +46 -0
  48. package/types/index.d.ts +52 -0
  49. package/types/local/authentication.d.ts +11 -0
  50. package/types/local/better-auth.d.ts +33 -0
  51. package/types/local/cli.d.ts +2 -0
  52. package/types/local/context.d.ts +14 -0
  53. package/types/local/environment.d.ts +23 -0
  54. package/types/local/index.d.ts +94 -0
  55. package/types/local/router.d.ts +66 -0
  56. package/types/local/server.d.ts +54 -0
  57. package/types/local/store.d.ts +106 -0
  58. package/types/local/test-environment.d.ts +25 -0
  59. package/types/management-contracts.d.ts +44 -0
  60. package/types/management.d.ts +6 -0
  61. package/types/native-origin.d.ts +23 -0
  62. package/types/postgres.d.ts +123 -0
  63. package/types/react/core.d.ts +366 -0
  64. package/types/react/index.d.ts +54 -0
  65. package/types/result.d.ts +161 -0
  66. package/types/schema.d.ts +145 -0
  67. package/types/sql.d.ts +288 -0
  68. package/types/svelte/index.d.ts +81 -0
  69. package/types/tanstack/index.d.ts +165 -0
  70. package/types/ts-auth-discovery.d.ts +11 -0
  71. package/types/ts-auth.d.ts +1826 -0
package/src/index.js ADDED
@@ -0,0 +1,79 @@
1
+ export { createTerraBaseClient, TerraBaseClient } from "./database.js";
2
+ export {
3
+ terraBaseDiscoveryOrigin,
4
+ parseTerraBaseRoutingDocument,
5
+ TerraBaseDiscoveryError,
6
+ TerraBaseDiscoveryRouter,
7
+ } from "./discovery.js";
8
+ export * from "./schema.js";
9
+ export * from "./sql.js";
10
+ export {
11
+ terraScaleReleaseBlockers,
12
+ TerraScaleCapabilityUnavailableError,
13
+ } from "./management-contracts.js";
14
+ export {
15
+ createDatabaseQuery,
16
+ DatabaseSubscriptionView,
17
+ DatabaseSubscriptionError,
18
+ } from "./database-view.js";
19
+
20
+ /** @typedef {import("./database.js").TerraBaseClientConfig} TerraBaseClientConfig */
21
+ /** @typedef {import("./database.js").PreparedDatabaseCommit} PreparedDatabaseCommit */
22
+ /** @typedef {import("./database.js").DatabaseRequestOptions} DatabaseRequestOptions */
23
+
24
+ /** @typedef {import("./database-types.js").CanonicalUuid} CanonicalUuid */
25
+ /** @typedef {import("./database-types.js").DatabaseScope} DatabaseScope */
26
+ /** @typedef {import("./database-types.js").CollectionBinding} CollectionBinding */
27
+ /** @typedef {import("./database-types.js").DatabaseValue} DatabaseValue */
28
+ /** @typedef {import("./database-types.js").DatabaseField} DatabaseField */
29
+ /** @typedef {import("./database-types.js").DatabaseObserved} DatabaseObserved */
30
+ /** @typedef {import("./database-types.js").DatabaseEvent} DatabaseEvent */
31
+ /** @typedef {import("./database-types.js").DatabaseAuthority} DatabaseAuthority */
32
+ /** @typedef {import("./database-types.js").DatabaseCondition} DatabaseCondition */
33
+ /** @typedef {import("./database-types.js").DatabaseWriteValue} DatabaseWriteValue */
34
+ /** @typedef {import("./database-types.js").DatabaseWrite} DatabaseWrite */
35
+ /** @typedef {import("./database-types.js").DatabaseTransactionId} DatabaseTransactionId */
36
+ /** @typedef {import("./database-types.js").DatabaseCommit} DatabaseCommit */
37
+ /** @typedef {import("./database-types.js").DatabaseSibling} DatabaseSibling */
38
+ /** @typedef {import("./database-types.js").DatabasePoint} DatabasePoint */
39
+ /** @typedef {import("./database-types.js").DatabaseQueryValue} DatabaseQueryValue */
40
+ /** @typedef {import("./database-types.js").DatabaseQueryPredicate} DatabaseQueryPredicate */
41
+ /** @typedef {import("./database-types.js").DatabaseQuery} DatabaseQuery */
42
+ /** @typedef {import("./database-types.js").DatabaseQueryRow} DatabaseQueryRow */
43
+ /** @typedef {import("./database-types.js").DatabaseQueryPage} DatabaseQueryPage */
44
+ /** @typedef {import("./database-types.js").DatabaseCommitReceipt} DatabaseCommitReceipt */
45
+ /** @typedef {import("./database-types.js").DatabaseTransactionIdentity} DatabaseTransactionIdentity */
46
+ /** @typedef {import("./database-types.js").DatabaseTransactionStatus} DatabaseTransactionStatus */
47
+ /**
48
+ * @template T
49
+ * @typedef {import("./database-types.js").DatabaseResult<T>} DatabaseResult
50
+ */
51
+ /** @typedef {import("./database-types.js").DatabaseSubscriptionRequest} DatabaseSubscriptionRequest */
52
+ /** @typedef {import("./database-types.js").DatabaseSubscriptionEffect} DatabaseSubscriptionEffect */
53
+ /** @typedef {import("./database-types.js").DatabaseSubscriptionGroup} DatabaseSubscriptionGroup */
54
+ /** @typedef {import("./database-types.js").DatabaseSubscriptionResponse} DatabaseSubscriptionResponse */
55
+
56
+ /** @typedef {import("./discovery.js").TerraBaseRoutingCapability} TerraBaseRoutingCapability */
57
+ /** @typedef {import("./discovery.js").TerraBaseRoutingCapabilities} TerraBaseRoutingCapabilities */
58
+ /** @typedef {import("./discovery.js").TerraBaseRoutingEndpoint} TerraBaseRoutingEndpoint */
59
+ /** @typedef {import("./discovery.js").TerraBaseRoutingDocument} TerraBaseRoutingDocument */
60
+
61
+ /** @typedef {import("./management-contracts.js").TerraScaleBlockedCapability} TerraScaleBlockedCapability */
62
+
63
+ /**
64
+ * @template TValue
65
+ * @typedef {import("./result.js").TerraScaleResult<TValue>} TerraScaleResult
66
+ */
67
+ /** @typedef {import("./result.js").TerraScaleError} TerraScaleError */
68
+ /** @typedef {import("./result.js").TerraScaleFailure} TerraScaleFailure */
69
+ /**
70
+ * @template TValue
71
+ * @typedef {import("./result.js").TerraScaleSuccess<TValue>} TerraScaleSuccess
72
+ */
73
+
74
+ /** @typedef {import("./database-view.js").DatabaseQueryOptions} DatabaseQueryOptions */
75
+ /** @typedef {import("./database-view.js").DatabaseViewBinding} DatabaseViewBinding */
76
+ /** @typedef {import("./database-view.js").DatabaseViewPosition} DatabaseViewPosition */
77
+ /** @typedef {import("./database-view.js").DatabaseViewSnapshot} DatabaseViewSnapshot */
78
+ /** @typedef {import("./database-view.js").DatabaseQueryCallbacks} DatabaseQueryCallbacks */
79
+ /** @typedef {import("./database-view.js").DatabaseQuerySource} DatabaseQuerySource */
@@ -0,0 +1,47 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+
3
+ const MINIMUM_BEARER_TOKEN_LENGTH = 32;
4
+ const MAXIMUM_BEARER_TOKEN_LENGTH = 512;
5
+ const BEARER_TOKEN_PATTERN = /^[A-Za-z0-9._~+\/-]+=*$/u;
6
+
7
+ /**
8
+ * @param {unknown} value
9
+ * @returns {string}
10
+ */
11
+ export function normalizeLocalBearerToken(value) {
12
+ if (typeof value !== "string") {
13
+ throw new TypeError('bearerToken is required when authentication is "required".');
14
+ }
15
+ if (
16
+ value.length < MINIMUM_BEARER_TOKEN_LENGTH
17
+ || value.length > MAXIMUM_BEARER_TOKEN_LENGTH
18
+ || !BEARER_TOKEN_PATTERN.test(value)
19
+ ) {
20
+ throw new TypeError(
21
+ `bearerToken must contain between ${MINIMUM_BEARER_TOKEN_LENGTH} and ${MAXIMUM_BEARER_TOKEN_LENGTH} ASCII bearer-token characters.`,
22
+ );
23
+ }
24
+ return value;
25
+ }
26
+
27
+ /**
28
+ * @param {string | null} authorization
29
+ * @param {string} expectedToken
30
+ * @returns {boolean}
31
+ */
32
+ export function matchesLocalBearerAuthorization(authorization, expectedToken) {
33
+ if (authorization === null) {
34
+ return false;
35
+ }
36
+ const match = /^Bearer ([A-Za-z0-9._~+\/-]+=*)$/iu.exec(authorization.trim());
37
+ if (match === null) {
38
+ return false;
39
+ }
40
+ const candidateToken = match[1];
41
+ if (candidateToken === undefined) {
42
+ return false;
43
+ }
44
+ const expectedDigest = createHash("sha256").update(expectedToken).digest();
45
+ const candidateDigest = createHash("sha256").update(candidateToken).digest();
46
+ return timingSafeEqual(expectedDigest, candidateDigest);
47
+ }
@@ -0,0 +1,517 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createAdapterFactory } from "@better-auth/core/db/adapter";
3
+ /** @import { CleanedWhere, CustomAdapter, DBAdapter, JoinConfig } from "@better-auth/core/db/adapter" */
4
+ /** @import { BetterAuthOptions } from "@better-auth/core" */
5
+ /** @import { TerraScaleSqliteStore } from "./store.js" */
6
+
7
+ /** @typedef {Record<string, unknown>} Row */
8
+ /**
9
+ * @typedef {Parameters<
10
+ * NonNullable<Parameters<typeof createAdapterFactory>[0]["adapter"]>
11
+ * >[0]} FactoryConfig
12
+ */
13
+ /**
14
+ * @typedef {(
15
+ * options: BetterAuthOptions,
16
+ * ) => DBAdapter<BetterAuthOptions>} LocalBetterAuthAdapterFactory
17
+ */
18
+
19
+ /**
20
+ * Better Auth 1.7.2 over explicitly enabled development SQLite only.
21
+ *
22
+ * @param {TerraScaleSqliteStore} store
23
+ * @returns {LocalBetterAuthAdapterFactory}
24
+ */
25
+ export function createLocalBetterAuthAdapter(store) {
26
+ /**
27
+ * @param {BetterAuthOptions} options
28
+ * @param {boolean} scoped
29
+ * @returns {DBAdapter<BetterAuthOptions>}
30
+ */
31
+ function make(options, scoped) {
32
+ /** @type {ReturnType<typeof createAdapterFactory<BetterAuthOptions>>} */
33
+ const factory = createAdapterFactory({
34
+ config: {
35
+ adapterId: "terrascale-local-sqlite",
36
+ supportsDates: false,
37
+ supportsJSON: true,
38
+ supportsArrays: true,
39
+ supportsBooleans: true,
40
+ supportsNumericIds: false,
41
+ supportsUUIDs: true,
42
+ transaction: async (callback) => {
43
+ if (scoped)
44
+ throw new Error("Nested local auth transactions are unavailable.");
45
+ return store.transactionAsync(async () =>
46
+ serializedCallback(make(options, true), callback),
47
+ );
48
+ },
49
+ },
50
+ adapter: (config) => customAdapter(store, config, scoped),
51
+ });
52
+ return factory(options);
53
+ }
54
+ return (options) => make(options, false);
55
+ }
56
+
57
+ /**
58
+ * @param {TerraScaleSqliteStore} store
59
+ * @param {FactoryConfig} config
60
+ * @param {boolean} scoped
61
+ * @returns {CustomAdapter}
62
+ */
63
+ function customAdapter(store, config, scoped) {
64
+ /**
65
+ * @param {string} model
66
+ * @returns {Row[]}
67
+ */
68
+ const rows = (model) => store.list("local-auth", model);
69
+ /**
70
+ * @param {string} model
71
+ * @param {readonly CleanedWhere[]} clauses
72
+ * @returns {readonly CleanedWhere[]}
73
+ */
74
+ const where = (model, clauses) =>
75
+ clauses.map((clause) => ({
76
+ ...clause,
77
+ field: config.getFieldName({ model, field: clause.field }),
78
+ }));
79
+ /**
80
+ * @param {string} model
81
+ * @param {readonly CleanedWhere[]} [clauses]
82
+ * @returns {Row[]}
83
+ */
84
+ const find = (model, clauses = []) =>
85
+ rows(model).filter((row) => matches(row, where(model, clauses)));
86
+ /**
87
+ * @param {string} model
88
+ * @returns {readonly (readonly string[])[]}
89
+ */
90
+ const indexes = (model) =>
91
+ /** @type {readonly (readonly string[])[] | undefined} */ (
92
+ store.getMeta(`local-auth-indexes:${model}`)
93
+ ) ?? [["id"]];
94
+ /** @returns {void} */
95
+ const installSchema = () => {
96
+ for (const [model, table] of Object.entries(config.schema)) {
97
+ const fields = [
98
+ ["id"],
99
+ ...Object.entries(table.fields)
100
+ .filter(([, field]) => field.unique === true)
101
+ .map(([field]) => [config.getFieldName({ model, field })]),
102
+ ...(table.indexes ?? [])
103
+ .filter((index) => index.unique === true)
104
+ .map((index) =>
105
+ index.fields.map((field) => config.getFieldName({ model, field })),
106
+ ),
107
+ ];
108
+ const name = config.getModelName(model);
109
+ const prior = indexes(name);
110
+ const merged = [...prior];
111
+ for (const index of fields)
112
+ if (
113
+ !merged.some(
114
+ (value) => JSON.stringify(value) === JSON.stringify(index),
115
+ )
116
+ )
117
+ merged.push(index);
118
+ for (const row of rows(name)) ensureUnique(name, row, merged);
119
+ store.setMeta(`local-auth-indexes:${name}`, merged);
120
+ }
121
+ };
122
+ /**
123
+ * @param {string} model
124
+ * @param {Row} candidate
125
+ * @param {readonly (readonly string[])[]} [constraints]
126
+ * @returns {void}
127
+ */
128
+ const ensureUnique = (model, candidate, constraints = indexes(model)) => {
129
+ for (const other of rows(model)) {
130
+ if (other.id === candidate.id) continue;
131
+ for (const fields of constraints) {
132
+ if (
133
+ fields.every(
134
+ (field) =>
135
+ candidate[field] !== null &&
136
+ candidate[field] !== undefined &&
137
+ equal(candidate[field], other[field]),
138
+ )
139
+ ) {
140
+ throw new LocalAuthConstraintError();
141
+ }
142
+ }
143
+ }
144
+ };
145
+ /**
146
+ * @param {string} model
147
+ * @param {Row} input
148
+ * @param {boolean} [creating]
149
+ * @returns {Row}
150
+ */
151
+ const save = (model, input, creating = false) => {
152
+ const row = jsonRow(input);
153
+ const id =
154
+ typeof row.id === "string" && row.id.length > 0 ? row.id : randomUUID();
155
+ const candidate = { ...row, id };
156
+ if (creating && store.get("local-auth", model, id) !== undefined)
157
+ throw new LocalAuthConstraintError();
158
+ ensureUnique(model, candidate);
159
+ store.put("local-auth", model, id, candidate);
160
+ return candidate;
161
+ };
162
+ /**
163
+ * @template T
164
+ * @param {() => T} work
165
+ * @returns {Promise<T>}
166
+ */
167
+ const run = async (work) => {
168
+ const operation = () => {
169
+ installSchema();
170
+ return work();
171
+ };
172
+ return scoped
173
+ ? operation()
174
+ : store.exclusive(() => store.transaction(operation));
175
+ };
176
+ /**
177
+ * @param {string} model
178
+ * @param {readonly Row[]} selected
179
+ * @param {readonly string[] | undefined} select
180
+ * @param {JoinConfig | undefined} join
181
+ * @returns {Row[]}
182
+ */
183
+ const page = (model, selected, select, join) =>
184
+ selected.map((row) => {
185
+ const result =
186
+ select === undefined || select.length === 0
187
+ ? { ...row }
188
+ : Object.fromEntries(
189
+ select
190
+ .map((field) => config.getFieldName({ model, field }))
191
+ .filter((field) => Object.hasOwn(row, field))
192
+ .map((field) => [field, row[field]]),
193
+ );
194
+ for (const [output, relation] of Object.entries(join ?? {})) {
195
+ const related = rows(config.getModelName(output)).filter((candidate) =>
196
+ equal(candidate[relation.on.to], row[relation.on.from]),
197
+ );
198
+ result[output] =
199
+ relation.relation === "one-to-one"
200
+ ? (related[0] ?? null)
201
+ : related.slice(0, bounded(relation.limit ?? 100, 1000));
202
+ }
203
+ return result;
204
+ });
205
+ return {
206
+ /**
207
+ * @template {Record<string, any>} T
208
+ * @param {Parameters<CustomAdapter["create"]>[0] & { data: T }} input
209
+ */
210
+ create: async (input) =>
211
+ run(() => /** @type {T} */ (save(input.model, input.data, true))),
212
+ /**
213
+ * @template T
214
+ * @param {Parameters<CustomAdapter["update"]>[0] & { update: T }} input
215
+ */
216
+ update: async (input) =>
217
+ /** @type {Promise<T | null>} */ (
218
+ run(() => {
219
+ const row = find(input.model, input.where)[0];
220
+ return row === undefined
221
+ ? null
222
+ : save(input.model, { ...row, ...jsonRow(input.update), id: row.id });
223
+ })
224
+ ),
225
+ updateMany: async (input) =>
226
+ run(() => {
227
+ const selected = find(input.model, input.where);
228
+ for (const row of selected)
229
+ save(input.model, { ...row, ...input.update, id: row.id });
230
+ return selected.length;
231
+ }),
232
+ /**
233
+ * @template T
234
+ * @param {Parameters<CustomAdapter["findOne"]>[0]} input
235
+ */
236
+ findOne: async (input) =>
237
+ /** @type {Promise<T | null>} */ (
238
+ run(
239
+ () =>
240
+ page(
241
+ input.model,
242
+ find(input.model, input.where).slice(0, 1),
243
+ input.select,
244
+ input.join,
245
+ )[0] ?? null,
246
+ )
247
+ ),
248
+ /**
249
+ * @template T
250
+ * @param {Parameters<CustomAdapter["findMany"]>[0]} input
251
+ */
252
+ findMany: async (input) =>
253
+ /** @type {Promise<T[]>} */ (run(() => {
254
+ let selected = find(input.model, input.where);
255
+ if (input.sortBy !== undefined) {
256
+ const field = config.getFieldName({
257
+ model: input.model,
258
+ field: input.sortBy.field,
259
+ });
260
+ const direction = input.sortBy.direction === "desc" ? -1 : 1;
261
+ selected = selected.sort(
262
+ (left, right) =>
263
+ compare(left[field], right[field]) * direction ||
264
+ String(left.id).localeCompare(String(right.id)),
265
+ );
266
+ }
267
+ const offset = bounded(input.offset ?? 0, 1_000_000);
268
+ return page(
269
+ input.model,
270
+ selected.slice(offset, offset + bounded(input.limit, 1000)),
271
+ input.select,
272
+ input.join,
273
+ );
274
+ })),
275
+ delete: async (input) =>
276
+ run(() => {
277
+ const row = find(input.model, input.where)[0];
278
+ if (row !== undefined)
279
+ store.delete("local-auth", input.model, String(row.id));
280
+ }),
281
+ deleteMany: async (input) =>
282
+ run(() => {
283
+ const selected = find(input.model, input.where);
284
+ for (const row of selected)
285
+ store.delete("local-auth", input.model, String(row.id));
286
+ return selected.length;
287
+ }),
288
+ /**
289
+ * @template T
290
+ * @param {Parameters<CustomAdapter["consumeOne"]>[0]} input
291
+ */
292
+ consumeOne: async (input) =>
293
+ /** @type {Promise<T | null>} */ (
294
+ run(() => {
295
+ const row = find(input.model, input.where)[0];
296
+ if (row !== undefined)
297
+ store.delete("local-auth", input.model, String(row.id));
298
+ return row ?? null;
299
+ })
300
+ ),
301
+ /**
302
+ * @template T
303
+ * @param {Parameters<CustomAdapter["incrementOne"]>[0]} input
304
+ */
305
+ incrementOne: async (input) =>
306
+ /** @type {Promise<T | null>} */ (run(() => {
307
+ const row = find(input.model, input.where)[0];
308
+ if (row === undefined) return null;
309
+ /** @type {Row} */
310
+ const updated = { ...row, ...input.set, id: row.id };
311
+ for (const [field, delta] of Object.entries(input.increment)) {
312
+ const name = config.getFieldName({ model: input.model, field });
313
+ const old = row[name] ?? 0;
314
+ if (
315
+ typeof old !== "number" ||
316
+ !Number.isSafeInteger(old) ||
317
+ !Number.isSafeInteger(delta) ||
318
+ !Number.isSafeInteger(old + delta)
319
+ )
320
+ throw new TypeError("Local auth counters require safe integers.");
321
+ updated[name] = old + delta;
322
+ }
323
+ return save(input.model, updated);
324
+ })),
325
+ count: async (input) => run(() => find(input.model, input.where).length),
326
+ createSchema: async () =>
327
+ run(() => ({ code: "local_sqlite_ready", path: store.databasePath })),
328
+ };
329
+ }
330
+
331
+ /**
332
+ * A caught operation failure still poisons the transaction; escaped calls refuse.
333
+ *
334
+ * @template T
335
+ * @param {DBAdapter<BetterAuthOptions>} adapter
336
+ * @param {(adapter: Omit<DBAdapter<BetterAuthOptions>, "transaction">) => Promise<T>} callback
337
+ * @returns {Promise<T>}
338
+ */
339
+ async function serializedCallback(adapter, callback) {
340
+ let queue = Promise.resolve();
341
+ let closed = false;
342
+ /** @type {unknown} */
343
+ let failure;
344
+ let failed = false;
345
+ let count = 0;
346
+ /** @param {unknown} error */
347
+ const poison = (error) => {
348
+ if (!failed) failure = error;
349
+ failed = true;
350
+ };
351
+ const scoped = new Proxy(adapter, {
352
+ get(target, property, receiver) {
353
+ if (property === "transaction") return undefined;
354
+ /** @type {unknown} */
355
+ const method = Reflect.get(target, property, receiver);
356
+ if (typeof method !== "function") return method;
357
+ /** @param {unknown[]} args */
358
+ return (...args) => {
359
+ if (closed)
360
+ return Promise.reject(
361
+ new Error("Local auth transaction callback has settled."),
362
+ );
363
+ if (++count > 100) {
364
+ const error = new Error(
365
+ "Local auth transaction exceeds 100 operations.",
366
+ );
367
+ poison(error);
368
+ return Promise.reject(error);
369
+ }
370
+ const operation = queue.then(() => {
371
+ if (failed) throw failure;
372
+ return /** @type {unknown} */ (Reflect.apply(method, target, args));
373
+ });
374
+ queue = operation.then(() => undefined, poison);
375
+ return operation;
376
+ };
377
+ },
378
+ });
379
+ /** @type {T | undefined} */
380
+ let value;
381
+ try {
382
+ value = await callback(scoped);
383
+ } catch (error) {
384
+ poison(error);
385
+ } finally {
386
+ closed = true;
387
+ await queue;
388
+ }
389
+ if (failed) throw failure;
390
+ return /** @type {T} */ (value);
391
+ }
392
+
393
+ export class LocalAuthConstraintError extends Error {
394
+ /** @readonly */
395
+ code = "unique_conflict";
396
+ constructor() {
397
+ super("A unique Better Auth record already exists.");
398
+ this.name = "LocalAuthConstraintError";
399
+ }
400
+ }
401
+ /**
402
+ * @param {number} value
403
+ * @param {number} maximum
404
+ * @returns {number}
405
+ */
406
+ function bounded(value, maximum) {
407
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum)
408
+ throw new TypeError("Local auth pagination exceeds its bound.");
409
+ return value;
410
+ }
411
+ /**
412
+ * @param {unknown} input
413
+ * @returns {Row}
414
+ */
415
+ function jsonRow(input) {
416
+ if (input === null || typeof input !== "object" || Array.isArray(input))
417
+ throw new TypeError("Auth records require objects.");
418
+ // Official factory handles Date conversion; undefined optional fields are omitted.
419
+ return /** @type {Row} */ (JSON.parse(JSON.stringify(input)));
420
+ }
421
+ /**
422
+ * @param {unknown} left
423
+ * @param {unknown} right
424
+ * @returns {boolean}
425
+ */
426
+ function equal(left, right) {
427
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
428
+ }
429
+ /**
430
+ * @param {unknown} left
431
+ * @param {unknown} right
432
+ * @returns {number}
433
+ */
434
+ function compare(left, right) {
435
+ if (equal(left, right)) return 0;
436
+ if (typeof left === "number" && typeof right === "number")
437
+ return left < right ? -1 : 1;
438
+ return String(left ?? "") < String(right ?? "") ? -1 : 1;
439
+ }
440
+ /**
441
+ * @param {Row} row
442
+ * @param {readonly CleanedWhere[]} clauses
443
+ * @returns {boolean}
444
+ */
445
+ function matches(row, clauses) {
446
+ let group = true;
447
+ let result = false;
448
+ for (const [index, clause] of clauses.entries()) {
449
+ if (index > 0 && clause.connector === "OR") {
450
+ result ||= group;
451
+ group = true;
452
+ }
453
+ let left = row[clause.field] ?? null;
454
+ /** @type {unknown} */
455
+ let right = clause.value;
456
+ if (clause.mode === "insensitive") {
457
+ if (typeof left === "string") left = left.toLowerCase();
458
+ right = Array.isArray(right)
459
+ ? right.map((value) =>
460
+ typeof value === "string" ? value.toLowerCase() : value,
461
+ )
462
+ : typeof right === "string"
463
+ ? right.toLowerCase()
464
+ : right;
465
+ }
466
+ /** @type {boolean} */
467
+ let matched;
468
+ switch (clause.operator ?? "eq") {
469
+ case "eq":
470
+ matched = equal(left, right);
471
+ break;
472
+ case "ne":
473
+ matched = !equal(left, right);
474
+ break;
475
+ case "lt":
476
+ matched = left !== null && right !== null && compare(left, right) < 0;
477
+ break;
478
+ case "lte":
479
+ matched = left !== null && right !== null && compare(left, right) <= 0;
480
+ break;
481
+ case "gt":
482
+ matched = left !== null && right !== null && compare(left, right) > 0;
483
+ break;
484
+ case "gte":
485
+ matched = left !== null && right !== null && compare(left, right) >= 0;
486
+ break;
487
+ case "in":
488
+ matched =
489
+ Array.isArray(right) && right.some((value) => equal(left, value));
490
+ break;
491
+ case "not_in":
492
+ matched =
493
+ Array.isArray(right) && !right.some((value) => equal(left, value));
494
+ break;
495
+ case "contains":
496
+ matched =
497
+ typeof left === "string" &&
498
+ typeof right === "string" &&
499
+ left.includes(right);
500
+ break;
501
+ case "starts_with":
502
+ matched =
503
+ typeof left === "string" &&
504
+ typeof right === "string" &&
505
+ left.startsWith(right);
506
+ break;
507
+ case "ends_with":
508
+ matched =
509
+ typeof left === "string" &&
510
+ typeof right === "string" &&
511
+ left.endsWith(right);
512
+ break;
513
+ }
514
+ group &&= matched;
515
+ }
516
+ return result || group;
517
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { createTerraScaleSqliteRuntime } from "./index.js";
5
+ const { values } = parseArgs({
6
+ options: {
7
+ database: { type: "string" },
8
+ host: { type: "string" },
9
+ port: { type: "string" },
10
+ "require-auth": { type: "boolean", default: false },
11
+ "allow-remote": { type: "boolean", default: false },
12
+ reset: { type: "boolean", default: false },
13
+ help: { type: "boolean", short: "h", default: false },
14
+ },
15
+ });
16
+ if (values.help) {
17
+ process.stdout.write(
18
+ "Usage: terrascale-local [--database PATH] [--host HOST] [--port PORT] [--require-auth] [--allow-remote] [--reset]\nExplicit local SQLite development listener for native document requests.\n",
19
+ );
20
+ } else {
21
+ const runtime = createTerraScaleSqliteRuntime({
22
+ sqlite: {
23
+ enabled: true,
24
+ databasePath: resolve(values.database ?? ".terrascale/local.sqlite"),
25
+ },
26
+ allowRemote: values["allow-remote"],
27
+ ...(values["require-auth"]
28
+ ? {
29
+ authentication: "required",
30
+ bearerToken: process.env.TERRASCALE_LOCAL_BEARER_TOKEN ?? "",
31
+ }
32
+ : {}),
33
+ });
34
+ try {
35
+ if (values.reset) await runtime.reset();
36
+ const address = await runtime.start({
37
+ ...(values.host === undefined ? {} : { host: values.host }),
38
+ port: values.port === undefined ? 0 : Number(values.port),
39
+ });
40
+ process.stdout.write(
41
+ `Local SQLite development listener: ${address.origin}\nScope: ${runtime.scope.tenant}/${runtime.scope.database}/${runtime.scope.shard}\nCollection bindings must be explicitly installed by the local application.\n`,
42
+ );
43
+ for (const signal of /** @type {const} */ (["SIGINT", "SIGTERM"]))
44
+ process.once(signal, () => {
45
+ void runtime.close().then(() => process.exit(0));
46
+ });
47
+ } catch (error) {
48
+ await runtime.close();
49
+ throw error;
50
+ }
51
+ }