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.
- package/LICENSE +202 -0
- package/README.md +143 -0
- package/package.json +159 -0
- package/sdk-current-contract.json +27 -0
- package/sdk-route-manifest.json +67 -0
- package/src/admin.js +9 -0
- package/src/better-auth.js +14 -0
- package/src/config.js +121 -0
- package/src/database-codec.js +845 -0
- package/src/database-types.js +237 -0
- package/src/database-view.js +422 -0
- package/src/database.js +420 -0
- package/src/discovery.js +374 -0
- package/src/http.js +887 -0
- package/src/index.js +79 -0
- package/src/local/authentication.js +47 -0
- package/src/local/better-auth.js +517 -0
- package/src/local/cli.js +51 -0
- package/src/local/context.js +23 -0
- package/src/local/environment.js +109 -0
- package/src/local/index.js +204 -0
- package/src/local/router.js +999 -0
- package/src/local/server.js +664 -0
- package/src/local/store.js +530 -0
- package/src/local/test-environment.js +74 -0
- package/src/management-contracts.js +72 -0
- package/src/management.js +12 -0
- package/src/native-origin.js +75 -0
- package/src/postgres.js +494 -0
- package/src/react/core.js +743 -0
- package/src/react/index.js +99 -0
- package/src/result.js +251 -0
- package/src/schema.js +366 -0
- package/src/sql.js +996 -0
- package/src/svelte/index.js +129 -0
- package/src/tanstack/index.js +511 -0
- package/src/ts-auth-discovery.js +190 -0
- package/src/ts-auth.js +3497 -0
- package/types/admin.d.ts +6 -0
- package/types/better-auth.d.ts +8 -0
- package/types/config.d.ts +58 -0
- package/types/database-codec.d.ts +111 -0
- package/types/database-types.d.ts +213 -0
- package/types/database-view.d.ts +183 -0
- package/types/database.d.ts +98 -0
- package/types/discovery.d.ts +114 -0
- package/types/http.d.ts +46 -0
- package/types/index.d.ts +52 -0
- package/types/local/authentication.d.ts +11 -0
- package/types/local/better-auth.d.ts +33 -0
- package/types/local/cli.d.ts +2 -0
- package/types/local/context.d.ts +14 -0
- package/types/local/environment.d.ts +23 -0
- package/types/local/index.d.ts +94 -0
- package/types/local/router.d.ts +66 -0
- package/types/local/server.d.ts +54 -0
- package/types/local/store.d.ts +106 -0
- package/types/local/test-environment.d.ts +25 -0
- package/types/management-contracts.d.ts +44 -0
- package/types/management.d.ts +6 -0
- package/types/native-origin.d.ts +23 -0
- package/types/postgres.d.ts +123 -0
- package/types/react/core.d.ts +366 -0
- package/types/react/index.d.ts +54 -0
- package/types/result.d.ts +161 -0
- package/types/schema.d.ts +145 -0
- package/types/sql.d.ts +288 -0
- package/types/svelte/index.d.ts +81 -0
- package/types/tanstack/index.d.ts +165 -0
- package/types/ts-auth-discovery.d.ts +11 -0
- 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
|
+
}
|