terrascale 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +143 -0
  3. package/package.json +159 -0
  4. package/sdk-current-contract.json +27 -0
  5. package/sdk-route-manifest.json +67 -0
  6. package/src/admin.js +9 -0
  7. package/src/better-auth.js +14 -0
  8. package/src/config.js +121 -0
  9. package/src/database-codec.js +845 -0
  10. package/src/database-types.js +237 -0
  11. package/src/database-view.js +422 -0
  12. package/src/database.js +420 -0
  13. package/src/discovery.js +374 -0
  14. package/src/http.js +887 -0
  15. package/src/index.js +79 -0
  16. package/src/local/authentication.js +47 -0
  17. package/src/local/better-auth.js +517 -0
  18. package/src/local/cli.js +51 -0
  19. package/src/local/context.js +23 -0
  20. package/src/local/environment.js +109 -0
  21. package/src/local/index.js +204 -0
  22. package/src/local/router.js +999 -0
  23. package/src/local/server.js +664 -0
  24. package/src/local/store.js +530 -0
  25. package/src/local/test-environment.js +74 -0
  26. package/src/management-contracts.js +72 -0
  27. package/src/management.js +12 -0
  28. package/src/native-origin.js +75 -0
  29. package/src/postgres.js +494 -0
  30. package/src/react/core.js +743 -0
  31. package/src/react/index.js +99 -0
  32. package/src/result.js +251 -0
  33. package/src/schema.js +366 -0
  34. package/src/sql.js +996 -0
  35. package/src/svelte/index.js +129 -0
  36. package/src/tanstack/index.js +511 -0
  37. package/src/ts-auth-discovery.js +190 -0
  38. package/src/ts-auth.js +3497 -0
  39. package/types/admin.d.ts +6 -0
  40. package/types/better-auth.d.ts +8 -0
  41. package/types/config.d.ts +58 -0
  42. package/types/database-codec.d.ts +111 -0
  43. package/types/database-types.d.ts +213 -0
  44. package/types/database-view.d.ts +183 -0
  45. package/types/database.d.ts +98 -0
  46. package/types/discovery.d.ts +114 -0
  47. package/types/http.d.ts +46 -0
  48. package/types/index.d.ts +52 -0
  49. package/types/local/authentication.d.ts +11 -0
  50. package/types/local/better-auth.d.ts +33 -0
  51. package/types/local/cli.d.ts +2 -0
  52. package/types/local/context.d.ts +14 -0
  53. package/types/local/environment.d.ts +23 -0
  54. package/types/local/index.d.ts +94 -0
  55. package/types/local/router.d.ts +66 -0
  56. package/types/local/server.d.ts +54 -0
  57. package/types/local/store.d.ts +106 -0
  58. package/types/local/test-environment.d.ts +25 -0
  59. package/types/management-contracts.d.ts +44 -0
  60. package/types/management.d.ts +6 -0
  61. package/types/native-origin.d.ts +23 -0
  62. package/types/postgres.d.ts +123 -0
  63. package/types/react/core.d.ts +366 -0
  64. package/types/react/index.d.ts +54 -0
  65. package/types/result.d.ts +161 -0
  66. package/types/schema.d.ts +145 -0
  67. package/types/sql.d.ts +288 -0
  68. package/types/svelte/index.d.ts +81 -0
  69. package/types/tanstack/index.d.ts +165 -0
  70. package/types/ts-auth-discovery.d.ts +11 -0
  71. package/types/ts-auth.d.ts +1826 -0
