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
@@ -0,0 +1,190 @@
1
+ const AUTH_DISCOVERY_URL =
2
+ "https://atlas.terrascale.tech/api/v1/auth/discovery";
3
+ const MAX_ENDPOINTS = 20;
4
+ const MAX_BYTES = 65_536;
5
+
6
+ /** The public auth routing document, validated before any credentials are sent. */
7
+ export class TsAuthEndpointDiscovery {
8
+ /**
9
+ * @type {typeof globalThis.fetch}
10
+ * @readonly
11
+ */
12
+ #fetch;
13
+ /** @type {{ readonly endpoints: readonly string[]; readonly expiresAt: number } | undefined} */
14
+ #cached;
15
+
16
+ /** @param {typeof globalThis.fetch} fetchHandler */
17
+ constructor(fetchHandler) {
18
+ this.#fetch = fetchHandler;
19
+ }
20
+
21
+ /**
22
+ * @param {AbortSignal} [signal]
23
+ * @returns {Promise<readonly string[]>}
24
+ */
25
+ async endpoints(signal) {
26
+ if (this.#cached !== undefined && this.#cached.expiresAt > Date.now())
27
+ return this.#cached.endpoints;
28
+ const deadline = AbortSignal.timeout(3_000);
29
+ const discoverySignal =
30
+ signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
31
+ const response = await awaitAbortable(
32
+ this.#fetch(
33
+ new Request(AUTH_DISCOVERY_URL, {
34
+ redirect: "manual",
35
+ signal: discoverySignal,
36
+ }),
37
+ ).then((response) => {
38
+ if (discoverySignal.aborted)
39
+ void response.body?.cancel().catch(() => undefined);
40
+ return response;
41
+ }),
42
+ discoverySignal,
43
+ );
44
+ if (!response.ok) {
45
+ void response.body?.cancel().catch(() => undefined);
46
+ throw new TypeError(
47
+ `Atlas auth discovery returned HTTP ${response.status}.`,
48
+ );
49
+ }
50
+ const body = await readBoundedBody(response, discoverySignal);
51
+ /** @type {unknown} */
52
+ let payload;
53
+ try {
54
+ payload = JSON.parse(body);
55
+ } catch {
56
+ throw new TypeError("Atlas auth discovery response is invalid JSON.");
57
+ }
58
+ if (
59
+ typeof payload !== "object" ||
60
+ payload === null ||
61
+ Array.isArray(payload)
62
+ )
63
+ throw new TypeError("Atlas auth discovery response is invalid.");
64
+ const doc = /** @type {Record<string, unknown>} */ (payload);
65
+ if (
66
+ doc.v !== 1 ||
67
+ !Number.isInteger(doc.refreshSeconds) ||
68
+ /** @type {number} */ (doc.refreshSeconds) < 1 ||
69
+ /** @type {number} */ (doc.refreshSeconds) > 3600 ||
70
+ !Array.isArray(doc.endpoints) ||
71
+ doc.endpoints.length < 1 ||
72
+ doc.endpoints.length > MAX_ENDPOINTS
73
+ ) {
74
+ throw new TypeError("Atlas auth discovery response is invalid.");
75
+ }
76
+ /** @type {Set<string>} */
77
+ const seen = new Set();
78
+ /** @type {string[]} */
79
+ const endpoints = [];
80
+ for (const entry of doc.endpoints) {
81
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
82
+ throw new TypeError("Atlas auth endpoint is invalid.");
83
+ const { region, url } = /** @type {Record<string, unknown>} */ (entry);
84
+ if (
85
+ typeof region !== "string" ||
86
+ !/^[A-Z0-9][A-Z0-9-]{0,15}$/u.test(region) ||
87
+ seen.has(region) ||
88
+ typeof url !== "string"
89
+ ) {
90
+ throw new TypeError("Atlas auth endpoint is invalid.");
91
+ }
92
+ /** @type {URL} */
93
+ let parsed;
94
+ try {
95
+ parsed = new URL(url);
96
+ } catch {
97
+ throw new TypeError("Atlas auth endpoint URL is invalid.");
98
+ }
99
+ if (
100
+ parsed.protocol !== "https:" ||
101
+ parsed.hostname !== `${region.toLowerCase()}.auth.terrascale.tech` ||
102
+ parsed.port !== "" ||
103
+ parsed.username !== "" ||
104
+ parsed.password !== "" ||
105
+ parsed.pathname !== "/" ||
106
+ parsed.search !== "" ||
107
+ parsed.hash !== ""
108
+ ) {
109
+ throw new TypeError(
110
+ "Atlas auth endpoint URL does not match its region.",
111
+ );
112
+ }
113
+ seen.add(region);
114
+ endpoints.push(parsed.origin);
115
+ }
116
+ this.#cached = {
117
+ endpoints,
118
+ expiresAt:
119
+ Date.now() +
120
+ Math.min(/** @type {number} */ (doc.refreshSeconds), 60) * 1000,
121
+ };
122
+ return endpoints;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @param {Response} response
128
+ * @param {AbortSignal} signal
129
+ * @returns {Promise<string>}
130
+ */
131
+ async function readBoundedBody(response, signal) {
132
+ if (Number(response.headers.get("content-length")) > MAX_BYTES) {
133
+ void response.body?.cancel().catch(() => undefined);
134
+ throw new TypeError("Atlas auth discovery response is too large.");
135
+ }
136
+ const reader = response.body?.getReader();
137
+ if (reader === undefined)
138
+ throw new TypeError("Atlas auth discovery response is empty.");
139
+ /** @type {Uint8Array[]} */
140
+ const parts = [];
141
+ let size = 0;
142
+ try {
143
+ while (true) {
144
+ const chunk = await awaitAbortable(reader.read(), signal);
145
+ if (chunk.done) break;
146
+ size += chunk.value.byteLength;
147
+ if (size > MAX_BYTES)
148
+ throw new TypeError("Atlas auth discovery response is too large.");
149
+ parts.push(chunk.value);
150
+ }
151
+ } catch (error) {
152
+ void reader.cancel().catch(() => undefined);
153
+ throw error;
154
+ } finally {
155
+ reader.releaseLock();
156
+ }
157
+ const bytes = new Uint8Array(size);
158
+ let offset = 0;
159
+ for (const part of parts) {
160
+ bytes.set(part, offset);
161
+ offset += part.byteLength;
162
+ }
163
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
164
+ }
165
+
166
+ /**
167
+ * @template T
168
+ * @param {Promise<T>} work
169
+ * @param {AbortSignal} signal
170
+ * @returns {Promise<T>}
171
+ */
172
+ async function awaitAbortable(work, signal) {
173
+ if (signal.aborted) {
174
+ void work.catch(() => undefined);
175
+ throw signal.reason;
176
+ }
177
+ /** @type {() => void} */
178
+ let onAbort = () => undefined;
179
+ /** @type {Promise<never>} */
180
+ const aborted = new Promise((_, reject) => {
181
+ onAbort = () => reject(signal.reason);
182
+ signal.addEventListener("abort", onAbort, { once: true });
183
+ if (signal.aborted) onAbort();
184
+ });
185
+ try {
186
+ return await Promise.race([work, aborted]);
187
+ } finally {
188
+ signal.removeEventListener("abort", onAbort);
189
+ }
190
+ }