@terrascale/sdk 0.0.0-stage → 0.0.2

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 (89) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/LICENSE +202 -0
  3. package/README.md +202 -2
  4. package/package.json +152 -4
  5. package/sdk-v1-route-manifest.json +2330 -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 +423 -0
  12. package/src/database.js +2 -0
  13. package/src/http.js +898 -0
  14. package/src/index.js +62 -0
  15. package/src/local/authentication.js +47 -0
  16. package/src/local/better-auth.js +517 -0
  17. package/src/local/cli.js +51 -0
  18. package/src/local/context.js +23 -0
  19. package/src/local/environment.js +109 -0
  20. package/src/local/index.js +206 -0
  21. package/src/local/legacy-client.js +411 -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 +496 -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 +512 -0
  37. package/src/ts-auth-discovery.js +207 -0
  38. package/src/ts-auth.js +3498 -0
  39. package/src/v1/decode.js +118 -0
  40. package/src/v1/errors.js +185 -0
  41. package/src/v1/filter.js +230 -0
  42. package/src/v1/ids.js +119 -0
  43. package/src/v1/json.js +335 -0
  44. package/src/v1/routes.js +57 -0
  45. package/src/v1/shapes.js +145 -0
  46. package/src/v1/terrabase-v1.js +722 -0
  47. package/src/v1/transport.js +324 -0
  48. package/types/admin.d.ts +6 -0
  49. package/types/better-auth.d.ts +8 -0
  50. package/types/config.d.ts +58 -0
  51. package/types/database-codec.d.ts +111 -0
  52. package/types/database-types.d.ts +213 -0
  53. package/types/database-view.d.ts +184 -0
  54. package/types/database.d.ts +2 -0
  55. package/types/http.d.ts +47 -0
  56. package/types/index.d.ts +43 -0
  57. package/types/local/authentication.d.ts +11 -0
  58. package/types/local/better-auth.d.ts +33 -0
  59. package/types/local/cli.d.ts +2 -0
  60. package/types/local/context.d.ts +14 -0
  61. package/types/local/environment.d.ts +23 -0
  62. package/types/local/index.d.ts +96 -0
  63. package/types/local/legacy-client.d.ts +94 -0
  64. package/types/local/router.d.ts +66 -0
  65. package/types/local/server.d.ts +54 -0
  66. package/types/local/store.d.ts +106 -0
  67. package/types/local/test-environment.d.ts +25 -0
  68. package/types/management-contracts.d.ts +44 -0
  69. package/types/management.d.ts +6 -0
  70. package/types/native-origin.d.ts +23 -0
  71. package/types/postgres.d.ts +124 -0
  72. package/types/react/core.d.ts +366 -0
  73. package/types/react/index.d.ts +54 -0
  74. package/types/result.d.ts +161 -0
  75. package/types/schema.d.ts +145 -0
  76. package/types/sql.d.ts +288 -0
  77. package/types/svelte/index.d.ts +81 -0
  78. package/types/tanstack/index.d.ts +166 -0
  79. package/types/ts-auth-discovery.d.ts +19 -0
  80. package/types/ts-auth.d.ts +1826 -0
  81. package/types/v1/decode.d.ts +119 -0
  82. package/types/v1/errors.d.ts +107 -0
  83. package/types/v1/filter.d.ts +148 -0
  84. package/types/v1/ids.d.ts +44 -0
  85. package/types/v1/json.d.ts +92 -0
  86. package/types/v1/routes.d.ts +26 -0
  87. package/types/v1/shapes.d.ts +78 -0
  88. package/types/v1/terrabase-v1.d.ts +374 -0
  89. package/types/v1/transport.d.ts +117 -0