package/src/ts-auth.js ADDED
@@ -0,0 +1,3497 @@
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
+ }),
2362
+ /** @returns {value is TsAuthOidcResourceIntrospection} */
2363
+ (value) => {
2364
+ if (isInactiveIntrospection(value)) return true;
2365
+ const now = Date.now() / 1000;
2366
+ return (
2367
+ isResourceAccessIntrospection(value) &&
2368
+ Number.isFinite(now) &&
2369
+ now > 0 &&
2370
+ value.iss === expectedIssuer &&
2371
+ value.aud === expectedAudience &&
2372
+ value.exp > now &&
2373
+ value.iat <= now
2374
+ );
2375
+ },
2376
+ );
2377
+ }
2378
+
2379
+ /**
2380
+ * RFC 7009 semantics: the service answers 200 with an empty body even for
2381
+ * unknown tokens, so success carries no payload.
2382
+ * @param {string} clientId
2383
+ * @param {string} clientSecret
2384
+ * @param {string} token
2385
+ * @returns {Promise<TerraScaleResult<void>>}
2386
+ */
2387
+ revoke(clientId, clientSecret, token) {
2388
+ const form = clientCredentialForm(clientId, clientSecret);
2389
+ form.set("token", requireText(token, "token"));
2390
+ return this.#postForm(
2391
+ `${this.#slug}/oauth/revoke`,
2392
+ "tsauth.oidc.revoke",
2393
+ form,
2394
+ );
2395
+ }
2396
+
2397
+ /**
2398
+ * Bearer access token issued by the token endpoint, not the sk_ key.
2399
+ * @param {string} accessToken
2400
+ * @returns {Promise<TerraScaleResult<TsAuthOidcUserInfo>>}
2401
+ */
2402
+ userinfo(accessToken) {
2403
+ requireText(accessToken, "accessToken");
2404
+ return this.#http.request({
2405
+ method: "GET",
2406
+ path: `${this.#slug}/oauth/userinfo`,
2407
+ routeName: "tsauth.oidc.userinfo",
2408
+ userinfoAccessToken: accessToken.trim(),
2409
+ });
2410
+ }
2411
+
2412
+ /**
2413
+ * @template T
2414
+ * @param {string} path
2415
+ * @param {`tsauth.${string}`} routeName
2416
+ * @param {URLSearchParams} form
2417
+ * @returns {Promise<TerraScaleResult<T>>}
2418
+ */
2419
+ #postForm(path, routeName, form) {
2420
+ return this.#http.request({
2421
+ method: "POST",
2422
+ path,
2423
+ routeName,
2424
+ formBody: form,
2425
+ });
2426
+ }
2427
+ }
2428
+
2429
+ /** The ts-auth management client: one hosted identity instance. */
2430
+ export class TsAuthClient {
2431
+ /**
2432
+ * @type {TsAuthHttp}
2433
+ * @readonly
2434
+ */
2435
+ #http;
2436
+ /**
2437
+ * @type {TsAuthUsersClient}
2438
+ * @readonly
2439
+ */
2440
+ users;
2441
+ /**
2442
+ * @type {TsAuthSessionsClient}
2443
+ * @readonly
2444
+ */
2445
+ sessions;
2446
+ /**
2447
+ * @type {TsAuthApiKeysClient}
2448
+ * @readonly
2449
+ */
2450
+ apiKeys;
2451
+ /**
2452
+ * @type {TsAuthOrganizationsClient}
2453
+ * @readonly
2454
+ */
2455
+ organizations;
2456
+ /**
2457
+ * @type {TsAuthConnectionsClient}
2458
+ * @readonly
2459
+ */
2460
+ connections;
2461
+ /**
2462
+ * @type {TsAuthDirectoriesClient}
2463
+ * @readonly
2464
+ */
2465
+ directories;
2466
+ /**
2467
+ * @type {TsAuthJwtTemplatesClient}
2468
+ * @readonly
2469
+ */
2470
+ jwtTemplates;
2471
+ /**
2472
+ * @type {TsAuthWebhooksClient}
2473
+ * @readonly
2474
+ */
2475
+ webhooks;
2476
+ /**
2477
+ * @type {TsAuthAuditEventsClient}
2478
+ * @readonly
2479
+ */
2480
+ auditEvents;
2481
+ /**
2482
+ * @type {TsAuthBillingClient}
2483
+ * @readonly
2484
+ */
2485
+ billing;
2486
+ /**
2487
+ * @type {TsAuthInstanceClient}
2488
+ * @readonly
2489
+ */
2490
+ instance;
2491
+ /**
2492
+ * @type {TsAuthOauthClientsClient}
2493
+ * @readonly
2494
+ */
2495
+ oauthClients;
2496
+ /**
2497
+ * @type {TsAuthOauthResourcesClient}
2498
+ * @readonly
2499
+ */
2500
+ oauthResources;
2501
+ /**
2502
+ * @type {TsAuthOidcClient}
2503
+ * @readonly
2504
+ */
2505
+ oidc;
2506
+
2507
+ /** @param {TsAuthClientConfig} config */
2508
+ constructor(config) {
2509
+ // The shared runtime config defaults to the product API; ts-auth has no
2510
+ // default origin and an sk_ key must never leak to the product host.
2511
+ if (
2512
+ typeof config?.apiBaseUrl !== "string" ||
2513
+ config.apiBaseUrl.trim().length === 0
2514
+ ) {
2515
+ throw new TypeError(
2516
+ "TsAuthClient requires the ts-auth deployment's public apiBaseUrl origin; there is no default.",
2517
+ );
2518
+ }
2519
+ const slug = validateInstanceSlug(config?.instanceSlug);
2520
+ this.#http = new TsAuthHttp(config, {
2521
+ authReverseProxy: config.useReverseProxy === true,
2522
+ });
2523
+ requireBareOrigin(this.#http.config.apiBaseUrl);
2524
+ this.users = new TsAuthUsersClient(this.#http, slug);
2525
+ this.sessions = new TsAuthSessionsClient(this.#http, slug);
2526
+ this.apiKeys = new TsAuthApiKeysClient(this.#http, slug);
2527
+ this.organizations = new TsAuthOrganizationsClient(this.#http, slug);
2528
+ this.connections = new TsAuthConnectionsClient(this.#http, slug);
2529
+ this.directories = new TsAuthDirectoriesClient(this.#http, slug);
2530
+ this.jwtTemplates = new TsAuthJwtTemplatesClient(this.#http, slug);
2531
+ this.webhooks = new TsAuthWebhooksClient(this.#http, slug);
2532
+ this.auditEvents = new TsAuthAuditEventsClient(this.#http, slug);
2533
+ this.billing = new TsAuthBillingClient(this.#http, slug);
2534
+ this.instance = new TsAuthInstanceClient(this.#http, slug);
2535
+ this.oauthClients = new TsAuthOauthClientsClient(this.#http, slug);
2536
+ this.oauthResources = new TsAuthOauthResourcesClient(this.#http, slug);
2537
+ this.oidc = new TsAuthOidcClient(this.#http, slug);
2538
+ }
2539
+
2540
+ get config() {
2541
+ return this.#http.config;
2542
+ }
2543
+ }
2544
+
2545
+ /**
2546
+ * @param {TsAuthClientConfig} config
2547
+ * @returns {TsAuthClient}
2548
+ */
2549
+ export function createTsAuthClient(config) {
2550
+ return new TsAuthClient(config);
2551
+ }
2552
+
2553
+ /**
2554
+ * Mirrors the ts-auth service rule: the deployment origin is a bare origin.
2555
+ * A path, query, or fragment would be silently rewritten or dropped by URL
2556
+ * composition, so it is a construction error instead.
2557
+ * @param {string} apiBaseUrl
2558
+ * @returns {void}
2559
+ */
2560
+ function requireBareOrigin(apiBaseUrl) {
2561
+ /** @type {URL} */
2562
+ let url;
2563
+ try {
2564
+ url = new URL(apiBaseUrl);
2565
+ } catch {
2566
+ // Malformed URLs keep their request-time transport failure.
2567
+ return;
2568
+ }
2569
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") {
2570
+ throw new TypeError(
2571
+ "The ts-auth apiBaseUrl must be a bare origin without a path, query, or fragment.",
2572
+ );
2573
+ }
2574
+ }
2575
+
2576
+ /**
2577
+ * Verifies a ts-auth webhook delivery signature: sha256=lowercaseHex(HMAC-
2578
+ * SHA256(key = UTF8(signingSecret), message = UTF8(timestamp + "." + rawBody)))
2579
+ * compared in constant time over the RAW body bytes. Malformed input returns
2580
+ * false; this function never throws.
2581
+ * @param {string} signingSecret
2582
+ * @param {string | undefined} timestamp
2583
+ * @param {string | undefined} signature
2584
+ * @param {Uint8Array} payload
2585
+ * @returns {Promise<boolean>}
2586
+ */
2587
+ export async function verifyTsAuthWebhook(
2588
+ signingSecret,
2589
+ timestamp,
2590
+ signature,
2591
+ payload,
2592
+ ) {
2593
+ const verified = await verifySignature(
2594
+ signingSecret,
2595
+ timestamp,
2596
+ signature,
2597
+ payload,
2598
+ );
2599
+ return verified === true;
2600
+ }
2601
+
2602
+ /**
2603
+ * verifyTsAuthWebhook plus a replay window: the timestamp header must be
2604
+ * within ±toleranceMs of `now` (milliseconds).
2605
+ * @param {string} signingSecret
2606
+ * @param {string | undefined} timestamp
2607
+ * @param {string | undefined} signature
2608
+ * @param {Uint8Array} payload
2609
+ * @param {number} toleranceMs
2610
+ * @param {number} now
2611
+ * @returns {Promise<boolean>}
2612
+ */
2613
+ export async function verifyTsAuthWebhookWithTolerance(
2614
+ signingSecret,
2615
+ timestamp,
2616
+ signature,
2617
+ payload,
2618
+ toleranceMs,
2619
+ now,
2620
+ ) {
2621
+ const verified = await verifySignature(
2622
+ signingSecret,
2623
+ timestamp,
2624
+ signature,
2625
+ payload,
2626
+ );
2627
+ if (verified !== true) return false;
2628
+ if (!Number.isFinite(toleranceMs) || toleranceMs < 0 || !Number.isFinite(now))
2629
+ return false;
2630
+ const seconds = Number(timestamp);
2631
+ if (!Number.isFinite(seconds)) return false;
2632
+ return Math.abs(now - seconds * 1_000) <= toleranceMs;
2633
+ }
2634
+
2635
+ /**
2636
+ * undefined means malformed input; true/false means the signature verdict.
2637
+ * @param {string} signingSecret
2638
+ * @param {string | undefined} timestamp
2639
+ * @param {string | undefined} signature
2640
+ * @param {Uint8Array} payload
2641
+ * @returns {Promise<boolean | undefined>}
2642
+ */
2643
+ async function verifySignature(signingSecret, timestamp, signature, payload) {
2644
+ if (typeof signingSecret !== "string" || signingSecret.length === 0)
2645
+ return undefined;
2646
+ if (typeof timestamp !== "string" || !/^[0-9]+$/u.test(timestamp))
2647
+ return undefined;
2648
+ if (typeof signature !== "string" || !signature.startsWith("sha256="))
2649
+ return undefined;
2650
+ if (!(payload instanceof Uint8Array)) return undefined;
2651
+ const hex = signature.slice("sha256=".length);
2652
+ if (!/^[0-9a-f]{64}$/u.test(hex)) return undefined;
2653
+ const prefix = utf8Encoder.encode(`${timestamp}.`);
2654
+ const message = new Uint8Array(prefix.byteLength + payload.byteLength);
2655
+ message.set(prefix, 0);
2656
+ message.set(payload, prefix.byteLength);
2657
+ const subtle = globalThis.crypto.subtle;
2658
+ const key = await subtle.importKey(
2659
+ "raw",
2660
+ utf8Encoder.encode(signingSecret),
2661
+ { name: "HMAC", hash: "SHA-256" },
2662
+ false,
2663
+ ["sign"],
2664
+ );
2665
+ const mac = new Uint8Array(await subtle.sign("HMAC", key, message));
2666
+ return timingSafeEqualHex(toLowercaseHex(mac), hex);
2667
+ }
2668
+
2669
+ /**
2670
+ * @param {Uint8Array} bytes
2671
+ * @returns {string}
2672
+ */
2673
+ function toLowercaseHex(bytes) {
2674
+ let hex = "";
2675
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
2676
+ return hex;
2677
+ }
2678
+
2679
+ /**
2680
+ * Fixed-length hex compare whose loop never short-circuits.
2681
+ * @param {string} a
2682
+ * @param {string} b
2683
+ * @returns {boolean}
2684
+ */
2685
+ function timingSafeEqualHex(a, b) {
2686
+ if (a.length !== b.length) return false;
2687
+ let difference = 0;
2688
+ for (let index = 0; index < a.length; index++) {
2689
+ difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
2690
+ }
2691
+ return difference === 0;
2692
+ }
2693
+
2694
+ /**
2695
+ * @param {string} instanceSlug
2696
+ * @returns {string}
2697
+ */
2698
+ function validateInstanceSlug(instanceSlug) {
2699
+ if (
2700
+ typeof instanceSlug !== "string" ||
2701
+ instanceSlug.length === 0 ||
2702
+ instanceSlug !== instanceSlug.trim() ||
2703
+ instanceSlug === "." ||
2704
+ instanceSlug === ".." ||
2705
+ instanceSlug.includes("/") ||
2706
+ /\s/u.test(instanceSlug) ||
2707
+ [...instanceSlug].some(
2708
+ (character) =>
2709
+ character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
2710
+ )
2711
+ ) {
2712
+ throw new TypeError(
2713
+ "instanceSlug must be a nonblank printable string without whitespace or slashes.",
2714
+ );
2715
+ }
2716
+ return encodeURIComponent(instanceSlug);
2717
+ }
2718
+
2719
+ const segmentControlCharacters = /[\u0000-\u001f\u007f]/u;
2720
+ const utf8Encoder = new TextEncoder();
2721
+
2722
+ /**
2723
+ * @param {string} value
2724
+ * @param {string} name
2725
+ * @returns {string}
2726
+ */
2727
+ function segment(value, name) {
2728
+ if (
2729
+ typeof value !== "string" ||
2730
+ value.length === 0 ||
2731
+ value !== value.trim() ||
2732
+ // Bare dot segments survive preflight but URL path normalization collapses
2733
+ // them, silently rewriting the route; reject them instead.
2734
+ value === "." ||
2735
+ value === ".." ||
2736
+ segmentControlCharacters.test(value) ||
2737
+ utf8Encoder.encode(value).byteLength > 255
2738
+ ) {
2739
+ throw new TypeError(
2740
+ `${name} must be a nonblank printable string of at most 255 UTF-8 bytes.`,
2741
+ );
2742
+ }
2743
+ return encodeURIComponent(value);
2744
+ }
2745
+
2746
+ /**
2747
+ * @param {unknown} value
2748
+ * @param {string} name
2749
+ * @returns {string}
2750
+ */
2751
+ function requireText(value, name) {
2752
+ if (
2753
+ typeof value !== "string" ||
2754
+ value.trim().length === 0 ||
2755
+ /\p{Cc}/u.test(value)
2756
+ ) {
2757
+ throw new TypeError(
2758
+ `${name} must be a nonblank string without control characters.`,
2759
+ );
2760
+ }
2761
+ return value;
2762
+ }
2763
+
2764
+ /**
2765
+ * @param {string} value
2766
+ * @param {string} name
2767
+ * @returns {string}
2768
+ */
2769
+ function requireAbsoluteHttpUrl(value, name) {
2770
+ requireText(value, name);
2771
+ if (!/^https?:\/\//u.test(value))
2772
+ throw new TypeError(`${name} must be an absolute http(s) URL.`);
2773
+ return value;
2774
+ }
2775
+
2776
+ /**
2777
+ * @param {string} value
2778
+ * @param {string} name
2779
+ * @returns {string}
2780
+ */
2781
+ function requirePatchText(value, name) {
2782
+ if (typeof value !== "string" || value.length === 0)
2783
+ throw new TypeError(`${name} must be a non-empty string.`);
2784
+ return value;
2785
+ }
2786
+
2787
+ /**
2788
+ * @param {Record<string, unknown>} metadata
2789
+ * @param {string} name
2790
+ * @returns {Record<string, unknown>}
2791
+ */
2792
+ function requirePatchMetadata(metadata, name) {
2793
+ if (
2794
+ typeof metadata !== "object" ||
2795
+ metadata === null ||
2796
+ Array.isArray(metadata)
2797
+ ) {
2798
+ throw new TypeError(`${name} must be an object.`);
2799
+ }
2800
+ return metadata;
2801
+ }
2802
+
2803
+ /**
2804
+ * @param {string} name
2805
+ * @returns {string}
2806
+ */
2807
+ function validateJwtTemplateName(name) {
2808
+ if (typeof name !== "string" || !jwtTemplateNamePattern.test(name)) {
2809
+ throw new TypeError(
2810
+ "Template names must be 1 to 48 characters of [a-z0-9-_].",
2811
+ );
2812
+ }
2813
+ return name;
2814
+ }
2815
+
2816
+ /**
2817
+ * @param {Record<string, string>} claims
2818
+ * @returns {Record<string, string>}
2819
+ */
2820
+ function validateJwtTemplateClaims(claims) {
2821
+ if (
2822
+ typeof claims !== "object" ||
2823
+ claims === null ||
2824
+ Array.isArray(claims) ||
2825
+ Object.keys(claims).length === 0 ||
2826
+ !Object.values(claims).every((value) => typeof value === "string")
2827
+ ) {
2828
+ throw new TypeError(
2829
+ "claims must be a non-empty object mapping strings to strings.",
2830
+ );
2831
+ }
2832
+ return claims;
2833
+ }
2834
+
2835
+ /**
2836
+ * @param {string} clientId
2837
+ * @param {string} clientSecret
2838
+ * @returns {URLSearchParams}
2839
+ */
2840
+ function clientCredentialForm(clientId, clientSecret) {
2841
+ return new URLSearchParams({
2842
+ client_id: requireText(clientId, "clientId"),
2843
+ client_secret: requireText(clientSecret, "clientSecret"),
2844
+ });
2845
+ }
2846
+
2847
+ /**
2848
+ * @template T
2849
+ * @param {Promise<TerraScaleResult<unknown>>} pending
2850
+ * @param {(value: unknown) => value is T} accepts
2851
+ * @returns {Promise<TerraScaleResult<T>>}
2852
+ */
2853
+ async function oauthResponse(pending, accepts) {
2854
+ const result = await pending;
2855
+ if (!result.ok) return result;
2856
+ if (!accepts(result.value))
2857
+ return err(
2858
+ result.routeName,
2859
+ {
2860
+ kind: "invalid-response",
2861
+ code: "invalid_response",
2862
+ message: "ts-auth returned an invalid OAuth response.",
2863
+ status: result.status,
2864
+ requestId:
2865
+ result.headers.get("x-request-id") ??
2866
+ result.headers.get("traceparent") ??
2867
+ undefined,
2868
+ },
2869
+ result.status,
2870
+ result.headers,
2871
+ );
2872
+ return { ...result, value: result.value };
2873
+ }
2874
+
2875
+ /**
2876
+ * @param {unknown} value
2877
+ * @returns {value is Record<string, unknown>}
2878
+ */
2879
+ function oauthRecord(value) {
2880
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2881
+ }
2882
+ /**
2883
+ * @param {unknown} value
2884
+ * @param {readonly string[]} fields
2885
+ * @returns {value is Record<string, unknown>}
2886
+ */
2887
+ function exactOauthFields(value, fields) {
2888
+ return (
2889
+ oauthRecord(value) &&
2890
+ Object.keys(value).length === fields.length &&
2891
+ fields.every((field) => Object.hasOwn(value, field))
2892
+ );
2893
+ }
2894
+ /**
2895
+ * @param {unknown} value
2896
+ * @param {readonly string[]} fields
2897
+ * @returns {asserts value is Record<string, unknown>}
2898
+ */
2899
+ function requireFields(value, fields) {
2900
+ if (
2901
+ !oauthRecord(value) ||
2902
+ Object.keys(value).some((field) => !fields.includes(field))
2903
+ )
2904
+ throw new TypeError("Unsupported OAuth request field.");
2905
+ }
2906
+ /**
2907
+ * @param {unknown} value
2908
+ * @returns {value is string}
2909
+ */
2910
+ function oauthText(value) {
2911
+ return (
2912
+ typeof value === "string" &&
2913
+ value.length > 0 &&
2914
+ !/\p{Cc}/u.test(value) &&
2915
+ !/[\uD800-\uDFFF]/u.test(value)
2916
+ );
2917
+ }
2918
+ /**
2919
+ * @param {unknown} value
2920
+ * @returns {value is readonly string[]}
2921
+ */
2922
+ function oauthStrings(value) {
2923
+ return Array.isArray(value) && value.every(oauthText);
2924
+ }
2925
+ /**
2926
+ * @param {unknown} value
2927
+ * @returns {value is number}
2928
+ */
2929
+ function exactUnsignedInteger(value) {
2930
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
2931
+ }
2932
+ /** @type {readonly TsAuthOauthGrantType[]} */
2933
+ const oauthGrants = [
2934
+ "authorization_code",
2935
+ "refresh_token",
2936
+ "client_credentials",
2937
+ "urn:ietf:params:oauth:grant-type:device_code",
2938
+ ];
2939
+ /**
2940
+ * @param {unknown} value
2941
+ * @returns {value is TsAuthOauthGrantType}
2942
+ */
2943
+ function oauthGrant(value) {
2944
+ return (
2945
+ typeof value === "string" && oauthGrants.some((grant) => grant === value)
2946
+ );
2947
+ }
2948
+ /**
2949
+ * @param {unknown} value
2950
+ * @returns {value is readonly TsAuthOauthGrantType[]}
2951
+ */
2952
+ function oauthGrantsList(value) {
2953
+ return Array.isArray(value) && value.every(oauthGrant);
2954
+ }
2955
+ /**
2956
+ * @param {unknown} value
2957
+ * @returns {value is TsAuthOauthClientAuthMethod}
2958
+ */
2959
+ function oauthAuthMethod(value) {
2960
+ return (
2961
+ value === "none" ||
2962
+ value === "client_secret_post" ||
2963
+ value === "client_secret_basic"
2964
+ );
2965
+ }
2966
+
2967
+ /**
2968
+ * @param {unknown} value
2969
+ * @returns {string}
2970
+ */
2971
+ function requireResourceAudience(value) {
2972
+ if (
2973
+ !oauthText(value) ||
2974
+ value.length > 2048 ||
2975
+ !value.startsWith("https://") ||
2976
+ /[^A-Za-z0-9\-._~:/?\[\]@!$&'()*+,;=%]/u.test(value) ||
2977
+ /%(?![0-9a-fA-F]{2})/u.test(value)
2978
+ )
2979
+ throw new TypeError("resource must be one exact HTTPS audience.");
2980
+ const remainder = value.slice(8);
2981
+ const authority = remainder.split(/[/?#]/u)[0] ?? "";
2982
+ if (
2983
+ authority.length === 0 ||
2984
+ authority.includes("@") ||
2985
+ /[\[\]]/u.test(remainder.slice(authority.length))
2986
+ )
2987
+ throw new TypeError("resource must be one exact HTTPS audience.");
2988
+ /** @type {URL} */
2989
+ let url;
2990
+ try {
2991
+ url = new URL(value);
2992
+ } catch {
2993
+ throw new TypeError("resource must be one exact HTTPS audience.");
2994
+ }
2995
+ if (
2996
+ url.protocol !== "https:" ||
2997
+ url.hostname === "" ||
2998
+ url.username !== "" ||
2999
+ url.password !== "" ||
3000
+ url.hash !== ""
3001
+ )
3002
+ throw new TypeError("resource must be one exact HTTPS audience.");
3003
+ return value;
3004
+ }
3005
+ /**
3006
+ * @param {unknown} value
3007
+ * @returns {value is string}
3008
+ */
3009
+ function validResourceAudience(value) {
3010
+ if (typeof value !== "string") return false;
3011
+ try {
3012
+ requireResourceAudience(value);
3013
+ return true;
3014
+ } catch {
3015
+ return false;
3016
+ }
3017
+ }
3018
+ /**
3019
+ * @param {string} value
3020
+ * @returns {void}
3021
+ */
3022
+ function requireOauthIssuer(value) {
3023
+ if (!oauthText(value))
3024
+ throw new TypeError(
3025
+ "issuer must be the configured canonical provider issuer.",
3026
+ );
3027
+ /** @type {URL} */
3028
+ let url;
3029
+ try {
3030
+ url = new URL(value);
3031
+ } catch {
3032
+ throw new TypeError(
3033
+ "issuer must be the configured canonical provider issuer.",
3034
+ );
3035
+ }
3036
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
3037
+ if (
3038
+ url.href !== value ||
3039
+ (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
3040
+ url.username !== "" ||
3041
+ url.password !== "" ||
3042
+ url.search !== "" ||
3043
+ url.hash !== ""
3044
+ )
3045
+ throw new TypeError(
3046
+ "issuer must be the configured canonical provider issuer.",
3047
+ );
3048
+ }
3049
+ /**
3050
+ * @param {unknown} value
3051
+ * @returns {value is string}
3052
+ */
3053
+ function oauthScopeToken(value) {
3054
+ return (
3055
+ typeof value === "string" &&
3056
+ value.length > 0 &&
3057
+ value.length <= 256 &&
3058
+ /^[\x21\x23-\x5b\x5d-\x7e]+$/u.test(value)
3059
+ );
3060
+ }
3061
+ /**
3062
+ * @param {unknown} value
3063
+ * @returns {string}
3064
+ */
3065
+ function requireOauthScope(value) {
3066
+ if (typeof value !== "string")
3067
+ throw new TypeError("scope must contain unique scope tokens.");
3068
+ const tokens = value.split(/[\t\n\f\r ]+/u).filter(Boolean);
3069
+ if (
3070
+ tokens.length === 0 ||
3071
+ tokens.length > 100 ||
3072
+ new Set(tokens).size !== tokens.length ||
3073
+ !tokens.every(oauthScopeToken)
3074
+ )
3075
+ throw new TypeError("scope must contain unique scope tokens.");
3076
+ return value;
3077
+ }
3078
+ /**
3079
+ * @param {unknown} value
3080
+ * @param {string} name
3081
+ * @param {(item: unknown) => boolean} [accepts=oauthText]
3082
+ * @returns {void}
3083
+ */
3084
+ function requireOauthList(value, name, accepts = oauthText) {
3085
+ if (
3086
+ !Array.isArray(value) ||
3087
+ value.length > 100 ||
3088
+ new Set(value).size !== value.length ||
3089
+ !value.every(accepts) ||
3090
+ utf8Encoder.encode(JSON.stringify(value)).length > 32768
3091
+ )
3092
+ throw new TypeError(
3093
+ `${name} must be a bounded unique list of admitted values.`,
3094
+ );
3095
+ }
3096
+ /**
3097
+ * @param {unknown} request
3098
+ * @param {boolean} update
3099
+ * @returns {void}
3100
+ */
3101
+ function validateOauthClientRequest(request, update) {
3102
+ requireFields(request, [
3103
+ "name",
3104
+ "redirect_uris",
3105
+ "post_logout_redirect_uris",
3106
+ "allowed_origins",
3107
+ "allow_anonymous",
3108
+ "allowed_resource_audiences",
3109
+ "allowed_scopes",
3110
+ "allowed_grant_types",
3111
+ ...(update
3112
+ ? ["expected_auth_epoch", "enabled", "rotate_secret"]
3113
+ : ["public", "token_endpoint_auth_method"]),
3114
+ ]);
3115
+ if (request.name !== undefined) requireText(request.name, "name");
3116
+ for (const field of [
3117
+ "redirect_uris",
3118
+ "post_logout_redirect_uris",
3119
+ "allowed_origins",
3120
+ ])
3121
+ if (request[field] !== undefined) requireOauthList(request[field], field);
3122
+ if (request.allowed_resource_audiences !== undefined)
3123
+ requireOauthList(
3124
+ request.allowed_resource_audiences,
3125
+ "allowed_resource_audiences",
3126
+ validResourceAudience,
3127
+ );
3128
+ if (request.allowed_scopes !== undefined)
3129
+ requireOauthList(request.allowed_scopes, "allowed_scopes", oauthScopeToken);
3130
+ if (request.allowed_grant_types !== undefined)
3131
+ requireOauthList(
3132
+ request.allowed_grant_types,
3133
+ "allowed_grant_types",
3134
+ oauthGrant,
3135
+ );
3136
+ for (const field of ["public", "allow_anonymous", "enabled", "rotate_secret"])
3137
+ if (request[field] !== undefined && typeof request[field] !== "boolean")
3138
+ throw new TypeError(`${field} must be a boolean.`);
3139
+ if (
3140
+ request.token_endpoint_auth_method !== undefined &&
3141
+ !oauthAuthMethod(request.token_endpoint_auth_method)
3142
+ )
3143
+ throw new TypeError("Unsupported OAuth client authentication method.");
3144
+ if (
3145
+ request.expected_auth_epoch !== undefined &&
3146
+ !exactUnsignedInteger(request.expected_auth_epoch)
3147
+ )
3148
+ throw new TypeError(
3149
+ "expected_auth_epoch must be an exact unsigned JSON integer.",
3150
+ );
3151
+ }
3152
+ /**
3153
+ * @param {unknown} request
3154
+ * @param {boolean} update
3155
+ * @returns {void}
3156
+ */
3157
+ function validateOauthResourceRequest(request, update) {
3158
+ requireFields(request, [
3159
+ "allowed_client_ids",
3160
+ "allowed_grant_types",
3161
+ "allowed_scopes",
3162
+ ...(update
3163
+ ? ["expected_auth_epoch", "enabled", "rotate_secret"]
3164
+ : ["audience"]),
3165
+ ]);
3166
+ if (!update) requireResourceAudience(request.audience);
3167
+ for (const [field, accepts] of /** @type {const} */ ([
3168
+ ["allowed_client_ids", oauthText],
3169
+ ["allowed_grant_types", oauthGrant],
3170
+ ["allowed_scopes", oauthScopeToken],
3171
+ ])) {
3172
+ if (!update || request[field] !== undefined)
3173
+ requireOauthList(request[field], field, accepts);
3174
+ }
3175
+ if (update && !exactUnsignedInteger(request.expected_auth_epoch))
3176
+ throw new TypeError(
3177
+ "expected_auth_epoch must be an exact unsigned JSON integer.",
3178
+ );
3179
+ for (const field of ["enabled", "rotate_secret"])
3180
+ if (request[field] !== undefined && typeof request[field] !== "boolean")
3181
+ throw new TypeError(`${field} must be a boolean.`);
3182
+ }
3183
+ /**
3184
+ * @param {unknown} request
3185
+ * @returns {void}
3186
+ */
3187
+ function validateAuthorizationRequest(request) {
3188
+ requireFields(request, [
3189
+ "client_id",
3190
+ "redirect_uri",
3191
+ "response_type",
3192
+ "scope",
3193
+ "resource",
3194
+ "state",
3195
+ "nonce",
3196
+ "code_challenge",
3197
+ "code_challenge_method",
3198
+ "prompt",
3199
+ ]);
3200
+ requireText(request.client_id, "client_id");
3201
+ requireText(request.redirect_uri, "redirect_uri");
3202
+ if (request.response_type !== "code")
3203
+ throw new TypeError("response_type must be code.");
3204
+ if (request.resource !== undefined) requireResourceAudience(request.resource);
3205
+ if (request.scope !== undefined) requireOauthScope(request.scope);
3206
+ for (const field of ["state", "nonce", "code_challenge"])
3207
+ if (request[field] !== undefined) requireText(request[field], field);
3208
+ if (
3209
+ request.code_challenge_method !== undefined &&
3210
+ (request.code_challenge_method !== "S256" ||
3211
+ request.code_challenge === undefined)
3212
+ )
3213
+ throw new TypeError("S256 requires code_challenge.");
3214
+ if (
3215
+ request.prompt !== undefined &&
3216
+ (typeof request.prompt !== "string" ||
3217
+ !["none", "login", "consent", "login consent", "consent login"].includes(
3218
+ request.prompt,
3219
+ ))
3220
+ )
3221
+ throw new TypeError("Unsupported authorization prompt.");
3222
+ }
3223
+ /**
3224
+ * @param {string} clientId
3225
+ * @param {string} secret
3226
+ * @returns {string}
3227
+ */
3228
+ function resourceBasic(clientId, secret) {
3229
+ if (!oauthText(clientId) || !oauthText(secret))
3230
+ throw new TypeError(
3231
+ "Resource introspection credentials must be nonempty well-formed text.",
3232
+ );
3233
+ /**
3234
+ * @param {string} value
3235
+ * @returns {string}
3236
+ */
3237
+ const component = (value) =>
3238
+ new URLSearchParams({ v: value }).toString().slice(2);
3239
+ return `Basic ${btoa(`${component(clientId)}:${component(secret)}`)}`;
3240
+ }
3241
+
3242
+ const clientViewFields = [
3243
+ "id",
3244
+ "client_id",
3245
+ "name",
3246
+ "redirect_uris",
3247
+ "post_logout_redirect_uris",
3248
+ "token_endpoint_auth_method",
3249
+ "public",
3250
+ "allowed_origins",
3251
+ "allow_anonymous",
3252
+ "created_at",
3253
+ "allowed_resource_audiences",
3254
+ "allowed_scopes",
3255
+ "allowed_grant_types",
3256
+ "auth_epoch",
3257
+ "enabled",
3258
+ ];
3259
+ /**
3260
+ * @param {unknown} value
3261
+ * @returns {value is TsAuthOauthClientView}
3262
+ */
3263
+ function isOauthClientView(value) {
3264
+ return (
3265
+ exactOauthFields(value, clientViewFields) &&
3266
+ [value.id, value.client_id, value.name].every(oauthText) &&
3267
+ [
3268
+ value.redirect_uris,
3269
+ value.post_logout_redirect_uris,
3270
+ value.allowed_origins,
3271
+ value.allowed_scopes,
3272
+ ].every(oauthStrings) &&
3273
+ Array.isArray(value.allowed_resource_audiences) &&
3274
+ value.allowed_resource_audiences.every(validResourceAudience) &&
3275
+ oauthGrantsList(value.allowed_grant_types) &&
3276
+ oauthAuthMethod(value.token_endpoint_auth_method) &&
3277
+ typeof value.public === "boolean" &&
3278
+ typeof value.allow_anonymous === "boolean" &&
3279
+ typeof value.enabled === "boolean" &&
3280
+ exactUnsignedInteger(value.created_at) &&
3281
+ exactUnsignedInteger(value.auth_epoch)
3282
+ );
3283
+ }
3284
+ /**
3285
+ * @param {unknown} value
3286
+ * @returns {value is TsAuthOauthClientWithSecret}
3287
+ */
3288
+ function isOauthClientWithSecret(value) {
3289
+ return (
3290
+ exactOauthFields(value, ["client", "client_secret"]) &&
3291
+ isOauthClientView(value.client) &&
3292
+ (value.client_secret === null || oauthText(value.client_secret))
3293
+ );
3294
+ }
3295
+ /**
3296
+ * @param {unknown} value
3297
+ * @returns {value is TsAuthOauthClientList}
3298
+ */
3299
+ function isOauthClientList(value) {
3300
+ return (
3301
+ exactOauthFields(value, ["clients"]) &&
3302
+ Array.isArray(value.clients) &&
3303
+ value.clients.every(isOauthClientView)
3304
+ );
3305
+ }
3306
+ const resourceViewFields = [
3307
+ "id",
3308
+ "audience",
3309
+ "allowed_client_ids",
3310
+ "allowed_grant_types",
3311
+ "allowed_scopes",
3312
+ "introspection_client_id",
3313
+ "token_endpoint_auth_method",
3314
+ "auth_epoch",
3315
+ "enabled",
3316
+ "created_at",
3317
+ "updated_at",
3318
+ ];
3319
+ /**
3320
+ * @param {unknown} value
3321
+ * @returns {value is TsAuthOauthResourceView}
3322
+ */
3323
+ function isOauthResourceView(value) {
3324
+ return (
3325
+ exactOauthFields(value, resourceViewFields) &&
3326
+ oauthText(value.id) &&
3327
+ validResourceAudience(value.audience) &&
3328
+ oauthStrings(value.allowed_client_ids) &&
3329
+ oauthGrantsList(value.allowed_grant_types) &&
3330
+ oauthStrings(value.allowed_scopes) &&
3331
+ oauthText(value.introspection_client_id) &&
3332
+ value.token_endpoint_auth_method === "client_secret_basic" &&
3333
+ exactUnsignedInteger(value.auth_epoch) &&
3334
+ typeof value.enabled === "boolean" &&
3335
+ exactUnsignedInteger(value.created_at) &&
3336
+ exactUnsignedInteger(value.updated_at)
3337
+ );
3338
+ }
3339
+ /**
3340
+ * @param {unknown} value
3341
+ * @returns {value is TsAuthOauthResourceWithSecret}
3342
+ */
3343
+ function isOauthResourceWithSecret(value) {
3344
+ return (
3345
+ exactOauthFields(value, ["resource", "introspection_secret"]) &&
3346
+ isOauthResourceView(value.resource) &&
3347
+ (value.introspection_secret === null ||
3348
+ oauthText(value.introspection_secret))
3349
+ );
3350
+ }
3351
+ /**
3352
+ * @param {unknown} value
3353
+ * @returns {value is TsAuthOauthResourceList}
3354
+ */
3355
+ function isOauthResourceList(value) {
3356
+ return (
3357
+ exactOauthFields(value, ["resources"]) &&
3358
+ Array.isArray(value.resources) &&
3359
+ value.resources.every(isOauthResourceView)
3360
+ );
3361
+ }
3362
+ /**
3363
+ * @param {unknown} value
3364
+ * @param {TsAuthOauthGrantType} grant
3365
+ * @returns {value is TsAuthOidcTokenResponse}
3366
+ */
3367
+ function isTokenResponse(value, grant) {
3368
+ if (
3369
+ !oauthRecord(value) ||
3370
+ !oauthText(value.access_token) ||
3371
+ value.token_type !== "Bearer" ||
3372
+ !exactUnsignedInteger(value.expires_in) ||
3373
+ typeof value.scope !== "string"
3374
+ )
3375
+ return false;
3376
+ const fields = ["access_token", "token_type", "expires_in", "scope"];
3377
+ if (grant !== "client_credentials") fields.push("refresh_token");
3378
+ const idToken =
3379
+ (grant === "authorization_code" ||
3380
+ grant === "urn:ietf:params:oauth:grant-type:device_code") &&
3381
+ value.scope.split(/[\t\n\f\r ]+/u).includes("openid");
3382
+ if (idToken) fields.push("id_token");
3383
+ return (
3384
+ exactOauthFields(value, fields) &&
3385
+ (grant === "client_credentials" ||
3386
+ (grant === "refresh_token"
3387
+ ? oauthText(value.refresh_token)
3388
+ : value.refresh_token === null || oauthText(value.refresh_token))) &&
3389
+ (!idToken || oauthText(value.id_token))
3390
+ );
3391
+ }
3392
+ /**
3393
+ * @param {unknown} value
3394
+ * @returns {value is TsAuthOidcDeviceAuthorizationResponse}
3395
+ */
3396
+ function isDeviceAuthorizationResponse(value) {
3397
+ return (
3398
+ exactOauthFields(value, [
3399
+ "device_code",
3400
+ "user_code",
3401
+ "verification_uri",
3402
+ "verification_uri_complete",
3403
+ "expires_in",
3404
+ "interval",
3405
+ ]) &&
3406
+ [
3407
+ value.device_code,
3408
+ value.user_code,
3409
+ value.verification_uri,
3410
+ value.verification_uri_complete,
3411
+ ].every(oauthText) &&
3412
+ exactUnsignedInteger(value.expires_in) &&
3413
+ exactUnsignedInteger(value.interval)
3414
+ );
3415
+ }
3416
+ /**
3417
+ * @param {unknown} value
3418
+ * @returns {value is TsAuthOidcInactiveIntrospection}
3419
+ */
3420
+ function isInactiveIntrospection(value) {
3421
+ return exactOauthFields(value, ["active"]) && value.active === false;
3422
+ }
3423
+ const introspectionFields = [
3424
+ "active",
3425
+ "token_type",
3426
+ "iss",
3427
+ "sub",
3428
+ "aud",
3429
+ "client_id",
3430
+ "scope",
3431
+ "exp",
3432
+ "iat",
3433
+ "sid",
3434
+ ];
3435
+ /**
3436
+ * @param {unknown} value
3437
+ * @returns {value is TsAuthOidcAccessIntrospection}
3438
+ */
3439
+ function isAccessIntrospection(value) {
3440
+ return (
3441
+ exactOauthFields(value, [...introspectionFields, "principal_kind"]) &&
3442
+ value.active === true &&
3443
+ value.token_type === "Bearer" &&
3444
+ [value.iss, value.sub, value.aud, value.client_id].every(oauthText) &&
3445
+ typeof value.scope === "string" &&
3446
+ exactUnsignedInteger(value.exp) &&
3447
+ exactUnsignedInteger(value.iat) &&
3448
+ value.exp > value.iat &&
3449
+ ((value.principal_kind === "user" &&
3450
+ (value.sid === null || oauthText(value.sid))) ||
3451
+ (value.principal_kind === "service" &&
3452
+ value.sid === null &&
3453
+ value.sub === value.client_id))
3454
+ );
3455
+ }
3456
+ /**
3457
+ * @param {unknown} value
3458
+ * @returns {value is TsAuthOidcResourceAccessIntrospection}
3459
+ */
3460
+ function isResourceAccessIntrospection(value) {
3461
+ return (
3462
+ isAccessIntrospection(value) &&
3463
+ (value.principal_kind === "user"
3464
+ ? oauthText(value.sid)
3465
+ : value.sid === null && value.sub === value.client_id)
3466
+ );
3467
+ }
3468
+ /**
3469
+ * @param {unknown} value
3470
+ * @returns {value is TsAuthOidcRefreshIntrospection}
3471
+ */
3472
+ function isRefreshIntrospection(value) {
3473
+ return (
3474
+ exactOauthFields(value, [...introspectionFields, "family_id"]) &&
3475
+ value.active === true &&
3476
+ value.token_type === "refresh_token" &&
3477
+ [value.iss, value.sub, value.aud, value.client_id, value.family_id].every(
3478
+ oauthText,
3479
+ ) &&
3480
+ typeof value.scope === "string" &&
3481
+ exactUnsignedInteger(value.exp) &&
3482
+ exactUnsignedInteger(value.iat) &&
3483
+ value.exp > value.iat &&
3484
+ (value.sid === null || oauthText(value.sid))
3485
+ );
3486
+ }
3487
+ /**
3488
+ * @param {unknown} value
3489
+ * @returns {value is TsAuthOidcIntrospection}
3490
+ */
3491
+ function isClientIntrospection(value) {
3492
+ return (
3493
+ isInactiveIntrospection(value) ||
3494
+ isAccessIntrospection(value) ||
3495
+ isRefreshIntrospection(value)
3496
+ );
3497
+ }