@verne-software/sdk 1.2.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/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
- static fromResponse(status, body) {
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: body.error.code,
24
- message: body.error.message,
47
+ code: typeof envelope?.code === "string" ? envelope.code : "unknown",
48
+ message: extractMessage(body, status),
25
49
  status,
26
- requestId: body.error.request_id
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 { skipAuth, body, signal, headers: extraHeaders, timeoutMs } = options;
57
- const url = `${this.baseUrl}${path}`;
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 new VerneError(`HTTP ${response.status}: ${method} ${path}`);
163
+ throw VerneAPIError.fromResponse(response.status, void 0, retryAfter);
164
+ }
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);
98
170
  }
99
- throw VerneAPIError.fromResponse(response.status, errorBody);
171
+ throw error;
100
172
  }
101
173
  if (response.status === 204) {
102
174
  return void 0;
@@ -104,60 +176,205 @@ 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
  }
110
202
 
203
+ // src/resources/clockwork/delayed.ts
204
+ var ClockworkDelayedResource = class {
205
+ constructor(http) {
206
+ this.http = http;
207
+ }
208
+ /**
209
+ * Returns all delayed (one-shot) jobs for the current tenant.
210
+ */
211
+ async list(options) {
212
+ return this.http.get("/v1/clockwork/delayed", options);
213
+ }
214
+ /**
215
+ * Schedules a new delayed job to run once at the given `run_at` time.
216
+ *
217
+ * @example
218
+ * const job = await clockwork.delayed.create({
219
+ * name: 'send-reminder',
220
+ * run_at: '2026-01-01T09:00:00Z',
221
+ * url: 'https://example.com/hooks/reminder',
222
+ * });
223
+ */
224
+ async create(params, options) {
225
+ return this.http.post("/v1/clockwork/delayed", { body: params, ...options });
226
+ }
227
+ /**
228
+ * Cancels a pending delayed job. Resolves on `204 No Content`.
229
+ */
230
+ async cancel(jobId, options) {
231
+ return this.http.delete(`/v1/clockwork/delayed/${jobId}`, options);
232
+ }
233
+ /**
234
+ * Returns the execution history for a delayed job.
235
+ */
236
+ async executions(jobId, options) {
237
+ return this.http.get(`/v1/clockwork/delayed/${jobId}/executions`, options);
238
+ }
239
+ };
240
+
241
+ // src/resources/clockwork/jobs.ts
242
+ var ClockworkJobsResource = class {
243
+ constructor(http) {
244
+ this.http = http;
245
+ }
246
+ /**
247
+ * Returns all cron jobs for the current tenant.
248
+ */
249
+ async list(options) {
250
+ return this.http.get("/v1/clockwork/jobs", options);
251
+ }
252
+ /**
253
+ * Creates a new recurring cron job.
254
+ *
255
+ * @example
256
+ * const job = await clockwork.jobs.create({
257
+ * name: 'nightly-report',
258
+ * schedule: '0 2 * * *',
259
+ * url: 'https://example.com/hooks/report',
260
+ * });
261
+ */
262
+ async create(params, options) {
263
+ return this.http.post("/v1/clockwork/jobs", { body: params, ...options });
264
+ }
265
+ /**
266
+ * Partially updates an existing cron job. Only the fields you provide are changed.
267
+ */
268
+ async update(jobId, params, options) {
269
+ return this.http.patch(`/v1/clockwork/jobs/${jobId}`, { body: params, ...options });
270
+ }
271
+ /**
272
+ * Deletes a cron job. Resolves on `204 No Content`.
273
+ */
274
+ async delete(jobId, options) {
275
+ return this.http.delete(`/v1/clockwork/jobs/${jobId}`, options);
276
+ }
277
+ /**
278
+ * Returns the execution history for a cron job (most recent runs first).
279
+ */
280
+ async executions(jobId, options) {
281
+ return this.http.get(`/v1/clockwork/jobs/${jobId}/executions`, options);
282
+ }
283
+ };
284
+
285
+ // src/resources/clockwork/clockwork.ts
286
+ var Clockwork = class {
287
+ constructor(config) {
288
+ this.http = new HttpClient({
289
+ apiKey: config.apiKey,
290
+ baseUrl: config.baseUrl,
291
+ timeoutMs: config.timeoutMs
292
+ });
293
+ this.jobs = new ClockworkJobsResource(this.http);
294
+ this.delayed = new ClockworkDelayedResource(this.http);
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
+ }
324
+ };
325
+
111
326
  // src/resources/gate/identities.ts