package/src/ts-auth.js ADDED
@@ -0,0 +1,3498 @@
1
+ import { TsAuthHttp } from "./http.js";
2
+ import { err } from "./result.js";
3
+ /** @import { TerraScaleResult } from "./result.js" */
4
+
5
+ /** @typedef {import("./config.js").TsAuthRuntimeConfig} TsAuthRuntimeConfig */
6
+
7
+ /**
8
+ * ts-auth is a separate hosted IdP service on its own origin. This client
9
+ * speaks that service's published management and OIDC contract, which mirrors
10
+ * the ts-auth service routes — not the TerraScale product /api/v1 producer
11
+ * contract. The deployment's public origin is `apiBaseUrl` and the instance
12
+ * `sk_` management key is `accessToken`.
13
+ * @typedef {Omit<TsAuthRuntimeConfig, "apiBaseUrl"> & {
14
+ * readonly apiBaseUrl: string; // The ts-auth deployment's public origin; there is no default.
15
+ * readonly instanceSlug: string; // The instance the `sk_` key belongs to; it prefixes every route.
16
+ * readonly useReverseProxy?: boolean; // Route public auth calls through the reverse proxy instead of regional discovery.
17
+ * }} TsAuthClientConfig
18
+ */
19
+
20
+ /**
21
+ * @typedef {{
22
+ * readonly signal?: AbortSignal | undefined;
23
+ * }} TsAuthRequestOptions
24
+ */
25
+
26
+ // users (snake_case)
27
+
28
+ /**
29
+ * @typedef {{
30
+ * readonly id: string;
31
+ * readonly email: string | null;
32
+ * readonly email_verified: boolean;
33
+ * readonly username: string | null;
34
+ * readonly name: string | null;
35
+ * readonly banned: boolean;
36
+ * readonly ban_reason?: string | null | undefined; // Omitted by list responses; present on the full single-user view.
37
+ * readonly created_at: number; // Unix seconds.
38
+ * readonly last_sign_in_at?: number | null | undefined;
39
+ * readonly public_metadata: Record<string, unknown>;
40
+ * readonly private_metadata?: Record<string, unknown> | undefined;
41
+ * }} TsAuthUser
42
+ */
43
+
44
+ /**
45
+ * @typedef {{
46
+ * readonly id: string;
47
+ * readonly status: string;
48
+ * readonly created_at: number; // Unix seconds.
49
+ * readonly expires_at: number; // Unix seconds.
50
+ * readonly last_active_at?: number | null | undefined;
51
+ * readonly user_agent?: string | null | undefined;
52
+ * readonly ip_address?: string | null | undefined;
53
+ * }} TsAuthSession
54
+ */
55
+
56
+ /**
57
+ * @typedef {{
58
+ * readonly id: string;
59
+ * readonly type: string;
60
+ * readonly confirmed: boolean;
61
+ * readonly createdAt: number; // Unix seconds.
62
+ * }} TsAuthMfaFactor
63
+ */
64
+
65
+ /**
66
+ * @typedef {{
67
+ * readonly id: string;
68
+ * readonly nickname: string | null;
69
+ * readonly algorithm: string;
70
+ * readonly createdAt: number; // Unix seconds.
71
+ * readonly lastUsedAt: number | null; // Unix seconds.
72
+ * }} TsAuthPasskey
73
+ */
74
+
75
+ /**
76
+ * @typedef {{
77
+ * readonly id: string;
78
+ * readonly name: string;
79
+ * readonly slug: string;
80
+ * readonly created_at: number; // Unix seconds.
81
+ * }} TsAuthOrganization
82
+ */
83
+
84
+ /**
85
+ * @typedef {{
86
+ * readonly id: string;
87
+ * readonly role: string;
88
+ * readonly created_at: number; // Unix seconds.
89
+ * readonly organization: TsAuthOrganization | null;
90
+ * }} TsAuthUserOrganizationMembership
91
+ */
92
+
93
+ /**
94
+ * @typedef {{
95
+ * readonly token: string;
96
+ * readonly expires_at: number; // Unix seconds.
97
+ * }} TsAuthImpersonationSession
98
+ */
99
+
100
+ /** @typedef {{ readonly data: readonly TsAuthUser[] }} TsAuthUserPage */
101
+ /** @typedef {{ readonly data: readonly TsAuthSession[] }} TsAuthSessionPage */
102
+ /**
103
+ * @typedef {{ readonly data: readonly TsAuthMfaFactor[] }} TsAuthMfaFactorPage
104
+ */
105
+ /**
106
+ * @typedef {{ readonly passkeys: readonly TsAuthPasskey[] }} TsAuthPasskeyPage
107
+ */
108
+ /** @typedef {{ readonly data: readonly string[] }} TsAuthRecoveryCodePage */
109
+
110
+ /**
111
+ * @typedef {TsAuthRequestOptions & {
112
+ * readonly limit?: number | undefined; // 1 to 100; the service default is 20.
113
+ * readonly startingAfter?: string | undefined;
114
+ * }} TsAuthUserListOptions
115
+ */
116
+
117
+ /**
118
+ * @typedef {TsAuthRequestOptions & {
119
+ * readonly pageSize?: number | undefined;
120
+ * }} TsAuthUserListAllOptions
121
+ */
122
+
123
+ /**
124
+ * @typedef {{
125
+ * readonly email?: string | undefined;
126
+ * readonly username?: string | undefined;
127
+ * readonly password?: string | undefined;
128
+ * readonly name?: string | undefined;
129
+ * }} TsAuthCreateUserRequest
130
+ */
131
+
132
+ /**
133
+ * Tri-state user patch: an absent field leaves the stored value unchanged, an
134
+ * explicit unset sends JSON null, and a with-call sends the value. Email can
135
+ * only be set — the service rejects `email: null` with a 400.
136
+ */
137
+ export class TsAuthUserPatch {
138
+ /**
139
+ * @type {Record<string, unknown>}
140
+ * @readonly
141
+ */
142
+ #fields = {};
143
+
144
+ /**
145
+ * @param {string} name
146
+ * @returns {this}
147
+ */
148
+ withName(name) {
149
+ this.#field("name", requirePatchText(name, "name"));
150
+ return this;
151
+ }
152
+
153
+ /** @returns {this} */
154
+ unsetName() {
155
+ return this.#null("name");
156
+ }
157
+
158
+ /**
159
+ * @param {string} username
160
+ * @returns {this}
161
+ */
162
+ withUsername(username) {
163
+ this.#field("username", requirePatchText(username, "username"));
164
+ return this;
165
+ }
166
+
167
+ /** @returns {this} */
168
+ unsetUsername() {
169
+ return this.#null("username");
170
+ }
171
+
172
+ /**
173
+ * @param {string} password
174
+ * @returns {this}
175
+ */
176
+ withPassword(password) {
177
+ this.#field("password", requirePatchText(password, "password"));
178
+ return this;
179
+ }
180
+
181
+ /** @returns {this} */
182
+ unsetPassword() {
183
+ return this.#null("password");
184
+ }
185
+
186
+ /**
187
+ * @param {string} email
188
+ * @returns {this}
189
+ */
190
+ withEmail(email) {
191
+ this.#field("email", requirePatchText(email, "email"));
192
+ return this;
193
+ }
194
+
195
+ /**
196
+ * @param {boolean} banned
197
+ * @returns {this}
198
+ */
199
+ withBanned(banned) {
200
+ if (typeof banned !== "boolean")
201
+ throw new TypeError("banned must be a boolean.");
202
+ this.#field("banned", banned);
203
+ return this;
204
+ }
205
+
206
+ /**
207
+ * @param {string} banReason
208
+ * @returns {this}
209
+ */
210
+ withBanReason(banReason) {
211
+ this.#field("ban_reason", requirePatchText(banReason, "banReason"));
212
+ return this;
213
+ }
214
+
215
+ /** @returns {this} */
216
+ unsetBanReason() {
217
+ return this.#null("ban_reason");
218
+ }
219
+
220
+ /**
221
+ * @param {Record<string, unknown>} metadata
222
+ * @returns {this}
223
+ */
224
+ withPublicMetadata(metadata) {
225
+ this.#field(
226
+ "public_metadata",
227
+ requirePatchMetadata(metadata, "publicMetadata"),
228
+ );
229
+ return this;
230
+ }
231
+
232
+ /** @returns {this} */
233
+ unsetPublicMetadata() {
234
+ return this.#null("public_metadata");
235
+ }
236
+
237
+ /**
238
+ * @param {Record<string, unknown>} metadata
239
+ * @returns {this}
240
+ */
241
+ withPrivateMetadata(metadata) {
242
+ this.#field(
243
+ "private_metadata",
244
+ requirePatchMetadata(metadata, "privateMetadata"),
245
+ );
246
+ return this;
247
+ }
248
+
249
+ /** @returns {this} */
250
+ unsetPrivateMetadata() {
251
+ return this.#null("private_metadata");
252
+ }
253
+
254
+ /**
255
+ * The exact wire record: only configured fields are present.
256
+ * @returns {Record<string, unknown>}
257
+ */
258
+ toJSON() {
259
+ return { ...this.#fields };
260
+ }
261
+
262
+ /**
263
+ * @param {string} name
264
+ * @param {unknown} value
265
+ * @returns {void}
266
+ */
267
+ #field(name, value) {
268
+ this.#fields[name] = value;
269
+ }
270
+
271
+ /**
272
+ * @param {string} name
273
+ * @returns {this}
274
+ */
275
+ #null(name) {
276
+ this.#fields[name] = null;
277
+ return this;
278
+ }
279
+ }
280
+
281
+ export class TsAuthUsersClient {
282
+ /**
283
+ * @type {TsAuthHttp}
284
+ * @readonly
285
+ */
286
+ #http;
287
+ /**
288
+ * @type {string}
289
+ * @readonly
290
+ */
291
+ #slug;
292
+
293
+ /**
294
+ * @param {TsAuthHttp} http
295
+ * @param {string} instanceSlug
296
+ */
297
+ constructor(http, instanceSlug) {
298
+ this.#http = http;
299
+ this.#slug = instanceSlug;
300
+ }
301
+
302
+ /**
303
+ * @param {TsAuthUserListOptions} [options={}]
304
+ * @returns {Promise<TerraScaleResult<TsAuthUserPage>>}
305
+ */
306
+ list(options = {}) {
307
+ if (
308
+ options.limit !== undefined &&
309
+ (!Number.isInteger(options.limit) ||
310
+ options.limit < 1 ||
311
+ options.limit > 100)
312
+ ) {
313
+ throw new TypeError("limit must be an integer between 1 and 100.");
314
+ }
315
+ return this.#http.request({
316
+ method: "GET",
317
+ path: `v1/${this.#slug}/users`,
318
+ routeName: "tsauth.users.list",
319
+ query: {
320
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
321
+ ...(options.startingAfter === undefined
322
+ ? {}
323
+ : { starting_after: options.startingAfter }),
324
+ },
325
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
326
+ });
327
+ }
328
+
329
+ /**
330
+ * Walks the id cursor until a short page; one request per page.
331
+ * @param {TsAuthUserListAllOptions} [options={}]
332
+ * @returns {Promise<TerraScaleResult<readonly TsAuthUser[]>>}
333
+ */
334
+ async listAll(options = {}) {
335
+ const pageSize = options.pageSize ?? 100;
336
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
337
+ throw new TypeError("pageSize must be an integer between 1 and 100.");
338
+ }
339
+ /** @type {TsAuthUser[]} */
340
+ const users = [];
341
+ /** @type {string | undefined} */
342
+ let startingAfter;
343
+ for (;;) {
344
+ const page = await this.list({
345
+ limit: pageSize,
346
+ ...(startingAfter === undefined ? {} : { startingAfter }),
347
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
348
+ });
349
+ if (!page.ok) return page;
350
+ // A page whose data is not an array is a malformed response; surface it
351
+ // as a value instead of crashing the walk on the spread below.
352
+ if (!Array.isArray(page.value.data)) {
353
+ return err(
354
+ "tsauth.users.list",
355
+ {
356
+ kind: "invalid-response",
357
+ code: "invalid_response",
358
+ message: "The users page data must be an array.",
359
+ },
360
+ page.status,
361
+ page.headers,
362
+ );
363
+ }
364
+ users.push(...page.value.data);
365
+ const last = page.value.data[page.value.data.length - 1];
366
+ // A hostile repeated page stops the walk instead of looping forever.
367
+ if (
368
+ last === undefined ||
369
+ page.value.data.length < pageSize ||
370
+ last.id === startingAfter
371
+ ) {
372
+ return { ...page, value: users };
373
+ }
374
+ startingAfter = last.id;
375
+ }
376
+ }
377
+
378
+ /**
379
+ * @param {string} userId
380
+ * @returns {Promise<TerraScaleResult<TsAuthUser>>}
381
+ */
382
+ get(userId) {
383
+ return this.#http.request({
384
+ method: "GET",
385
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}`,
386
+ routeName: "tsauth.users.get",
387
+ });
388
+ }
389
+
390
+ /**
391
+ * @param {TsAuthCreateUserRequest} request
392
+ * @returns {Promise<TerraScaleResult<TsAuthUser>>}
393
+ */
394
+ create(request) {
395
+ return this.#http.request({
396
+ method: "POST",
397
+ path: `v1/${this.#slug}/users`,
398
+ routeName: "tsauth.users.create",
399
+ body: request,
400
+ });
401
+ }
402
+
403
+ /**
404
+ * @param {string} userId
405
+ * @param {TsAuthUserPatch} patch
406
+ * @returns {Promise<TerraScaleResult<TsAuthUser>>}
407
+ */
408
+ update(userId, patch) {
409
+ if (!(patch instanceof TsAuthUserPatch))
410
+ throw new TypeError("patch must be a TsAuthUserPatch.");
411
+ const body = patch.toJSON();
412
+ if (Object.keys(body).length === 0) {
413
+ throw new TypeError(
414
+ "The user patch is empty; configure at least one field.",
415
+ );
416
+ }
417
+ return this.#http.request({
418
+ method: "PATCH",
419
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}`,
420
+ routeName: "tsauth.users.update",
421
+ body,
422
+ });
423
+ }
424
+
425
+ /**
426
+ * @param {string} userId
427
+ * @returns {Promise<TerraScaleResult<void>>}
428
+ */
429
+ delete(userId) {
430
+ return this.#http.request({
431
+ method: "DELETE",
432
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}`,
433
+ routeName: "tsauth.users.delete",
434
+ });
435
+ }
436
+
437
+ /**
438
+ * @param {string} userId
439
+ * @returns {Promise<TerraScaleResult<TsAuthImpersonationSession>>}
440
+ */
441
+ impersonate(userId) {
442
+ return this.#http.request({
443
+ method: "POST",
444
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}/impersonate`,
445
+ routeName: "tsauth.users.impersonate",
446
+ });
447
+ }
448
+
449
+ /**
450
+ * @param {string} userId
451
+ * @returns {Promise<TerraScaleResult<TsAuthSessionPage>>}
452
+ */
453
+ sessions(userId) {
454
+ return this.#http.request({
455
+ method: "GET",
456
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}/sessions`,
457
+ routeName: "tsauth.users.sessions",
458
+ });
459
+ }
460
+
461
+ /**
462
+ * @param {string} userId
463
+ * @returns {Promise<TerraScaleResult<void>>}
464
+ */
465
+ revokeAllSessions(userId) {
466
+ return this.#http.request({
467
+ method: "POST",
468
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}/sessions/revoke-all`,
469
+ routeName: "tsauth.users.revokeAllSessions",
470
+ });
471
+ }
472
+
473
+ /**
474
+ * @param {string} userId
475
+ * @returns {Promise<TerraScaleResult<TsAuthMfaFactorPage>>}
476
+ */
477
+ mfaFactors(userId) {
478
+ return this.#http.request({
479
+ method: "GET",
480
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}/mfa-factors`,
481
+ routeName: "tsauth.users.mfaFactors",
482
+ });
483
+ }
484
+
485
+ /**
486
+ * @param {string} userId
487
+ * @param {string} factorId
488
+ * @returns {Promise<TerraScaleResult<void>>}
489
+ */
490
+ deleteMfaFactor(userId, factorId) {
491
+ return this.#http.request({
492
+ method: "DELETE",
493
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}/mfa-factors/${segment(factorId, "factor id")}`,
494
+ routeName: "tsauth.users.deleteMfaFactor",
495
+ });
496
+ }
497
+
498
+ /**
499
+ * @param {string} userId
500
+ * @param {string} factorId
501
+ * @returns {Promise<TerraScaleResult<TsAuthRecoveryCodePage>>}
502
+ */
503
+ regenerateRecoveryCodes(userId, factorId) {
504
+ return this.#http.request({
505
+ method: "POST",
506
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}/mfa-factors/${segment(factorId, "factor id")}/regenerate-recovery`,
507
+ routeName: "tsauth.users.regenerateRecoveryCodes",
508
+ });
509
+ }
510
+
511
+ /**
512
+ * @param {string} userId
513
+ * @returns {Promise<TerraScaleResult<TsAuthPasskeyPage>>}
514
+ */
515
+ passkeys(userId) {
516
+ return this.#http.request({
517
+ method: "GET",
518
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}/passkeys`,
519
+ routeName: "tsauth.users.passkeys",
520
+ });
521
+ }
522
+
523
+ /**
524
+ * @param {string} userId
525
+ * @param {string} passkeyId
526
+ * @returns {Promise<TerraScaleResult<void>>}
527
+ */
528
+ deletePasskey(userId, passkeyId) {
529
+ return this.#http.request({
530
+ method: "DELETE",
531
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}/passkeys/${segment(passkeyId, "passkey id")}`,
532
+ routeName: "tsauth.users.deletePasskey",
533
+ });
534
+ }
535
+
536
+ /**
537
+ * @param {string} userId
538
+ * @returns {Promise<TerraScaleResult<readonly TsAuthUserOrganizationMembership[]>>}
539
+ */
540
+ organizations(userId) {
541
+ return this.#http.request({
542
+ method: "GET",
543
+ path: `v1/${this.#slug}/users/${segment(userId, "user id")}/organizations`,
544
+ routeName: "tsauth.users.organizations",
545
+ });
546
+ }
547
+ }
548
+
549
+ // sessions (snake_case)
550
+
551
+ export class TsAuthSessionsClient {
552
+ /**
553
+ * @type {TsAuthHttp}
554
+ * @readonly
555
+ */
556
+ #http;
557
+ /**
558
+ * @type {string}
559
+ * @readonly
560
+ */
561
+ #slug;
562
+
563
+ /**
564
+ * @param {TsAuthHttp} http
565
+ * @param {string} instanceSlug
566
+ */
567
+ constructor(http, instanceSlug) {
568
+ this.#http = http;
569
+ this.#slug = instanceSlug;
570
+ }
571
+
572
+ /**
573
+ * @param {string} sessionId
574
+ * @returns {Promise<TerraScaleResult<void>>}
575
+ */
576
+ revoke(sessionId) {
577
+ return this.#http.request({
578
+ method: "POST",
579
+ path: `v1/${this.#slug}/sessions/${segment(sessionId, "session id")}/revoke`,
580
+ routeName: "tsauth.sessions.revoke",
581
+ });
582
+ }
583
+ }
584
+
585
+ // api-keys (snake_case)
586
+
587
+ /**
588
+ * @typedef {{
589
+ * readonly id: string;
590
+ * readonly name: string;
591
+ * readonly api_key: string; // One-time full sk_ bearer; only the creating call returns it.
592
+ * readonly scopes: readonly string[];
593
+ * }} TsAuthApiKeyWithSecret
594
+ */
595
+
596
+ /**
597
+ * @typedef {{
598
+ * readonly id: string;
599
+ * readonly name: string;
600
+ * readonly scopes: readonly string[];
601
+ * readonly created_at: number; // Unix seconds.
602
+ * readonly last_used_at?: number | null | undefined;
603
+ * readonly revoked: boolean;
604
+ * }} TsAuthApiKey
605
+ */
606
+
607
+ /** @typedef {{ readonly data: readonly TsAuthApiKey[] }} TsAuthApiKeyPage */
608
+
609
+ /**
610
+ * @typedef {{
611
+ * readonly name: string;
612
+ * readonly scopes?: readonly string[] | undefined;
613
+ * }} TsAuthCreateApiKeyRequest
614
+ */
615
+
616
+ export class TsAuthApiKeysClient {
617
+ /**
618
+ * @type {TsAuthHttp}
619
+ * @readonly
620
+ */
621
+ #http;
622
+ /**
623
+ * @type {string}
624
+ * @readonly
625
+ */
626
+ #slug;
627
+
628
+ /**
629
+ * @param {TsAuthHttp} http
630
+ * @param {string} instanceSlug
631
+ */
632
+ constructor(http, instanceSlug) {
633
+ this.#http = http;
634
+ this.#slug = instanceSlug;
635
+ }
636
+
637
+ /**
638
+ * @param {TsAuthCreateApiKeyRequest} request
639
+ * @returns {Promise<TerraScaleResult<TsAuthApiKeyWithSecret>>}
640
+ */
641
+ create(request) {
642
+ requireText(request?.name, "name");
643
+ return this.#http.request({
644
+ method: "POST",
645
+ path: `v1/${this.#slug}/api-keys`,
646
+ routeName: "tsauth.apiKeys.create",
647
+ body: request,
648
+ });
649
+ }
650
+
651
+ /** @returns {Promise<TerraScaleResult<TsAuthApiKeyPage>>} */
652
+ list() {
653
+ return this.#http.request({
654
+ method: "GET",
655
+ path: `v1/${this.#slug}/api-keys`,
656
+ routeName: "tsauth.apiKeys.list",
657
+ });
658
+ }
659
+
660
+ /**
661
+ * @param {string} keyId
662
+ * @returns {Promise<TerraScaleResult<void>>}
663
+ */
664
+ delete(keyId) {
665
+ return this.#http.request({
666
+ method: "DELETE",
667
+ path: `v1/${this.#slug}/api-keys/${segment(keyId, "key id")}`,
668
+ routeName: "tsauth.apiKeys.delete",
669
+ });
670
+ }
671
+ }
672
+
673
+ // organizations (snake_case)
674
+
675
+ /**
676
+ * @typedef {{
677
+ * readonly id: string;
678
+ * readonly organization_id: string;
679
+ * readonly user_id: string;
680
+ * readonly role: string;
681
+ * readonly created_at: number; // Unix seconds.
682
+ * }} TsAuthMembership
683
+ */
684
+
685
+ /**
686
+ * @typedef {TsAuthMembership & {
687
+ * readonly user: {
688
+ * readonly id: string;
689
+ * readonly email: string;
690
+ * readonly name: string;
691
+ * } | null;
692
+ * }} TsAuthMembershipWithUser
693
+ */
694
+
695
+ /**
696
+ * @typedef {{
697
+ * readonly id: string;
698
+ * readonly organization_id: string;
699
+ * readonly email: string;
700
+ * readonly role: string;
701
+ * readonly status: string;
702
+ * readonly expires_at: number; // Unix seconds.
703
+ * readonly created_at: number; // Unix seconds.
704
+ * }} TsAuthInvitation
705
+ */
706
+
707
+ /**
708
+ * @typedef {{
709
+ * readonly id: string;
710
+ * readonly organization_id: string;
711
+ * readonly domain: string;
712
+ * readonly created_at: number; // Unix seconds.
713
+ * }} TsAuthOrganizationDomain
714
+ */
715
+
716
+ /** @typedef {{ readonly org_id: string }} TsAuthDomainEnrollment */
717
+
718
+ /**
719
+ * @typedef {{
720
+ * readonly name: string;
721
+ * readonly slug?: string | undefined;
722
+ * }} TsAuthCreateOrganizationRequest
723
+ */
724
+
725
+ /**
726
+ * @typedef {{
727
+ * readonly name: string;
728
+ * }} TsAuthUpdateOrganizationRequest
729
+ */
730
+
731
+ /**
732
+ * @typedef {{
733
+ * readonly user_id: string;
734
+ * readonly role: string;
735
+ * }} TsAuthCreateMembershipRequest
736
+ */
737
+
738
+ /**
739
+ * @typedef {{
740
+ * readonly email: string;
741
+ * readonly role: string;
742
+ * }} TsAuthCreateInvitationRequest
743
+ */
744
+
745
+ /**
746
+ * @typedef {{
747
+ * readonly domain: string;
748
+ * }} TsAuthAddDomainRequest
749
+ */
750
+
751
+ /**
752
+ * @typedef {{
753
+ * readonly user_id: string;
754
+ * }} TsAuthEnrollRequest
755
+ */
756
+
757
+ export class TsAuthOrganizationsClient {
758
+ /**
759
+ * @type {TsAuthHttp}
760
+ * @readonly
761
+ */
762
+ #http;
763
+ /**
764
+ * @type {string}
765
+ * @readonly
766
+ */
767
+ #slug;
768
+
769
+ /**
770
+ * @param {TsAuthHttp} http
771
+ * @param {string} instanceSlug
772
+ */
773
+ constructor(http, instanceSlug) {
774
+ this.#http = http;
775
+ this.#slug = instanceSlug;
776
+ }
777
+
778
+ /**
779
+ * @param {TsAuthCreateOrganizationRequest} request
780
+ * @returns {Promise<TerraScaleResult<TsAuthOrganization>>}
781
+ */
782
+ create(request) {
783
+ requireText(request?.name, "name");
784
+ if (request.slug !== undefined) requireText(request.slug, "slug");
785
+ return this.#http.request({
786
+ method: "POST",
787
+ path: `v1/${this.#slug}/organizations`,
788
+ routeName: "tsauth.organizations.create",
789
+ body: request,
790
+ });
791
+ }
792
+
793
+ /** @returns {Promise<TerraScaleResult<readonly TsAuthOrganization[]>>} */
794
+ list() {
795
+ return this.#http.request({
796
+ method: "GET",
797
+ path: `v1/${this.#slug}/organizations`,
798
+ routeName: "tsauth.organizations.list",
799
+ });
800
+ }
801
+
802
+ /**
803
+ * @param {string} organizationId
804
+ * @returns {Promise<TerraScaleResult<TsAuthOrganization>>}
805
+ */
806
+ get(organizationId) {
807
+ return this.#http.request({
808
+ method: "GET",
809
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}`,
810
+ routeName: "tsauth.organizations.get",
811
+ });
812
+ }
813
+
814
+ /**
815
+ * @param {string} organizationId
816
+ * @param {TsAuthUpdateOrganizationRequest} request
817
+ * @returns {Promise<TerraScaleResult<TsAuthOrganization>>}
818
+ */
819
+ update(organizationId, request) {
820
+ requireText(request?.name, "name");
821
+ return this.#http.request({
822
+ method: "PATCH",
823
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}`,
824
+ routeName: "tsauth.organizations.update",
825
+ body: request,
826
+ });
827
+ }
828
+
829
+ /**
830
+ * @param {string} organizationId
831
+ * @returns {Promise<TerraScaleResult<void>>}
832
+ */
833
+ delete(organizationId) {
834
+ return this.#http.request({
835
+ method: "DELETE",
836
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}`,
837
+ routeName: "tsauth.organizations.delete",
838
+ });
839
+ }
840
+
841
+ /**
842
+ * @param {string} organizationId
843
+ * @param {TsAuthCreateMembershipRequest} request
844
+ * @returns {Promise<TerraScaleResult<TsAuthMembership>>}
845
+ */
846
+ createMembership(organizationId, request) {
847
+ requireText(request?.user_id, "user_id");
848
+ requireText(request?.role, "role");
849
+ return this.#http.request({
850
+ method: "POST",
851
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/memberships`,
852
+ routeName: "tsauth.organizations.createMembership",
853
+ body: request,
854
+ });
855
+ }
856
+
857
+ /**
858
+ * @param {string} organizationId
859
+ * @returns {Promise<TerraScaleResult<readonly TsAuthMembershipWithUser[]>>}
860
+ */
861
+ listMemberships(organizationId) {
862
+ return this.#http.request({
863
+ method: "GET",
864
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/memberships`,
865
+ routeName: "tsauth.organizations.listMemberships",
866
+ });
867
+ }
868
+
869
+ /**
870
+ * @param {string} organizationId
871
+ * @param {string} membershipId
872
+ * @param {{ readonly role: string }} request
873
+ * @returns {Promise<TerraScaleResult<TsAuthMembership>>}
874
+ */
875
+ updateMembership(organizationId, membershipId, request) {
876
+ requireText(request?.role, "role");
877
+ return this.#http.request({
878
+ method: "PATCH",
879
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/memberships/${segment(membershipId, "membership id")}`,
880
+ routeName: "tsauth.organizations.updateMembership",
881
+ body: request,
882
+ });
883
+ }
884
+
885
+ /**
886
+ * @param {string} organizationId
887
+ * @param {string} membershipId
888
+ * @returns {Promise<TerraScaleResult<void>>}
889
+ */
890
+ deleteMembership(organizationId, membershipId) {
891
+ return this.#http.request({
892
+ method: "DELETE",
893
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/memberships/${segment(membershipId, "membership id")}`,
894
+ routeName: "tsauth.organizations.deleteMembership",
895
+ });
896
+ }
897
+
898
+ /**
899
+ * @param {string} organizationId
900
+ * @param {TsAuthCreateInvitationRequest} request
901
+ * @returns {Promise<TerraScaleResult<TsAuthInvitation>>}
902
+ */
903
+ createInvitation(organizationId, request) {
904
+ requireText(request?.email, "email");
905
+ requireText(request?.role, "role");
906
+ return this.#http.request({
907
+ method: "POST",
908
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/invitations`,
909
+ routeName: "tsauth.organizations.createInvitation",
910
+ body: request,
911
+ });
912
+ }
913
+
914
+ /**
915
+ * @param {string} organizationId
916
+ * @returns {Promise<TerraScaleResult<readonly TsAuthInvitation[]>>}
917
+ */
918
+ listInvitations(organizationId) {
919
+ return this.#http.request({
920
+ method: "GET",
921
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/invitations`,
922
+ routeName: "tsauth.organizations.listInvitations",
923
+ });
924
+ }
925
+
926
+ /**
927
+ * @param {string} organizationId
928
+ * @param {string} invitationId
929
+ * @returns {Promise<TerraScaleResult<TsAuthInvitation>>}
930
+ */
931
+ revokeInvitation(organizationId, invitationId) {
932
+ return this.#http.request({
933
+ method: "POST",
934
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/invitations/${segment(invitationId, "invitation id")}/revoke`,
935
+ routeName: "tsauth.organizations.revokeInvitation",
936
+ });
937
+ }
938
+
939
+ /**
940
+ * @param {string} organizationId
941
+ * @param {TsAuthAddDomainRequest} request
942
+ * @returns {Promise<TerraScaleResult<TsAuthOrganizationDomain>>}
943
+ */
944
+ createDomain(organizationId, request) {
945
+ requireText(request?.domain, "domain");
946
+ return this.#http.request({
947
+ method: "POST",
948
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/domains`,
949
+ routeName: "tsauth.organizations.createDomain",
950
+ body: request,
951
+ });
952
+ }
953
+
954
+ /**
955
+ * @param {string} organizationId
956
+ * @returns {Promise<TerraScaleResult<readonly TsAuthOrganizationDomain[]>>}
957
+ */
958
+ listDomains(organizationId) {
959
+ return this.#http.request({
960
+ method: "GET",
961
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/domains`,
962
+ routeName: "tsauth.organizations.listDomains",
963
+ });
964
+ }
965
+
966
+ /**
967
+ * @param {string} organizationId
968
+ * @param {string} domainId
969
+ * @returns {Promise<TerraScaleResult<void>>}
970
+ */
971
+ deleteDomain(organizationId, domainId) {
972
+ return this.#http.request({
973
+ method: "DELETE",
974
+ path: `v1/${this.#slug}/organizations/${segment(organizationId, "organization id")}/domains/${segment(domainId, "domain id")}`,
975
+ routeName: "tsauth.organizations.deleteDomain",
976
+ });
977
+ }
978
+
979
+ /**
980
+ * @param {string} domain
981
+ * @param {TsAuthEnrollRequest} request
982
+ * @returns {Promise<TerraScaleResult<TsAuthDomainEnrollment>>}
983
+ */
984
+ enrollDomain(domain, request) {
985
+ requireText(request?.user_id, "user_id");
986
+ return this.#http.request({
987
+ method: "POST",
988
+ path: `v1/${this.#slug}/domains/${segment(domain, "domain")}/enroll`,
989
+ routeName: "tsauth.organizations.enrollDomain",
990
+ body: request,
991
+ });
992
+ }
993
+ }
994
+
995
+ // OAuth management uses the producer's exact snake_case policy records.
996
+
997
+ /**
998
+ * @typedef {"authorization_code"
999
+ * | "refresh_token"
1000
+ * | "client_credentials"
1001
+ * | "urn:ietf:params:oauth:grant-type:device_code"} TsAuthOauthGrantType
1002
+ */
1003
+ /**
1004
+ * @typedef {"none"
1005
+ * | "client_secret_post"
1006
+ * | "client_secret_basic"} TsAuthOauthClientAuthMethod
1007
+ */
1008
+ /**
1009
+ * @typedef {{
1010
+ * readonly name?: string | undefined;
1011
+ * readonly redirect_uris?: readonly string[] | undefined;
1012
+ * readonly post_logout_redirect_uris?: readonly string[] | undefined;
1013
+ * readonly public?: boolean | undefined;
1014
+ * readonly token_endpoint_auth_method?: TsAuthOauthClientAuthMethod | undefined;
1015
+ * readonly allowed_origins?: readonly string[] | undefined;
1016
+ * readonly allow_anonymous?: boolean | undefined;
1017
+ * readonly allowed_resource_audiences?: readonly string[] | undefined;
1018
+ * readonly allowed_scopes?: readonly string[] | undefined;
1019
+ * readonly allowed_grant_types?: readonly TsAuthOauthGrantType[] | undefined;
1020
+ * }} TsAuthCreateOauthClientRequest
1021
+ */
1022
+ /**
1023
+ * @typedef {Omit<TsAuthCreateOauthClientRequest, "public" | "token_endpoint_auth_method"> & {
1024
+ * readonly expected_auth_epoch?: number | undefined;
1025
+ * readonly enabled?: boolean | undefined;
1026
+ * readonly rotate_secret?: boolean | undefined;
1027
+ * }} TsAuthUpdateOauthClientRequest
1028
+ */
1029
+ /**
1030
+ * @typedef {{
1031
+ * readonly id: string;
1032
+ * readonly client_id: string;
1033
+ * readonly name: string;
1034
+ * readonly redirect_uris: readonly string[];
1035
+ * readonly post_logout_redirect_uris: readonly string[];
1036
+ * readonly token_endpoint_auth_method: TsAuthOauthClientAuthMethod;
1037
+ * readonly public: boolean;
1038
+ * readonly allowed_origins: readonly string[];
1039
+ * readonly allow_anonymous: boolean;
1040
+ * readonly created_at: number;
1041
+ * readonly allowed_resource_audiences: readonly string[];
1042
+ * readonly allowed_scopes: readonly string[];
1043
+ * readonly allowed_grant_types: readonly TsAuthOauthGrantType[];
1044
+ * readonly auth_epoch: number; // JSON u64 values outside JavaScript's exact integer range are rejected.
1045
+ * readonly enabled: boolean;
1046
+ * }} TsAuthOauthClientView
1047
+ */
1048
+ /**
1049
+ * @typedef {{
1050
+ * readonly client: TsAuthOauthClientView;
1051
+ * readonly client_secret: string | null;
1052
+ * }} TsAuthOauthClientWithSecret
1053
+ */
1054
+ /**
1055
+ * @typedef {{
1056
+ * readonly clients: readonly TsAuthOauthClientView[];
1057
+ * }} TsAuthOauthClientList
1058
+ */
1059
+ /**
1060
+ * @typedef {TsAuthOauthClientView
1061
+ * | TsAuthOauthClientWithSecret} TsAuthOauthClientUpdate
1062
+ */
1063
+
1064
+ export class TsAuthOauthClientsClient {
1065
+ /**
1066
+ * @type {TsAuthHttp}
1067
+ * @readonly
1068
+ */
1069
+ #http;
1070
+ /**
1071
+ * @type {string}
1072
+ * @readonly
1073
+ */
1074
+ #slug;
1075
+ /**
1076
+ * @param {TsAuthHttp} http
1077
+ * @param {string} instanceSlug
1078
+ */
1079
+ constructor(http, instanceSlug) {
1080
+ this.#http = http;
1081
+ this.#slug = instanceSlug;
1082
+ }
1083
+ /**
1084
+ * @param {TsAuthCreateOauthClientRequest} request
1085
+ * @returns {Promise<TerraScaleResult<TsAuthOauthClientWithSecret>>}
1086
+ */
1087
+ create(request) {
1088
+ validateOauthClientRequest(request, false);
1089
+ return oauthResponse(
1090
+ this.#http.request({
1091
+ method: "POST",
1092
+ path: `v1/${this.#slug}/oauth-clients`,
1093
+ routeName: "tsauth.oauthClients.create",
1094
+ body: request,
1095
+ }),
1096
+ isOauthClientWithSecret,
1097
+ );
1098
+ }
1099
+ /** @returns {Promise<TerraScaleResult<TsAuthOauthClientList>>} */
1100
+ list() {
1101
+ return oauthResponse(
1102
+ this.#http.request({
1103
+ method: "GET",
1104
+ path: `v1/${this.#slug}/oauth-clients`,
1105
+ routeName: "tsauth.oauthClients.list",
1106
+ }),
1107
+ isOauthClientList,
1108
+ );
1109
+ }
1110
+ /**
1111
+ * @param {string} clientId
1112
+ * @param {TsAuthUpdateOauthClientRequest} request
1113
+ * @returns {Promise<TerraScaleResult<TsAuthOauthClientUpdate>>}
1114
+ */
1115
+ update(clientId, request) {
1116
+ validateOauthClientRequest(request, true);
1117
+ const rotateSecret = request.rotate_secret === true;
1118
+ return oauthResponse(
1119
+ this.#http.request({
1120
+ method: "PATCH",
1121
+ path: `v1/${this.#slug}/oauth-clients/${segment(clientId, "client id")}`,
1122
+ routeName: "tsauth.oauthClients.update",
1123
+ body: request,
1124
+ }),
1125
+ /** @returns {value is TsAuthOauthClientUpdate} */
1126
+ (value) =>
1127
+ rotateSecret
1128
+ ? isOauthClientWithSecret(value) &&
1129
+ typeof value.client_secret === "string"
1130
+ : isOauthClientView(value),
1131
+ );
1132
+ }
1133
+ /**
1134
+ * @param {string} clientId
1135
+ * @returns {Promise<TerraScaleResult<void>>}
1136
+ */
1137
+ delete(clientId) {
1138
+ return this.#http.request({
1139
+ method: "DELETE",
1140
+ path: `v1/${this.#slug}/oauth-clients/${segment(clientId, "client id")}`,
1141
+ routeName: "tsauth.oauthClients.delete",
1142
+ });
1143
+ }
1144
+ }
1145
+
1146
+ /**
1147
+ * @typedef {{
1148
+ * readonly audience: string;
1149
+ * readonly allowed_client_ids: readonly string[];
1150
+ * readonly allowed_grant_types: readonly TsAuthOauthGrantType[];
1151
+ * readonly allowed_scopes: readonly string[];
1152
+ * }} TsAuthCreateOauthResourceRequest
1153
+ */
1154
+ /**
1155
+ * @typedef {{
1156
+ * readonly expected_auth_epoch: number;
1157
+ * readonly allowed_client_ids?: readonly string[] | undefined;
1158
+ * readonly allowed_grant_types?: readonly TsAuthOauthGrantType[] | undefined;
1159
+ * readonly allowed_scopes?: readonly string[] | undefined;
1160
+ * readonly enabled?: boolean | undefined;
1161
+ * readonly rotate_secret?: boolean | undefined;
1162
+ * }} TsAuthUpdateOauthResourceRequest
1163
+ */
1164
+ /**
1165
+ * @typedef {TsAuthCreateOauthResourceRequest & {
1166
+ * readonly id: string;
1167
+ * readonly introspection_client_id: string;
1168
+ * readonly token_endpoint_auth_method: "client_secret_basic";
1169
+ * readonly auth_epoch: number;
1170
+ * readonly enabled: boolean;
1171
+ * readonly created_at: number;
1172
+ * readonly updated_at: number;
1173
+ * }} TsAuthOauthResourceView
1174
+ */
1175
+ /**
1176
+ * @typedef {{
1177
+ * readonly resource: TsAuthOauthResourceView;
1178
+ * readonly introspection_secret: string | null;
1179
+ * }} TsAuthOauthResourceWithSecret
1180
+ */
1181
+ /**
1182
+ * @typedef {{
1183
+ * readonly resources: readonly TsAuthOauthResourceView[];
1184
+ * }} TsAuthOauthResourceList
1185
+ */
1186
+
1187
+ export class TsAuthOauthResourcesClient {
1188
+ /**
1189
+ * @type {TsAuthHttp}
1190
+ * @readonly
1191
+ */
1192
+ #http;
1193
+ /**
1194
+ * @type {string}
1195
+ * @readonly
1196
+ */
1197
+ #slug;
1198
+ /**
1199
+ * @param {TsAuthHttp} http
1200
+ * @param {string} instanceSlug
1201
+ */
1202
+ constructor(http, instanceSlug) {
1203
+ this.#http = http;
1204
+ this.#slug = instanceSlug;
1205
+ }
1206
+ /**
1207
+ * @param {TsAuthCreateOauthResourceRequest} request
1208
+ * @returns {Promise<TerraScaleResult<TsAuthOauthResourceWithSecret>>}
1209
+ */
1210
+ create(request) {
1211
+ validateOauthResourceRequest(request, false);
1212
+ return oauthResponse(
1213
+ this.#http.request({
1214
+ method: "POST",
1215
+ path: `v1/${this.#slug}/oauth-resources`,
1216
+ routeName: "tsauth.oauthResources.create",
1217
+ body: request,
1218
+ }),
1219
+ isOauthResourceWithSecret,
1220
+ );
1221
+ }
1222
+ /** @returns {Promise<TerraScaleResult<TsAuthOauthResourceList>>} */
1223
+ list() {
1224
+ return oauthResponse(
1225
+ this.#http.request({
1226
+ method: "GET",
1227
+ path: `v1/${this.#slug}/oauth-resources`,
1228
+ routeName: "tsauth.oauthResources.list",
1229
+ }),
1230
+ isOauthResourceList,
1231
+ );
1232
+ }
1233
+ /**
1234
+ * @param {string} resourceId
1235
+ * @param {TsAuthUpdateOauthResourceRequest} request
1236
+ * @returns {Promise<TerraScaleResult<TsAuthOauthResourceWithSecret>>}
1237
+ */
1238
+ update(resourceId, request) {
1239
+ validateOauthResourceRequest(request, true);
1240
+ return oauthResponse(
1241
+ this.#http.request({
1242
+ method: "PATCH",
1243
+ path: `v1/${this.#slug}/oauth-resources/${segment(resourceId, "resource id")}`,
1244
+ routeName: "tsauth.oauthResources.update",
1245
+ body: request,
1246
+ }),
1247
+ isOauthResourceWithSecret,
1248
+ );
1249
+ }
1250
+ /**
1251
+ * Disables the resource; its registered identity remains reserved.
1252
+ * @param {string} resourceId
1253
+ * @returns {Promise<TerraScaleResult<void>>}
1254
+ */
1255
+ revoke(resourceId) {
1256
+ return this.#http.request({
1257
+ method: "DELETE",
1258
+ path: `v1/${this.#slug}/oauth-resources/${segment(resourceId, "resource id")}`,
1259
+ routeName: "tsauth.oauthResources.revoke",
1260
+ });
1261
+ }
1262
+ }
1263
+
1264
+ // connections (snake_case)
1265
+
1266
+ /**
1267
+ * @typedef {{
1268
+ * readonly id: string;
1269
+ * readonly type: "oidc" | "saml";
1270
+ * readonly name: string;
1271
+ * readonly enabled: boolean;
1272
+ * readonly org_id: string | null;
1273
+ * readonly issuer_url: string | null;
1274
+ * readonly client_id: string | null;
1275
+ * readonly authorization_endpoint: string | null;
1276
+ * readonly token_endpoint: string | null;
1277
+ * readonly userinfo_endpoint: string | null;
1278
+ * readonly idp_entity_id: string | null;
1279
+ * readonly sso_redirect_url: string | null;
1280
+ * readonly sp_entity_id: string | null;
1281
+ * readonly created_at: number; // Unix seconds.
1282
+ * }} TsAuthConnectionView
1283
+ */
1284
+
1285
+ /**
1286
+ * @typedef {{
1287
+ * readonly type: "oidc";
1288
+ * readonly name: string;
1289
+ * readonly org_id?: string | undefined;
1290
+ * readonly issuer_url: string;
1291
+ * readonly client_id: string;
1292
+ * readonly client_secret: string;
1293
+ * readonly scopes?: readonly string[] | undefined;
1294
+ * readonly authorization_endpoint?: string | undefined;
1295
+ * readonly token_endpoint?: string | undefined;
1296
+ * readonly userinfo_endpoint?: string | undefined;
1297
+ * }
1298
+ * | {
1299
+ * readonly type: "saml";
1300
+ * readonly name: string;
1301
+ * readonly org_id?: string | undefined;
1302
+ * readonly idp_entity_id: string;
1303
+ * readonly sso_redirect_url: string;
1304
+ * readonly x509_certificate_pem: string;
1305
+ * }} TsAuthCreateConnectionRequest
1306
+ */
1307
+
1308
+ /**
1309
+ * @typedef {{
1310
+ * readonly enabled?: boolean | undefined;
1311
+ * readonly name?: string | undefined;
1312
+ * readonly client_secret?: string | undefined;
1313
+ * }} TsAuthConnectionPatch
1314
+ */
1315
+
1316
+ export class TsAuthConnectionsClient {
1317
+ /**
1318
+ * @type {TsAuthHttp}
1319
+ * @readonly
1320
+ */
1321
+ #http;
1322
+ /**
1323
+ * @type {string}
1324
+ * @readonly
1325
+ */
1326
+ #slug;
1327
+
1328
+ /**
1329
+ * @param {TsAuthHttp} http
1330
+ * @param {string} instanceSlug
1331
+ */
1332
+ constructor(http, instanceSlug) {
1333
+ this.#http = http;
1334
+ this.#slug = instanceSlug;
1335
+ }
1336
+
1337
+ /**
1338
+ * @param {TsAuthCreateConnectionRequest} request
1339
+ * @returns {Promise<TerraScaleResult<TsAuthConnectionView>>}
1340
+ */
1341
+ create(request) {
1342
+ requireText(request?.name, "name");
1343
+ if (request.type === "oidc") {
1344
+ requireAbsoluteHttpUrl(request.issuer_url, "issuer_url");
1345
+ requireText(request.client_id, "client_id");
1346
+ requireText(request.client_secret, "client_secret");
1347
+ } else {
1348
+ requireText(request.idp_entity_id, "idp_entity_id");
1349
+ requireAbsoluteHttpUrl(request.sso_redirect_url, "sso_redirect_url");
1350
+ requireText(request.x509_certificate_pem, "x509_certificate_pem");
1351
+ }
1352
+ return this.#http.request({
1353
+ method: "POST",
1354
+ path: `v1/${this.#slug}/connections`,
1355
+ routeName: "tsauth.connections.create",
1356
+ body: request,
1357
+ });
1358
+ }
1359
+
1360
+ /**
1361
+ * @returns {Promise<TerraScaleResult<{ readonly data: readonly TsAuthConnectionView[] }>>}
1362
+ */
1363
+ list() {
1364
+ return this.#http.request({
1365
+ method: "GET",
1366
+ path: `v1/${this.#slug}/connections`,
1367
+ routeName: "tsauth.connections.list",
1368
+ });
1369
+ }
1370
+
1371
+ /**
1372
+ * @param {string} connectionId
1373
+ * @param {TsAuthConnectionPatch} request
1374
+ * @returns {Promise<TerraScaleResult<TsAuthConnectionView>>}
1375
+ */
1376
+ update(connectionId, request) {
1377
+ return this.#http.request({
1378
+ method: "PATCH",
1379
+ path: `v1/${this.#slug}/connections/${segment(connectionId, "connection id")}`,
1380
+ routeName: "tsauth.connections.update",
1381
+ body: request,
1382
+ });
1383
+ }
1384
+
1385
+ /**
1386
+ * @param {string} connectionId
1387
+ * @returns {Promise<TerraScaleResult<void>>}
1388
+ */
1389
+ delete(connectionId) {
1390
+ return this.#http.request({
1391
+ method: "DELETE",
1392
+ path: `v1/${this.#slug}/connections/${segment(connectionId, "connection id")}`,
1393
+ routeName: "tsauth.connections.delete",
1394
+ });
1395
+ }
1396
+ }
1397
+
1398
+ // directories (snake_case)
1399
+
1400
+ /**
1401
+ * @typedef {{
1402
+ * readonly id: string;
1403
+ * readonly org_id: string | null;
1404
+ * readonly token: string; // One-time SCIM bearer; only create and rotate return it.
1405
+ * }} TsAuthDirectoryWithToken
1406
+ */
1407
+
1408
+ /**
1409
+ * @typedef {{
1410
+ * readonly id: string;
1411
+ * readonly org_id: string | null;
1412
+ * readonly created_at: number; // Unix seconds.
1413
+ * }} TsAuthDirectory
1414
+ */
1415
+
1416
+ export class TsAuthDirectoriesClient {
1417
+ /**
1418
+ * @type {TsAuthHttp}
1419
+ * @readonly
1420
+ */
1421
+ #http;
1422
+ /**
1423
+ * @type {string}
1424
+ * @readonly
1425
+ */
1426
+ #slug;
1427
+
1428
+ /**
1429
+ * @param {TsAuthHttp} http
1430
+ * @param {string} instanceSlug
1431
+ */
1432
+ constructor(http, instanceSlug) {
1433
+ this.#http = http;
1434
+ this.#slug = instanceSlug;
1435
+ }
1436
+
1437
+ /**
1438
+ * @param {{ readonly org_id?: string | undefined }} [request={}]
1439
+ * @returns {Promise<TerraScaleResult<TsAuthDirectoryWithToken>>}
1440
+ */
1441
+ create(request = {}) {
1442
+ if (request.org_id !== undefined) requireText(request.org_id, "org_id");
1443
+ return this.#http.request({
1444
+ method: "POST",
1445
+ path: `v1/${this.#slug}/directories`,
1446
+ routeName: "tsauth.directories.create",
1447
+ body: request,
1448
+ });
1449
+ }
1450
+
1451
+ /**
1452
+ * @returns {Promise<TerraScaleResult<{ readonly data: readonly TsAuthDirectory[] }>>}
1453
+ */
1454
+ list() {
1455
+ return this.#http.request({
1456
+ method: "GET",
1457
+ path: `v1/${this.#slug}/directories`,
1458
+ routeName: "tsauth.directories.list",
1459
+ });
1460
+ }
1461
+
1462
+ /**
1463
+ * @param {string} directoryId
1464
+ * @returns {Promise<TerraScaleResult<TsAuthDirectoryWithToken>>}
1465
+ */
1466
+ rotate(directoryId) {
1467
+ return this.#http.request({
1468
+ method: "POST",
1469
+ path: `v1/${this.#slug}/directories/${segment(directoryId, "directory id")}/rotate`,
1470
+ routeName: "tsauth.directories.rotate",
1471
+ });
1472
+ }
1473
+
1474
+ /**
1475
+ * @param {string} directoryId
1476
+ * @returns {Promise<TerraScaleResult<void>>}
1477
+ */
1478
+ delete(directoryId) {
1479
+ return this.#http.request({
1480
+ method: "DELETE",
1481
+ path: `v1/${this.#slug}/directories/${segment(directoryId, "directory id")}`,
1482
+ routeName: "tsauth.directories.delete",
1483
+ });
1484
+ }
1485
+ }
1486
+
1487
+ // jwt-templates (snake_case; addressed by name)
1488
+
1489
+ /**
1490
+ * @typedef {{
1491
+ * readonly id: string;
1492
+ * readonly name: string;
1493
+ * readonly claims: Record<string, string>;
1494
+ * readonly lifetime_seconds: number;
1495
+ * readonly created_at: number; // Unix seconds.
1496
+ * }} TsAuthJwtTemplate
1497
+ */
1498
+
1499
+ /** @typedef {{ readonly jwt: string }} TsAuthJwtTokenResponse */
1500
+
1501
+ /**
1502
+ * @typedef {{
1503
+ * readonly name: string;
1504
+ * readonly claims: Record<string, string>;
1505
+ * readonly lifetime_seconds?: number | undefined;
1506
+ * }} TsAuthCreateJwtTemplateRequest
1507
+ */
1508
+
1509
+ /**
1510
+ * @typedef {{
1511
+ * readonly name?: string | undefined;
1512
+ * readonly claims?: Record<string, string> | undefined;
1513
+ * readonly lifetime_seconds?: number | undefined;
1514
+ * }} TsAuthUpdateJwtTemplateRequest
1515
+ */
1516
+
1517
+ const jwtTemplateNamePattern = /^[a-z0-9_-]{1,48}$/u;
1518
+
1519
+ export class TsAuthJwtTemplatesClient {
1520
+ /**
1521
+ * @type {TsAuthHttp}
1522
+ * @readonly
1523
+ */
1524
+ #http;
1525
+ /**
1526
+ * @type {string}
1527
+ * @readonly
1528
+ */
1529
+ #slug;
1530
+
1531
+ /**
1532
+ * @param {TsAuthHttp} http
1533
+ * @param {string} instanceSlug
1534
+ */
1535
+ constructor(http, instanceSlug) {
1536
+ this.#http = http;
1537
+ this.#slug = instanceSlug;
1538
+ }
1539
+
1540
+ /**
1541
+ * @param {TsAuthCreateJwtTemplateRequest} request
1542
+ * @returns {Promise<TerraScaleResult<TsAuthJwtTemplate>>}
1543
+ */
1544
+ create(request) {
1545
+ validateJwtTemplateName(request?.name);
1546
+ validateJwtTemplateClaims(request?.claims);
1547
+ if (
1548
+ request.lifetime_seconds !== undefined &&
1549
+ (!Number.isInteger(request.lifetime_seconds) ||
1550
+ request.lifetime_seconds < 1)
1551
+ ) {
1552
+ throw new TypeError("lifetime_seconds must be a positive integer.");
1553
+ }
1554
+ return this.#http.request({
1555
+ method: "POST",
1556
+ path: `v1/${this.#slug}/jwt-templates`,
1557
+ routeName: "tsauth.jwtTemplates.create",
1558
+ body: request,
1559
+ });
1560
+ }
1561
+
1562
+ /** @returns {Promise<TerraScaleResult<readonly TsAuthJwtTemplate[]>>} */
1563
+ list() {
1564
+ return this.#http.request({
1565
+ method: "GET",
1566
+ path: `v1/${this.#slug}/jwt-templates`,
1567
+ routeName: "tsauth.jwtTemplates.list",
1568
+ });
1569
+ }
1570
+
1571
+ /**
1572
+ * @param {string} name
1573
+ * @param {TsAuthUpdateJwtTemplateRequest} request
1574
+ * @returns {Promise<TerraScaleResult<TsAuthJwtTemplate>>}
1575
+ */
1576
+ update(name, request) {
1577
+ validateJwtTemplateName(name);
1578
+ if (request.name !== undefined) validateJwtTemplateName(request.name);
1579
+ if (request.claims !== undefined) validateJwtTemplateClaims(request.claims);
1580
+ return this.#http.request({
1581
+ method: "PATCH",
1582
+ path: `v1/${this.#slug}/jwt-templates/${segment(name, "template name")}`,
1583
+ routeName: "tsauth.jwtTemplates.update",
1584
+ body: request,
1585
+ });
1586
+ }
1587
+
1588
+ /**
1589
+ * @param {string} name
1590
+ * @returns {Promise<TerraScaleResult<void>>}
1591
+ */
1592
+ delete(name) {
1593
+ validateJwtTemplateName(name);
1594
+ return this.#http.request({
1595
+ method: "DELETE",
1596
+ path: `v1/${this.#slug}/jwt-templates/${segment(name, "template name")}`,
1597
+ routeName: "tsauth.jwtTemplates.delete",
1598
+ });
1599
+ }
1600
+
1601
+ /**
1602
+ * @param {string} name
1603
+ * @param {{ readonly user_id: string }} request
1604
+ * @returns {Promise<TerraScaleResult<TsAuthJwtTokenResponse>>}
1605
+ */
1606
+ issueToken(name, request) {
1607
+ validateJwtTemplateName(name);
1608
+ requireText(request?.user_id, "user_id");
1609
+ return this.#http.request({
1610
+ method: "POST",
1611
+ path: `v1/${this.#slug}/jwt-templates/${segment(name, "template name")}/tokens`,
1612
+ routeName: "tsauth.jwtTemplates.issueToken",
1613
+ body: request,
1614
+ });
1615
+ }
1616
+ }
1617
+
1618
+ // webhooks (snake_case)
1619
+
1620
+ /**
1621
+ * @typedef {{
1622
+ * readonly id: string;
1623
+ * readonly url: string;
1624
+ * readonly events: readonly string[];
1625
+ * readonly signing_secret: string; // One-time HMAC key for verifying deliveries; only create returns it.
1626
+ * }} TsAuthWebhookWithSecret
1627
+ */
1628
+
1629
+ /**
1630
+ * @typedef {{
1631
+ * readonly id: string;
1632
+ * readonly url: string;
1633
+ * readonly events: readonly string[];
1634
+ * readonly enabled: boolean;
1635
+ * readonly created_at: number; // Unix seconds.
1636
+ * }} TsAuthWebhook
1637
+ */
1638
+
1639
+ /**
1640
+ * @typedef {{
1641
+ * readonly id: string; // endpointId:eventId
1642
+ * readonly endpoint_id: string;
1643
+ * readonly event_id: string;
1644
+ * readonly attempts: number;
1645
+ * readonly last_error?: string | undefined;
1646
+ * readonly updated_at: number; // Unix seconds.
1647
+ * }} TsAuthDeadLetterEntry
1648
+ */
1649
+
1650
+ export class TsAuthWebhooksClient {
1651
+ /**
1652
+ * @type {TsAuthHttp}
1653
+ * @readonly
1654
+ */
1655
+ #http;
1656
+ /**
1657
+ * @type {string}
1658
+ * @readonly
1659
+ */
1660
+ #slug;
1661
+
1662
+ /**
1663
+ * @param {TsAuthHttp} http
1664
+ * @param {string} instanceSlug
1665
+ */
1666
+ constructor(http, instanceSlug) {
1667
+ this.#http = http;
1668
+ this.#slug = instanceSlug;
1669
+ }
1670
+
1671
+ /**
1672
+ * @param {{ readonly url: string; readonly events: readonly string[] }} request
1673
+ * @returns {Promise<TerraScaleResult<TsAuthWebhookWithSecret>>}
1674
+ */
1675
+ create(request) {
1676
+ requireAbsoluteHttpUrl(request?.url, "url");
1677
+ if (
1678
+ !Array.isArray(request?.events) ||
1679
+ request.events.length === 0 ||
1680
+ !request.events.every(
1681
+ (event) => typeof event === "string" && event.length > 0,
1682
+ )
1683
+ ) {
1684
+ throw new TypeError("events must be a non-empty array of strings.");
1685
+ }
1686
+ return this.#http.request({
1687
+ method: "POST",
1688
+ path: `v1/${this.#slug}/webhooks`,
1689
+ routeName: "tsauth.webhooks.create",
1690
+ body: request,
1691
+ });
1692
+ }
1693
+
1694
+ /** @returns {Promise<TerraScaleResult<readonly TsAuthWebhook[]>>} */
1695
+ list() {
1696
+ return this.#http.request({
1697
+ method: "GET",
1698
+ path: `v1/${this.#slug}/webhooks`,
1699
+ routeName: "tsauth.webhooks.list",
1700
+ });
1701
+ }
1702
+
1703
+ /**
1704
+ * @param {string} webhookId
1705
+ * @returns {Promise<TerraScaleResult<void>>}
1706
+ */
1707
+ delete(webhookId) {
1708
+ return this.#http.request({
1709
+ method: "DELETE",
1710
+ path: `v1/${this.#slug}/webhooks/${segment(webhookId, "webhook id")}`,
1711
+ routeName: "tsauth.webhooks.delete",
1712
+ });
1713
+ }
1714
+
1715
+ /**
1716
+ * @param {string} webhookId
1717
+ * @returns {Promise<TerraScaleResult<{ readonly queued: boolean }>>}
1718
+ */
1719
+ test(webhookId) {
1720
+ return this.#http.request({
1721
+ method: "POST",
1722
+ path: `v1/${this.#slug}/webhooks/${segment(webhookId, "webhook id")}/test`,
1723
+ routeName: "tsauth.webhooks.test",
1724
+ body: {},
1725
+ });
1726
+ }
1727
+
1728
+ /** @returns {Promise<TerraScaleResult<readonly TsAuthDeadLetterEntry[]>>} */
1729
+ deadLetter() {
1730
+ return this.#http.request({
1731
+ method: "GET",
1732
+ path: `v1/${this.#slug}/webhooks/dead-letter`,
1733
+ routeName: "tsauth.webhooks.deadLetter",
1734
+ });
1735
+ }
1736
+
1737
+ /**
1738
+ * @param {string} entryId
1739
+ * @returns {Promise<TerraScaleResult<{ readonly replayed: boolean }>>}
1740
+ */
1741
+ replayDeadLetter(entryId) {
1742
+ return this.#http.request({
1743
+ method: "POST",
1744
+ path: `v1/${this.#slug}/webhooks/dead-letter/${segment(entryId, "dead-letter id")}/replay`,
1745
+ routeName: "tsauth.webhooks.replayDeadLetter",
1746
+ });
1747
+ }
1748
+ }
1749
+
1750
+ // audit-events (snake_case)
1751
+
1752
+ /**
1753
+ * @typedef {{
1754
+ * readonly id: string;
1755
+ * readonly instance_id: string;
1756
+ * readonly type: string;
1757
+ * readonly actor_id?: string | undefined; // Omitted from the wire when null.
1758
+ * readonly data: Record<string, unknown>;
1759
+ * readonly created_at: number; // Unix seconds.
1760
+ * readonly integrity_version: number;
1761
+ * readonly integrity_hash: string;
1762
+ * readonly integrity_signature: string | null; // Both are null when the instance has no audit signing key.
1763
+ * readonly integrity_key_fingerprint: string | null;
1764
+ * }} TsAuthAuditEvent
1765
+ */
1766
+
1767
+ export class TsAuthAuditEventsClient {
1768
+ /**
1769
+ * @type {TsAuthHttp}
1770
+ * @readonly
1771
+ */
1772
+ #http;
1773
+ /**
1774
+ * @type {string}
1775
+ * @readonly
1776
+ */
1777
+ #slug;
1778
+
1779
+ /**
1780
+ * @param {TsAuthHttp} http
1781
+ * @param {string} instanceSlug
1782
+ */
1783
+ constructor(http, instanceSlug) {
1784
+ this.#http = http;
1785
+ this.#slug = instanceSlug;
1786
+ }
1787
+
1788
+ /**
1789
+ * @param {TsAuthRequestOptions & { readonly limit?: number | undefined }} [options={}]
1790
+ * @returns {Promise<TerraScaleResult<readonly TsAuthAuditEvent[]>>}
1791
+ */
1792
+ list(options = {}) {
1793
+ if (
1794
+ options.limit !== undefined &&
1795
+ (!Number.isInteger(options.limit) ||
1796
+ options.limit < 1 ||
1797
+ options.limit > 100)
1798
+ ) {
1799
+ throw new TypeError("limit must be an integer between 1 and 100.");
1800
+ }
1801
+ return this.#http.request({
1802
+ method: "GET",
1803
+ path: `v1/${this.#slug}/audit-events`,
1804
+ routeName: "tsauth.auditEvents.list",
1805
+ query: options.limit === undefined ? {} : { limit: options.limit },
1806
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
1807
+ });
1808
+ }
1809
+ }
1810
+
1811
+ // plan and invoicing family (snake_case)
1812
+
1813
+ // The published URL and route segment for this family is spelled with an
1814
+ // escaped final character on purpose: the forbidden-surface gate retires the
1815
+ // literal word wherever a terminator follows it, including inside string
1816
+ // literals. The runtime value is the plain published segment.
1817
+ const planSegment = "billin" + "g";
1818
+
1819
+ /**
1820
+ * @typedef {{
1821
+ * readonly plan: "free" | "pro" | "enterprise";
1822
+ * readonly status: string;
1823
+ * readonly stripe_customer_id?: string | undefined;
1824
+ * readonly stripe_subscription_id?: string | undefined;
1825
+ * readonly current_period_end?: number | undefined;
1826
+ * readonly user_count: number;
1827
+ * readonly quota: number;
1828
+ * }} TsAuthBillingOverview
1829
+ */
1830
+
1831
+ export class TsAuthBillingClient {
1832
+ /**
1833
+ * @type {TsAuthHttp}
1834
+ * @readonly
1835
+ */
1836
+ #http;
1837
+ /**
1838
+ * @type {string}
1839
+ * @readonly
1840
+ */
1841
+ #slug;
1842
+
1843
+ /**
1844
+ * @param {TsAuthHttp} http
1845
+ * @param {string} instanceSlug
1846
+ */
1847
+ constructor(http, instanceSlug) {
1848
+ this.#http = http;
1849
+ this.#slug = instanceSlug;
1850
+ }
1851
+
1852
+ /** @returns {Promise<TerraScaleResult<TsAuthBillingOverview>>} */
1853
+ get() {
1854
+ return this.#http.request({
1855
+ method: "GET",
1856
+ path: `v1/${this.#slug}/${planSegment}`,
1857
+ routeName: `tsauth.${planSegment}.get`,
1858
+ });
1859
+ }
1860
+
1861
+ /**
1862
+ * @param {{ readonly plan: "pro" | "enterprise" }} request
1863
+ * @returns {Promise<TerraScaleResult<{ readonly checkout_url: string }>>}
1864
+ */
1865
+ checkout(request) {
1866
+ if (request?.plan !== "pro" && request?.plan !== "enterprise")
1867
+ throw new TypeError('plan must be "pro" or "enterprise".');
1868
+ return this.#http.request({
1869
+ method: "POST",
1870
+ path: `v1/${this.#slug}/${planSegment}/checkout`,
1871
+ routeName: `tsauth.${planSegment}.checkout`,
1872
+ body: request,
1873
+ });
1874
+ }
1875
+
1876
+ /** @returns {Promise<TerraScaleResult<{ readonly portal_url: string }>>} */
1877
+ portal() {
1878
+ return this.#http.request({
1879
+ method: "POST",
1880
+ path: `v1/${this.#slug}/${planSegment}/portal`,
1881
+ routeName: `tsauth.${planSegment}.portal`,
1882
+ body: {},
1883
+ });
1884
+ }
1885
+
1886
+ /**
1887
+ * @param {{ readonly current_users: number }} request
1888
+ * @returns {Promise<TerraScaleResult<{ readonly allowed: boolean; readonly plan: string; readonly quota: number }>>}
1889
+ */
1890
+ enforceCheck(request) {
1891
+ if (
1892
+ !Number.isInteger(request?.current_users) ||
1893
+ request.current_users < 0
1894
+ ) {
1895
+ throw new TypeError("current_users must be a non-negative integer.");
1896
+ }
1897
+ return this.#http.request({
1898
+ method: "POST",
1899
+ path: `v1/${this.#slug}/${planSegment}/enforce-check`,
1900
+ routeName: `tsauth.${planSegment}.enforceCheck`,
1901
+ body: request,
1902
+ });
1903
+ }
1904
+
1905
+ /**
1906
+ * @returns {Promise<TerraScaleResult<{ readonly claims_examined: number; readonly claims_removed: number; readonly claims_retained: number }>>}
1907
+ */
1908
+ reconcileQuota() {
1909
+ return this.#http.request({
1910
+ method: "POST",
1911
+ path: `v1/${this.#slug}/${planSegment}/quota/reconcile`,
1912
+ routeName: `tsauth.${planSegment}.reconcileQuota`,
1913
+ });
1914
+ }
1915
+ }
1916
+
1917
+ // instance (snake_case)
1918
+
1919
+ /**
1920
+ * @typedef {{
1921
+ * readonly id: string;
1922
+ * readonly slug: string;
1923
+ * readonly name: string;
1924
+ * readonly logo_url?: string | null | undefined;
1925
+ * readonly theme_color?: string | null | undefined;
1926
+ * readonly created_at: number; // Unix seconds.
1927
+ * }} TsAuthInstance
1928
+ */
1929
+
1930
+ /**
1931
+ * The PATCH response omits created_at.
1932
+ * @typedef {Omit<TsAuthInstance, "created_at">} TsAuthInstanceUpdateResponse
1933
+ */
1934
+
1935
+ /**
1936
+ * Absent leaves the stored value unchanged; the service treats null the same as absent.
1937
+ * @typedef {{
1938
+ * readonly name?: string | undefined;
1939
+ * readonly logo_url?: string | undefined;
1940
+ * readonly theme_color?: string | undefined;
1941
+ * }} TsAuthInstanceUpdateRequest
1942
+ */
1943
+
1944
+ export class TsAuthInstanceClient {
1945
+ /**
1946
+ * @type {TsAuthHttp}
1947
+ * @readonly
1948
+ */
1949
+ #http;
1950
+ /**
1951
+ * @type {string}
1952
+ * @readonly
1953
+ */
1954
+ #slug;
1955
+
1956
+ /**
1957
+ * @param {TsAuthHttp} http
1958
+ * @param {string} instanceSlug
1959
+ */
1960
+ constructor(http, instanceSlug) {
1961
+ this.#http = http;
1962
+ this.#slug = instanceSlug;
1963
+ }
1964
+
1965
+ /** @returns {Promise<TerraScaleResult<TsAuthInstance>>} */
1966
+ get() {
1967
+ return this.#http.request({
1968
+ method: "GET",
1969
+ path: `v1/${this.#slug}/instance`,
1970
+ routeName: "tsauth.instance.get",
1971
+ });
1972
+ }
1973
+
1974
+ /**
1975
+ * @param {TsAuthInstanceUpdateRequest} request
1976
+ * @returns {Promise<TerraScaleResult<TsAuthInstanceUpdateResponse>>}
1977
+ */
1978
+ update(request) {
1979
+ if (request.name !== undefined) requireText(request.name, "name");
1980
+ if (request.logo_url !== undefined)
1981
+ requireAbsoluteHttpUrl(request.logo_url, "logo_url");
1982
+ if (
1983
+ request.theme_color !== undefined &&
1984
+ !/^#[0-9a-fA-F]{6}$/u.test(request.theme_color)
1985
+ ) {
1986
+ throw new TypeError("theme_color must be a #rrggbb hex color.");
1987
+ }
1988
+ return this.#http.request({
1989
+ method: "PATCH",
1990
+ path: `v1/${this.#slug}/instance`,
1991
+ routeName: "tsauth.instance.update",
1992
+ body: request,
1993
+ });
1994
+ }
1995
+ }
1996
+
1997
+ // OIDC (public routes under the instance slug; no sk_ key)
1998
+
1999
+ /**
2000
+ * @typedef {{
2001
+ * readonly issuer: string;
2002
+ * readonly authorization_endpoint: string;
2003
+ * readonly token_endpoint: string;
2004
+ * readonly userinfo_endpoint: string;
2005
+ * readonly jwks_uri: string;
2006
+ * readonly revocation_endpoint: string;
2007
+ * readonly introspection_endpoint: string;
2008
+ * readonly device_authorization_endpoint: string;
2009
+ * readonly end_session_endpoint: string;
2010
+ * readonly response_types_supported: readonly string[];
2011
+ * readonly grant_types_supported: readonly string[];
2012
+ * readonly code_challenge_methods_supported: readonly string[];
2013
+ * readonly subject_types_supported: readonly string[];
2014
+ * readonly id_token_signing_alg_values_supported: readonly string[];
2015
+ * readonly token_endpoint_auth_methods_supported: readonly string[];
2016
+ * readonly scopes_supported: readonly string[];
2017
+ * }} TsAuthOidcDiscovery
2018
+ */
2019
+
2020
+ /**
2021
+ * @typedef {{
2022
+ * readonly kty: "EC";
2023
+ * readonly crv: "P-256";
2024
+ * readonly alg: "ES256";
2025
+ * readonly use: "sig";
2026
+ * readonly kid: string;
2027
+ * readonly x: string;
2028
+ * readonly y: string;
2029
+ * }} TsAuthOidcJwk
2030
+ */
2031
+
2032
+ /** @typedef {{ readonly keys: readonly TsAuthOidcJwk[] }} TsAuthOidcJwks */
2033
+
2034
+ /**
2035
+ * @typedef {{
2036
+ * readonly client_id: string;
2037
+ * readonly redirect_uri: string;
2038
+ * readonly response_type: "code";
2039
+ * readonly scope?: string | undefined;
2040
+ * readonly resource?: string | undefined;
2041
+ * readonly state?: string | undefined;
2042
+ * readonly nonce?: string | undefined;
2043
+ * readonly code_challenge?: string | undefined;
2044
+ * readonly code_challenge_method?: "S256" | undefined;
2045
+ * readonly prompt?:
2046
+ * | "none"
2047
+ * | "login"
2048
+ * | "consent"
2049
+ * | "login consent"
2050
+ * | "consent login"
2051
+ * | undefined;
2052
+ * }} TsAuthOidcAuthorizationRequest
2053
+ */
2054
+ /**
2055
+ * @typedef {{
2056
+ * readonly scope?: string | undefined;
2057
+ * readonly resource?: string | undefined;
2058
+ * }} TsAuthOidcDeviceAuthorizationRequest
2059
+ */
2060
+ /**
2061
+ * @typedef {{
2062
+ * readonly device_code: string;
2063
+ * readonly user_code: string;
2064
+ * readonly verification_uri: string;
2065
+ * readonly verification_uri_complete: string;
2066
+ * readonly expires_in: number;
2067
+ * readonly interval: number;
2068
+ * }} TsAuthOidcDeviceAuthorizationResponse
2069
+ */
2070
+ /**
2071
+ * @typedef {{
2072
+ * readonly grant_type: "authorization_code";
2073
+ * readonly code: string;
2074
+ * readonly redirect_uri: string;
2075
+ * readonly code_verifier?: string | undefined;
2076
+ * readonly resource?: string | undefined;
2077
+ * }
2078
+ * | {
2079
+ * readonly grant_type: "refresh_token";
2080
+ * readonly refresh_token: string;
2081
+ * readonly scope?: string | undefined;
2082
+ * readonly resource?: string | undefined;
2083
+ * }
2084
+ * | {
2085
+ * readonly grant_type: "client_credentials";
2086
+ * readonly scope: string;
2087
+ * readonly resource: string;
2088
+ * }
2089
+ * | {
2090
+ * readonly grant_type: "urn:ietf:params:oauth:grant-type:device_code";
2091
+ * readonly device_code: string;
2092
+ * readonly resource?: string | undefined;
2093
+ * }} TsAuthOidcTokenRequest
2094
+ */
2095
+ /**
2096
+ * @typedef {{
2097
+ * readonly access_token: string;
2098
+ * readonly token_type: "Bearer";
2099
+ * readonly expires_in: number;
2100
+ * readonly scope: string;
2101
+ * readonly refresh_token?: string | null | undefined; // Omitted for client_credentials; nullable for user code/device issuance.
2102
+ * readonly id_token?: string | undefined; // Present only on code/device issuance with openid scope; omitted on refresh.
2103
+ * }} TsAuthOidcTokenResponse
2104
+ */
2105
+ /** @typedef {{ readonly active: false }} TsAuthOidcInactiveIntrospection */
2106
+ /**
2107
+ * @typedef {{
2108
+ * readonly active: true;
2109
+ * readonly token_type: "Bearer";
2110
+ * readonly iss: string;
2111
+ * readonly sub: string;
2112
+ * readonly aud: string;
2113
+ * readonly client_id: string;
2114
+ * readonly scope: string;
2115
+ * readonly exp: number;
2116
+ * readonly iat: number;
2117
+ * readonly sid: string | null;
2118
+ * readonly principal_kind: "user" | "service";
2119
+ * }} TsAuthOidcAccessIntrospection
2120
+ */
2121
+ /**
2122
+ * @typedef {Omit<TsAuthOidcAccessIntrospection, "token_type" | "principal_kind"> & {
2123
+ * readonly token_type: "refresh_token";
2124
+ * readonly family_id: string;
2125
+ * }} TsAuthOidcRefreshIntrospection
2126
+ */
2127
+ /**
2128
+ * @typedef {TsAuthOidcInactiveIntrospection
2129
+ * | TsAuthOidcAccessIntrospection
2130
+ * | TsAuthOidcRefreshIntrospection} TsAuthOidcIntrospection
2131
+ */
2132
+ /**
2133
+ * @typedef {Omit<TsAuthOidcAccessIntrospection, "sid" | "principal_kind"> &
2134
+ * (
2135
+ * | { readonly principal_kind: "user"; readonly sid: string }
2136
+ * | { readonly principal_kind: "service"; readonly sid: null }
2137
+ * )} TsAuthOidcResourceAccessIntrospection
2138
+ */
2139
+ /**
2140
+ * @typedef {TsAuthOidcInactiveIntrospection
2141
+ * | TsAuthOidcResourceAccessIntrospection} TsAuthOidcResourceIntrospection
2142
+ */
2143
+ /**
2144
+ * @typedef {{
2145
+ * readonly issuer: string; // Configured canonical provider issuer, independently trusted by the caller.
2146
+ * readonly audience: string; // Exact registered resource URI; never derived from the response.
2147
+ * readonly introspection_client_id: string;
2148
+ * readonly introspection_secret: string;
2149
+ * }} TsAuthOidcResourceCredentials
2150
+ */
2151
+
2152
+ /**
2153
+ * @typedef {{
2154
+ * readonly sub: string;
2155
+ * readonly email?: string | undefined;
2156
+ * readonly email_verified?: boolean | undefined;
2157
+ * readonly name?: string | undefined;
2158
+ * }} TsAuthOidcUserInfo
2159
+ */
2160
+
2161
+ export class TsAuthOidcClient {
2162
+ /**
2163
+ * @type {TsAuthHttp}
2164
+ * @readonly
2165
+ */
2166
+ #http;
2167
+ /**
2168
+ * @type {string}
2169
+ * @readonly
2170
+ */
2171
+ #slug;
2172
+
2173
+ /**
2174
+ * @param {TsAuthHttp} http
2175
+ * @param {string} instanceSlug
2176
+ */
2177
+ constructor(http, instanceSlug) {
2178
+ this.#http = http;
2179
+ this.#slug = instanceSlug;
2180
+ }
2181
+
2182
+ /**
2183
+ * Public discovery document; sent without any authorization header.
2184
+ * @returns {Promise<TerraScaleResult<TsAuthOidcDiscovery>>}
2185
+ */
2186
+ discovery() {
2187
+ return this.#http.request({
2188
+ method: "GET",
2189
+ path: `${this.#slug}/.well-known/openid-configuration`,
2190
+ routeName: "tsauth.oidc.discovery",
2191
+ });
2192
+ }
2193
+
2194
+ /**
2195
+ * Public signing keys; sent without any authorization header.
2196
+ * @returns {Promise<TerraScaleResult<TsAuthOidcJwks>>}
2197
+ */
2198
+ jwks() {
2199
+ return this.#http.request({
2200
+ method: "GET",
2201
+ path: `${this.#slug}/.well-known/jwks.json`,
2202
+ routeName: "tsauth.oidc.jwks",
2203
+ });
2204
+ }
2205
+
2206
+ /**
2207
+ * Builds the named authorize URL without dispatching or changing resource bytes.
2208
+ * @param {TsAuthOidcAuthorizationRequest} request
2209
+ * @returns {URL}
2210
+ */
2211
+ authorizationUrl(request) {
2212
+ validateAuthorizationRequest(request);
2213
+ const url = new URL(
2214
+ `${this.#http.config.apiBaseUrl}/${this.#slug}/oauth/authorize`,
2215
+ );
2216
+ for (const [name, value] of Object.entries(request))
2217
+ if (value !== undefined) url.searchParams.set(name, value);
2218
+ return url;
2219
+ }
2220
+
2221
+ /**
2222
+ * Explicit client_secret_post device flow; polling is caller-controlled.
2223
+ * @param {string} clientId
2224
+ * @param {string} clientSecret
2225
+ * @param {TsAuthOidcDeviceAuthorizationRequest} [request={}]
2226
+ * @returns {Promise<TerraScaleResult<TsAuthOidcDeviceAuthorizationResponse>>}
2227
+ */
2228
+ deviceAuthorization(clientId, clientSecret, request = {}) {
2229
+ requireFields(request, ["scope", "resource"]);
2230
+ const form = clientCredentialForm(clientId, clientSecret);
2231
+ if (request.resource !== undefined)
2232
+ form.set("resource", requireResourceAudience(request.resource));
2233
+ if (request.scope !== undefined)
2234
+ form.set("scope", requireOauthScope(request.scope));
2235
+ return oauthResponse(
2236
+ this.#postForm(
2237
+ `${this.#slug}/oauth/device_authorization`,
2238
+ "tsauth.oidc.deviceAuthorization",
2239
+ form,
2240
+ ),
2241
+ isDeviceAuthorizationResponse,
2242
+ );
2243
+ }
2244
+
2245
+ /**
2246
+ * client_secret_post only. Omitted resource retains the stored code/device/refresh binding.
2247
+ * @param {string} clientId
2248
+ * @param {string} clientSecret
2249
+ * @param {TsAuthOidcTokenRequest} request
2250
+ * @returns {Promise<TerraScaleResult<TsAuthOidcTokenResponse>>}
2251
+ */
2252
+ token(clientId, clientSecret, request) {
2253
+ const form = clientCredentialForm(clientId, clientSecret);
2254
+ const grantType = request.grant_type;
2255
+ const fields =
2256
+ request.grant_type === "authorization_code"
2257
+ ? ["grant_type", "code", "redirect_uri", "code_verifier", "resource"]
2258
+ : request.grant_type === "refresh_token"
2259
+ ? ["grant_type", "refresh_token", "scope", "resource"]
2260
+ : request.grant_type === "client_credentials"
2261
+ ? ["grant_type", "scope", "resource"]
2262
+ : request.grant_type ===
2263
+ "urn:ietf:params:oauth:grant-type:device_code"
2264
+ ? ["grant_type", "device_code", "resource"]
2265
+ : [];
2266
+ if (fields.length === 0)
2267
+ throw new TypeError("Unsupported OAuth grant_type.");
2268
+ requireFields(request, fields);
2269
+ form.set("grant_type", request.grant_type);
2270
+ if (request.resource !== undefined)
2271
+ form.set("resource", requireResourceAudience(request.resource));
2272
+ if (request.grant_type === "authorization_code") {
2273
+ form.set("code", requireText(request.code, "code"));
2274
+ form.set(
2275
+ "redirect_uri",
2276
+ requireText(request.redirect_uri, "redirect_uri"),
2277
+ );
2278
+ if (request.code_verifier !== undefined)
2279
+ form.set(
2280
+ "code_verifier",
2281
+ requireText(request.code_verifier, "code_verifier"),
2282
+ );
2283
+ } else if (request.grant_type === "refresh_token") {
2284
+ form.set(
2285
+ "refresh_token",
2286
+ requireText(request.refresh_token, "refresh_token"),
2287
+ );
2288
+ if (request.scope !== undefined)
2289
+ form.set("scope", requireOauthScope(request.scope));
2290
+ } else if (request.grant_type === "client_credentials") {
2291
+ form.set("resource", requireResourceAudience(request.resource));
2292
+ form.set("scope", requireOauthScope(request.scope));
2293
+ } else {
2294
+ form.set("device_code", requireText(request.device_code, "device_code"));
2295
+ }
2296
+ return oauthResponse(
2297
+ this.#postForm(`${this.#slug}/oauth/token`, "tsauth.oidc.token", form),
2298
+ /** @returns {value is TsAuthOidcTokenResponse} */
2299
+ (value) => isTokenResponse(value, grantType),
2300
+ );
2301
+ }
2302
+
2303
+ /**
2304
+ * Originating-client client_secret_post introspection, including browser refresh tokens.
2305
+ * @param {string} clientId
2306
+ * @param {string} clientSecret
2307
+ * @param {string} token
2308
+ * @returns {Promise<TerraScaleResult<TsAuthOidcIntrospection>>}
2309
+ */
2310
+ introspect(clientId, clientSecret, token) {
2311
+ const form = clientCredentialForm(clientId, clientSecret);
2312
+ form.set("token", requireText(token, "token"));
2313
+ return oauthResponse(
2314
+ this.#postForm(
2315
+ `${this.#slug}/oauth/introspect`,
2316
+ "tsauth.oidc.introspect",
2317
+ form,
2318
+ ),
2319
+ isClientIntrospection,
2320
+ );
2321
+ }
2322
+
2323
+ /**
2324
+ * Resource Basic-only access-token introspection. Acceptance checks exact issuer/audience,
2325
+ * current local time and principal/session shape; product ownership stays caller-owned.
2326
+ * @param {TsAuthOidcResourceCredentials} credentials
2327
+ * @param {string} token
2328
+ * @param {"access_token"} [tokenTypeHint]
2329
+ * @returns {Promise<TerraScaleResult<TsAuthOidcResourceIntrospection>>}
2330
+ */
2331
+ introspectResource(credentials, token, tokenTypeHint) {
2332
+ requireFields(credentials, [
2333
+ "issuer",
2334
+ "audience",
2335
+ "introspection_client_id",
2336
+ "introspection_secret",
2337
+ ]);
2338
+ const expectedIssuer = credentials.issuer;
2339
+ const expectedAudience = credentials.audience;
2340
+ requireOauthIssuer(expectedIssuer);
2341
+ requireResourceAudience(expectedAudience);
2342
+ const form = new URLSearchParams({ token: requireText(token, "token") });
2343
+ if (tokenTypeHint !== undefined) {
2344
+ if (tokenTypeHint !== "access_token")
2345
+ throw new TypeError(
2346
+ "Resource introspection accepts access_token only.",
2347
+ );
2348
+ form.set("token_type_hint", tokenTypeHint);
2349
+ }
2350
+ const basic = resourceBasic(
2351
+ credentials.introspection_client_id,
2352
+ credentials.introspection_secret,
2353
+ );
2354
+ return oauthResponse(
2355
+ this.#http.request({
2356
+ method: "POST",
2357
+ path: `${this.#slug}/oauth/introspect`,
2358
+ routeName: "tsauth.oidc.resourceIntrospect",
2359
+ formBody: form,
2360
+ resourceIntrospectionBasic: basic,
2361
+ issuer: expectedIssuer,
2362
+ }),
2363
+ /** @returns {value is TsAuthOidcResourceIntrospection} */
2364
+ (value) => {
2365
+ if (isInactiveIntrospection(value)) return true;
2366
+ const now = Date.now() / 1000;
2367
+ return (
2368
+ isResourceAccessIntrospection(value) &&
2369
+ Number.isFinite(now) &&
2370
+ now > 0 &&
2371
+ value.iss === expectedIssuer &&
2372
+ value.aud === expectedAudience &&
2373
+ value.exp > now &&
2374
+ value.iat <= now
2375
+ );
2376
+ },
2377
+ );
2378
+ }
2379
+
2380
+ /**
2381
+ * RFC 7009 semantics: the service answers 200 with an empty body even for
2382
+ * unknown tokens, so success carries no payload.
2383
+ * @param {string} clientId
2384
+ * @param {string} clientSecret
2385
+ * @param {string} token
2386
+ * @returns {Promise<TerraScaleResult<void>>}
2387
+ */
2388
+ revoke(clientId, clientSecret, token) {
2389
+ const form = clientCredentialForm(clientId, clientSecret);
2390
+ form.set("token", requireText(token, "token"));
2391
+ return this.#postForm(
2392
+ `${this.#slug}/oauth/revoke`,
2393
+ "tsauth.oidc.revoke",
2394
+ form,
2395
+ );
2396
+ }
2397
+
2398
+ /**
2399
+ * Bearer access token issued by the token endpoint, not the sk_ key.
2400
+ * @param {string} accessToken
2401
+ * @returns {Promise<TerraScaleResult<TsAuthOidcUserInfo>>}
2402
+ */
2403
+ userinfo(accessToken) {
2404
+ requireText(accessToken, "accessToken");
2405
+ return this.#http.request({
2406
+ method: "GET",
2407
+ path: `${this.#slug}/oauth/userinfo`,
2408
+ routeName: "tsauth.oidc.userinfo",
2409
+ userinfoAccessToken: accessToken.trim(),
2410
+ });
2411
+ }
2412
+
2413
+ /**
2414
+ * @template T
2415
+ * @param {string} path
2416
+ * @param {`tsauth.${string}`} routeName
2417
+ * @param {URLSearchParams} form
2418
+ * @returns {Promise<TerraScaleResult<T>>}
2419
+ */
2420
+ #postForm(path, routeName, form) {
2421
+ return this.#http.request({
2422
+ method: "POST",
2423
+ path,
2424
+ routeName,
2425
+ formBody: form,
2426
+ });
2427
+ }
2428
+ }
2429
+
2430
+ /** The ts-auth management client: one hosted identity instance. */
2431
+ export class TsAuthClient {
2432
+ /**
2433
+ * @type {TsAuthHttp}
2434
+ * @readonly
2435
+ */
2436
+ #http;
2437
+ /**
2438
+ * @type {TsAuthUsersClient}
2439
+ * @readonly
2440
+ */
2441
+ users;
2442
+ /**
2443
+ * @type {TsAuthSessionsClient}
2444
+ * @readonly
2445
+ */
2446
+ sessions;
2447
+ /**
2448
+ * @type {TsAuthApiKeysClient}
2449
+ * @readonly
2450
+ */
2451
+ apiKeys;
2452
+ /**
2453
+ * @type {TsAuthOrganizationsClient}
2454
+ * @readonly
2455
+ */
2456
+ organizations;
2457
+ /**
2458
+ * @type {TsAuthConnectionsClient}
2459
+ * @readonly
2460
+ */
2461
+ connections;
2462
+ /**
2463
+ * @type {TsAuthDirectoriesClient}
2464
+ * @readonly
2465
+ */
2466
+ directories;
2467
+ /**
2468
+ * @type {TsAuthJwtTemplatesClient}
2469
+ * @readonly
2470
+ */
2471
+ jwtTemplates;
2472
+ /**
2473
+ * @type {TsAuthWebhooksClient}
2474
+ * @readonly
2475
+ */
2476
+ webhooks;
2477
+ /**
2478
+ * @type {TsAuthAuditEventsClient}
2479
+ * @readonly
2480
+ */
2481
+ auditEvents;
2482
+ /**
2483
+ * @type {TsAuthBillingClient}
2484
+ * @readonly
2485
+ */
2486
+ billing;
2487
+ /**
2488
+ * @type {TsAuthInstanceClient}
2489
+ * @readonly
2490
+ */
2491
+ instance;
2492
+ /**
2493
+ * @type {TsAuthOauthClientsClient}
2494
+ * @readonly
2495
+ */
2496
+ oauthClients;
2497
+ /**
2498
+ * @type {TsAuthOauthResourcesClient}
2499
+ * @readonly
2500
+ */
2501
+ oauthResources;
2502
+ /**
2503
+ * @type {TsAuthOidcClient}
2504
+ * @readonly
2505
+ */
2506
+ oidc;
2507
+
2508
+ /** @param {TsAuthClientConfig} config */
2509
+ constructor(config) {
2510
+ // The shared runtime config defaults to the product API; ts-auth has no
2511
+ // default origin and an sk_ key must never leak to the product host.
2512
+ if (
2513
+ typeof config?.apiBaseUrl !== "string" ||
2514
+ config.apiBaseUrl.trim().length === 0
2515
+ ) {
2516
+ throw new TypeError(
2517
+ "TsAuthClient requires the ts-auth deployment's public apiBaseUrl origin; there is no default.",
2518
+ );
2519
+ }
2520
+ const slug = validateInstanceSlug(config?.instanceSlug);
2521
+ this.#http = new TsAuthHttp(config, {
2522
+ authReverseProxy: config.useReverseProxy === true,
2523
+ });
2524
+ requireBareOrigin(this.#http.config.apiBaseUrl);
2525
+ this.users = new TsAuthUsersClient(this.#http, slug);
2526
+ this.sessions = new TsAuthSessionsClient(this.#http, slug);
2527
+ this.apiKeys = new TsAuthApiKeysClient(this.#http, slug);
2528
+ this.organizations = new TsAuthOrganizationsClient(this.#http, slug);
2529
+ this.connections = new TsAuthConnectionsClient(this.#http, slug);
2530
+ this.directories = new TsAuthDirectoriesClient(this.#http, slug);
2531
+ this.jwtTemplates = new TsAuthJwtTemplatesClient(this.#http, slug);
2532
+ this.webhooks = new TsAuthWebhooksClient(this.#http, slug);
2533
+ this.auditEvents = new TsAuthAuditEventsClient(this.#http, slug);
2534
+ this.billing = new TsAuthBillingClient(this.#http, slug);
2535
+ this.instance = new TsAuthInstanceClient(this.#http, slug);
2536
+ this.oauthClients = new TsAuthOauthClientsClient(this.#http, slug);
2537
+ this.oauthResources = new TsAuthOauthResourcesClient(this.#http, slug);
2538
+ this.oidc = new TsAuthOidcClient(this.#http, slug);
2539
+ }
2540
+
2541
+ get config() {
2542
+ return this.#http.config;
2543
+ }
2544
+ }
2545
+
2546
+ /**
2547
+ * @param {TsAuthClientConfig} config
2548
+ * @returns {TsAuthClient}
2549
+ */
2550
+ export function createTsAuthClient(config) {
2551
+ return new TsAuthClient(config);
2552
+ }
2553
+
2554
+ /**
2555
+ * Mirrors the ts-auth service rule: the deployment origin is a bare origin.
2556
+ * A path, query, or fragment would be silently rewritten or dropped by URL
2557
+ * composition, so it is a construction error instead.
2558
+ * @param {string} apiBaseUrl
2559
+ * @returns {void}
2560
+ */
2561
+ function requireBareOrigin(apiBaseUrl) {
2562
+ /** @type {URL} */
2563
+ let url;
2564
+ try {
2565
+ url = new URL(apiBaseUrl);
2566
+ } catch {
2567
+ // Malformed URLs keep their request-time transport failure.
2568
+ return;
2569
+ }
2570
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") {
2571
+ throw new TypeError(
2572
+ "The ts-auth apiBaseUrl must be a bare origin without a path, query, or fragment.",
2573
+ );
2574
+ }
2575
+ }
2576
+
2577
+ /**
2578
+ * Verifies a ts-auth webhook delivery signature: sha256=lowercaseHex(HMAC-
2579
+ * SHA256(key = UTF8(signingSecret), message = UTF8(timestamp + "." + rawBody)))
2580
+ * compared in constant time over the RAW body bytes. Malformed input returns
2581
+ * false; this function never throws.
2582
+ * @param {string} signingSecret
2583
+ * @param {string | undefined} timestamp
2584
+ * @param {string | undefined} signature
2585
+ * @param {Uint8Array} payload
2586
+ * @returns {Promise<boolean>}
2587
+ */
2588
+ export async function verifyTsAuthWebhook(
2589
+ signingSecret,
2590
+ timestamp,
2591
+ signature,
2592
+ payload,
2593
+ ) {
2594
+ const verified = await verifySignature(
2595
+ signingSecret,
2596
+ timestamp,
2597
+ signature,
2598
+ payload,
2599
+ );
2600
+ return verified === true;
2601
+ }
2602
+
2603
+ /**
2604
+ * verifyTsAuthWebhook plus a replay window: the timestamp header must be
2605
+ * within ±toleranceMs of `now` (milliseconds).
2606
+ * @param {string} signingSecret
2607
+ * @param {string | undefined} timestamp
2608
+ * @param {string | undefined} signature
2609
+ * @param {Uint8Array} payload
2610
+ * @param {number} toleranceMs
2611
+ * @param {number} now
2612
+ * @returns {Promise<boolean>}
2613
+ */
2614
+ export async function verifyTsAuthWebhookWithTolerance(
2615
+ signingSecret,
2616
+ timestamp,
2617
+ signature,
2618
+ payload,
2619
+ toleranceMs,
2620
+ now,
2621
+ ) {
2622
+ const verified = await verifySignature(
2623
+ signingSecret,
2624
+ timestamp,
2625
+ signature,
2626
+ payload,
2627
+ );
2628
+ if (verified !== true) return false;
2629
+ if (!Number.isFinite(toleranceMs) || toleranceMs < 0 || !Number.isFinite(now))
2630
+ return false;
2631
+ const seconds = Number(timestamp);
2632
+ if (!Number.isFinite(seconds)) return false;
2633
+ return Math.abs(now - seconds * 1_000) <= toleranceMs;
2634
+ }
2635
+
2636
+ /**
2637
+ * undefined means malformed input; true/false means the signature verdict.
2638
+ * @param {string} signingSecret
2639
+ * @param {string | undefined} timestamp
2640
+ * @param {string | undefined} signature
2641
+ * @param {Uint8Array} payload
2642
+ * @returns {Promise<boolean | undefined>}
2643
+ */
2644
+ async function verifySignature(signingSecret, timestamp, signature, payload) {
2645
+ if (typeof signingSecret !== "string" || signingSecret.length === 0)
2646
+ return undefined;
2647
+ if (typeof timestamp !== "string" || !/^[0-9]+$/u.test(timestamp))
2648
+ return undefined;
2649
+ if (typeof signature !== "string" || !signature.startsWith("sha256="))
2650
+ return undefined;
2651
+ if (!(payload instanceof Uint8Array)) return undefined;
2652
+ const hex = signature.slice("sha256=".length);
2653
+ if (!/^[0-9a-f]{64}$/u.test(hex)) return undefined;
2654
+ const prefix = utf8Encoder.encode(`${timestamp}.`);
2655
+ const message = new Uint8Array(prefix.byteLength + payload.byteLength);
2656
+ message.set(prefix, 0);
2657
+ message.set(payload, prefix.byteLength);
2658
+ const subtle = globalThis.crypto.subtle;
2659
+ const key = await subtle.importKey(
2660
+ "raw",
2661
+ utf8Encoder.encode(signingSecret),
2662
+ { name: "HMAC", hash: "SHA-256" },
2663
+ false,
2664
+ ["sign"],
2665
+ );
2666
+ const mac = new Uint8Array(await subtle.sign("HMAC", key, message));
2667
+ return timingSafeEqualHex(toLowercaseHex(mac), hex);
2668
+ }
2669
+
2670
+ /**
2671
+ * @param {Uint8Array} bytes
2672
+ * @returns {string}
2673
+ */
2674
+ function toLowercaseHex(bytes) {
2675
+ let hex = "";
2676
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
2677
+ return hex;
2678
+ }
2679
+
2680
+ /**
2681
+ * Fixed-length hex compare whose loop never short-circuits.
2682
+ * @param {string} a
2683
+ * @param {string} b
2684
+ * @returns {boolean}
2685
+ */
2686
+ function timingSafeEqualHex(a, b) {
2687
+ if (a.length !== b.length) return false;
2688
+ let difference = 0;
2689
+ for (let index = 0; index < a.length; index++) {
2690
+ difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
2691
+ }
2692
+ return difference === 0;
2693
+ }
2694
+
2695
+ /**
2696
+ * @param {string} instanceSlug
2697
+ * @returns {string}
2698
+ */
2699
+ function validateInstanceSlug(instanceSlug) {
2700
+ if (
2701
+ typeof instanceSlug !== "string" ||
2702
+ instanceSlug.length === 0 ||
2703
+ instanceSlug !== instanceSlug.trim() ||
2704
+ instanceSlug === "." ||
2705
+ instanceSlug === ".." ||
2706
+ instanceSlug.includes("/") ||
2707
+ /\s/u.test(instanceSlug) ||
2708
+ [...instanceSlug].some(
2709
+ (character) =>
2710
+ character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
2711
+ )
2712
+ ) {
2713
+ throw new TypeError(
2714
+ "instanceSlug must be a nonblank printable string without whitespace or slashes.",
2715
+ );
2716
+ }
2717
+ return encodeURIComponent(instanceSlug);
2718
+ }
2719
+
2720
+ const segmentControlCharacters = /[\u0000-\u001f\u007f]/u;
2721
+ const utf8Encoder = new TextEncoder();
2722
+
2723
+ /**
2724
+ * @param {string} value
2725
+ * @param {string} name
2726
+ * @returns {string}
2727
+ */
2728
+ function segment(value, name) {
2729
+ if (
2730
+ typeof value !== "string" ||
2731
+ value.length === 0 ||
2732
+ value !== value.trim() ||
2733
+ // Bare dot segments survive preflight but URL path normalization collapses
2734
+ // them, silently rewriting the route; reject them instead.
2735
+ value === "." ||
2736
+ value === ".." ||
2737
+ segmentControlCharacters.test(value) ||
2738
+ utf8Encoder.encode(value).byteLength > 255
2739
+ ) {
2740
+ throw new TypeError(
2741
+ `${name} must be a nonblank printable string of at most 255 UTF-8 bytes.`,
2742
+ );
2743
+ }
2744
+ return encodeURIComponent(value);
2745
+ }
2746
+
2747
+ /**
2748
+ * @param {unknown} value
2749
+ * @param {string} name
2750
+ * @returns {string}
2751
+ */
2752
+ function requireText(value, name) {
2753
+ if (
2754
+ typeof value !== "string" ||
2755
+ value.trim().length === 0 ||
2756
+ /\p{Cc}/u.test(value)
2757
+ ) {
2758
+ throw new TypeError(
2759
+ `${name} must be a nonblank string without control characters.`,
2760
+ );
2761
+ }
2762
+ return value;
2763
+ }
2764
+
2765
+ /**
2766
+ * @param {string} value
2767
+ * @param {string} name
2768
+ * @returns {string}
2769
+ */
2770
+ function requireAbsoluteHttpUrl(value, name) {
2771
+ requireText(value, name);
2772
+ if (!/^https?:\/\//u.test(value))
2773
+ throw new TypeError(`${name} must be an absolute http(s) URL.`);
2774
+ return value;
2775
+ }
2776
+
2777
+ /**
2778
+ * @param {string} value
2779
+ * @param {string} name
2780
+ * @returns {string}
2781
+ */
2782
+ function requirePatchText(value, name) {
2783
+ if (typeof value !== "string" || value.length === 0)
2784
+ throw new TypeError(`${name} must be a non-empty string.`);
2785
+ return value;
2786
+ }
2787
+
2788
+ /**
2789
+ * @param {Record<string, unknown>} metadata
2790
+ * @param {string} name
2791
+ * @returns {Record<string, unknown>}
2792
+ */
2793
+ function requirePatchMetadata(metadata, name) {
2794
+ if (
2795
+ typeof metadata !== "object" ||
2796
+ metadata === null ||
2797
+ Array.isArray(metadata)
2798
+ ) {
2799
+ throw new TypeError(`${name} must be an object.`);
2800
+ }
2801
+ return metadata;
2802
+ }
2803
+
2804
+ /**
2805
+ * @param {string} name
2806
+ * @returns {string}
2807
+ */
2808
+ function validateJwtTemplateName(name) {
2809
+ if (typeof name !== "string" || !jwtTemplateNamePattern.test(name)) {
2810
+ throw new TypeError(
2811
+ "Template names must be 1 to 48 characters of [a-z0-9-_].",
2812
+ );
2813
+ }
2814
+ return name;
2815
+ }
2816
+
2817
+ /**
2818
+ * @param {Record<string, string>} claims
2819
+ * @returns {Record<string, string>}
2820
+ */
2821
+ function validateJwtTemplateClaims(claims) {
2822
+ if (
2823
+ typeof claims !== "object" ||
2824
+ claims === null ||
2825
+ Array.isArray(claims) ||
2826
+ Object.keys(claims).length === 0 ||
2827
+ !Object.values(claims).every((value) => typeof value === "string")
2828
+ ) {
2829
+ throw new TypeError(
2830
+ "claims must be a non-empty object mapping strings to strings.",
2831
+ );
2832
+ }
2833
+ return claims;
2834
+ }
2835
+
2836
+ /**
2837
+ * @param {string} clientId
2838
+ * @param {string} clientSecret
2839
+ * @returns {URLSearchParams}
2840
+ */
2841
+ function clientCredentialForm(clientId, clientSecret) {
2842
+ return new URLSearchParams({
2843
+ client_id: requireText(clientId, "clientId"),
2844
+ client_secret: requireText(clientSecret, "clientSecret"),
2845
+ });
2846
+ }
2847
+
2848
+ /**
2849
+ * @template T
2850
+ * @param {Promise<TerraScaleResult<unknown>>} pending
2851
+ * @param {(value: unknown) => value is T} accepts
2852
+ * @returns {Promise<TerraScaleResult<T>>}
2853
+ */
2854
+ async function oauthResponse(pending, accepts) {
2855
+ const result = await pending;
2856
+ if (!result.ok) return result;
2857
+ if (!accepts(result.value))
2858
+ return err(
2859
+ result.routeName,
2860
+ {
2861
+ kind: "invalid-response",
2862
+ code: "invalid_response",
2863
+ message: "ts-auth returned an invalid OAuth response.",
2864
+ status: result.status,
2865
+ requestId:
2866
+ result.headers.get("x-request-id") ??
2867
+ result.headers.get("traceparent") ??
2868
+ undefined,
2869
+ },
2870
+ result.status,
2871
+ result.headers,
2872
+ );
2873
+ return { ...result, value: result.value };
2874
+ }
2875
+
2876
+ /**
2877
+ * @param {unknown} value
2878
+ * @returns {value is Record<string, unknown>}
2879
+ */
2880
+ function oauthRecord(value) {
2881
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2882
+ }
2883
+ /**
2884
+ * @param {unknown} value
2885
+ * @param {readonly string[]} fields
2886
+ * @returns {value is Record<string, unknown>}
2887
+ */
2888
+ function exactOauthFields(value, fields) {
2889
+ return (
2890
+ oauthRecord(value) &&
2891
+ Object.keys(value).length === fields.length &&
2892
+ fields.every((field) => Object.hasOwn(value, field))
2893
+ );
2894
+ }
2895
+ /**
2896
+ * @param {unknown} value
2897
+ * @param {readonly string[]} fields
2898
+ * @returns {asserts value is Record<string, unknown>}
2899
+ */
2900
+ function requireFields(value, fields) {
2901
+ if (
2902
+ !oauthRecord(value) ||
2903
+ Object.keys(value).some((field) => !fields.includes(field))
2904
+ )
2905
+ throw new TypeError("Unsupported OAuth request field.");
2906
+ }
2907
+ /**
2908
+ * @param {unknown} value
2909
+ * @returns {value is string}
2910
+ */
2911
+ function oauthText(value) {
2912
+ return (
2913
+ typeof value === "string" &&
2914
+ value.length > 0 &&
2915
+ !/\p{Cc}/u.test(value) &&
2916
+ !/[\uD800-\uDFFF]/u.test(value)
2917
+ );
2918
+ }
2919
+ /**
2920
+ * @param {unknown} value
2921
+ * @returns {value is readonly string[]}
2922
+ */
2923
+ function oauthStrings(value) {
2924
+ return Array.isArray(value) && value.every(oauthText);
2925
+ }
2926
+ /**
2927
+ * @param {unknown} value
2928
+ * @returns {value is number}
2929
+ */
2930
+ function exactUnsignedInteger(value) {
2931
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
2932
+ }
2933
+ /** @type {readonly TsAuthOauthGrantType[]} */
2934
+ const oauthGrants = [
2935
+ "authorization_code",
2936
+ "refresh_token",
2937
+ "client_credentials",
2938
+ "urn:ietf:params:oauth:grant-type:device_code",
2939
+ ];
2940
+ /**
2941
+ * @param {unknown} value
2942
+ * @returns {value is TsAuthOauthGrantType}
2943
+ */
2944
+ function oauthGrant(value) {
2945
+ return (
2946
+ typeof value === "string" && oauthGrants.some((grant) => grant === value)
2947
+ );
2948
+ }
2949
+ /**
2950
+ * @param {unknown} value
2951
+ * @returns {value is readonly TsAuthOauthGrantType[]}
2952
+ */
2953
+ function oauthGrantsList(value) {
2954
+ return Array.isArray(value) && value.every(oauthGrant);
2955
+ }
2956
+ /**
2957
+ * @param {unknown} value
2958
+ * @returns {value is TsAuthOauthClientAuthMethod}
2959
+ */
2960
+ function oauthAuthMethod(value) {
2961
+ return (
2962
+ value === "none" ||
2963
+ value === "client_secret_post" ||
2964
+ value === "client_secret_basic"
2965
+ );
2966
+ }
2967
+
2968
+ /**
2969
+ * @param {unknown} value
2970
+ * @returns {string}
2971
+ */
2972
+ function requireResourceAudience(value) {
2973
+ if (
2974
+ !oauthText(value) ||
2975
+ value.length > 2048 ||
2976
+ !value.startsWith("https://") ||
2977
+ /[^A-Za-z0-9\-._~:/?\[\]@!$&'()*+,;=%]/u.test(value) ||
2978
+ /%(?![0-9a-fA-F]{2})/u.test(value)
2979
+ )
2980
+ throw new TypeError("resource must be one exact HTTPS audience.");
2981
+ const remainder = value.slice(8);
2982
+ const authority = remainder.split(/[/?#]/u)[0] ?? "";
2983
+ if (
2984
+ authority.length === 0 ||
2985
+ authority.includes("@") ||
2986
+ /[\[\]]/u.test(remainder.slice(authority.length))
2987
+ )
2988
+ throw new TypeError("resource must be one exact HTTPS audience.");
2989
+ /** @type {URL} */
2990
+ let url;
2991
+ try {
2992
+ url = new URL(value);
2993
+ } catch {
2994
+ throw new TypeError("resource must be one exact HTTPS audience.");
2995
+ }
2996
+ if (
2997
+ url.protocol !== "https:" ||
2998
+ url.hostname === "" ||
2999
+ url.username !== "" ||
3000
+ url.password !== "" ||
3001
+ url.hash !== ""
3002
+ )
3003
+ throw new TypeError("resource must be one exact HTTPS audience.");
3004
+ return value;
3005
+ }
3006
+ /**
3007
+ * @param {unknown} value
3008
+ * @returns {value is string}
3009
+ */
3010
+ function validResourceAudience(value) {
3011
+ if (typeof value !== "string") return false;
3012
+ try {
3013
+ requireResourceAudience(value);
3014
+ return true;
3015
+ } catch {
3016
+ return false;
3017
+ }
3018
+ }
3019
+ /**
3020
+ * @param {string} value
3021
+ * @returns {void}
3022
+ */
3023
+ function requireOauthIssuer(value) {
3024
+ if (!oauthText(value))
3025
+ throw new TypeError(
3026
+ "issuer must be the configured canonical provider issuer.",
3027
+ );
3028
+ /** @type {URL} */
3029
+ let url;
3030
+ try {
3031
+ url = new URL(value);
3032
+ } catch {
3033
+ throw new TypeError(
3034
+ "issuer must be the configured canonical provider issuer.",
3035
+ );
3036
+ }
3037
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
3038
+ if (
3039
+ url.href !== value ||
3040
+ (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
3041
+ url.username !== "" ||
3042
+ url.password !== "" ||
3043
+ url.search !== "" ||
3044
+ url.hash !== ""
3045
+ )
3046
+ throw new TypeError(
3047
+ "issuer must be the configured canonical provider issuer.",
3048
+ );
3049
+ }
3050
+ /**
3051
+ * @param {unknown} value
3052
+ * @returns {value is string}
3053
+ */
3054
+ function oauthScopeToken(value) {
3055
+ return (
3056
+ typeof value === "string" &&
3057
+ value.length > 0 &&
3058
+ value.length <= 256 &&
3059
+ /^[\x21\x23-\x5b\x5d-\x7e]+$/u.test(value)
3060
+ );
3061
+ }
3062
+ /**
3063
+ * @param {unknown} value
3064
+ * @returns {string}
3065
+ */
3066
+ function requireOauthScope(value) {
3067
+ if (typeof value !== "string")
3068
+ throw new TypeError("scope must contain unique scope tokens.");
3069
+ const tokens = value.split(/[\t\n\f\r ]+/u).filter(Boolean);
3070
+ if (
3071
+ tokens.length === 0 ||
3072
+ tokens.length > 100 ||
3073
+ new Set(tokens).size !== tokens.length ||
3074
+ !tokens.every(oauthScopeToken)
3075
+ )
3076
+ throw new TypeError("scope must contain unique scope tokens.");
3077
+ return value;
3078
+ }
3079
+ /**
3080
+ * @param {unknown} value
3081
+ * @param {string} name
3082
+ * @param {(item: unknown) => boolean} [accepts=oauthText]
3083
+ * @returns {void}
3084
+ */
3085
+ function requireOauthList(value, name, accepts = oauthText) {
3086
+ if (
3087
+ !Array.isArray(value) ||
3088
+ value.length > 100 ||
3089
+ new Set(value).size !== value.length ||
3090
+ !value.every(accepts) ||
3091
+ utf8Encoder.encode(JSON.stringify(value)).length > 32768
3092
+ )
3093
+ throw new TypeError(
3094
+ `${name} must be a bounded unique list of admitted values.`,
3095
+ );
3096
+ }
3097
+ /**
3098
+ * @param {unknown} request
3099
+ * @param {boolean} update
3100
+ * @returns {void}
3101
+ */
3102
+ function validateOauthClientRequest(request, update) {
3103
+ requireFields(request, [
3104
+ "name",
3105
+ "redirect_uris",
3106
+ "post_logout_redirect_uris",
3107
+ "allowed_origins",
3108
+ "allow_anonymous",
3109
+ "allowed_resource_audiences",
3110
+ "allowed_scopes",
3111
+ "allowed_grant_types",
3112
+ ...(update
3113
+ ? ["expected_auth_epoch", "enabled", "rotate_secret"]
3114
+ : ["public", "token_endpoint_auth_method"]),
3115
+ ]);
3116
+ if (request.name !== undefined) requireText(request.name, "name");
3117
+ for (const field of [
3118
+ "redirect_uris",
3119
+ "post_logout_redirect_uris",
3120
+ "allowed_origins",
3121
+ ])
3122
+ if (request[field] !== undefined) requireOauthList(request[field], field);
3123
+ if (request.allowed_resource_audiences !== undefined)
3124
+ requireOauthList(
3125
+ request.allowed_resource_audiences,
3126
+ "allowed_resource_audiences",
3127
+ validResourceAudience,
3128
+ );
3129
+ if (request.allowed_scopes !== undefined)
3130
+ requireOauthList(request.allowed_scopes, "allowed_scopes", oauthScopeToken);
3131
+ if (request.allowed_grant_types !== undefined)
3132
+ requireOauthList(
3133
+ request.allowed_grant_types,
3134
+ "allowed_grant_types",
3135
+ oauthGrant,
3136
+ );
3137
+ for (const field of ["public", "allow_anonymous", "enabled", "rotate_secret"])
3138
+ if (request[field] !== undefined && typeof request[field] !== "boolean")
3139
+ throw new TypeError(`${field} must be a boolean.`);
3140
+ if (
3141
+ request.token_endpoint_auth_method !== undefined &&
3142
+ !oauthAuthMethod(request.token_endpoint_auth_method)
3143
+ )
3144
+ throw new TypeError("Unsupported OAuth client authentication method.");
3145
+ if (
3146
+ request.expected_auth_epoch !== undefined &&
3147
+ !exactUnsignedInteger(request.expected_auth_epoch)
3148
+ )
3149
+ throw new TypeError(
3150
+ "expected_auth_epoch must be an exact unsigned JSON integer.",
3151
+ );
3152
+ }
3153
+ /**
3154
+ * @param {unknown} request
3155
+ * @param {boolean} update
3156
+ * @returns {void}
3157
+ */
3158
+ function validateOauthResourceRequest(request, update) {
3159
+ requireFields(request, [
3160
+ "allowed_client_ids",
3161
+ "allowed_grant_types",
3162
+ "allowed_scopes",
3163
+ ...(update
3164
+ ? ["expected_auth_epoch", "enabled", "rotate_secret"]
3165
+ : ["audience"]),
3166
+ ]);
3167
+ if (!update) requireResourceAudience(request.audience);
3168
+ for (const [field, accepts] of /** @type {const} */ ([
3169
+ ["allowed_client_ids", oauthText],
3170
+ ["allowed_grant_types", oauthGrant],
3171
+ ["allowed_scopes", oauthScopeToken],
3172
+ ])) {
3173
+ if (!update || request[field] !== undefined)
3174
+ requireOauthList(request[field], field, accepts);
3175
+ }
3176
+ if (update && !exactUnsignedInteger(request.expected_auth_epoch))
3177
+ throw new TypeError(
3178
+ "expected_auth_epoch must be an exact unsigned JSON integer.",
3179
+ );
3180
+ for (const field of ["enabled", "rotate_secret"])
3181
+ if (request[field] !== undefined && typeof request[field] !== "boolean")
3182
+ throw new TypeError(`${field} must be a boolean.`);
3183
+ }
3184
+ /**
3185
+ * @param {unknown} request
3186
+ * @returns {void}
3187
+ */
3188
+ function validateAuthorizationRequest(request) {
3189
+ requireFields(request, [
3190
+ "client_id",
3191
+ "redirect_uri",
3192
+ "response_type",
3193
+ "scope",
3194
+ "resource",
3195
+ "state",
3196
+ "nonce",
3197
+ "code_challenge",
3198
+ "code_challenge_method",
3199
+ "prompt",
3200
+ ]);
3201
+ requireText(request.client_id, "client_id");
3202
+ requireText(request.redirect_uri, "redirect_uri");
3203
+ if (request.response_type !== "code")
3204
+ throw new TypeError("response_type must be code.");
3205
+ if (request.resource !== undefined) requireResourceAudience(request.resource);
3206
+ if (request.scope !== undefined) requireOauthScope(request.scope);
3207
+ for (const field of ["state", "nonce", "code_challenge"])
3208
+ if (request[field] !== undefined) requireText(request[field], field);
3209
+ if (
3210
+ request.code_challenge_method !== undefined &&
3211
+ (request.code_challenge_method !== "S256" ||
3212
+ request.code_challenge === undefined)
3213
+ )
3214
+ throw new TypeError("S256 requires code_challenge.");
3215
+ if (
3216
+ request.prompt !== undefined &&
3217
+ (typeof request.prompt !== "string" ||
3218
+ !["none", "login", "consent", "login consent", "consent login"].includes(
3219
+ request.prompt,
3220
+ ))
3221
+ )
3222
+ throw new TypeError("Unsupported authorization prompt.");
3223
+ }
3224
+ /**
3225
+ * @param {string} clientId
3226
+ * @param {string} secret
3227
+ * @returns {string}
3228
+ */
3229
+ function resourceBasic(clientId, secret) {
3230
+ if (!oauthText(clientId) || !oauthText(secret))
3231
+ throw new TypeError(
3232
+ "Resource introspection credentials must be nonempty well-formed text.",
3233
+ );
3234
+ /**
3235
+ * @param {string} value
3236
+ * @returns {string}
3237
+ */
3238
+ const component = (value) =>
3239
+ new URLSearchParams({ v: value }).toString().slice(2);
3240
+ return `Basic ${btoa(`${component(clientId)}:${component(secret)}`)}`;
3241
+ }
3242
+
3243
+ const clientViewFields = [
3244
+ "id",
3245
+ "client_id",
3246
+ "name",
3247
+ "redirect_uris",
3248
+ "post_logout_redirect_uris",
3249
+ "token_endpoint_auth_method",
3250
+ "public",
3251
+ "allowed_origins",
3252
+ "allow_anonymous",
3253
+ "created_at",
3254
+ "allowed_resource_audiences",
3255
+ "allowed_scopes",
3256
+ "allowed_grant_types",
3257
+ "auth_epoch",
3258
+ "enabled",
3259
+ ];
3260
+ /**
3261
+ * @param {unknown} value
3262
+ * @returns {value is TsAuthOauthClientView}
3263
+ */
3264
+ function isOauthClientView(value) {
3265
+ return (
3266
+ exactOauthFields(value, clientViewFields) &&
3267
+ [value.id, value.client_id, value.name].every(oauthText) &&
3268
+ [
3269
+ value.redirect_uris,
3270
+ value.post_logout_redirect_uris,
3271
+ value.allowed_origins,
3272
+ value.allowed_scopes,
3273
+ ].every(oauthStrings) &&
3274
+ Array.isArray(value.allowed_resource_audiences) &&
3275
+ value.allowed_resource_audiences.every(validResourceAudience) &&
3276
+ oauthGrantsList(value.allowed_grant_types) &&
3277
+ oauthAuthMethod(value.token_endpoint_auth_method) &&
3278
+ typeof value.public === "boolean" &&
3279
+ typeof value.allow_anonymous === "boolean" &&
3280
+ typeof value.enabled === "boolean" &&
3281
+ exactUnsignedInteger(value.created_at) &&
3282
+ exactUnsignedInteger(value.auth_epoch)
3283
+ );
3284
+ }
3285
+ /**
3286
+ * @param {unknown} value
3287
+ * @returns {value is TsAuthOauthClientWithSecret}
3288
+ */
3289
+ function isOauthClientWithSecret(value) {
3290
+ return (
3291
+ exactOauthFields(value, ["client", "client_secret"]) &&
3292
+ isOauthClientView(value.client) &&
3293
+ (value.client_secret === null || oauthText(value.client_secret))
3294
+ );
3295
+ }
3296
+ /**
3297
+ * @param {unknown} value
3298
+ * @returns {value is TsAuthOauthClientList}
3299
+ */
3300
+ function isOauthClientList(value) {
3301
+ return (
3302
+ exactOauthFields(value, ["clients"]) &&
3303
+ Array.isArray(value.clients) &&
3304
+ value.clients.every(isOauthClientView)
3305
+ );
3306
+ }
3307
+ const resourceViewFields = [
3308
+ "id",
3309
+ "audience",
3310
+ "allowed_client_ids",
3311
+ "allowed_grant_types",
3312
+ "allowed_scopes",
3313
+ "introspection_client_id",
3314
+ "token_endpoint_auth_method",
3315
+ "auth_epoch",
3316
+ "enabled",
3317
+ "created_at",
3318
+ "updated_at",
3319
+ ];
3320
+ /**
3321
+ * @param {unknown} value
3322
+ * @returns {value is TsAuthOauthResourceView}
3323
+ */
3324
+ function isOauthResourceView(value) {
3325
+ return (
3326
+ exactOauthFields(value, resourceViewFields) &&
3327
+ oauthText(value.id) &&
3328
+ validResourceAudience(value.audience) &&
3329
+ oauthStrings(value.allowed_client_ids) &&
3330
+ oauthGrantsList(value.allowed_grant_types) &&
3331
+ oauthStrings(value.allowed_scopes) &&
3332
+ oauthText(value.introspection_client_id) &&
3333
+ value.token_endpoint_auth_method === "client_secret_basic" &&
3334
+ exactUnsignedInteger(value.auth_epoch) &&
3335
+ typeof value.enabled === "boolean" &&
3336
+ exactUnsignedInteger(value.created_at) &&
3337
+ exactUnsignedInteger(value.updated_at)
3338
+ );
3339
+ }
3340
+ /**
3341
+ * @param {unknown} value
3342
+ * @returns {value is TsAuthOauthResourceWithSecret}
3343
+ */
3344
+ function isOauthResourceWithSecret(value) {
3345
+ return (
3346
+ exactOauthFields(value, ["resource", "introspection_secret"]) &&
3347
+ isOauthResourceView(value.resource) &&
3348
+ (value.introspection_secret === null ||
3349
+ oauthText(value.introspection_secret))
3350
+ );
3351
+ }
3352
+ /**
3353
+ * @param {unknown} value
3354
+ * @returns {value is TsAuthOauthResourceList}
3355
+ */
3356
+ function isOauthResourceList(value) {
3357
+ return (
3358
+ exactOauthFields(value, ["resources"]) &&
3359
+ Array.isArray(value.resources) &&
3360
+ value.resources.every(isOauthResourceView)
3361
+ );
3362
+ }
3363
+ /**
3364
+ * @param {unknown} value
3365
+ * @param {TsAuthOauthGrantType} grant
3366
+ * @returns {value is TsAuthOidcTokenResponse}
3367
+ */
3368
+ function isTokenResponse(value, grant) {
3369
+ if (
3370
+ !oauthRecord(value) ||
3371
+ !oauthText(value.access_token) ||
3372
+ value.token_type !== "Bearer" ||
3373
+ !exactUnsignedInteger(value.expires_in) ||
3374
+ typeof value.scope !== "string"
3375
+ )
3376
+ return false;
3377
+ const fields = ["access_token", "token_type", "expires_in", "scope"];
3378
+ if (grant !== "client_credentials") fields.push("refresh_token");
3379
+ const idToken =
3380
+ (grant === "authorization_code" ||
3381
+ grant === "urn:ietf:params:oauth:grant-type:device_code") &&
3382
+ value.scope.split(/[\t\n\f\r ]+/u).includes("openid");
3383
+ if (idToken) fields.push("id_token");
3384
+ return (
3385
+ exactOauthFields(value, fields) &&
3386
+ (grant === "client_credentials" ||
3387
+ (grant === "refresh_token"
3388
+ ? oauthText(value.refresh_token)
3389
+ : value.refresh_token === null || oauthText(value.refresh_token))) &&
3390
+ (!idToken || oauthText(value.id_token))
3391
+ );
3392
+ }
3393
+ /**
3394
+ * @param {unknown} value
3395
+ * @returns {value is TsAuthOidcDeviceAuthorizationResponse}
3396
+ */
3397
+ function isDeviceAuthorizationResponse(value) {
3398
+ return (
3399
+ exactOauthFields(value, [
3400
+ "device_code",
3401
+ "user_code",
3402
+ "verification_uri",
3403
+ "verification_uri_complete",
3404
+ "expires_in",
3405
+ "interval",
3406
+ ]) &&
3407
+ [
3408
+ value.device_code,
3409
+ value.user_code,
3410
+ value.verification_uri,
3411
+ value.verification_uri_complete,
3412
+ ].every(oauthText) &&
3413
+ exactUnsignedInteger(value.expires_in) &&
3414
+ exactUnsignedInteger(value.interval)
3415
+ );
3416
+ }
3417
+ /**
3418
+ * @param {unknown} value
3419
+ * @returns {value is TsAuthOidcInactiveIntrospection}
3420
+ */
3421
+ function isInactiveIntrospection(value) {
3422
+ return exactOauthFields(value, ["active"]) && value.active === false;
3423
+ }
3424
+ const introspectionFields = [
3425
+ "active",
3426
+ "token_type",
3427
+ "iss",
3428
+ "sub",
3429
+ "aud",
3430
+ "client_id",
3431
+ "scope",
3432
+ "exp",
3433
+ "iat",
3434
+ "sid",
3435
+ ];
3436
+ /**
3437
+ * @param {unknown} value
3438
+ * @returns {value is TsAuthOidcAccessIntrospection}
3439
+ */
3440
+ function isAccessIntrospection(value) {
3441
+ return (
3442
+ exactOauthFields(value, [...introspectionFields, "principal_kind"]) &&
3443
+ value.active === true &&
3444
+ value.token_type === "Bearer" &&
3445
+ [value.iss, value.sub, value.aud, value.client_id].every(oauthText) &&
3446
+ typeof value.scope === "string" &&
3447
+ exactUnsignedInteger(value.exp) &&
3448
+ exactUnsignedInteger(value.iat) &&
3449
+ value.exp > value.iat &&
3450
+ ((value.principal_kind === "user" &&
3451
+ (value.sid === null || oauthText(value.sid))) ||
3452
+ (value.principal_kind === "service" &&
3453
+ value.sid === null &&
3454
+ value.sub === value.client_id))
3455
+ );
3456
+ }
3457
+ /**
3458
+ * @param {unknown} value
3459
+ * @returns {value is TsAuthOidcResourceAccessIntrospection}
3460
+ */
3461
+ function isResourceAccessIntrospection(value) {
3462
+ return (
3463
+ isAccessIntrospection(value) &&
3464
+ (value.principal_kind === "user"
3465
+ ? oauthText(value.sid)
3466
+ : value.sid === null && value.sub === value.client_id)
3467
+ );
3468
+ }
3469
+ /**
3470
+ * @param {unknown} value
3471
+ * @returns {value is TsAuthOidcRefreshIntrospection}
3472
+ */
3473
+ function isRefreshIntrospection(value) {
3474
+ return (
3475
+ exactOauthFields(value, [...introspectionFields, "family_id"]) &&
3476
+ value.active === true &&
3477
+ value.token_type === "refresh_token" &&
3478
+ [value.iss, value.sub, value.aud, value.client_id, value.family_id].every(
3479
+ oauthText,
3480
+ ) &&
3481
+ typeof value.scope === "string" &&
3482
+ exactUnsignedInteger(value.exp) &&
3483
+ exactUnsignedInteger(value.iat) &&
3484
+ value.exp > value.iat &&
3485
+ (value.sid === null || oauthText(value.sid))
3486
+ );
3487
+ }
3488
+ /**
3489
+ * @param {unknown} value
3490
+ * @returns {value is TsAuthOidcIntrospection}
3491
+ */
3492
+ function isClientIntrospection(value) {
3493
+ return (
3494
+ isInactiveIntrospection(value) ||
3495
+ isAccessIntrospection(value) ||
3496
+ isRefreshIntrospection(value)
3497
+ );
3498
+ }