@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.
- package/CHANGELOG.md +43 -0
- package/README.md +193 -56
- package/package.json +14 -5
- package/sdk-public-v1-route-manifest.json +81 -0
- package/sdk-v1-route-manifest.json +8996 -1697
- package/src/database-codec.js +21 -26
- package/src/database-types.js +11 -11
- package/src/database-view.js +6 -6
- package/src/http.js +13 -48
- package/src/ids.js +22 -0
- package/src/index.js +77 -2
- package/src/local/better-auth.js +17 -6
- package/src/local/environment.js +0 -1
- package/src/local/index.js +2 -2
- package/src/local/legacy-client.js +5 -5
- package/src/local/router.js +12 -12
- package/src/local/store.js +29 -28
- package/src/local/test-environment.js +3 -2
- package/src/postgres.js +5 -4
- package/src/react/core.js +2 -2
- package/src/ts-auth-ids.js +122 -0
- package/src/ts-auth.js +75 -77
- package/src/v1/backups.js +111 -0
- package/src/v1/catalog.js +210 -0
- package/src/v1/changes.js +225 -0
- package/src/v1/database-schema.js +45 -0
- package/src/v1/decode.js +20 -8
- package/src/v1/errors.js +23 -3
- package/src/v1/ids.js +9 -59
- package/src/v1/imports.js +72 -0
- package/src/v1/indexes.js +76 -0
- package/src/v1/json.js +4 -3
- package/src/v1/management-decode.js +24 -0
- package/src/v1/management-routes.js +67 -0
- package/src/v1/management-validation.js +25 -0
- package/src/v1/routes.js +36 -11
- package/src/v1/saved-queries.js +58 -0
- package/src/v1/shape-crypto.js +125 -0
- package/src/v1/shape-manifest.js +31 -0
- package/src/v1/shape-reader.js +212 -0
- package/src/v1/shape-subscription.js +208 -0
- package/src/v1/shapes.js +15 -10
- package/src/v1/sql.js +98 -0
- package/src/v1/system.js +80 -0
- package/src/v1/terrabase-v1.js +192 -33
- package/src/v1/transport.js +80 -34
- package/src/v1/usage.js +101 -0
- package/types/database-codec.d.ts +1 -6
- package/types/database-types.d.ts +11 -11
- package/types/http.d.ts +2 -8
- package/types/ids.d.ts +10 -0
- package/types/index.d.ts +70 -1
- package/types/local/environment.d.ts +1 -1
- package/types/local/store.d.ts +10 -0
- package/types/local/test-environment.d.ts +2 -2
- package/types/postgres.d.ts +4 -4
- package/types/ts-auth-ids.d.ts +14 -0
- package/types/ts-auth.d.ts +99 -101
- package/types/v1/backups.d.ts +128 -0
- package/types/v1/catalog.d.ts +92 -0
- package/types/v1/changes.d.ts +53 -0
- package/types/v1/database-schema.d.ts +82 -0
- package/types/v1/decode.d.ts +17 -4
- package/types/v1/errors.d.ts +16 -1
- package/types/v1/ids.d.ts +4 -16
- package/types/v1/imports.d.ts +132 -0
- package/types/v1/indexes.d.ts +75 -0
- package/types/v1/management-decode.d.ts +15 -0
- package/types/v1/management-routes.d.ts +3 -0
- package/types/v1/management-validation.d.ts +8 -0
- package/types/v1/routes.d.ts +5 -2
- package/types/v1/saved-queries.d.ts +33 -0
- package/types/v1/shape-crypto.d.ts +39 -0
- package/types/v1/shape-manifest.d.ts +28 -0
- package/types/v1/shape-reader.d.ts +45 -0
- package/types/v1/shape-subscription.d.ts +35 -0
- package/types/v1/shapes.d.ts +14 -0
- package/types/v1/sql.d.ts +47 -0
- package/types/v1/system.d.ts +85 -0
- package/types/v1/terrabase-v1.d.ts +116 -7
- package/types/v1/transport.d.ts +27 -10
- package/types/v1/usage.d.ts +171 -0
- package/src/ts-auth-discovery.js +0 -207
- package/types/ts-auth-discovery.d.ts +0 -19
package/src/local/router.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
105
|
-
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:
|
|
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
|
-
|
|
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 =
|
|
174
|
-
const document =
|
|
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 =
|
|
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 =
|
|
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:
|
|
815
|
+
collection: requireDatabaseId(write.collection),
|
|
816
816
|
schema_generation: decimal(write.schema_generation),
|
|
817
817
|
schema_digest: text(write.schema_digest),
|
|
818
|
-
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);
|
package/src/local/store.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync } from "node:fs";
|
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
3
|
import { dirname, isAbsolute } from "node:path";
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const FORMAT_VERSION = 2;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A small synchronous JSON record store for Node local adapters.
|
|
@@ -55,7 +55,7 @@ export class TerraScaleSqliteStore {
|
|
|
55
55
|
if (databasePath !== ":memory:") {
|
|
56
56
|
this.#database.exec("PRAGMA journal_mode = WAL;");
|
|
57
57
|
}
|
|
58
|
-
this.#
|
|
58
|
+
this.#initialize();
|
|
59
59
|
} catch (error) {
|
|
60
60
|
this.#database.close();
|
|
61
61
|
this.#closed = true;
|
|
@@ -128,6 +128,25 @@ export class TerraScaleSqliteStore {
|
|
|
128
128
|
return rows.map((row) => /** @type {T} */ (parseJson(row.value, "record")));
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* @template T
|
|
133
|
+
* @param {string} namespace
|
|
134
|
+
* @param {string} scope
|
|
135
|
+
* @returns {readonly { readonly id: string; readonly value: T }[]}
|
|
136
|
+
*/
|
|
137
|
+
listEntries(namespace, scope) {
|
|
138
|
+
this.#assertOpen();
|
|
139
|
+
assertKey(namespace, "namespace");
|
|
140
|
+
assertKey(scope, "scope");
|
|
141
|
+
const rows = this.#database
|
|
142
|
+
.prepare("SELECT id, value FROM records WHERE namespace = ? AND scope = ? ORDER BY id ASC")
|
|
143
|
+
.all(namespace, scope);
|
|
144
|
+
return rows.map(row => ({
|
|
145
|
+
id: String(row.id),
|
|
146
|
+
value: /** @type {T} */ (parseJson(row.value, "record")),
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
131
150
|
/**
|
|
132
151
|
* @template T
|
|
133
152
|
* @param {string} namespace
|
|
@@ -339,32 +358,18 @@ export class TerraScaleSqliteStore {
|
|
|
339
358
|
}
|
|
340
359
|
|
|
341
360
|
/** @returns {void} */
|
|
342
|
-
#
|
|
361
|
+
#initialize() {
|
|
343
362
|
this.#database.exec("BEGIN IMMEDIATE;");
|
|
344
363
|
try {
|
|
345
|
-
this.#database.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
applied_at TEXT NOT NULL
|
|
349
|
-
) WITHOUT ROWID;
|
|
350
|
-
`);
|
|
351
|
-
|
|
352
|
-
const row = this.#database
|
|
353
|
-
.prepare(
|
|
354
|
-
"SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1",
|
|
355
|
-
)
|
|
356
|
-
.get();
|
|
357
|
-
const version = row?.version;
|
|
358
|
-
if (version !== undefined && typeof version !== "number") {
|
|
359
|
-
throw new Error("Invalid schema migration version.");
|
|
360
|
-
}
|
|
361
|
-
if (version !== undefined && version > SCHEMA_VERSION) {
|
|
364
|
+
const version = this.#database.prepare("PRAGMA user_version").get()?.user_version;
|
|
365
|
+
const table = this.#database.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
|
|
366
|
+
if (version !== FORMAT_VERSION && !(version === 0 && table === undefined)) {
|
|
362
367
|
throw new Error(
|
|
363
|
-
`
|
|
368
|
+
`TS-754: unsupported local SQLite format. Stop the local runtime, delete ${this.databasePath} and its -wal/-shm files, then restart with fresh ULID storage.`,
|
|
364
369
|
);
|
|
365
370
|
}
|
|
366
371
|
|
|
367
|
-
if (version ===
|
|
372
|
+
if (version === 0) {
|
|
368
373
|
this.#database.exec(`
|
|
369
374
|
CREATE TABLE records (
|
|
370
375
|
namespace TEXT NOT NULL,
|
|
@@ -379,11 +384,7 @@ export class TerraScaleSqliteStore {
|
|
|
379
384
|
value TEXT NOT NULL CHECK (json_valid(value) = 1)
|
|
380
385
|
) WITHOUT ROWID;
|
|
381
386
|
`);
|
|
382
|
-
this.#database
|
|
383
|
-
.prepare(
|
|
384
|
-
"INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)",
|
|
385
|
-
)
|
|
386
|
-
.run(SCHEMA_VERSION, new Date().toISOString());
|
|
387
|
+
this.#database.exec(`PRAGMA user_version = ${FORMAT_VERSION}`);
|
|
387
388
|
}
|
|
388
389
|
this.#database.exec("COMMIT;");
|
|
389
390
|
} catch (error) {
|
|
@@ -406,7 +407,7 @@ export class TerraScaleSqliteStore {
|
|
|
406
407
|
);
|
|
407
408
|
}
|
|
408
409
|
} catch {
|
|
409
|
-
// Preserve the original
|
|
410
|
+
// Preserve the original operation exception. SQLite will normally
|
|
410
411
|
// have already rolled back a failed transaction automatically.
|
|
411
412
|
}
|
|
412
413
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { generateId } from "../ids.js";
|
|
1
2
|
import { assertLocalSqliteEnvironment } from "./environment.js";
|
|
2
3
|
/** Local development tests never relax CI/deployment/qualification guards. */
|
|
3
4
|
export const localSqliteUnitTestsAllowed = (() => {
|
|
@@ -11,7 +12,7 @@ export const localSqliteUnitTestsAllowed = (() => {
|
|
|
11
12
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
13
|
import { join } from "node:path";
|
|
13
14
|
import { tmpdir } from "node:os";
|
|
14
|
-
import { randomBytes
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
15
16
|
import { createTerraScaleSqliteRuntime } from "./index.js";
|
|
16
17
|
/** @import { TerraScaleSqliteRuntimeOptions } from "./index.js" */
|
|
17
18
|
/** @import { DatabaseCommit, DatabaseField, DatabasePoint } from "../database-types.js" */
|
|
@@ -25,7 +26,7 @@ export async function localFixture(options = {}) {
|
|
|
25
26
|
...options,
|
|
26
27
|
sqlite: { enabled: true, databasePath },
|
|
27
28
|
});
|
|
28
|
-
const collection =
|
|
29
|
+
const collection = generateId();
|
|
29
30
|
const binding = {
|
|
30
31
|
collection,
|
|
31
32
|
schema_generation: 1n,
|
package/src/postgres.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isValidId } from "./ids.js";
|
|
1
2
|
/** @deprecated The C# TerraBase has no PostgreSQL listener.
|
|
2
3
|
* Node-only PostgreSQL client for the signed INT8/TEXT point SQL listener. */
|
|
3
4
|
import { lookup } from "node:dns/promises";
|
|
@@ -38,8 +39,8 @@ const MAX_INT8 = (1n << 63n) - 1n;
|
|
|
38
39
|
* readonly port: number;
|
|
39
40
|
* readonly tlsServerName: string;
|
|
40
41
|
* readonly caCertificatePem: string;
|
|
41
|
-
* readonly database:
|
|
42
|
-
* readonly keyId:
|
|
42
|
+
* readonly database: import("./ids.js").EntityId;
|
|
43
|
+
* readonly keyId: import("./ids.js").EntityId;
|
|
43
44
|
* readonly password: string;
|
|
44
45
|
* readonly connectTimeoutMs?: number;
|
|
45
46
|
* }} PostgresConfig
|
|
@@ -168,8 +169,8 @@ function configuration(config) {
|
|
|
168
169
|
!Number.isInteger(timeout) ||
|
|
169
170
|
timeout < 1 ||
|
|
170
171
|
timeout > 10000 ||
|
|
171
|
-
|
|
172
|
-
!
|
|
172
|
+
!isValidId(config.database) ||
|
|
173
|
+
!isValidId(config.keyId) ||
|
|
173
174
|
parts.length !== 3 ||
|
|
174
175
|
parts[0] !== "tbk1" ||
|
|
175
176
|
parts[1] !== config.keyId ||
|
package/src/react/core.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { generateId } from "../ids.js";
|
|
1
2
|
/** @import { TerraScaleError, TerraScaleResult } from "../result.js" */
|
|
2
3
|
/** @import { DatabaseResult } from "../database-types.js" */
|
|
3
4
|
|
|
@@ -733,8 +734,7 @@ function normalizeIdempotencyKey(value) {
|
|
|
733
734
|
|
|
734
735
|
/** @returns {string} */
|
|
735
736
|
function createIdempotencyKey() {
|
|
736
|
-
|
|
737
|
-
return randomUuid === undefined ? `tsm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}` : randomUuid.call(globalThis.crypto);
|
|
737
|
+
return generateId();
|
|
738
738
|
}
|
|
739
739
|
|
|
740
740
|
/** @returns {ReactiveMutationError} */
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { requireId } from "./ids.js";
|
|
2
|
+
|
|
3
|
+
const singleEntityRoutes = new Set([
|
|
4
|
+
"tsauth.users.get", "tsauth.users.create", "tsauth.users.update",
|
|
5
|
+
"tsauth.apiKeys.create",
|
|
6
|
+
"tsauth.organizations.get", "tsauth.organizations.create", "tsauth.organizations.update",
|
|
7
|
+
"tsauth.organizations.createMembership", "tsauth.organizations.updateMembership",
|
|
8
|
+
"tsauth.organizations.createInvitation", "tsauth.organizations.revokeInvitation", "tsauth.organizations.createDomain",
|
|
9
|
+
"tsauth.connections.create", "tsauth.connections.update",
|
|
10
|
+
"tsauth.directories.create", "tsauth.directories.rotate",
|
|
11
|
+
"tsauth.jwtTemplates.create", "tsauth.jwtTemplates.update",
|
|
12
|
+
"tsauth.webhooks.create",
|
|
13
|
+
"tsauth.instance.get", "tsauth.instance.update",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const requiredRootReferences = new Map([
|
|
17
|
+
["tsauth.oidc.userinfo", "sub"],
|
|
18
|
+
["tsauth.organizations.enrollDomain", "org_id"],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/** @type {ReadonlyMap<string, readonly string[]>} */
|
|
22
|
+
const entityReferenceFields = new Map([
|
|
23
|
+
["tsauth.organizations.createMembership", ["organization_id", "user_id"]],
|
|
24
|
+
["tsauth.organizations.updateMembership", ["organization_id", "user_id"]],
|
|
25
|
+
["tsauth.organizations.listMemberships", ["organization_id", "user_id"]],
|
|
26
|
+
["tsauth.organizations.createInvitation", ["organization_id"]],
|
|
27
|
+
["tsauth.organizations.revokeInvitation", ["organization_id"]],
|
|
28
|
+
["tsauth.organizations.listInvitations", ["organization_id"]],
|
|
29
|
+
["tsauth.organizations.createDomain", ["organization_id"]],
|
|
30
|
+
["tsauth.organizations.listDomains", ["organization_id"]],
|
|
31
|
+
["tsauth.auditEvents.list", ["instance_id"]],
|
|
32
|
+
["tsauth.webhooks.deadLetter", ["endpoint_id", "event_id"]],
|
|
33
|
+
["tsauth.oauthClients.create", ["client_id"]],
|
|
34
|
+
["tsauth.oauthClients.update", ["client_id"]],
|
|
35
|
+
["tsauth.oauthClients.list", ["client_id"]],
|
|
36
|
+
["tsauth.oauthResources.create", ["introspection_client_id"]],
|
|
37
|
+
["tsauth.oauthResources.update", ["introspection_client_id"]],
|
|
38
|
+
["tsauth.oauthResources.list", ["introspection_client_id"]],
|
|
39
|
+
["tsauth.connections.create", ["org_id"]],
|
|
40
|
+
["tsauth.connections.update", ["org_id"]],
|
|
41
|
+
["tsauth.connections.list", ["org_id"]],
|
|
42
|
+
["tsauth.directories.create", ["org_id"]],
|
|
43
|
+
["tsauth.directories.rotate", ["org_id"]],
|
|
44
|
+
["tsauth.directories.list", ["org_id"]],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate the documented endpointId:eventId delivery selector.
|
|
49
|
+
* @param {unknown} value
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function requireDeadLetterId(value) {
|
|
53
|
+
if (typeof value !== "string" || value.split(":").length !== 2)
|
|
54
|
+
throw new TypeError("Dead-letter selector must contain endpoint and event ULIDs separated by a colon.");
|
|
55
|
+
const [endpoint, event] = value.split(":");
|
|
56
|
+
requireId(endpoint, "endpoint id");
|
|
57
|
+
requireId(event, "event id");
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate identity fields in the hosted-auth DTOs without inspecting customer metadata.
|
|
63
|
+
* @param {unknown} value
|
|
64
|
+
* @param {string} route
|
|
65
|
+
* @param {boolean} [entity]
|
|
66
|
+
* @returns {void}
|
|
67
|
+
*/
|
|
68
|
+
export function validateTsAuthIds(value, route, entity = singleEntityRoutes.has(route)) {
|
|
69
|
+
const requiredReference = requiredRootReferences.get(route);
|
|
70
|
+
if ((entity || requiredReference !== undefined) && (typeof value !== "object" || value === null || Array.isArray(value)))
|
|
71
|
+
throw new TypeError("Hosted-auth response must be an object with its required ULID.");
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
if (route === "tsauth.users.regenerateRecoveryCodes") {
|
|
74
|
+
if (!value.every(code => typeof code === "string"))
|
|
75
|
+
throw new TypeError("Recovery codes must be strings.");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const entry of value) validateTsAuthIds(entry, route, true);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (typeof value !== "object" || value === null) return;
|
|
82
|
+
if (requiredReference !== undefined) requireId(Reflect.get(value, requiredReference), requiredReference);
|
|
83
|
+
const deadLetter = route === "tsauth.webhooks.deadLetter";
|
|
84
|
+
if (entity) {
|
|
85
|
+
if (deadLetter) requireDeadLetterId(Reflect.get(value, "id"));
|
|
86
|
+
else requireId(Reflect.get(value, "id"), "entity id");
|
|
87
|
+
}
|
|
88
|
+
const fields = ["id", "user_id", "organization_id", "org_id", "endpoint_id", "event_id", "instance_id", "actor_id"];
|
|
89
|
+
const nullableOrgId = route.startsWith("tsauth.connections.") || route.startsWith("tsauth.directories.");
|
|
90
|
+
if (/^tsauth\.(oauthClients|oauthResources|oidc)\./u.test(route))
|
|
91
|
+
fields.push("client_id", "introspection_client_id", "sub", "sid", "jti");
|
|
92
|
+
const requiredFields = entityReferenceFields.get(route) ?? [];
|
|
93
|
+
const validateReferences = entity || Object.hasOwn(value, "id");
|
|
94
|
+
for (const field of fields) {
|
|
95
|
+
if (Object.hasOwn(value, field) || (validateReferences && requiredFields.includes(field))) {
|
|
96
|
+
const id = Reflect.get(value, field);
|
|
97
|
+
if (field === "id" && deadLetter) requireDeadLetterId(id);
|
|
98
|
+
else if (id !== null || !(field === "sid" || (field === "org_id" && nullableOrgId))) requireId(id, field);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// `data` objects in audit events are customer payloads; only page arrays contain DTOs.
|
|
102
|
+
if (Object.hasOwn(value, "data") && Array.isArray(Reflect.get(value, "data")))
|
|
103
|
+
validateTsAuthIds(Reflect.get(value, "data"), route);
|
|
104
|
+
for (const field of ["passkeys", "clients", "resources"])
|
|
105
|
+
if (Object.hasOwn(value, field)) validateTsAuthIds(Reflect.get(value, field), route);
|
|
106
|
+
for (const field of ["organization", "user"]) {
|
|
107
|
+
if (Object.hasOwn(value, field) && Reflect.get(value, field) !== null) {
|
|
108
|
+
const relatedRoute = field === "organization" ? "tsauth.organizations.get" : "tsauth.users.get";
|
|
109
|
+
validateTsAuthIds(Reflect.get(value, field), relatedRoute, true);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let oauthEntity;
|
|
113
|
+
if (route.startsWith("tsauth.oauthClients.")) oauthEntity = "client";
|
|
114
|
+
else if (route.startsWith("tsauth.oauthResources.")) oauthEntity = "resource";
|
|
115
|
+
if (oauthEntity !== undefined && Object.hasOwn(value, oauthEntity))
|
|
116
|
+
validateTsAuthIds(Reflect.get(value, oauthEntity), route, true);
|
|
117
|
+
if (Object.hasOwn(value, "allowed_client_ids")) {
|
|
118
|
+
const ids = Reflect.get(value, "allowed_client_ids");
|
|
119
|
+
if (!Array.isArray(ids)) throw new TypeError("allowed_client_ids must be an array of ULIDs.");
|
|
120
|
+
for (const id of ids) requireId(id, "allowed_client_ids");
|
|
121
|
+
}
|
|
122
|
+
}
|