@verne-software/sdk 1.3.0 → 1.4.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/README.md +240 -77
- package/dist/index.d.mts +868 -75
- package/dist/index.d.ts +868 -75
- package/dist/index.js +789 -103
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +788 -104
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -10,31 +10,89 @@ var VerneAPIError = class _VerneAPIError extends VerneError {
|
|
|
10
10
|
code,
|
|
11
11
|
message,
|
|
12
12
|
status,
|
|
13
|
-
requestId
|
|
13
|
+
requestId,
|
|
14
|
+
details,
|
|
15
|
+
retryAfter,
|
|
16
|
+
body
|
|
14
17
|
}) {
|
|
15
18
|
super(message);
|
|
16
19
|
this.name = "VerneAPIError";
|
|
17
20
|
this.code = code;
|
|
18
21
|
this.status = status;
|
|
19
22
|
this.requestId = requestId;
|
|
23
|
+
this.details = details ?? {};
|
|
24
|
+
this.retryAfter = retryAfter;
|
|
25
|
+
this.body = body;
|
|
20
26
|
}
|
|
21
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Builds an error from a parsed response body of *unknown* shape.
|
|
29
|
+
*
|
|
30
|
+
* Every API route answers with the envelope `{ error: { code, message, request_id,
|
|
31
|
+
* ...extra } }`. Other shapes are still read, as a fallback that costs nothing: the
|
|
32
|
+
* string form `{ error: "<sentence>" }` Clockwork used before October 2026, and an
|
|
33
|
+
* identity-service body whose `code` is a number (still what Kratos' own `/auth/*`
|
|
34
|
+
* self-service routes answer). Anything unrecognised degrades — to code `'unknown'`,
|
|
35
|
+
* to `HTTP <status>` — rather than throwing: an error path must never be the thing
|
|
36
|
+
* that fails.
|
|
37
|
+
*/
|
|
38
|
+
static fromResponse(status, body, retryAfter) {
|
|
39
|
+
const envelope = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
40
|
+
const details = {};
|
|
41
|
+
if (envelope) {
|
|
42
|
+
for (const [key, value] of Object.entries(envelope)) {
|
|
43
|
+
if (key !== "code" && key !== "message" && key !== "request_id") details[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
22
46
|
return new _VerneAPIError({
|
|
23
|
-
code:
|
|
24
|
-
message: body
|
|
47
|
+
code: typeof envelope?.code === "string" ? envelope.code : "unknown",
|
|
48
|
+
message: extractMessage(body, status),
|
|
25
49
|
status,
|
|
26
|
-
requestId:
|
|
50
|
+
requestId: typeof envelope?.request_id === "string" ? envelope.request_id : void 0,
|
|
51
|
+
details,
|
|
52
|
+
retryAfter,
|
|
53
|
+
body
|
|
27
54
|
});
|
|
28
55
|
}
|
|
29
56
|
};
|
|
57
|
+
var VerneTokenError = class extends VerneError {
|
|
58
|
+
constructor(reason, message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "VerneTokenError";
|
|
61
|
+
this.reason = reason;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function isRecord(value) {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
function extractMessage(body, status) {
|
|
68
|
+
if (isRecord(body)) {
|
|
69
|
+
const envelope = body.error;
|
|
70
|
+
if (isRecord(envelope) && typeof envelope.message === "string") {
|
|
71
|
+
return envelope.message;
|
|
72
|
+
}
|
|
73
|
+
if (typeof envelope === "string" && envelope.trim() !== "") {
|
|
74
|
+
return envelope;
|
|
75
|
+
}
|
|
76
|
+
const { detail } = body;
|
|
77
|
+
if (typeof detail === "string") return detail;
|
|
78
|
+
if (Array.isArray(detail)) {
|
|
79
|
+
const messages = detail.map((item) => isRecord(item) && typeof item.msg === "string" ? item.msg : null).filter((msg) => msg !== null);
|
|
80
|
+
if (messages.length > 0) return messages.join("; ");
|
|
81
|
+
}
|
|
82
|
+
if (typeof body.message === "string") return body.message;
|
|
83
|
+
}
|
|
84
|
+
return `HTTP ${status}`;
|
|
85
|
+
}
|
|
30
86
|
|
|
31
87
|
// src/core/http.ts
|
|
32
88
|
var DEFAULT_BASE_URL = "https://api.vernesoft.com";
|
|
33
89
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
90
|
+
var MAX_AUTO_RETRY_AFTER_SECONDS = 60;
|
|
91
|
+
var NON_RETRYABLE_429_CODES = /* @__PURE__ */ new Set(["RELAY_MESSAGE_LIMIT_REACHED"]);
|
|
34
92
|
var HttpClient = class {
|
|
35
93
|
constructor(config) {
|
|
36
94
|
this.apiKey = config.apiKey;
|
|
37
|
-
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(
|
|
95
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
38
96
|
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
39
97
|
}
|
|
40
98
|
async get(path, options) {
|
|
@@ -53,16 +111,29 @@ var HttpClient = class {
|
|
|
53
111
|
return this.request("DELETE", path, options);
|
|
54
112
|
}
|
|
55
113
|
async request(method, path, options = {}, attempt = 1) {
|
|
56
|
-
const {
|
|
57
|
-
|
|
114
|
+
const {
|
|
115
|
+
skipAuth,
|
|
116
|
+
body,
|
|
117
|
+
signal,
|
|
118
|
+
headers: extraHeaders,
|
|
119
|
+
timeoutMs,
|
|
120
|
+
query,
|
|
121
|
+
sessionToken
|
|
122
|
+
} = options;
|
|
123
|
+
const qs = buildQuery(query);
|
|
124
|
+
const url = `${this.baseUrl}${path}${qs ? (path.includes("?") ? "&" : "?") + qs : ""}`;
|
|
58
125
|
const timeout = timeoutMs ?? this.timeoutMs;
|
|
59
126
|
const headers = {
|
|
60
127
|
"Content-Type": "application/json",
|
|
128
|
+
Accept: "application/json",
|
|
61
129
|
...extraHeaders
|
|
62
130
|
};
|
|
63
131
|
if (!skipAuth) {
|
|
64
132
|
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
65
133
|
}
|
|
134
|
+
if (sessionToken !== void 0) {
|
|
135
|
+
headers["X-Session-Token"] = sessionToken;
|
|
136
|
+
}
|
|
66
137
|
const controller = new AbortController();
|
|
67
138
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
68
139
|
const combinedSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
@@ -83,20 +154,21 @@ var HttpClient = class {
|
|
|
83
154
|
} finally {
|
|
84
155
|
clearTimeout(timeoutId);
|
|
85
156
|
}
|
|
86
|
-
if (response.status === 429 && attempt === 1 && method === "POST") {
|
|
87
|
-
const retryAfter = response.headers.get("Retry-After");
|
|
88
|
-
const waitMs = retryAfter ? parseFloat(retryAfter) * 1e3 : 1e3;
|
|
89
|
-
await sleep(waitMs);
|
|
90
|
-
return this.request(method, path, options, 2);
|
|
91
|
-
}
|
|
92
157
|
if (!response.ok) {
|
|
158
|
+
const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
|
|
93
159
|
let errorBody;
|
|
94
160
|
try {
|
|
95
161
|
errorBody = await response.json();
|
|
96
162
|
} catch {
|
|
97
|
-
throw
|
|
163
|
+
throw VerneAPIError.fromResponse(response.status, void 0, retryAfter);
|
|
98
164
|
}
|
|
99
|
-
|
|
165
|
+
const error = VerneAPIError.fromResponse(response.status, errorBody, retryAfter);
|
|
166
|
+
const wait = retryAfter ?? 1;
|
|
167
|
+
if (response.status === 429 && attempt === 1 && method === "POST" && wait <= MAX_AUTO_RETRY_AFTER_SECONDS && !NON_RETRYABLE_429_CODES.has(error.code)) {
|
|
168
|
+
await sleep(wait * 1e3);
|
|
169
|
+
return this.request(method, path, options, 2);
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
100
172
|
}
|
|
101
173
|
if (response.status === 204) {
|
|
102
174
|
return void 0;
|
|
@@ -104,6 +176,26 @@ var HttpClient = class {
|
|
|
104
176
|
return response.json();
|
|
105
177
|
}
|
|
106
178
|
};
|
|
179
|
+
function parseRetryAfter(raw) {
|
|
180
|
+
if (raw === null || raw.trim() === "") return void 0;
|
|
181
|
+
const seconds = Number(raw);
|
|
182
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds;
|
|
183
|
+
const at = Date.parse(raw);
|
|
184
|
+
if (!Number.isNaN(at)) return Math.max(0, Math.ceil((at - Date.now()) / 1e3));
|
|
185
|
+
return void 0;
|
|
186
|
+
}
|
|
187
|
+
function buildQuery(params) {
|
|
188
|
+
if (!params) return "";
|
|
189
|
+
const search = new URLSearchParams();
|
|
190
|
+
for (const [key, value] of Object.entries(params)) {
|
|
191
|
+
if (value === void 0 || value === null) continue;
|
|
192
|
+
search.set(key, value instanceof Date ? value.toISOString() : String(value));
|
|
193
|
+
}
|
|
194
|
+
return search.toString();
|
|
195
|
+
}
|
|
196
|
+
function seg(value) {
|
|
197
|
+
return encodeURIComponent(value);
|
|
198
|
+
}
|
|
107
199
|
function sleep(ms) {
|
|
108
200
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
201
|
}
|
|
@@ -201,6 +293,34 @@ var Clockwork = class {
|
|
|
201
293
|
this.jobs = new ClockworkJobsResource(this.http);
|
|
202
294
|
this.delayed = new ClockworkDelayedResource(this.http);
|
|
203
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* How many jobs exist against the plan's limits. Creating a job over a limit, or
|
|
298
|
+
* resuming a paused one, is refused with `402` (`UPGRADE_REQUIRED` or
|
|
299
|
+
* `JOB_LIMIT_REACHED`) — never retried.
|
|
300
|
+
*/
|
|
301
|
+
async usage(options) {
|
|
302
|
+
return this.http.get("/v1/clockwork/usage", options);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/resources/gate/audit-log.ts
|
|
307
|
+
var AuditLogResource = class {
|
|
308
|
+
constructor(http) {
|
|
309
|
+
this.http = http;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* One page of entries, newest first. Filters combine; keep the same filters while
|
|
313
|
+
* paging with `cursor`.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* const page = await gate.auditLog.list({ action: 'identity.login', actor_id: userId });
|
|
317
|
+
*/
|
|
318
|
+
async list(params, options) {
|
|
319
|
+
return this.http.get("/v1/gate/audit-log", {
|
|
320
|
+
query: { ...params },
|
|
321
|
+
...options
|
|
322
|
+
});
|
|
323
|
+
}
|
|
204
324
|
};
|
|
205
325
|
|
|
206
326
|
// src/resources/gate/identities.ts
|
|
@@ -209,50 +329,52 @@ var IdentitiesResource = class {
|
|
|
209
329
|
this.http = http;
|
|
210
330
|
}
|
|
211
331
|
/**
|
|
212
|
-
* Creates a new identity (end
|
|
213
|
-
*
|
|
332
|
+
* Creates a new identity (end user) bound to your tenant. Answers `201`.
|
|
333
|
+
*
|
|
334
|
+
* A `409 conflict` means you already have an end user with this address; the same
|
|
335
|
+
* address at another tenant is a different person and does not conflict. Traits are
|
|
336
|
+
* `email` and `locale` only — keep your own fields in custom metadata.
|
|
214
337
|
*/
|
|
215
338
|
async create(params, options) {
|
|
216
339
|
return this.http.post("/v1/gate/identities", { body: params, ...options });
|
|
217
340
|
}
|
|
218
341
|
/**
|
|
219
|
-
* Returns an identity by ID.
|
|
220
|
-
* The Gateway will reject the request if the identity does not belong to your tenant.
|
|
342
|
+
* Returns an identity by ID. An identity of another tenant is refused.
|
|
221
343
|
*/
|
|
222
344
|
async get(identityId, options) {
|
|
223
|
-
return this.http.get(`/v1/gate/identities/${identityId}`, options);
|
|
345
|
+
return this.http.get(`/v1/gate/identities/${seg(identityId)}`, options);
|
|
224
346
|
}
|
|
225
347
|
/**
|
|
226
348
|
* Partially updates an identity using a JSON Patch document (RFC 6902).
|
|
227
349
|
*
|
|
228
350
|
* @example
|
|
229
351
|
* await gate.identities.patch('identity_123', [
|
|
230
|
-
* { op: 'replace', path: '/traits/
|
|
352
|
+
* { op: 'replace', path: '/traits/email', value: 'new@example.com' },
|
|
231
353
|
* ]);
|
|
232
354
|
*/
|
|
233
355
|
async patch(identityId, ops, options) {
|
|
234
|
-
return this.http.patch(`/v1/gate/identities/${identityId}`, {
|
|
356
|
+
return this.http.patch(`/v1/gate/identities/${seg(identityId)}`, {
|
|
235
357
|
body: ops,
|
|
236
358
|
...options
|
|
237
359
|
});
|
|
238
360
|
}
|
|
239
361
|
/**
|
|
240
|
-
* Deletes an identity
|
|
241
|
-
* The Gateway will reject the request if the identity does not belong to your tenant.
|
|
362
|
+
* Deletes an identity, its sessions and its memberships. Fires `identity.deleted`.
|
|
242
363
|
*/
|
|
243
364
|
async delete(identityId, options) {
|
|
244
|
-
return this.http.delete(`/v1/gate/identities/${identityId}`, options);
|
|
365
|
+
return this.http.delete(`/v1/gate/identities/${seg(identityId)}`, options);
|
|
245
366
|
}
|
|
246
367
|
/**
|
|
247
|
-
* Activates or deactivates an identity. An `inactive` identity cannot
|
|
248
|
-
*
|
|
249
|
-
*
|
|
368
|
+
* Activates or deactivates an identity. An `inactive` identity cannot sign in, its
|
|
369
|
+
* sessions stop working (until it is active again), its access tokens introspect as
|
|
370
|
+
* `active: false`, and no signed token is issued for it. Nothing is deleted. Fires the
|
|
371
|
+
* `identity.state_changed` webhook event.
|
|
250
372
|
*
|
|
251
373
|
* @example
|
|
252
374
|
* await gate.identities.setState('identity_123', 'inactive');
|
|
253
375
|
*/
|
|
254
376
|
async setState(identityId, state, options) {
|
|
255
|
-
return this.http.patch(`/v1/gate/identities/${identityId}/state`, {
|
|
377
|
+
return this.http.patch(`/v1/gate/identities/${seg(identityId)}/state`, {
|
|
256
378
|
body: { state },
|
|
257
379
|
...options
|
|
258
380
|
});
|
|
@@ -272,9 +394,272 @@ var IdentitiesResource = class {
|
|
|
272
394
|
*/
|
|
273
395
|
async resendVerification(identityId, options) {
|
|
274
396
|
return this.http.post(
|
|
275
|
-
`/v1/gate/identities/${identityId}/resend-verification`,
|
|
397
|
+
`/v1/gate/identities/${seg(identityId)}/resend-verification`,
|
|
398
|
+
options
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Reads an identity's custom metadata (`metadata_public`) — the place for your own
|
|
403
|
+
* fields about a user. `{}` when nothing is set.
|
|
404
|
+
*/
|
|
405
|
+
async getMetadata(identityId, options) {
|
|
406
|
+
const res = await this.http.get(
|
|
407
|
+
`/v1/gate/identities/${seg(identityId)}/metadata`,
|
|
276
408
|
options
|
|
277
409
|
);
|
|
410
|
+
return res.metadata_public;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Merges `patch` into an identity's custom metadata and returns the result. Keys you
|
|
414
|
+
* leave out are kept; a key set to `null` is removed.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* await gate.identities.updateMetadata(id, { plan: 'pro', seats: null });
|
|
418
|
+
*/
|
|
419
|
+
async updateMetadata(identityId, patch, options) {
|
|
420
|
+
const res = await this.http.patch(
|
|
421
|
+
`/v1/gate/identities/${seg(identityId)}/metadata`,
|
|
422
|
+
{ body: patch, ...options }
|
|
423
|
+
);
|
|
424
|
+
return res.metadata_public;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* The organizations of the key's project that an end user belongs to, with the role
|
|
428
|
+
* held in each and its permissions — live, oldest membership first, cursor-paged. This
|
|
429
|
+
* is the complete list that `orgs_truncated: true` (in a token, an introspection or a
|
|
430
|
+
* session check) points to.
|
|
431
|
+
*/
|
|
432
|
+
async listOrganizations(identityId, params, options) {
|
|
433
|
+
return this.http.get(
|
|
434
|
+
`/v1/gate/identities/${seg(identityId)}/organizations`,
|
|
435
|
+
{ query: { limit: params?.limit, cursor: params?.cursor }, ...options }
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/resources/gate/lifecycle.ts
|
|
441
|
+
var rulePath = (kind) => `/v1/gate/lifecycle/rules/${seg(kind)}`;
|
|
442
|
+
var LifecycleResource = class {
|
|
443
|
+
constructor(http) {
|
|
444
|
+
this.http = http;
|
|
445
|
+
}
|
|
446
|
+
/** All four rules of the key's project, set up or not, and the plan. */
|
|
447
|
+
async listRules(options) {
|
|
448
|
+
return this.http.get("/v1/gate/lifecycle/rules", options);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Creates, changes, or switches a rule on or off. Leave `params` out to keep the saved
|
|
452
|
+
* settings. Switching off or changing settings cancels what the rule had scheduled.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* await gate.lifecycle.setRule('delete_unverified', {
|
|
456
|
+
* enabled: true,
|
|
457
|
+
* params: { delete_after_days: 14, warn_before_days: 3 },
|
|
458
|
+
* });
|
|
459
|
+
*/
|
|
460
|
+
async setRule(kind, params, options) {
|
|
461
|
+
const body = { enabled: params.enabled };
|
|
462
|
+
if (params.params !== void 0) body.params = params.params;
|
|
463
|
+
return this.http.put(rulePath(kind), { body, ...options });
|
|
464
|
+
}
|
|
465
|
+
/** Removes a rule and cancels what it scheduled. Succeeds also when there was none. */
|
|
466
|
+
async deleteRule(kind, options) {
|
|
467
|
+
return this.http.delete(rulePath(kind), options);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* How many people a rule with these settings would affect right now. Writes nothing;
|
|
471
|
+
* works whether or not the rule is on or in your plan. `expire_roles` takes `{}`.
|
|
472
|
+
*/
|
|
473
|
+
async previewRule(kind, params, options) {
|
|
474
|
+
return this.http.post(`${rulePath(kind)}/preview`, {
|
|
475
|
+
body: { params },
|
|
476
|
+
...options
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/** What the rules have scheduled and done, oldest first, cursor-paged. */
|
|
480
|
+
async listActions(params, options) {
|
|
481
|
+
return this.http.get("/v1/gate/lifecycle/actions", {
|
|
482
|
+
query: { ...params },
|
|
483
|
+
...options
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/resources/gate/organizations.ts
|
|
489
|
+
var orgPath = (orgId) => `/v1/gate/organizations/${seg(orgId)}`;
|
|
490
|
+
var pageQuery = (params) => ({ limit: params?.limit, cursor: params?.cursor });
|
|
491
|
+
var isoOrNull = (value) => value instanceof Date ? value.toISOString() : value;
|
|
492
|
+
var OrganizationsResource = class {
|
|
493
|
+
constructor(http) {
|
|
494
|
+
this.http = http;
|
|
495
|
+
this.roles = new RolesResource(http);
|
|
496
|
+
this.members = new MembersResource(http);
|
|
497
|
+
}
|
|
498
|
+
/** Creates an organization. `name` is free text, 1–120 characters, not unique. */
|
|
499
|
+
async create(params, options) {
|
|
500
|
+
return this.http.post("/v1/gate/organizations", { body: params, ...options });
|
|
501
|
+
}
|
|
502
|
+
/** Lists the project's organizations, oldest first, cursor-paged. */
|
|
503
|
+
async list(params, options) {
|
|
504
|
+
return this.http.get("/v1/gate/organizations", {
|
|
505
|
+
query: pageQuery(params),
|
|
506
|
+
...options
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
async get(orgId, options) {
|
|
510
|
+
return this.http.get(orgPath(orgId), options);
|
|
511
|
+
}
|
|
512
|
+
/** Renames an organization. */
|
|
513
|
+
async update(orgId, params, options) {
|
|
514
|
+
return this.http.patch(orgPath(orgId), { body: params, ...options });
|
|
515
|
+
}
|
|
516
|
+
/** Deletes an organization with its roles and memberships. */
|
|
517
|
+
async delete(orgId, options) {
|
|
518
|
+
return this.http.delete(orgPath(orgId), options);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* *May this end user do this in this organization?* — answered from the role the user
|
|
522
|
+
* holds there and its permissions, as they are now. `permission` must match exactly
|
|
523
|
+
* (no wildcards). An organization of another project looks like a missing one
|
|
524
|
+
* (`no_such_organization`). An invalid permission (upper case, `*`, the `nautilus:`
|
|
525
|
+
* prefix) is a `400` error, never `allowed: false`. Not billed, not audited.
|
|
526
|
+
*/
|
|
527
|
+
async authorize(orgId, identityId, permission, options) {
|
|
528
|
+
return this.http.post("/v1/gate/authorize", {
|
|
529
|
+
body: { subject: identityId, action: permission, resource: `organization:${orgId}` },
|
|
530
|
+
...options
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* `true` when the end user holds `permission` in the organization — the boolean of
|
|
535
|
+
* {@link authorize}. Errors are thrown, not turned into `false`.
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* if (!(await gate.organizations.can(orgId, userId, 'invoices:write'))) {
|
|
539
|
+
* throw new Error('Forbidden');
|
|
540
|
+
* }
|
|
541
|
+
*/
|
|
542
|
+
async can(orgId, identityId, permission, options) {
|
|
543
|
+
const decision = await this.authorize(orgId, identityId, permission, options);
|
|
544
|
+
return decision.allowed === true;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
var RolesResource = class {
|
|
548
|
+
constructor(http) {
|
|
549
|
+
this.http = http;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Defines a role. `409 conflict` if the name is taken in the organization,
|
|
553
|
+
* `409 limit_reached` past 100 roles.
|
|
554
|
+
*/
|
|
555
|
+
async create(orgId, params, options) {
|
|
556
|
+
return this.http.post(`${orgPath(orgId)}/roles`, { body: params, ...options });
|
|
557
|
+
}
|
|
558
|
+
async list(orgId, params, options) {
|
|
559
|
+
return this.http.get(`${orgPath(orgId)}/roles`, {
|
|
560
|
+
query: pageQuery(params),
|
|
561
|
+
...options
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
async get(orgId, roleId, options) {
|
|
565
|
+
return this.http.get(`${orgPath(orgId)}/roles/${seg(roleId)}`, options);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Renames a role and/or **replaces** its permissions. Everyone who holds it has the new
|
|
569
|
+
* permissions the next time they are read.
|
|
570
|
+
*/
|
|
571
|
+
async update(orgId, roleId, params, options) {
|
|
572
|
+
return this.http.patch(`${orgPath(orgId)}/roles/${seg(roleId)}`, {
|
|
573
|
+
body: params,
|
|
574
|
+
...options
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/** Deletes a role. A role someone holds is refused with `409 conflict`. */
|
|
578
|
+
async delete(orgId, roleId, options) {
|
|
579
|
+
return this.http.delete(`${orgPath(orgId)}/roles/${seg(roleId)}`, options);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
var MembersResource = class {
|
|
583
|
+
constructor(http) {
|
|
584
|
+
this.http = http;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Gives an end user a role in the organization. `404` if the identity is not one of
|
|
588
|
+
* your end users, `409` if they already are a member (change the role with
|
|
589
|
+
* {@link update}).
|
|
590
|
+
*/
|
|
591
|
+
async add(orgId, params, options) {
|
|
592
|
+
const body = {
|
|
593
|
+
identity_id: params.identity_id,
|
|
594
|
+
role_id: params.role_id
|
|
595
|
+
};
|
|
596
|
+
if (params.expires_at !== void 0) body.expires_at = isoOrNull(params.expires_at);
|
|
597
|
+
return this.http.post(`${orgPath(orgId)}/members`, { body, ...options });
|
|
598
|
+
}
|
|
599
|
+
async list(orgId, params, options) {
|
|
600
|
+
return this.http.get(`${orgPath(orgId)}/members`, {
|
|
601
|
+
query: pageQuery(params),
|
|
602
|
+
...options
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Changes a member's role and/or deadline. `expires_at`: leave it out to keep the
|
|
607
|
+
* deadline, `null` to remove it, a time to set it.
|
|
608
|
+
*/
|
|
609
|
+
async update(orgId, identityId, params, options) {
|
|
610
|
+
const body = {};
|
|
611
|
+
if (params.role_id !== void 0) body.role_id = params.role_id;
|
|
612
|
+
if (params.expires_at !== void 0) body.expires_at = isoOrNull(params.expires_at);
|
|
613
|
+
return this.http.patch(`${orgPath(orgId)}/members/${seg(identityId)}`, {
|
|
614
|
+
body,
|
|
615
|
+
...options
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
/** Removes the membership. The person stays your end user. */
|
|
619
|
+
async remove(orgId, identityId, options) {
|
|
620
|
+
return this.http.delete(`${orgPath(orgId)}/members/${seg(identityId)}`, options);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// src/resources/gate/sessions.ts
|
|
625
|
+
var SessionsResource = class {
|
|
626
|
+
constructor(http) {
|
|
627
|
+
this.http = http;
|
|
628
|
+
}
|
|
629
|
+
/** The active sessions of one of your end users (a bare array, not paged). */
|
|
630
|
+
async list(identityId, options) {
|
|
631
|
+
return this.http.get(`/v1/gate/identities/${seg(identityId)}/sessions`, options);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Revokes every session of an end user — "log out everywhere". Signed tokens already
|
|
635
|
+
* issued keep verifying until their own `exp` (at most 15 minutes).
|
|
636
|
+
*
|
|
637
|
+
* Idempotent: a user of yours with no session at all is a success too. An id that is not
|
|
638
|
+
* one of your users — another account's, unknown, or one you deleted — is
|
|
639
|
+
* `403 forbidden`. `404 not_found` is rare: a user removed from the identity service
|
|
640
|
+
* other than through `identities.delete()`, which Gate still remembered as yours.
|
|
641
|
+
*/
|
|
642
|
+
async revokeAll(identityId, options) {
|
|
643
|
+
return this.http.delete(`/v1/gate/identities/${seg(identityId)}/sessions`, options);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Revokes one session by its id. Revoking an already revoked session of your user is a
|
|
647
|
+
* success too; an id that is not one of your users' sessions is `404`.
|
|
648
|
+
*/
|
|
649
|
+
async revoke(sessionId, options) {
|
|
650
|
+
return this.http.delete(`/v1/gate/sessions/${seg(sessionId)}`, options);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Whether an end user's session meets what your tenant requires — the one call to
|
|
654
|
+
* gate your own routes on. Gate on `mfa_satisfied`. Also lists the user's
|
|
655
|
+
* organizations in the key's project, read now.
|
|
656
|
+
*
|
|
657
|
+
* Throws `VerneAPIError` with `status 401` for a dead, foreign or unknown session, and
|
|
658
|
+
* `code 'mfa_required'` (`403`) when a second factor is owed: start a step-up flow with
|
|
659
|
+
* `gate.createLoginFlow({ aal: 'aal2', session_token })`.
|
|
660
|
+
*/
|
|
661
|
+
async check(sessionToken, options) {
|
|
662
|
+
return this.http.get("/v1/gate/auth/session", { sessionToken, ...options });
|
|
278
663
|
}
|
|
279
664
|
};
|
|
280
665
|
|
|
@@ -284,7 +669,8 @@ var SettingsResource = class {
|
|
|
284
669
|
this.http = http;
|
|
285
670
|
}
|
|
286
671
|
/**
|
|
287
|
-
* Returns the tenant's security settings (passwordless / MFA).
|
|
672
|
+
* Returns the tenant's security settings (passwordless / MFA). With `mfa_enabled`,
|
|
673
|
+
* `sessions.check()` refuses a session without a second factor (`403 mfa_required`).
|
|
288
674
|
*/
|
|
289
675
|
async getSecurity(options) {
|
|
290
676
|
return this.http.get("/v1/gate/settings/security", options);
|
|
@@ -331,29 +717,258 @@ var SettingsResource = class {
|
|
|
331
717
|
);
|
|
332
718
|
return res.providers;
|
|
333
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* The project's social-login return URLs — the addresses on your backend that receive
|
|
722
|
+
* the user (with a one-time `code`) after a social login. The first is the default.
|
|
723
|
+
*/
|
|
724
|
+
async getReturnUrls(options) {
|
|
725
|
+
const res = await this.http.get(
|
|
726
|
+
"/v1/gate/settings/return-urls",
|
|
727
|
+
options
|
|
728
|
+
);
|
|
729
|
+
return res.return_urls;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Replaces the project's return URLs (at most 10; an empty list switches the feature
|
|
733
|
+
* off) and returns them as they will be matched — exactly, never by prefix. `https`
|
|
734
|
+
* only, except `http://localhost` and `http://127.0.0.1`; no query, fragment, `@` or
|
|
735
|
+
* wildcard (`400 invalid_payload` names the entry).
|
|
736
|
+
*/
|
|
737
|
+
async updateReturnUrls(urls, options) {
|
|
738
|
+
const res = await this.http.put("/v1/gate/settings/return-urls", {
|
|
739
|
+
body: { return_urls: urls },
|
|
740
|
+
...options
|
|
741
|
+
});
|
|
742
|
+
return res.return_urls;
|
|
743
|
+
}
|
|
334
744
|
};
|
|
335
745
|
|
|
746
|
+
// src/resources/gate/verifier.ts
|
|
747
|
+
var DEFAULT_JWKS_MAX_AGE_MS = 3e5;
|
|
748
|
+
var MIN_REFETCH_INTERVAL_MS = 3e4;
|
|
749
|
+
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 10;
|
|
750
|
+
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
751
|
+
var GateTokenVerifier = class {
|
|
752
|
+
constructor(config) {
|
|
753
|
+
this.keys = /* @__PURE__ */ new Map();
|
|
754
|
+
this.fetchedAt = 0;
|
|
755
|
+
this.maxAgeMs = DEFAULT_JWKS_MAX_AGE_MS;
|
|
756
|
+
if (!config.tenantId) {
|
|
757
|
+
throw new VerneError("GateTokenVerifier needs your tenantId.");
|
|
758
|
+
}
|
|
759
|
+
const base = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
760
|
+
this.issuer = `${base}/auth/${config.tenantId}`;
|
|
761
|
+
this.jwksUrl = `${this.issuer}/.well-known/jwks.json`;
|
|
762
|
+
this.toleranceSeconds = config.clockToleranceSeconds ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
763
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Verifies `token` and returns its claims. Throws {@link VerneTokenError} (with a
|
|
767
|
+
* `reason`) when the token must not be trusted.
|
|
768
|
+
*/
|
|
769
|
+
async verify(token) {
|
|
770
|
+
const parts = typeof token === "string" ? token.split(".") : [];
|
|
771
|
+
if (parts.length !== 3) {
|
|
772
|
+
throw new VerneTokenError("malformed", "Not a compact JWT.");
|
|
773
|
+
}
|
|
774
|
+
const [rawHeader, rawPayload, rawSignature] = parts;
|
|
775
|
+
const header = decodeJson(rawHeader, "header");
|
|
776
|
+
const payload = decodeJson(rawPayload, "payload");
|
|
777
|
+
if (header.alg !== "ES256") {
|
|
778
|
+
throw new VerneTokenError(
|
|
779
|
+
"unsupported_alg",
|
|
780
|
+
`Token algorithm ${JSON.stringify(header.alg)} is not accepted; Gate signs with ES256.`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (typeof header.kid !== "string" || header.kid === "") {
|
|
784
|
+
throw new VerneTokenError("malformed", "Token header has no `kid`.");
|
|
785
|
+
}
|
|
786
|
+
const key = await this.keyFor(header.kid);
|
|
787
|
+
const signature = base64UrlDecode(rawSignature);
|
|
788
|
+
if (!signature || signature.length !== 64) {
|
|
789
|
+
throw new VerneTokenError("bad_signature", "Token signature is not a P-256 signature.");
|
|
790
|
+
}
|
|
791
|
+
const subtle = await subtleCrypto();
|
|
792
|
+
const valid = await subtle.verify(
|
|
793
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
794
|
+
key,
|
|
795
|
+
signature,
|
|
796
|
+
new TextEncoder().encode(`${rawHeader}.${rawPayload}`)
|
|
797
|
+
);
|
|
798
|
+
if (!valid) {
|
|
799
|
+
throw new VerneTokenError("bad_signature", "Token signature does not verify.");
|
|
800
|
+
}
|
|
801
|
+
if (payload.iss !== this.issuer) {
|
|
802
|
+
throw new VerneTokenError(
|
|
803
|
+
"wrong_issuer",
|
|
804
|
+
`Token issuer ${JSON.stringify(payload.iss)} is not ${this.issuer}.`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
808
|
+
if (typeof payload.exp !== "number") {
|
|
809
|
+
throw new VerneTokenError("missing_claim", "Token has no `exp`.");
|
|
810
|
+
}
|
|
811
|
+
if (now > payload.exp + this.toleranceSeconds) {
|
|
812
|
+
throw new VerneTokenError("expired", "Token has expired.");
|
|
813
|
+
}
|
|
814
|
+
if (typeof payload.nbf === "number" && payload.nbf > now + this.toleranceSeconds) {
|
|
815
|
+
throw new VerneTokenError("not_yet_valid", "Token is not valid yet.");
|
|
816
|
+
}
|
|
817
|
+
if (typeof payload.sub !== "string" || payload.sub === "") {
|
|
818
|
+
throw new VerneTokenError("missing_claim", "Token has no `sub`.");
|
|
819
|
+
}
|
|
820
|
+
return payload;
|
|
821
|
+
}
|
|
822
|
+
async keyFor(kid) {
|
|
823
|
+
const stale = Date.now() - this.fetchedAt > this.maxAgeMs;
|
|
824
|
+
if (stale) {
|
|
825
|
+
await this.refresh(this.keys.size === 0);
|
|
826
|
+
}
|
|
827
|
+
let key = this.keys.get(kid);
|
|
828
|
+
if (!key && Date.now() - this.fetchedAt >= MIN_REFETCH_INTERVAL_MS) {
|
|
829
|
+
await this.refresh(true);
|
|
830
|
+
key = this.keys.get(kid);
|
|
831
|
+
}
|
|
832
|
+
if (!key) {
|
|
833
|
+
throw new VerneTokenError("unknown_kid", `No key ${kid} in ${this.jwksUrl}.`);
|
|
834
|
+
}
|
|
835
|
+
return key;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Fetches the key set. With `required` false a failure keeps the keys already cached
|
|
839
|
+
* (a stale key set is still the right one unless a token says otherwise).
|
|
840
|
+
*/
|
|
841
|
+
async refresh(required) {
|
|
842
|
+
this.inflight ?? (this.inflight = this.load().finally(() => {
|
|
843
|
+
this.inflight = void 0;
|
|
844
|
+
}));
|
|
845
|
+
try {
|
|
846
|
+
await this.inflight;
|
|
847
|
+
} catch (err) {
|
|
848
|
+
if (required) throw err;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async load() {
|
|
852
|
+
let response;
|
|
853
|
+
const controller = new AbortController();
|
|
854
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
855
|
+
try {
|
|
856
|
+
response = await fetch(this.jwksUrl, {
|
|
857
|
+
headers: { Accept: "application/json" },
|
|
858
|
+
signal: controller.signal
|
|
859
|
+
});
|
|
860
|
+
} catch (err) {
|
|
861
|
+
throw new VerneTokenError(
|
|
862
|
+
"jwks_unavailable",
|
|
863
|
+
`Could not fetch ${this.jwksUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
864
|
+
);
|
|
865
|
+
} finally {
|
|
866
|
+
clearTimeout(timer);
|
|
867
|
+
}
|
|
868
|
+
if (!response.ok) {
|
|
869
|
+
throw new VerneTokenError(
|
|
870
|
+
"jwks_unavailable",
|
|
871
|
+
`${this.jwksUrl} answered HTTP ${response.status}.`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
let body;
|
|
875
|
+
try {
|
|
876
|
+
body = await response.json();
|
|
877
|
+
} catch {
|
|
878
|
+
throw new VerneTokenError("jwks_unavailable", `${this.jwksUrl} did not answer JSON.`);
|
|
879
|
+
}
|
|
880
|
+
const jwks = body.keys;
|
|
881
|
+
if (!Array.isArray(jwks)) {
|
|
882
|
+
throw new VerneTokenError("jwks_unavailable", `${this.jwksUrl} has no \`keys\`.`);
|
|
883
|
+
}
|
|
884
|
+
const subtle = await subtleCrypto();
|
|
885
|
+
const keys = /* @__PURE__ */ new Map();
|
|
886
|
+
for (const jwk of jwks) {
|
|
887
|
+
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.kid !== "string") continue;
|
|
888
|
+
if (jwk.use !== void 0 && jwk.use !== "sig") continue;
|
|
889
|
+
const { kty, crv, x, y } = jwk;
|
|
890
|
+
try {
|
|
891
|
+
keys.set(
|
|
892
|
+
jwk.kid,
|
|
893
|
+
await subtle.importKey(
|
|
894
|
+
"jwk",
|
|
895
|
+
{ kty, crv, x, y },
|
|
896
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
897
|
+
false,
|
|
898
|
+
["verify"]
|
|
899
|
+
)
|
|
900
|
+
);
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
this.keys = keys;
|
|
905
|
+
this.fetchedAt = Date.now();
|
|
906
|
+
this.maxAgeMs = maxAgeMs(response.headers.get("Cache-Control"));
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
function maxAgeMs(cacheControl) {
|
|
910
|
+
const match = cacheControl ? /max-age=(\d+)/i.exec(cacheControl) : null;
|
|
911
|
+
return match ? Number(match[1]) * 1e3 : DEFAULT_JWKS_MAX_AGE_MS;
|
|
912
|
+
}
|
|
913
|
+
function decodeJson(segment, what) {
|
|
914
|
+
const bytes = base64UrlDecode(segment);
|
|
915
|
+
if (bytes) {
|
|
916
|
+
try {
|
|
917
|
+
const value = JSON.parse(new TextDecoder().decode(bytes));
|
|
918
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
throw new VerneTokenError("malformed", `Token ${what} is not base64url JSON.`);
|
|
925
|
+
}
|
|
926
|
+
function base64UrlDecode(segment) {
|
|
927
|
+
if (!/^[A-Za-z0-9_-]*$/.test(segment)) return null;
|
|
928
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
929
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
930
|
+
try {
|
|
931
|
+
const binary = atob(padded);
|
|
932
|
+
const bytes = new Uint8Array(binary.length);
|
|
933
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
934
|
+
return bytes;
|
|
935
|
+
} catch {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
var subtlePromise;
|
|
940
|
+
function subtleCrypto() {
|
|
941
|
+
subtlePromise ?? (subtlePromise = (async () => {
|
|
942
|
+
const global = globalThis.crypto;
|
|
943
|
+
if (global?.subtle) return global.subtle;
|
|
944
|
+
const nodeCrypto = await import('crypto');
|
|
945
|
+
return nodeCrypto.webcrypto.subtle;
|
|
946
|
+
})());
|
|
947
|
+
return subtlePromise;
|
|
948
|
+
}
|
|
949
|
+
|
|
336
950
|
// src/resources/gate/tokens.ts
|
|
337
951
|
var TokensResource = class {
|
|
338
|
-
constructor(http
|
|
952
|
+
constructor(http) {
|
|
339
953
|
this.http = http;
|
|
340
|
-
this.
|
|
954
|
+
this.verifiers = /* @__PURE__ */ new Map();
|
|
341
955
|
}
|
|
342
956
|
/**
|
|
343
|
-
* Exchanges the Gate API key for a short-lived access token
|
|
957
|
+
* Exchanges the Gate API key for a short-lived access token (`gat_live_…`), for
|
|
958
|
+
* `subject` — a user or a service account. Answers `201`.
|
|
344
959
|
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
960
|
+
* The key travels in the `Authorization` header like on every other call. (Until 1.4.0
|
|
961
|
+
* this SDK sent it in the body without the header, which the API refuses with `401`.)
|
|
347
962
|
*/
|
|
348
963
|
async create(params, options) {
|
|
349
|
-
return this.http.post("/v1/gate/tokens", {
|
|
350
|
-
body: { api_key: this.apiKey, ...params },
|
|
351
|
-
skipAuth: true,
|
|
352
|
-
...options
|
|
353
|
-
});
|
|
964
|
+
return this.http.post("/v1/gate/tokens", { body: params, ...options });
|
|
354
965
|
}
|
|
355
966
|
/**
|
|
356
|
-
* Validates a Gate access token and returns its
|
|
967
|
+
* Validates a Gate access token and returns its attributes. An unknown, expired or
|
|
968
|
+
* foreign token, or one whose user you deactivated or deleted, is `{ active: false }`.
|
|
969
|
+
*
|
|
970
|
+
* When the subject is an end user, `orgs` lists their organizations in the key's
|
|
971
|
+
* project, read at the moment you ask.
|
|
357
972
|
*/
|
|
358
973
|
async introspect(accessToken, options) {
|
|
359
974
|
return this.http.post("/v1/gate/tokens/introspect", {
|
|
@@ -361,6 +976,51 @@ var TokensResource = class {
|
|
|
361
976
|
...options
|
|
362
977
|
});
|
|
363
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Gets a **signed JWT** (ES256) for a signed-in end user's session, for a platform that
|
|
981
|
+
* verifies signatures instead of calling Gate (Supabase, an API gateway). The session
|
|
982
|
+
* token is the credential: no API key is sent.
|
|
983
|
+
*
|
|
984
|
+
* The token is valid for 15 minutes and **cannot be revoked**; its `orgs` claim is as old
|
|
985
|
+
* as the token. Every call mints a new one — ask again before the one you hold expires.
|
|
986
|
+
* A revoked or expired session, or a deactivated user, is a `401`.
|
|
987
|
+
*
|
|
988
|
+
* @example
|
|
989
|
+
* const { token } = await gate.tokens.createSigned(sessionToken);
|
|
990
|
+
*/
|
|
991
|
+
async createSigned(sessionToken, options) {
|
|
992
|
+
const res = await this.http.get("/auth/sessions/whoami", {
|
|
993
|
+
query: { tokenize_as: "gate_jwt" },
|
|
994
|
+
sessionToken,
|
|
995
|
+
skipAuth: true,
|
|
996
|
+
...options
|
|
997
|
+
});
|
|
998
|
+
const { tokenized, ...session } = res;
|
|
999
|
+
return { token: tokenized, session };
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Verifies a Gate signed token offline against your tenant's public keys (fetched once
|
|
1003
|
+
* and cached) and returns its claims. Throws `VerneTokenError` when the token must not
|
|
1004
|
+
* be trusted — wrong issuer (another tenant), bad signature, expired, any algorithm but
|
|
1005
|
+
* ES256. See {@link GateTokenVerifier} to verify without a Gate client.
|
|
1006
|
+
*
|
|
1007
|
+
* @example
|
|
1008
|
+
* const claims = await gate.tokens.verifySigned(token, { tenantId: 'your-tenant-id' });
|
|
1009
|
+
* claims.sub; // the end user's id
|
|
1010
|
+
*/
|
|
1011
|
+
async verifySigned(token, options) {
|
|
1012
|
+
const cacheKey = `${options.tenantId}|${options.clockToleranceSeconds ?? ""}`;
|
|
1013
|
+
let verifier = this.verifiers.get(cacheKey);
|
|
1014
|
+
if (!verifier) {
|
|
1015
|
+
verifier = new GateTokenVerifier({
|
|
1016
|
+
tenantId: options.tenantId,
|
|
1017
|
+
baseUrl: this.http.baseUrl,
|
|
1018
|
+
clockToleranceSeconds: options.clockToleranceSeconds
|
|
1019
|
+
});
|
|
1020
|
+
this.verifiers.set(cacheKey, verifier);
|
|
1021
|
+
}
|
|
1022
|
+
return verifier.verify(token);
|
|
1023
|
+
}
|
|
364
1024
|
};
|
|
365
1025
|
|
|
366
1026
|
// src/resources/gate/gate.ts
|
|
@@ -372,20 +1032,31 @@ var Gate = class {
|
|
|
372
1032
|
timeoutMs: config.timeoutMs
|
|
373
1033
|
});
|
|
374
1034
|
this.identities = new IdentitiesResource(this.http);
|
|
375
|
-
this.tokens = new TokensResource(this.http
|
|
1035
|
+
this.tokens = new TokensResource(this.http);
|
|
376
1036
|
this.settings = new SettingsResource(this.http);
|
|
1037
|
+
this.organizations = new OrganizationsResource(this.http);
|
|
1038
|
+
this.sessions = new SessionsResource(this.http);
|
|
1039
|
+
this.auditLog = new AuditLogResource(this.http);
|
|
1040
|
+
this.lifecycle = new LifecycleResource(this.http);
|
|
377
1041
|
}
|
|
378
1042
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
1043
|
+
* Asks whether a subject may perform an action on a resource. The answer is a decision;
|
|
1044
|
+
* your service enforces it.
|
|
1045
|
+
*
|
|
1046
|
+
* - `resource: 'organization:<id>'` — may this end user do `action` (a permission) in
|
|
1047
|
+
* that organization? `reason` is one of a fixed set and `role` is the role they hold.
|
|
1048
|
+
* Prefer `gate.organizations.authorize()` / `can()`, which build the resource.
|
|
1049
|
+
* - any other resource — only *is `subject` one of your active end users?*; `reason` is
|
|
1050
|
+
* `owned_by_tenant` (allowed), `subject_inactive`, `not_owned_by_tenant` or
|
|
1051
|
+
* `not_a_known_identity`, and `message` says it in a sentence for your logs.
|
|
381
1052
|
*
|
|
382
1053
|
* @example
|
|
383
|
-
* const
|
|
384
|
-
* subject:
|
|
385
|
-
* action: '
|
|
386
|
-
* resource:
|
|
1054
|
+
* const decision = await gate.authorize({
|
|
1055
|
+
* subject: userId,
|
|
1056
|
+
* action: 'invoices:write',
|
|
1057
|
+
* resource: `organization:${orgId}`,
|
|
387
1058
|
* });
|
|
388
|
-
* if (!
|
|
1059
|
+
* if (!decision.allowed) throw new Error('Forbidden');
|
|
389
1060
|
*/
|
|
390
1061
|
async authorize(params, options) {
|
|
391
1062
|
return this.http.post("/v1/gate/authorize", { body: params, ...options });
|
|
@@ -401,23 +1072,47 @@ var Gate = class {
|
|
|
401
1072
|
*/
|
|
402
1073
|
async getEnabledProviders(tenantId, options) {
|
|
403
1074
|
const res = await this.http.get(
|
|
404
|
-
`/public/gate/providers/${
|
|
1075
|
+
`/public/gate/providers/${seg(tenantId)}`,
|
|
405
1076
|
{ skipAuth: true, ...options }
|
|
406
1077
|
);
|
|
407
1078
|
return res.providers;
|
|
408
1079
|
}
|
|
409
1080
|
/**
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
1081
|
+
* Starts a Kratos login flow with your Gate API key and returns it; render its
|
|
1082
|
+
* `ui.nodes` or post credentials to its `ui.action`. With a return URL registered,
|
|
1083
|
+
* pass `return_to` and always a `state` for social login. A step-up (second factor) is
|
|
1084
|
+
* `{ aal: 'aal2', session_token }`.
|
|
414
1085
|
*
|
|
415
1086
|
* @example
|
|
416
|
-
* const flow = await gate.createLoginFlow();
|
|
417
|
-
* // send `flow` to the browser as JSON
|
|
1087
|
+
* const flow = await gate.createLoginFlow({ state: csrfBoundValue });
|
|
418
1088
|
*/
|
|
419
|
-
async createLoginFlow(options) {
|
|
420
|
-
return this.http.get("/v1/gate/auth/login",
|
|
1089
|
+
async createLoginFlow(params, options) {
|
|
1090
|
+
return this.http.get("/v1/gate/auth/login", {
|
|
1091
|
+
query: { return_to: params?.return_to, state: params?.state, aal: params?.aal },
|
|
1092
|
+
sessionToken: params?.session_token,
|
|
1093
|
+
...options
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Starts a Kratos registration flow. Refused with `402 ACTIVE_USER_LIMIT_REACHED` when
|
|
1098
|
+
* the account is past its active-user limit and the month to move up has ended.
|
|
1099
|
+
*/
|
|
1100
|
+
async createRegistrationFlow(params, options) {
|
|
1101
|
+
return this.http.get("/v1/gate/auth/registration", {
|
|
1102
|
+
query: { return_to: params?.return_to, state: params?.state },
|
|
1103
|
+
...options
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Exchanges the one-time `code` your return URL received after a social login for the
|
|
1108
|
+
* user's session. The code works once, for about 60 seconds, and only with a key of the
|
|
1109
|
+
* project that started the login (otherwise `404`). Keep the session token server-side.
|
|
1110
|
+
*/
|
|
1111
|
+
async exchangeOidcCode(code, options) {
|
|
1112
|
+
return this.http.post("/v1/gate/auth/oidc/exchange", {
|
|
1113
|
+
body: { code },
|
|
1114
|
+
...options
|
|
1115
|
+
});
|
|
421
1116
|
}
|
|
422
1117
|
};
|
|
423
1118
|
|
|
@@ -446,6 +1141,10 @@ var Passepartout = class {
|
|
|
446
1141
|
* Returns the current state of a login attempt. While the user hasn't
|
|
447
1142
|
* finished, `status` is `pending`; once done it becomes `completed` and
|
|
448
1143
|
* carries the `access_token` and `user`.
|
|
1144
|
+
*
|
|
1145
|
+
* Throws `VerneAPIError` with `403 account_disabled` when the person is deactivated,
|
|
1146
|
+
* and `402 ACTIVE_USER_LIMIT_REACHED` when a *new* person signs in past the account's
|
|
1147
|
+
* active-user limit (retrying does not help; existing users are unaffected).
|
|
449
1148
|
*/
|
|
450
1149
|
async loginStatus(nonce, options) {
|
|
451
1150
|
return this.http.get(
|
|
@@ -472,7 +1171,13 @@ var MessagesResource = class {
|
|
|
472
1171
|
}
|
|
473
1172
|
/**
|
|
474
1173
|
* Publish an event to all endpoints subscribed to the given event_type.
|
|
475
|
-
*
|
|
1174
|
+
* Retries once on a short `429` (a `Retry-After` of a minute or less). The monthly
|
|
1175
|
+
* message limit (`429 RELAY_MESSAGE_LIMIT_REACHED`, `Retry-After` until the 1st) is
|
|
1176
|
+
* thrown at once, with `retryAfter` on the error.
|
|
1177
|
+
*
|
|
1178
|
+
* Safe to retry beyond that, if you pass an `idempotency_key`: a repeat
|
|
1179
|
+
* returns the originally accepted message rather than creating a second event
|
|
1180
|
+
* or failing.
|
|
476
1181
|
*
|
|
477
1182
|
* @returns The accepted message with its assigned ID and timestamp.
|
|
478
1183
|
*/
|
|
@@ -483,12 +1188,12 @@ var MessagesResource = class {
|
|
|
483
1188
|
* Returns a paginated list of previously sent events for the current tenant.
|
|
484
1189
|
*/
|
|
485
1190
|
async list(params, options) {
|
|
486
|
-
const query =
|
|
1191
|
+
const query = buildQuery2(params);
|
|
487
1192
|
const path = query ? `/v1/relay/messages?${query}` : "/v1/relay/messages";
|
|
488
1193
|
return this.http.get(path, options);
|
|
489
1194
|
}
|
|
490
1195
|
};
|
|
491
|
-
function
|
|
1196
|
+
function buildQuery2(params) {
|
|
492
1197
|
if (!params) return "";
|
|
493
1198
|
const searchParams = new URLSearchParams();
|
|
494
1199
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -510,69 +1215,48 @@ var Relay = class {
|
|
|
510
1215
|
};
|
|
511
1216
|
|
|
512
1217
|
// src/verne.ts
|
|
1218
|
+
var SERVICE_LABELS = {
|
|
1219
|
+
relay: "Relay",
|
|
1220
|
+
gate: "Gate",
|
|
1221
|
+
passepartout: "Passepartout",
|
|
1222
|
+
clockwork: "Clockwork"
|
|
1223
|
+
};
|
|
513
1224
|
var Verne = class {
|
|
514
1225
|
constructor(config) {
|
|
515
|
-
if (!config.relay && !config.gate && !config.passepartout && !config.clockwork) {
|
|
1226
|
+
if (!config.apiKey && !config.relay && !config.gate && !config.passepartout && !config.clockwork) {
|
|
516
1227
|
throw new VerneError(
|
|
517
|
-
"
|
|
1228
|
+
"No API key provided. Pass `apiKey` (or a per-service key: relay, gate, passepartout, clockwork)."
|
|
518
1229
|
);
|
|
519
1230
|
}
|
|
520
1231
|
this.config = config;
|
|
521
1232
|
}
|
|
522
1233
|
get relay() {
|
|
523
|
-
|
|
524
|
-
throw new VerneError(
|
|
525
|
-
"Relay API key not provided. Pass `relay` in the Verne constructor config."
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
this._relay ?? (this._relay = new Relay({
|
|
529
|
-
apiKey: this.config.relay,
|
|
530
|
-
baseUrl: this.config.baseUrl,
|
|
531
|
-
timeoutMs: this.config.timeoutMs
|
|
532
|
-
}));
|
|
1234
|
+
this._relay ?? (this._relay = new Relay(this.serviceConfig("relay")));
|
|
533
1235
|
return this._relay;
|
|
534
1236
|
}
|
|
535
1237
|
get gate() {
|
|
536
|
-
|
|
537
|
-
throw new VerneError(
|
|
538
|
-
"Gate API key not provided. Pass `gate` in the Verne constructor config."
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
this._gate ?? (this._gate = new Gate({
|
|
542
|
-
apiKey: this.config.gate,
|
|
543
|
-
baseUrl: this.config.baseUrl,
|
|
544
|
-
timeoutMs: this.config.timeoutMs
|
|
545
|
-
}));
|
|
1238
|
+
this._gate ?? (this._gate = new Gate(this.serviceConfig("gate")));
|
|
546
1239
|
return this._gate;
|
|
547
1240
|
}
|
|
548
1241
|
get passepartout() {
|
|
549
|
-
|
|
550
|
-
throw new VerneError(
|
|
551
|
-
"Passepartout API key not provided. Pass `passepartout` in the Verne constructor config."
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
this._passepartout ?? (this._passepartout = new Passepartout({
|
|
555
|
-
apiKey: this.config.passepartout,
|
|
556
|
-
baseUrl: this.config.baseUrl,
|
|
557
|
-
timeoutMs: this.config.timeoutMs
|
|
558
|
-
}));
|
|
1242
|
+
this._passepartout ?? (this._passepartout = new Passepartout(this.serviceConfig("passepartout")));
|
|
559
1243
|
return this._passepartout;
|
|
560
1244
|
}
|
|
561
1245
|
get clockwork() {
|
|
562
|
-
|
|
1246
|
+
this._clockwork ?? (this._clockwork = new Clockwork(this.serviceConfig("clockwork")));
|
|
1247
|
+
return this._clockwork;
|
|
1248
|
+
}
|
|
1249
|
+
serviceConfig(service) {
|
|
1250
|
+
const apiKey = this.config[service] ?? this.config.apiKey;
|
|
1251
|
+
if (!apiKey) {
|
|
563
1252
|
throw new VerneError(
|
|
564
|
-
|
|
1253
|
+
`${SERVICE_LABELS[service]} API key not provided. Pass \`apiKey\` (or \`${service}\`) in the Verne constructor config.`
|
|
565
1254
|
);
|
|
566
1255
|
}
|
|
567
|
-
this.
|
|
568
|
-
apiKey: this.config.clockwork,
|
|
569
|
-
baseUrl: this.config.baseUrl,
|
|
570
|
-
timeoutMs: this.config.timeoutMs
|
|
571
|
-
}));
|
|
572
|
-
return this._clockwork;
|
|
1256
|
+
return { apiKey, baseUrl: this.config.baseUrl, timeoutMs: this.config.timeoutMs };
|
|
573
1257
|
}
|
|
574
1258
|
};
|
|
575
1259
|
|
|
576
|
-
export { Clockwork, Gate, Passepartout, Relay, Verne, VerneAPIError, VerneError };
|
|
1260
|
+
export { Clockwork, Gate, GateTokenVerifier, Passepartout, Relay, Verne, VerneAPIError, VerneError, VerneTokenError };
|
|
577
1261
|
//# sourceMappingURL=index.mjs.map
|
|
578
1262
|
//# sourceMappingURL=index.mjs.map
|