@terrascale/sdk 0.0.2 → 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.
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: string;
42
- * readonly keyId: string;
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
- !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(config.database) ||
172
- !canonicalBase64(config.keyId, 16) ||
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
- const randomUuid = globalThis.crypto?.randomUUID;
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
+ }
package/src/ts-auth.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { TsAuthHttp } from "./http.js";
2
2
  import { err } from "./result.js";
3
+ import { isValidId, requireId } from "./ids.js";
4
+ import { requireDeadLetterId } from "./ts-auth-ids.js";
3
5
  /** @import { TerraScaleResult } from "./result.js" */
4
6
 
5
7
  /** @typedef {import("./config.js").TsAuthRuntimeConfig} TsAuthRuntimeConfig */
@@ -27,7 +29,7 @@ import { err } from "./result.js";
27
29
 
28
30
  /**
29
31
  * @typedef {{
30
- * readonly id: string;
32
+ * readonly id: import("./ids.js").EntityId;
31
33
  * readonly email: string | null;
32
34
  * readonly email_verified: boolean;
33
35
  * readonly username: string | null;
@@ -43,7 +45,7 @@ import { err } from "./result.js";
43
45
 
44
46
  /**
45
47
  * @typedef {{
46
- * readonly id: string;
48
+ * readonly id: import("./ids.js").EntityId;
47
49
  * readonly status: string;
48
50
  * readonly created_at: number; // Unix seconds.
49
51
  * readonly expires_at: number; // Unix seconds.
@@ -55,7 +57,7 @@ import { err } from "./result.js";
55
57
 
56
58
  /**
57
59
  * @typedef {{
58
- * readonly id: string;
60
+ * readonly id: import("./ids.js").EntityId;
59
61
  * readonly type: string;
60
62
  * readonly confirmed: boolean;
61
63
  * readonly createdAt: number; // Unix seconds.
@@ -64,7 +66,7 @@ import { err } from "./result.js";
64
66
 
65
67
  /**
66
68
  * @typedef {{
67
- * readonly id: string;
69
+ * readonly id: import("./ids.js").EntityId;
68
70
  * readonly nickname: string | null;
69
71
  * readonly algorithm: string;
70
72
  * readonly createdAt: number; // Unix seconds.
@@ -74,7 +76,7 @@ import { err } from "./result.js";
74
76
 
75
77
  /**
76
78
  * @typedef {{
77
- * readonly id: string;
79
+ * readonly id: import("./ids.js").EntityId;
78
80
  * readonly name: string;
79
81
  * readonly slug: string;
80
82
  * readonly created_at: number; // Unix seconds.
@@ -83,7 +85,7 @@ import { err } from "./result.js";
83
85
 
84
86
  /**
85
87
  * @typedef {{
86
- * readonly id: string;
88
+ * readonly id: import("./ids.js").EntityId;
87
89
  * readonly role: string;
88
90
  * readonly created_at: number; // Unix seconds.
89
91
  * readonly organization: TsAuthOrganization | null;
@@ -320,7 +322,7 @@ export class TsAuthUsersClient {
320
322
  ...(options.limit === undefined ? {} : { limit: options.limit }),
321
323
  ...(options.startingAfter === undefined
322
324
  ? {}
323
- : { starting_after: options.startingAfter }),
325
+ : { starting_after: requireId(options.startingAfter, "startingAfter") }),
324
326
  },
325
327
  ...(options.signal === undefined ? {} : { signal: options.signal }),
326
328
  });
@@ -586,7 +588,7 @@ export class TsAuthSessionsClient {
586
588
 
587
589
  /**
588
590
  * @typedef {{
589
- * readonly id: string;
591
+ * readonly id: import("./ids.js").EntityId;
590
592
  * readonly name: string;
591
593
  * readonly api_key: string; // One-time full sk_ bearer; only the creating call returns it.
592
594
  * readonly scopes: readonly string[];
@@ -595,7 +597,7 @@ export class TsAuthSessionsClient {
595
597
 
596
598
  /**
597
599
  * @typedef {{
598
- * readonly id: string;
600
+ * readonly id: import("./ids.js").EntityId;
599
601
  * readonly name: string;
600
602
  * readonly scopes: readonly string[];
601
603
  * readonly created_at: number; // Unix seconds.
@@ -674,9 +676,9 @@ export class TsAuthApiKeysClient {
674
676
 
675
677
  /**
676
678
  * @typedef {{
677
- * readonly id: string;
678
- * readonly organization_id: string;
679
- * readonly user_id: string;
679
+ * readonly id: import("./ids.js").EntityId;
680
+ * readonly organization_id: import("./ids.js").EntityId;
681
+ * readonly user_id: import("./ids.js").EntityId;
680
682
  * readonly role: string;
681
683
  * readonly created_at: number; // Unix seconds.
682
684
  * }} TsAuthMembership
@@ -685,7 +687,7 @@ export class TsAuthApiKeysClient {
685
687
  /**
686
688
  * @typedef {TsAuthMembership & {
687
689
  * readonly user: {
688
- * readonly id: string;
690
+ * readonly id: import("./ids.js").EntityId;
689
691
  * readonly email: string;
690
692
  * readonly name: string;
691
693
  * } | null;
@@ -694,8 +696,8 @@ export class TsAuthApiKeysClient {
694
696
 
695
697
  /**
696
698
  * @typedef {{
697
- * readonly id: string;
698
- * readonly organization_id: string;
699
+ * readonly id: import("./ids.js").EntityId;
700
+ * readonly organization_id: import("./ids.js").EntityId;
699
701
  * readonly email: string;
700
702
  * readonly role: string;
701
703
  * readonly status: string;
@@ -706,14 +708,14 @@ export class TsAuthApiKeysClient {
706
708
 
707
709
  /**
708
710
  * @typedef {{
709
- * readonly id: string;
710
- * readonly organization_id: string;
711
+ * readonly id: import("./ids.js").EntityId;
712
+ * readonly organization_id: import("./ids.js").EntityId;
711
713
  * readonly domain: string;
712
714
  * readonly created_at: number; // Unix seconds.
713
715
  * }} TsAuthOrganizationDomain
714
716
  */