112
327
  var IdentitiesResource = class {
113
328
  constructor(http) {
114
329
  this.http = http;
115
330
  }
116
331
  /**
117
- * Creates a new identity (end-user) bound to your tenant.
118
- * The `traits.tenant_id` is injected automatically by the Edge Gateway.
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.
119
337
  */
120
338
  async create(params, options) {
121
339
  return this.http.post("/v1/gate/identities", { body: params, ...options });
122
340
  }
123
341
  /**
124
- * Returns an identity by ID.
125
- * 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.
126
343
  */
127
344
  async get(identityId, options) {
128
- return this.http.get(`/v1/gate/identities/${identityId}`, options);
345
+ return this.http.get(`/v1/gate/identities/${seg(identityId)}`, options);
129
346
  }
130
347
  /**
131
348
  * Partially updates an identity using a JSON Patch document (RFC 6902).
132
349
  *
133
350
  * @example
134
351
  * await gate.identities.patch('identity_123', [
135
- * { op: 'replace', path: '/traits/custom_data/role', value: 'admin' }
352
+ * { op: 'replace', path: '/traits/email', value: 'new@example.com' },
136
353
  * ]);
137
354
  */
138
355
  async patch(identityId, ops, options) {
139
- return this.http.patch(`/v1/gate/identities/${identityId}`, {
356
+ return this.http.patch(`/v1/gate/identities/${seg(identityId)}`, {
140
357
  body: ops,
141
358
  ...options
142
359
  });
143
360
  }
144
361
  /**
145
- * Deletes an identity from Kratos.
146
- * 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`.
147
363
  */
148
364
  async delete(identityId, options) {
149
- return this.http.delete(`/v1/gate/identities/${identityId}`, options);
365
+ return this.http.delete(`/v1/gate/identities/${seg(identityId)}`, options);
150
366
  }
151
367
  /**
152
- * Activates or deactivates an identity. An `inactive` identity cannot log in —
153
- * Kratos rejects its credentials automatically — until it is reactivated.
154
- * The identity is not deleted. Fires the `identity.state_changed` webhook event.
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.
155
372
  *
156
373
  * @example
157
374
  * await gate.identities.setState('identity_123', 'inactive');
158
375
  */
159
376
  async setState(identityId, state, options) {
160
- return this.http.patch(`/v1/gate/identities/${identityId}/state`, {
377
+ return this.http.patch(`/v1/gate/identities/${seg(identityId)}/state`, {
161
378
  body: { state },
162
379
  ...options
163
380
  });
@@ -177,9 +394,272 @@ var IdentitiesResource = class {
177
394
  */
178
395
  async resendVerification(identityId, options) {
179
396
  return this.http.post(
180
- `/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`,
181
408
  options
182
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 });
183
663
  }
184
664
  };
185
665
 
@@ -189,7 +669,8 @@ var SettingsResource = class {
189
669
  this.http = http;
190
670
  }
191
671
  /**
192
- * 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`).
193
674
  */
194
675
  async getSecurity(options) {
195
676
  return this.http.get("/v1/gate/settings/security", options);
@@ -236,29 +717,258 @@ var SettingsResource = class {
236
717
  );
237
718
  return res.providers;
238
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
+ }
744
+ };
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
+ }
239
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
+ }
240
949
 
241
950
  // src/resources/gate/tokens.ts
242
951
  var TokensResource = class {
243
- constructor(http, apiKey) {
952
+ constructor(http) {
244
953
  this.http = http;
245
- this.apiKey = apiKey;
954
+ this.verifiers = /* @__PURE__ */ new Map();
246
955
  }
247
956
  /**
248
- * 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`.
249
959
  *
250
- * Note: This endpoint does not use the Authorization header — the API key
251
- * is passed in the request body per the Gate API specification.
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`.)
252
962
  */
253
963
  async create(params, options) {
254
- return this.http.post("/v1/gate/tokens", {
255
- body: { api_key: this.apiKey, ...params },
256
- skipAuth: true,
257
- ...options
258
- });
964
+ return this.http.post("/v1/gate/tokens", { body: params, ...options });
259
965
  }
260
966
  /**
261
- * Validates a Gate access token and returns its decoded attributes.
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.
262
972
  */
263
973
  async introspect(accessToken, options) {
264
974
  return this.http.post("/v1/gate/tokens/introspect", {
@@ -266,6 +976,51 @@ var TokensResource = class {
266
976
  ...options
267
977
  });
268
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
+ }
269
1024
  };
270
1025
 
271
1026
  // src/resources/gate/gate.ts
@@ -277,20 +1032,31 @@ var Gate = class {
277
1032
  timeoutMs: config.timeoutMs
278
1033
  });
279
1034
  this.identities = new IdentitiesResource(this.http);
280
- this.tokens = new TokensResource(this.http, config.apiKey);
1035
+ this.tokens = new TokensResource(this.http);
281
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);
282
1041
  }
283
1042
  /**
284
- * Checks whether a given subject is allowed to perform an action on a resource.
285
- * This is the core building block for enforcing authorization in your services.
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.
286
1052
  *
287
1053
  * @example
288
- * const result = await gate.authorize({
289
- * subject: 'usr_123',
290
- * action: 'relay.messages.read',
291
- * resource: 'tenant:ten_001',
1054
+ * const decision = await gate.authorize({
1055
+ * subject: userId,
1056
+ * action: 'invoices:write',
1057
+ * resource: `organization:${orgId}`,
292
1058
  * });
293
- * if (!result.allowed) throw new Error('Forbidden');
1059
+ * if (!decision.allowed) throw new Error('Forbidden');
294
1060
  */
295
1061
  async authorize(params, options) {
296
1062
  return this.http.post("/v1/gate/authorize", { body: params, ...options });
@@ -306,23 +1072,95 @@ var Gate = class {
306
1072
  */
307
1073
  async getEnabledProviders(tenantId, options) {
308
1074
  const res = await this.http.get(
309
- `/public/gate/providers/${encodeURIComponent(tenantId)}`,
1075
+ `/public/gate/providers/${seg(tenantId)}`,
310
1076
  { skipAuth: true, ...options }
311
1077
  );
312
1078
  return res.providers;
313
1079
  }
314
1080
  /**
315
- * Initializes a Kratos login flow using your Gate API key. Call this from
316
- * your server and pass the returned flow JSON to your browser-side code to
317
- * render social login buttons. The flow already contains only the providers
318
- * your tenant has enabled.
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 }`.
1085
+ *
1086
+ * @example
1087
+ * const flow = await gate.createLoginFlow({ state: csrfBoundValue });
1088
+ */
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
+ });
1116
+ }
1117
+ };
1118
+
1119
+ // src/resources/passepartout/passepartout.ts
1120
+ var Passepartout = class {
1121
+ constructor(config) {
1122
+ this.http = new HttpClient({
1123
+ apiKey: config.apiKey,
1124
+ baseUrl: config.baseUrl,
1125
+ timeoutMs: config.timeoutMs
1126
+ });
1127
+ }
1128
+ /**
1129
+ * Starts a Telegram login and returns a nonce + deep link. Send the user to
1130
+ * `deep_link`, then poll {@link loginStatus} with `nonce` until it completes.
319
1131
  *
320
1132
  * @example
321
- * const flow = await gate.createLoginFlow();
322
- * // send `flow` to the browser as JSON
1133
+ * const { nonce, deep_link } = await passepartout.loginStart();
1134
+ * // redirect the user to `deep_link`, then poll:
1135
+ * const status = await passepartout.loginStatus(nonce);
1136
+ */
1137
+ async loginStart(options) {
1138
+ return this.http.post("/v1/passepartout/login/start", options);
1139
+ }
1140
+ /**
1141
+ * Returns the current state of a login attempt. While the user hasn't
1142
+ * finished, `status` is `pending`; once done it becomes `completed` and
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).
1148
+ */
1149
+ async loginStatus(nonce, options) {
1150
+ return this.http.get(
1151
+ `/v1/passepartout/login/status?nonce=${encodeURIComponent(nonce)}`,
1152
+ options
1153
+ );
1154
+ }
1155
+ /**
1156
+ * Validates a Passepartout access token and returns its attributes.
1157
+ * Tokens owned by another tenant are reported as `{ active: false }`.
323
1158
  */
324
- async createLoginFlow(options) {
325
- return this.http.get("/v1/gate/auth/login", options);
1159
+ async introspect(accessToken, options) {
1160
+ return this.http.post("/v1/passepartout/tokens/introspect", {
1161
+ body: { access_token: accessToken },
1162
+ ...options
1163
+ });
326
1164
  }
327
1165
  };
328
1166
 
@@ -333,7 +1171,13 @@ var MessagesResource = class {
333
1171
  }
334
1172
  /**
335
1173
  * Publish an event to all endpoints subscribed to the given event_type.
336
- * Automatically retries once on 429 (respects Retry-After header).
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.
337
1181
  *
338
1182
  * @returns The accepted message with its assigned ID and timestamp.
339
1183
  */
@@ -344,12 +1188,12 @@ var MessagesResource = class {
344
1188
  * Returns a paginated list of previously sent events for the current tenant.
345
1189
  */
346
1190
  async list(params, options) {
347
- const query = buildQuery(params);
1191
+ const query = buildQuery2(params);
348
1192
  const path = query ? `/v1/relay/messages?${query}` : "/v1/relay/messages";
349
1193
  return this.http.get(path, options);
350
1194
  }
351
1195
  };
352
- function buildQuery(params) {
1196
+ function buildQuery2(params) {
353
1197
  if (!params) return "";
354
1198
  const searchParams = new URLSearchParams();
355
1199
  for (const [k, v] of Object.entries(params)) {
@@ -371,41 +1215,48 @@ var Relay = class {
371
1215
  };
372
1216
 
373
1217
  // src/verne.ts
1218
+ var SERVICE_LABELS = {
1219
+ relay: "Relay",
1220
+ gate: "Gate",
1221
+ passepartout: "Passepartout",
1222
+ clockwork: "Clockwork"
1223
+ };
374
1224
  var Verne = class {
375
1225
  constructor(config) {
376
- if (!config.relay && !config.gate) {
377
- throw new VerneError("At least one service key (relay or gate) must be provided.");
1226
+ if (!config.apiKey && !config.relay && !config.gate && !config.passepartout && !config.clockwork) {
1227
+ throw new VerneError(
1228
+ "No API key provided. Pass `apiKey` (or a per-service key: relay, gate, passepartout, clockwork)."
1229
+ );
378
1230
  }
379
1231
  this.config = config;
380
1232
  }
381
1233
  get relay() {
382
- if (!this.config.relay) {
383
- throw new VerneError(
384
- "Relay API key not provided. Pass `relay` in the Verne constructor config."
385
- );
386
- }
387
- this._relay ?? (this._relay = new Relay({
388
- apiKey: this.config.relay,
389
- baseUrl: this.config.baseUrl,
390
- timeoutMs: this.config.timeoutMs
391
- }));
1234
+ this._relay ?? (this._relay = new Relay(this.serviceConfig("relay")));
392
1235
  return this._relay;
393
1236
  }
394
1237
  get gate() {
395
- if (!this.config.gate) {
1238
+ this._gate ?? (this._gate = new Gate(this.serviceConfig("gate")));
1239
+ return this._gate;
1240
+ }
1241
+ get passepartout() {
1242
+ this._passepartout ?? (this._passepartout = new Passepartout(this.serviceConfig("passepartout")));
1243
+ return this._passepartout;
1244
+ }
1245
+ get clockwork() {
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) {
396
1252
  throw new VerneError(
397
- "Gate API key not provided. Pass `gate` in the Verne constructor config."
1253
+ `${SERVICE_LABELS[service]} API key not provided. Pass \`apiKey\` (or \`${service}\`) in the Verne constructor config.`
398
1254
  );
399
1255
  }
400
- this._gate ?? (this._gate = new Gate({
401
- apiKey: this.config.gate,
402
- baseUrl: this.config.baseUrl,
403
- timeoutMs: this.config.timeoutMs
404
- }));
405
- return this._gate;
1256
+ return { apiKey, baseUrl: this.config.baseUrl, timeoutMs: this.config.timeoutMs };
406
1257
  }
407
1258
  };
408
1259
 
409
- export { Gate, Relay, Verne, VerneAPIError, VerneError };
1260
+ export { Clockwork, Gate, GateTokenVerifier, Passepartout, Relay, Verne, VerneAPIError, VerneError, VerneTokenError };
410
1261
  //# sourceMappingURL=index.mjs.map
411
1262
  //# sourceMappingURL=index.mjs.map