@cboxdk/id-js 0.18.0 → 0.19.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/nextjs.cjs CHANGED
@@ -339,6 +339,10 @@ function roles(source) {
339
339
  function permissions(source) {
340
340
  return stringList(claimsOf(source)["permissions"]);
341
341
  }
342
+ function featureFlags(source) {
343
+ const claim = claimsOf(source)["feature_flags"];
344
+ return Array.isArray(claim) ? stringList(claim) : null;
345
+ }
342
346
  var MAX_ACTOR_DEPTH = 8;
343
347
  function parseActor(claim, depth) {
344
348
  if (claim === void 0 || claim === null) {
@@ -365,6 +369,43 @@ function isUser(source) {
365
369
  }
366
370
 
367
371
  // src/vault.ts
372
+ function withConnectReturn(connectUrl, options = {}) {
373
+ const url = new URL(connectUrl);
374
+ if (options.clientId) url.searchParams.set("client_id", options.clientId);
375
+ if (options.returnTo) url.searchParams.set("return_to", options.returnTo);
376
+ return url.toString();
377
+ }
378
+ function pipeConnectUrl(issuer, provider, options = {}) {
379
+ return withConnectReturn(
380
+ `${issuer.replace(/\/$/, "")}/account/connected-services/${encodeURIComponent(provider)}/connect`,
381
+ options
382
+ );
383
+ }
384
+ var PipeLeaseError = class extends CboxIdError {
385
+ constructor(message, error, status, connectUrl = null, retryAfter = null) {
386
+ super(message);
387
+ this.error = error;
388
+ this.status = status;
389
+ this.connectUrl = connectUrl;
390
+ this.retryAfter = retryAfter;
391
+ }
392
+ error;
393
+ status;
394
+ connectUrl;
395
+ retryAfter;
396
+ /** `connectUrl` with your `client_id` and `return_to` on it, or null when there is none. */
397
+ connectUrlWith(options) {
398
+ return this.connectUrl === null ? null : withConnectReturn(this.connectUrl, options);
399
+ }
400
+ };
401
+ var PipeNotConnectedError = class extends PipeLeaseError {
402
+ };
403
+ var PipeReauthorizationRequiredError = class extends PipeLeaseError {
404
+ };
405
+ var PipeTemporarilyUnavailableError = class extends PipeLeaseError {
406
+ };
407
+ var PipeLeaseDeniedError = class extends PipeLeaseError {
408
+ };
368
409
  var VaultClient = class {
369
410
  constructor(issuer, token, timeoutMs = 1e4) {
370
411
  this.token = token;
@@ -420,11 +461,69 @@ var VaultClient = class {
420
461
  return { secretId: r.secret_id, provider: r.provider, secret: r.secret, expiresAt: r.expires_at };
421
462
  });
422
463
  }
464
+ /**
465
+ * Lease a fresh access token for a person's connected account at `provider` (Pipes,
466
+ * scope `vault.lease`). The token is refreshed first when it is about to expire, so
467
+ * you never handle a refresh token.
468
+ *
469
+ * With a client-credentials token, name the person in `userId`. With a token issued to
470
+ * your app FOR a person, leave it out — the lease is theirs.
471
+ *
472
+ * ```ts
473
+ * try {
474
+ * const { accessToken } = await vault.leasePipeToken('github', { userId, purpose: 'list-repos' })
475
+ * } catch (e) {
476
+ * if (e instanceof PipeNotConnectedError || e instanceof PipeReauthorizationRequiredError) {
477
+ * return redirect(e.connectUrlWith({ clientId, returnTo }))
478
+ * }
479
+ * throw e
480
+ * }
481
+ * ```
482
+ */
483
+ async leasePipeToken(provider, options) {
484
+ const path = `/pipes/${encodeURIComponent(provider)}/token`;
485
+ const response = await this.send("POST", path, {
486
+ purpose: options.purpose,
487
+ ...options.userId !== void 0 ? { user_id: options.userId } : {}
488
+ });
489
+ let body = {};
490
+ try {
491
+ const parsed = await response.json();
492
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
493
+ body = parsed;
494
+ }
495
+ } catch {
496
+ }
497
+ if (!response.ok) {
498
+ throw pipeLeaseError(response, body);
499
+ }
500
+ if (typeof body.access_token !== "string") {
501
+ throw new PipeLeaseError("The pipe lease answered without an access token.", null, response.status);
502
+ }
503
+ return {
504
+ accessToken: body.access_token,
505
+ tokenType: "Bearer",
506
+ provider: String(body.provider ?? provider),
507
+ userId: String(body.user_id ?? ""),
508
+ connectionId: String(body.connection_id ?? ""),
509
+ scopes: Array.isArray(body.scopes) ? body.scopes.filter((s) => typeof s === "string") : [],
510
+ expiresAt: typeof body.expires_at === "string" ? body.expires_at : null,
511
+ leaseExpiresAt: String(body.lease_expires_at ?? ""),
512
+ metadata: isStringRecord(body.metadata) ? body.metadata : {}
513
+ };
514
+ }
423
515
  async json(method, path, body) {
424
516
  const response = await this.request(method, path, body);
425
517
  return await response.json();
426
518
  }
427
519
  async request(method, path, body) {
520
+ const response = await this.send(method, path, body);
521
+ if (!response.ok) {
522
+ throw new AuthenticationError(`Vault ${method} ${path} failed with status ${response.status}.`);
523
+ }
524
+ return response;
525
+ }
526
+ async send(method, path, body) {
428
527
  const controller = new AbortController();
429
528
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
430
529
  try {
@@ -439,16 +538,35 @@ var VaultClient = class {
439
538
  if (body) {
440
539
  init.body = JSON.stringify(body);
441
540
  }
442
- const response = await fetch(`${this.base}${path}`, init);
443
- if (!response.ok) {
444
- throw new AuthenticationError(`Vault ${method} ${path} failed with status ${response.status}.`);
445
- }
446
- return response;
541
+ return await fetch(`${this.base}${path}`, init);
447
542
  } finally {
448
543
  clearTimeout(timer);
449
544
  }
450
545
  }
451
546
  };
547
+ function pipeLeaseError(response, body) {
548
+ const error = typeof body.error === "string" ? body.error : null;
549
+ const message = typeof body.message === "string" && body.message !== "" ? body.message : `The pipe lease failed with status ${response.status}.`;
550
+ const connectUrl = typeof body.connect_url === "string" && body.connect_url !== "" ? body.connect_url : null;
551
+ const header = response.headers.get("retry-after")?.trim() ?? "";
552
+ const retryAfter = /^\d+$/.test(header) ? Number(header) : null;
553
+ if (response.status === 404 && connectUrl !== null) {
554
+ return new PipeNotConnectedError(message, error, 404, connectUrl, retryAfter);
555
+ }
556
+ if (response.status === 409 && connectUrl !== null) {
557
+ return new PipeReauthorizationRequiredError(message, error, 409, connectUrl, retryAfter);
558
+ }
559
+ if (response.status === 503) {
560
+ return new PipeTemporarilyUnavailableError(message, error, 503, connectUrl, retryAfter);
561
+ }
562
+ if (response.status === 403 && error !== "insufficient_scope") {
563
+ return new PipeLeaseDeniedError(message, error, 403, connectUrl, retryAfter);
564
+ }
565
+ return new PipeLeaseError(message, error, response.status, connectUrl, retryAfter);
566
+ }
567
+ function isStringRecord(value) {
568
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
569
+ }
452
570
 
453
571
  // src/webhook.ts
454
572
  function timingSafeEqual(a, b) {
@@ -666,6 +784,7 @@ var CboxIdClient = class {
666
784
  organization: organization(claims),
667
785
  roles: roles(claims),
668
786
  permissions: permissions(claims),
787
+ featureFlags: featureFlags(claims),
669
788
  actor: actor(claims),
670
789
  sessionId: sessionId(claims),
671
790
  // Only present when the instance emitted the claim (exactOptionalPropertyTypes).
@@ -845,6 +964,18 @@ var CboxIdClient = class {
845
964
  * registered. `organization` picks which of the person's organizations the keys act in;
846
965
  * omitted, the page uses the one they are in.
847
966
  */
967
+ /**
968
+ * The hosted page where the signed-in person connects their account at `provider`
969
+ * (Pipes) — `/account/connected-services/{provider}/connect`, preselected to this app.
970
+ * They come back to `returnTo` with `?provider=…&status=connected|cancelled|failed`,
971
+ * honoured only on an origin the app registered.
972
+ */
973
+ pipeConnectUrl(provider, returnTo) {
974
+ return pipeConnectUrl(this.config.issuer, provider, {
975
+ clientId: this.config.clientId,
976
+ ...returnTo !== void 0 ? { returnTo } : {}
977
+ });
978
+ }
848
979
  apiKeysUrl(options = {}) {
849
980
  const params = new URLSearchParams({ client_id: options.clientId ?? this.config.clientId });
850
981
  if (options.returnTo) {