715
717
 
716
- /** @typedef {{ readonly org_id: string }} TsAuthDomainEnrollment */
718
+ /** @typedef {{ readonly org_id: import("./ids.js").EntityId }} TsAuthDomainEnrollment */
717
719
 
718
720
  /**
719
721
  * @typedef {{
@@ -730,7 +732,7 @@ export class TsAuthApiKeysClient {
730
732
 
731
733
  /**
732
734
  * @typedef {{
733
- * readonly user_id: string;
735
+ * readonly user_id: import("./ids.js").EntityId;
734
736
  * readonly role: string;
735
737
  * }} TsAuthCreateMembershipRequest
736
738
  */
@@ -750,7 +752,7 @@ export class TsAuthApiKeysClient {
750
752
 
751
753
  /**
752
754
  * @typedef {{
753
- * readonly user_id: string;
755
+ * readonly user_id: import("./ids.js").EntityId;
754
756
  * }} TsAuthEnrollRequest
755
757
  */
756
758
 
@@ -844,7 +846,7 @@ export class TsAuthOrganizationsClient {
844
846
  * @returns {Promise<TerraScaleResult<TsAuthMembership>>}
845
847
  */
846
848
  createMembership(organizationId, request) {
847
- requireText(request?.user_id, "user_id");
849
+ requireId(request?.user_id, "user_id");
848
850
  requireText(request?.role, "role");
849
851
  return this.#http.request({
850
852
  method: "POST",
@@ -982,7 +984,7 @@ export class TsAuthOrganizationsClient {
982
984
  * @returns {Promise<TerraScaleResult<TsAuthDomainEnrollment>>}
983
985
  */
984
986
  enrollDomain(domain, request) {
985
- requireText(request?.user_id, "user_id");
987
+ requireId(request?.user_id, "user_id");
986
988
  return this.#http.request({
987
989
  method: "POST",
988
990
  path: `v1/${this.#slug}/domains/${segment(domain, "domain")}/enroll`,
@@ -1028,8 +1030,8 @@ export class TsAuthOrganizationsClient {
1028
1030
  */
1029
1031
  /**
1030
1032
  * @typedef {{
1031
- * readonly id: string;
1032
- * readonly client_id: string;
1033
+ * readonly id: import("./ids.js").EntityId;
1034
+ * readonly client_id: import("./ids.js").EntityId;
1033
1035
  * readonly name: string;
1034
1036
  * readonly redirect_uris: readonly string[];
1035
1037
  * readonly post_logout_redirect_uris: readonly string[];
@@ -1146,7 +1148,7 @@ export class TsAuthOauthClientsClient {
1146
1148
  /**
1147
1149
  * @typedef {{
1148
1150
  * readonly audience: string;
1149
- * readonly allowed_client_ids: readonly string[];
1151
+ * readonly allowed_client_ids: readonly import("./ids.js").EntityId[];
1150
1152
  * readonly allowed_grant_types: readonly TsAuthOauthGrantType[];
1151
1153
  * readonly allowed_scopes: readonly string[];
1152
1154
  * }} TsAuthCreateOauthResourceRequest
@@ -1154,7 +1156,7 @@ export class TsAuthOauthClientsClient {
1154
1156
  /**
1155
1157
  * @typedef {{
1156
1158
  * readonly expected_auth_epoch: number;
1157
- * readonly allowed_client_ids?: readonly string[] | undefined;
1159
+ * readonly allowed_client_ids?: readonly import("./ids.js").EntityId[] | undefined;
1158
1160
  * readonly allowed_grant_types?: readonly TsAuthOauthGrantType[] | undefined;
1159
1161
  * readonly allowed_scopes?: readonly string[] | undefined;
1160
1162
  * readonly enabled?: boolean | undefined;
@@ -1163,8 +1165,8 @@ export class TsAuthOauthClientsClient {
1163
1165
  */
1164
1166
  /**
1165
1167
  * @typedef {TsAuthCreateOauthResourceRequest & {
1166
- * readonly id: string;
1167
- * readonly introspection_client_id: string;
1168
+ * readonly id: import("./ids.js").EntityId;
1169
+ * readonly introspection_client_id: import("./ids.js").EntityId;
1168
1170
  * readonly token_endpoint_auth_method: "client_secret_basic";
1169
1171
  * readonly auth_epoch: number;
1170
1172
  * readonly enabled: boolean;
@@ -1265,11 +1267,11 @@ export class TsAuthOauthResourcesClient {
1265
1267
 
1266
1268
  /**
1267
1269
  * @typedef {{
1268
- * readonly id: string;
1270
+ * readonly id: import("./ids.js").EntityId;
1269
1271
  * readonly type: "oidc" | "saml";
1270
1272
  * readonly name: string;
1271
1273
  * readonly enabled: boolean;
1272
- * readonly org_id: string | null;
1274
+ * readonly org_id: import("./ids.js").EntityId | null;
1273
1275
  * readonly issuer_url: string | null;
1274
1276
  * readonly client_id: string | null;
1275
1277
  * readonly authorization_endpoint: string | null;
@@ -1342,7 +1344,7 @@ export class TsAuthConnectionsClient {
1342
1344
  requireText(request?.name, "name");
1343
1345
  if (request.type === "oidc") {
1344
1346
  requireAbsoluteHttpUrl(request.issuer_url, "issuer_url");
1345
- requireText(request.client_id, "client_id");
1347
+ requireText(request.client_id, "client_id"); // This client belongs to the external IdP.
1346
1348
  requireText(request.client_secret, "client_secret");
1347
1349
  } else {
1348
1350
  requireText(request.idp_entity_id, "idp_entity_id");
@@ -1399,16 +1401,16 @@ export class TsAuthConnectionsClient {
1399
1401
 
1400
1402
  /**
1401
1403
  * @typedef {{
1402
- * readonly id: string;
1403
- * readonly org_id: string | null;
1404
+ * readonly id: import("./ids.js").EntityId;
1405
+ * readonly org_id: import("./ids.js").EntityId | null;
1404
1406
  * readonly token: string; // One-time SCIM bearer; only create and rotate return it.
1405
1407
  * }} TsAuthDirectoryWithToken
1406
1408
  */
1407
1409
 
1408
1410
  /**
1409
1411
  * @typedef {{
1410
- * readonly id: string;
1411
- * readonly org_id: string | null;
1412
+ * readonly id: import("./ids.js").EntityId;
1413
+ * readonly org_id: import("./ids.js").EntityId | null;
1412
1414
  * readonly created_at: number; // Unix seconds.
1413
1415
  * }} TsAuthDirectory
1414
1416
  */
@@ -1439,7 +1441,7 @@ export class TsAuthDirectoriesClient {
1439
1441
  * @returns {Promise<TerraScaleResult<TsAuthDirectoryWithToken>>}
1440
1442
  */
1441
1443
  create(request = {}) {
1442
- if (request.org_id !== undefined) requireText(request.org_id, "org_id");
1444
+ if (request.org_id !== undefined) requireId(request.org_id, "org_id");
1443
1445
  return this.#http.request({
1444
1446
  method: "POST",
1445
1447
  path: `v1/${this.#slug}/directories`,
@@ -1488,7 +1490,7 @@ export class TsAuthDirectoriesClient {
1488
1490
 
1489
1491
  /**
1490
1492
  * @typedef {{
1491
- * readonly id: string;
1493
+ * readonly id: import("./ids.js").EntityId;
1492
1494
  * readonly name: string;
1493
1495
  * readonly claims: Record<string, string>;
1494
1496
  * readonly lifetime_seconds: number;
@@ -1600,12 +1602,12 @@ export class TsAuthJwtTemplatesClient {
1600
1602
 
1601
1603
  /**
1602
1604
  * @param {string} name
1603
- * @param {{ readonly user_id: string }} request
1605
+ * @param {{ readonly user_id: import("./ids.js").EntityId }} request
1604
1606
  * @returns {Promise<TerraScaleResult<TsAuthJwtTokenResponse>>}
1605
1607
  */
1606
1608
  issueToken(name, request) {
1607
1609
  validateJwtTemplateName(name);
1608
- requireText(request?.user_id, "user_id");
1610
+ requireId(request?.user_id, "user_id");
1609
1611
  return this.#http.request({
1610
1612
  method: "POST",
1611
1613
  path: `v1/${this.#slug}/jwt-templates/${segment(name, "template name")}/tokens`,
@@ -1619,7 +1621,7 @@ export class TsAuthJwtTemplatesClient {
1619
1621
 
1620
1622
  /**
1621
1623
  * @typedef {{
1622
- * readonly id: string;
1624
+ * readonly id: import("./ids.js").EntityId;
1623
1625
  * readonly url: string;
1624
1626
  * readonly events: readonly string[];
1625
1627
  * readonly signing_secret: string; // One-time HMAC key for verifying deliveries; only create returns it.
@@ -1628,7 +1630,7 @@ export class TsAuthJwtTemplatesClient {
1628
1630
 
1629
1631
  /**
1630
1632
  * @typedef {{
1631
- * readonly id: string;
1633
+ * readonly id: import("./ids.js").EntityId;
1632
1634
  * readonly url: string;
1633
1635
  * readonly events: readonly string[];
1634
1636
  * readonly enabled: boolean;
@@ -1638,9 +1640,9 @@ export class TsAuthJwtTemplatesClient {
1638
1640
 
1639
1641
  /**
1640
1642
  * @typedef {{
1641
- * readonly id: string; // endpointId:eventId
1642
- * readonly endpoint_id: string;
1643
- * readonly event_id: string;
1643
+ * readonly id: `${import("./ids.js").EntityId}:${import("./ids.js").EntityId}`; // endpointId:eventId delivery selector.
1644
+ * readonly endpoint_id: import("./ids.js").EntityId;
1645
+ * readonly event_id: import("./ids.js").EntityId;
1644
1646
  * readonly attempts: number;
1645
1647
  * readonly last_error?: string | undefined;
1646
1648
  * readonly updated_at: number; // Unix seconds.
@@ -1741,7 +1743,7 @@ export class TsAuthWebhooksClient {
1741
1743
  replayDeadLetter(entryId) {
1742
1744
  return this.#http.request({
1743
1745
  method: "POST",
1744
- path: `v1/${this.#slug}/webhooks/dead-letter/${segment(entryId, "dead-letter id")}/replay`,
1746
+ path: `v1/${this.#slug}/webhooks/dead-letter/${encodeURIComponent(requireDeadLetterId(entryId))}/replay`,
1745
1747
  routeName: "tsauth.webhooks.replayDeadLetter",
1746
1748
  });
1747
1749
  }
@@ -1751,10 +1753,10 @@ export class TsAuthWebhooksClient {
1751
1753
 
1752
1754
  /**
1753
1755
  * @typedef {{
1754
- * readonly id: string;
1755
- * readonly instance_id: string;
1756
+ * readonly id: import("./ids.js").EntityId;
1757
+ * readonly instance_id: import("./ids.js").EntityId;
1756
1758
  * readonly type: string;
1757
- * readonly actor_id?: string | undefined; // Omitted from the wire when null.
1759
+ * readonly actor_id?: import("./ids.js").EntityId | undefined; // Omitted from the wire when null.
1758
1760
  * readonly data: Record<string, unknown>;
1759
1761
  * readonly created_at: number; // Unix seconds.
1760
1762
  * readonly integrity_version: number;
@@ -1918,7 +1920,7 @@ export class TsAuthBillingClient {
1918
1920
 
1919
1921
  /**
1920
1922
  * @typedef {{
1921
- * readonly id: string;
1923
+ * readonly id: import("./ids.js").EntityId;
1922
1924
  * readonly slug: string;
1923
1925
  * readonly name: string;
1924
1926
  * readonly logo_url?: string | null | undefined;
@@ -2033,7 +2035,7 @@ export class TsAuthInstanceClient {
2033
2035
 
2034
2036
  /**
2035
2037
  * @typedef {{
2036
- * readonly client_id: string;
2038
+ * readonly client_id: import("./ids.js").EntityId;
2037
2039
  * readonly redirect_uri: string;
2038
2040
  * readonly response_type: "code";
2039
2041
  * readonly scope?: string | undefined;
@@ -2108,20 +2110,20 @@ export class TsAuthInstanceClient {
2108
2110
  * readonly active: true;
2109
2111
  * readonly token_type: "Bearer";
2110
2112
  * readonly iss: string;
2111
- * readonly sub: string;
2113
+ * readonly sub: import("./ids.js").EntityId;
2112
2114
  * readonly aud: string;
2113
- * readonly client_id: string;
2115
+ * readonly client_id: import("./ids.js").EntityId;
2114
2116
  * readonly scope: string;
2115
2117
  * readonly exp: number;
2116
2118
  * readonly iat: number;
2117
- * readonly sid: string | null;
2119
+ * readonly sid: import("./ids.js").EntityId | null;
2118
2120
  * readonly principal_kind: "user" | "service";
2119
2121
  * }} TsAuthOidcAccessIntrospection
2120
2122
  */
2121
2123
  /**
2122
2124
  * @typedef {Omit<TsAuthOidcAccessIntrospection, "token_type" | "principal_kind"> & {
2123
2125
  * readonly token_type: "refresh_token";
2124
- * readonly family_id: string;
2126
+ * readonly family_id: import("./ids.js").EntityId;
2125
2127
  * }} TsAuthOidcRefreshIntrospection
2126
2128
  */
2127
2129
  /**
@@ -2132,7 +2134,7 @@ export class TsAuthInstanceClient {
2132
2134
  /**
2133
2135
  * @typedef {Omit<TsAuthOidcAccessIntrospection, "sid" | "principal_kind"> &
2134
2136
  * (
2135
- * | { readonly principal_kind: "user"; readonly sid: string }
2137
+ * | { readonly principal_kind: "user"; readonly sid: import("./ids.js").EntityId }
2136
2138
  * | { readonly principal_kind: "service"; readonly sid: null }
2137
2139
  * )} TsAuthOidcResourceAccessIntrospection
2138
2140
  */
@@ -2144,14 +2146,14 @@ export class TsAuthInstanceClient {
2144
2146
  * @typedef {{
2145
2147
  * readonly issuer: string; // Configured canonical provider issuer, independently trusted by the caller.
2146
2148
  * readonly audience: string; // Exact registered resource URI; never derived from the response.
2147
- * readonly introspection_client_id: string;
2149
+ * readonly introspection_client_id: import("./ids.js").EntityId;
2148
2150
  * readonly introspection_secret: string;
2149
2151
  * }} TsAuthOidcResourceCredentials
2150
2152
  */
2151
2153
 
2152
2154
  /**
2153
2155
  * @typedef {{
2154
- * readonly sub: string;
2156
+ * readonly sub: import("./ids.js").EntityId;
2155
2157
  * readonly email?: string | undefined;
2156
2158
  * readonly email_verified?: boolean | undefined;
2157
2159
  * readonly name?: string | undefined;
@@ -2726,6 +2728,7 @@ const utf8Encoder = new TextEncoder();
2726
2728
  * @returns {string}
2727
2729
  */
2728
2730
  function segment(value, name) {
2731
+ if (name.endsWith(" id")) return requireId(value, name);
2729
2732
  if (
2730
2733
  typeof value !== "string" ||
2731
2734
  value.length === 0 ||
@@ -2840,7 +2843,7 @@ function validateJwtTemplateClaims(claims) {
2840
2843
  */
2841
2844
  function clientCredentialForm(clientId, clientSecret) {
2842
2845
  return new URLSearchParams({
2843
- client_id: requireText(clientId, "clientId"),
2846
+ client_id: requireId(clientId, "clientId"),
2844
2847
  client_secret: requireText(clientSecret, "clientSecret"),
2845
2848
  });
2846
2849
  }
@@ -3166,7 +3169,7 @@ function validateOauthResourceRequest(request, update) {
3166
3169
  ]);
3167
3170
  if (!update) requireResourceAudience(request.audience);
3168
3171
  for (const [field, accepts] of /** @type {const} */ ([
3169
- ["allowed_client_ids", oauthText],
3172
+ ["allowed_client_ids", isValidId],
3170
3173
  ["allowed_grant_types", oauthGrant],
3171
3174
  ["allowed_scopes", oauthScopeToken],
3172
3175
  ])) {
@@ -3198,7 +3201,7 @@ function validateAuthorizationRequest(request) {
3198
3201
  "code_challenge_method",
3199
3202
  "prompt",
3200
3203
  ]);
3201
- requireText(request.client_id, "client_id");
3204
+ requireId(request.client_id, "client_id");
3202
3205
  requireText(request.redirect_uri, "redirect_uri");
3203
3206
  if (request.response_type !== "code")
3204
3207
  throw new TypeError("response_type must be code.");
@@ -3227,7 +3230,8 @@ function validateAuthorizationRequest(request) {
3227
3230
  * @returns {string}
3228
3231
  */
3229
3232
  function resourceBasic(clientId, secret) {
3230
- if (!oauthText(clientId) || !oauthText(secret))
3233
+ requireId(clientId, "introspection_client_id");
3234
+ if (!oauthText(secret))
3231
3235
  throw new TypeError(
3232
3236
  "Resource introspection credentials must be nonempty well-formed text.",
3233
3237
  );
@@ -3264,7 +3268,7 @@ const clientViewFields = [
3264
3268
  function isOauthClientView(value) {
3265
3269
  return (
3266
3270
  exactOauthFields(value, clientViewFields) &&
3267
- [value.id, value.client_id, value.name].every(oauthText) &&
3271
+ isValidId(value.id) && isValidId(value.client_id) && oauthText(value.name) &&
3268
3272
  [
3269
3273
  value.redirect_uris,
3270
3274
  value.post_logout_redirect_uris,
@@ -3324,12 +3328,12 @@ const resourceViewFields = [
3324
3328
  function isOauthResourceView(value) {
3325
3329
  return (
3326
3330
  exactOauthFields(value, resourceViewFields) &&
3327
- oauthText(value.id) &&
3331
+ isValidId(value.id) &&
3328
3332
  validResourceAudience(value.audience) &&
3329
- oauthStrings(value.allowed_client_ids) &&
3333
+ Array.isArray(value.allowed_client_ids) && value.allowed_client_ids.every(isValidId) &&
3330
3334
  oauthGrantsList(value.allowed_grant_types) &&
3331
3335
  oauthStrings(value.allowed_scopes) &&
3332
- oauthText(value.introspection_client_id) &&
3336
+ isValidId(value.introspection_client_id) &&
3333
3337
  value.token_endpoint_auth_method === "client_secret_basic" &&
3334
3338
  exactUnsignedInteger(value.auth_epoch) &&
3335
3339
  typeof value.enabled === "boolean" &&
@@ -3442,13 +3446,13 @@ function isAccessIntrospection(value) {
3442
3446
  exactOauthFields(value, [...introspectionFields, "principal_kind"]) &&
3443
3447
  value.active === true &&
3444
3448
  value.token_type === "Bearer" &&
3445
- [value.iss, value.sub, value.aud, value.client_id].every(oauthText) &&
3449
+ [value.iss, value.aud].every(oauthText) && [value.sub, value.client_id].every(isValidId) &&
3446
3450
  typeof value.scope === "string" &&
3447
3451
  exactUnsignedInteger(value.exp) &&
3448
3452
  exactUnsignedInteger(value.iat) &&
3449
3453
  value.exp > value.iat &&
3450
3454
  ((value.principal_kind === "user" &&
3451
- (value.sid === null || oauthText(value.sid))) ||
3455
+ (value.sid === null || isValidId(value.sid))) ||
3452
3456
  (value.principal_kind === "service" &&
3453
3457
  value.sid === null &&
3454
3458
  value.sub === value.client_id))
@@ -3462,7 +3466,7 @@ function isResourceAccessIntrospection(value) {
3462
3466
  return (
3463
3467
  isAccessIntrospection(value) &&
3464
3468
  (value.principal_kind === "user"
3465
- ? oauthText(value.sid)
3469
+ ? isValidId(value.sid)
3466
3470
  : value.sid === null && value.sub === value.client_id)
3467
3471
  );
3468
3472
  }
@@ -3475,14 +3479,12 @@ function isRefreshIntrospection(value) {
3475
3479
  exactOauthFields(value, [...introspectionFields, "family_id"]) &&
3476
3480
  value.active === true &&
3477
3481
  value.token_type === "refresh_token" &&
3478
- [value.iss, value.sub, value.aud, value.client_id, value.family_id].every(
3479
- oauthText,
3480
- ) &&
3482
+ [value.iss, value.aud].every(oauthText) && [value.sub, value.client_id, value.family_id].every(isValidId) &&
3481
3483
  typeof value.scope === "string" &&
3482
3484
  exactUnsignedInteger(value.exp) &&
3483
3485
  exactUnsignedInteger(value.iat) &&
3484
3486
  value.exp > value.iat &&
3485
- (value.sid === null || oauthText(value.sid))
3487
+ (value.sid === null || isValidId(value.sid))
3486
3488
  );
3487
3489
  }
3488
3490
  /**