@tula-tech/core 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +1132 -92
  2. package/dist/index.js +684 -399
  3. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -325,10 +325,293 @@ function isStepUpPrepared(value) {
325
325
  return isPhoneCodeSent(value) && (method === "email_code" || method === "sms_code");
326
326
  }
327
327
 
328
- // src/passkey.ts
328
+ // src/oauth.ts
329
+ import { ERROR_DEFINITIONS as ERROR_DEFINITIONS2 } from "@tula-tech/contract/error-codes";
330
+ import {
331
+ EMAIL_LINK_ATTEMPT_PARAM as EMAIL_LINK_ATTEMPT_PARAM2,
332
+ OAUTH_ERROR_PARAM,
333
+ OAUTH_TICKET_PARAM
334
+ } from "@tula-tech/contract/headers";
335
+ var OAUTH_BINDING_TTL_MS = 15 * 60000;
336
+ var OAUTH_TICKET_HOLD_MS = 60000;
337
+ var RETRYABLE = new Set([
338
+ "network.failed",
339
+ "network.timeout",
340
+ "rate_limited"
341
+ ]);
342
+ function isRetryableOAuthError(error) {
343
+ return isTulaError(error) && RETRYABLE.has(error.code);
344
+ }
345
+ var OAUTH_STORAGE_PREFIX = "tula.oauth.";
329
346
  function isRecord3(value) {
330
347
  return typeof value === "object" && value !== null && !Array.isArray(value);
331
348
  }
349
+ function createOAuthStore(environment, scope) {
350
+ const storage = environment.tabStorage;
351
+ const keyOf = (attemptId) => `${OAUTH_STORAGE_PREFIX}${attemptId}`;
352
+ function guarded(work, fallback) {
353
+ try {
354
+ return work();
355
+ } catch {
356
+ return fallback;
357
+ }
358
+ }
359
+ function prune() {
360
+ if (!storage) {
361
+ return;
362
+ }
363
+ const stale = [];
364
+ for (let index = 0;index < storage.length; index++) {
365
+ const key = storage.key(index);
366
+ if (key?.startsWith(OAUTH_STORAGE_PREFIX)) {
367
+ const expiry = guarded(() => JSON.parse(storage.getItem(key) ?? "null")?.e, undefined);
368
+ if (typeof expiry !== "number" || expiry <= environment.now()) {
369
+ stale.push(key);
370
+ }
371
+ }
372
+ }
373
+ for (const key of stale) {
374
+ storage.removeItem(key);
375
+ }
376
+ }
377
+ return {
378
+ available: () => guarded(() => {
379
+ if (!storage) {
380
+ return false;
381
+ }
382
+ const probe = `${OAUTH_STORAGE_PREFIX}probe`;
383
+ storage.setItem(probe, "1");
384
+ storage.removeItem(probe);
385
+ return true;
386
+ }, false),
387
+ save: (attemptId, binding, intent) => guarded(() => {
388
+ if (!storage) {
389
+ return false;
390
+ }
391
+ prune();
392
+ storage.setItem(keyOf(attemptId), JSON.stringify({
393
+ b: binding,
394
+ e: environment.now() + OAUTH_BINDING_TTL_MS,
395
+ s: scope,
396
+ k: intent
397
+ }));
398
+ return true;
399
+ }, false),
400
+ read: (attemptId) => guarded(() => {
401
+ prune();
402
+ const entry = JSON.parse(storage?.getItem(keyOf(attemptId)) ?? "null");
403
+ return isRecord3(entry) && typeof entry.b === "string" && entry.s === scope && (entry.k === "sign_in" || entry.k === "link") ? { binding: entry.b, intent: entry.k } : null;
404
+ }, null),
405
+ remove(attemptId) {
406
+ guarded(() => storage?.removeItem(keyOf(attemptId)), undefined);
407
+ }
408
+ };
409
+ }
410
+ function readOAuthFragment(url) {
411
+ const at = url.indexOf("#");
412
+ if (at === -1) {
413
+ return null;
414
+ }
415
+ const fragment = new URLSearchParams(url.slice(at + 1));
416
+ const ticket = fragment.get(OAUTH_TICKET_PARAM);
417
+ const error = fragment.get(OAUTH_ERROR_PARAM);
418
+ if (!ticket && !error) {
419
+ return null;
420
+ }
421
+ const attemptId = fragment.get(EMAIL_LINK_ATTEMPT_PARAM2) ?? "";
422
+ for (const name of [OAUTH_TICKET_PARAM, OAUTH_ERROR_PARAM, EMAIL_LINK_ATTEMPT_PARAM2]) {
423
+ fragment.delete(name);
424
+ }
425
+ const rest = fragment.toString();
426
+ return {
427
+ attemptId,
428
+ ticket: ticket || null,
429
+ error: ticket ? null : error,
430
+ cleanUrl: `${url.slice(0, at)}${rest ? `#${rest}` : ""}`
431
+ };
432
+ }
433
+ function createOAuthTicketHolder() {
434
+ let held = null;
435
+ return {
436
+ get: () => held,
437
+ set(next) {
438
+ held = next;
439
+ }
440
+ };
441
+ }
442
+ function discardOAuthCallback(context) {
443
+ const held = context.held.get();
444
+ if (held) {
445
+ context.held.set(null);
446
+ context.oauth.remove(held.attemptId);
447
+ }
448
+ }
449
+ function isLinkStart(value) {
450
+ return isRecord3(value) && typeof value.attemptId === "string" && value.attemptId !== "" && typeof value.authorizationUrl === "string" && typeof value.binding === "string";
451
+ }
452
+ function isIdentity(value) {
453
+ return isRecord3(value) && typeof value.id === "string" && typeof value.provider === "string";
454
+ }
455
+ function isIdentityList(value) {
456
+ return isRecord3(value) && Array.isArray(value.data) && value.data.every(isIdentity);
457
+ }
458
+ async function startOAuth(context, input, intent) {
459
+ const { environment, oauth, transport, session } = context;
460
+ discardOAuthCallback(context);
461
+ if (!oauth.available()) {
462
+ throw clientError("storage.failed", context.messages());
463
+ }
464
+ const page = environment.page;
465
+ if (page && originOf(input.redirectUrl) !== originOf(page.url())) {
466
+ throw clientError("link.cross_origin", context.messages());
467
+ }
468
+ const body = { provider: input.provider, redirectUrl: input.redirectUrl };
469
+ let attemptId;
470
+ let started;
471
+ if (intent === "link") {
472
+ const answer = await session.authorized("startIdentityLink", { body });
473
+ if (!isLinkStart(answer)) {
474
+ throw clientError("response.invalid", context.messages());
475
+ }
476
+ attemptId = answer.attemptId;
477
+ started = answer;
478
+ } else {
479
+ const answer = await transport.call("startOAuthSignIn", { body });
480
+ if (!isRecord3(answer) || !isRecord3(answer.attempt) || typeof answer.attempt.id !== "string" || typeof answer.authorizationUrl !== "string" || typeof answer.binding !== "string") {
481
+ throw clientError("response.invalid", context.messages());
482
+ }
483
+ attemptId = answer.attempt.id;
484
+ started = { authorizationUrl: answer.authorizationUrl, binding: answer.binding };
485
+ }
486
+ return keepAndNavigate(context, attemptId, started, input.navigate, intent);
487
+ }
488
+ function keepAndNavigate(context, attemptId, started, navigate, intent) {
489
+ const { oauth, environment } = context;
490
+ if (!/^https?:\/\//i.test(started.authorizationUrl)) {
491
+ throw clientError("response.invalid", context.messages());
492
+ }
493
+ if (!oauth.save(attemptId, started.binding, intent)) {
494
+ throw clientError("storage.failed", context.messages());
495
+ }
496
+ if (navigate !== false) {
497
+ environment.page?.assign?.(started.authorizationUrl);
498
+ }
499
+ return { url: started.authorizationUrl };
500
+ }
501
+ async function startSso(context, bound, input) {
502
+ discardOAuthCallback(context);
503
+ if (!context.oauth.available()) {
504
+ throw clientError("storage.failed", context.messages());
505
+ }
506
+ const page = context.environment.page;
507
+ if (page && originOf(input.redirectUrl) !== originOf(page.url())) {
508
+ throw clientError("link.cross_origin", context.messages());
509
+ }
510
+ const answer = await context.transport.call("startSsoSignIn", {
511
+ ...bound,
512
+ body: { redirectUrl: input.redirectUrl }
513
+ });
514
+ if (!isRecord3(answer) || typeof answer.authorizationUrl !== "string" || typeof answer.binding !== "string") {
515
+ throw clientError("response.invalid", context.messages());
516
+ }
517
+ return keepAndNavigate(context, bound.params.attemptId, { authorizationUrl: answer.authorizationUrl, binding: answer.binding }, input.navigate, "sign_in");
518
+ }
519
+ function originOf(url) {
520
+ try {
521
+ return new URL(url).origin;
522
+ } catch {
523
+ return null;
524
+ }
525
+ }
526
+ function failed(code, messages) {
527
+ const known = Object.hasOwn(ERROR_DEFINITIONS2, code) ? code : "oauth.provider_error";
528
+ return {
529
+ status: "error",
530
+ code: known,
531
+ message: formatMessage(known, { messages })
532
+ };
533
+ }
534
+ async function handleOAuthCallback(context) {
535
+ const { environment, oauth, held, transport, session } = context;
536
+ const page = environment.page;
537
+ if (!page) {
538
+ return { status: "none" };
539
+ }
540
+ const arrived = readOAuthFragment(page.url());
541
+ let answer;
542
+ if (arrived) {
543
+ try {
544
+ page.replaceUrl(arrived.cleanUrl);
545
+ } catch {}
546
+ discardOAuthCallback(context);
547
+ if (arrived.error !== null || arrived.ticket === null) {
548
+ oauth.remove(arrived.attemptId);
549
+ return arrived.error !== null ? failed(arrived.error, context.messages()) : { status: "different_browser" };
550
+ }
551
+ answer = {
552
+ attemptId: arrived.attemptId,
553
+ ticket: arrived.ticket,
554
+ until: environment.now() + OAUTH_TICKET_HOLD_MS
555
+ };
556
+ } else {
557
+ const waiting = held.get();
558
+ if (!waiting) {
559
+ return { status: "none" };
560
+ }
561
+ if (waiting.until <= environment.now()) {
562
+ discardOAuthCallback(context);
563
+ return failed("oauth.ticket_invalid", context.messages());
564
+ }
565
+ answer = waiting;
566
+ }
567
+ const kept = oauth.read(answer.attemptId);
568
+ const settle = () => {
569
+ held.set(null);
570
+ oauth.remove(answer.attemptId);
571
+ };
572
+ if (!kept) {
573
+ settle();
574
+ return { status: "different_browser" };
575
+ }
576
+ held.set(answer);
577
+ const body = { ticket: answer.ticket, attemptId: answer.attemptId, binding: kept.binding };
578
+ try {
579
+ let outcome;
580
+ if (kept.intent === "link") {
581
+ const identity = await session.authorized("exchangeIdentityLinkTicket", { body });
582
+ if (!isIdentity(identity)) {
583
+ throw clientError("response.invalid", context.messages());
584
+ }
585
+ outcome = { status: "linked", identity };
586
+ } else {
587
+ const attempt = await transport.call("exchangeOAuthTicket", { body });
588
+ const complete = isRecord3(attempt) && isRecord3(attempt.step) && attempt.step.status === "complete";
589
+ outcome = {
590
+ status: complete ? "complete" : "needs_step",
591
+ flow: await signInFlow(context, attempt)
592
+ };
593
+ }
594
+ settle();
595
+ return outcome;
596
+ } catch (error) {
597
+ if (isRetryableOAuthError(error)) {
598
+ throw error;
599
+ }
600
+ settle();
601
+ if (!isTulaError(error) || error.status === 0) {
602
+ throw error;
603
+ }
604
+ if (error.code === "oauth.different_browser") {
605
+ return { status: "different_browser" };
606
+ }
607
+ return { status: "error", code: error.code, message: error.message };
608
+ }
609
+ }
610
+
611
+ // src/passkey.ts
612
+ function isRecord4(value) {
613
+ return typeof value === "object" && value !== null && !Array.isArray(value);
614
+ }
332
615
  function base64UrlToBytes(text) {
333
616
  const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
334
617
  const bytes = new Uint8Array(binary.length);
@@ -346,7 +629,7 @@ function bytesToBase64Url(buffer) {
346
629
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
347
630
  }
348
631
  function descriptors(list) {
349
- return Array.isArray(list) ? list.map((entry) => isRecord3(entry) && typeof entry.id === "string" ? { ...entry, id: base64UrlToBytes(entry.id) } : entry) : undefined;
632
+ return Array.isArray(list) ? list.map((entry) => isRecord4(entry) && typeof entry.id === "string" ? { ...entry, id: base64UrlToBytes(entry.id) } : entry) : undefined;
350
633
  }
351
634
  function creationOptionsFromJson(options) {
352
635
  return {
@@ -368,7 +651,7 @@ function isBinary(value) {
368
651
  return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
369
652
  }
370
653
  function credentialToJson(credential) {
371
- const response = isRecord3(credential.response) ? credential.response : {};
654
+ const response = isRecord4(credential.response) ? credential.response : {};
372
655
  const encoded = {};
373
656
  for (const name of [
374
657
  "clientDataJSON",
@@ -401,7 +684,7 @@ function call(target, method) {
401
684
  return typeof candidate === "function" ? candidate.call(target) : undefined;
402
685
  }
403
686
  function toJson(credential) {
404
- if (!isRecord3(credential)) {
687
+ if (!isRecord4(credential)) {
405
688
  return null;
406
689
  }
407
690
  if (typeof credential.toJSON === "function") {
@@ -415,13 +698,13 @@ function isBase64Url(value) {
415
698
  return typeof value === "string" && /^[A-Za-z0-9_-]+$/.test(value);
416
699
  }
417
700
  function isRegistration(value) {
418
- return isRecord3(value) && isBase64Url(value.id) && value.type === "public-key" && isRecord3(value.response) && isBase64Url(value.response.clientDataJSON) && isBase64Url(value.response.attestationObject);
701
+ return isRecord4(value) && isBase64Url(value.id) && value.type === "public-key" && isRecord4(value.response) && isBase64Url(value.response.clientDataJSON) && isBase64Url(value.response.attestationObject);
419
702
  }
420
703
  function isAssertion(value) {
421
- return isRecord3(value) && isBase64Url(value.id) && value.type === "public-key" && isRecord3(value.response) && isBase64Url(value.response.clientDataJSON) && isBase64Url(value.response.authenticatorData) && isBase64Url(value.response.signature);
704
+ return isRecord4(value) && isBase64Url(value.id) && value.type === "public-key" && isRecord4(value.response) && isBase64Url(value.response.clientDataJSON) && isBase64Url(value.response.authenticatorData) && isBase64Url(value.response.signature);
422
705
  }
423
706
  function ceremonyCode(error) {
424
- const name = isRecord3(error) || error instanceof Error ? error.name : "";
707
+ const name = isRecord4(error) || error instanceof Error ? error.name : "";
425
708
  if (name === "InvalidStateError") {
426
709
  return "passkey.already_on_device";
427
710
  }
@@ -473,7 +756,7 @@ function browserProvider(globals) {
473
756
  };
474
757
  }
475
758
  function isPasskey(value) {
476
- return isRecord3(value) && typeof value.id === "string" && value.id !== "" && typeof value.name === "string" && typeof value.synced === "boolean" && typeof value.createdAt === "string" && (value.lastUsedAt === null || typeof value.lastUsedAt === "string");
759
+ return isRecord4(value) && typeof value.id === "string" && value.id !== "" && typeof value.name === "string" && typeof value.synced === "boolean" && typeof value.createdAt === "string" && (value.lastUsedAt === null || typeof value.lastUsedAt === "string");
477
760
  }
478
761
  function toPasskey(value) {
479
762
  const { id, name, synced, createdAt, lastUsedAt } = value;
@@ -483,22 +766,23 @@ function isPasskeyAnswer(value) {
483
766
  return isPasskey(value);
484
767
  }
485
768
  function isPasskeyList(value) {
486
- return isRecord3(value) && Array.isArray(value.passkeys) && value.passkeys.every(isPasskey);
769
+ return isRecord4(value) && Array.isArray(value.passkeys) && value.passkeys.every(isPasskey);
487
770
  }
488
771
  function isCreationOptions(value) {
489
- return isRecord3(value) && isBase64Url(value.challenge) && isRecord3(value.rp) && typeof value.rp.id === "string" && isRecord3(value.user) && isBase64Url(value.user.id) && Array.isArray(value.pubKeyCredParams);
772
+ return isRecord4(value) && isBase64Url(value.challenge) && isRecord4(value.rp) && typeof value.rp.id === "string" && isRecord4(value.user) && isBase64Url(value.user.id) && Array.isArray(value.pubKeyCredParams);
490
773
  }
491
774
  function isRequestOptions(value) {
492
- return isRecord3(value) && isBase64Url(value.challenge) && typeof value.rpId === "string";
775
+ return isRecord4(value) && isBase64Url(value.challenge) && typeof value.rpId === "string";
493
776
  }
494
777
  function isPasskeySignInStart(value) {
495
- return isRecord3(value) && isRecord3(value.attempt) && isRequestOptions(value.options);
778
+ return isRecord4(value) && isRecord4(value.attempt) && isRequestOptions(value.options);
496
779
  }
497
780
 
498
781
  // src/session.ts
499
782
  var ACCESS_TOKEN_EXPIRY_SKEW_MS = 1e4;
500
783
  var REFRESH_TIMEOUT_MS = 8000;
501
784
  var REFRESH_RETRY_WINDOW_MS = 1e4;
785
+ var MAX_UNDELIVERED = 8;
502
786
  var MIN_REFRESH_RETRY_TIMEOUT_MS = 1000;
503
787
  var MAX_REFRESH_BACKOFF_MS = 5 * 60000;
504
788
  function refreshBudgetMs(refreshTimeoutMs) {
@@ -507,27 +791,40 @@ function refreshBudgetMs(refreshTimeoutMs) {
507
791
  var LOCK_WAIT_MARGIN_MS = 2000;
508
792
  var MAX_ENDED_SESSIONS = 32;
509
793
  var CHANNEL_VERSION2 = 1;
510
- function isRecord4(value) {
794
+ function isRecord5(value) {
511
795
  return typeof value === "object" && value !== null;
512
796
  }
513
797
  function jwtLifetimeMs(token) {
514
798
  try {
515
799
  const payload = token.split(".")[1] ?? "";
516
800
  const claims = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
517
- if (isRecord4(claims) && typeof claims.iat === "number" && typeof claims.exp === "number" && claims.exp > claims.iat) {
801
+ if (isRecord5(claims) && typeof claims.iat === "number" && typeof claims.exp === "number" && claims.exp > claims.iat) {
518
802
  return (claims.exp - claims.iat) * 1000;
519
803
  }
520
804
  } catch {}
521
805
  return;
522
806
  }
807
+ function organizationIdOf(token) {
808
+ try {
809
+ const payload = token.split(".")[1] ?? "";
810
+ const claims = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
811
+ if (isRecord5(claims) && typeof claims.org_id === "string" && claims.org_id !== "") {
812
+ return claims.org_id;
813
+ }
814
+ } catch {}
815
+ return null;
816
+ }
817
+ function isActiveOrganization(value) {
818
+ return isRecord5(value) && typeof value.sessionId === "string" && value.sessionId !== "" && (value.organizationId === null || typeof value.organizationId === "string" && value.organizationId !== "") && (value.accessToken === undefined || typeof value.accessToken === "string" && value.accessToken !== "" && (jwtLifetimeMs(value.accessToken) !== undefined || typeof value.accessTokenExpiresAt === "string" && !Number.isNaN(Date.parse(value.accessTokenExpiresAt))));
819
+ }
523
820
  function accessTokenLifetimeMs(issued, now) {
524
821
  return jwtLifetimeMs(issued.accessToken) ?? Math.max(0, Date.parse(issued.accessTokenExpiresAt ?? "") - now);
525
822
  }
526
823
  function isSessionTokens(value) {
527
- return isRecord4(value) && typeof value.accessToken === "string" && value.accessToken !== "" && typeof value.sessionId === "string" && value.sessionId !== "" && (value.refreshToken === undefined || typeof value.refreshToken === "string") && (jwtLifetimeMs(value.accessToken) !== undefined || typeof value.accessTokenExpiresAt === "string" && !Number.isNaN(Date.parse(value.accessTokenExpiresAt)));
824
+ return isRecord5(value) && typeof value.accessToken === "string" && value.accessToken !== "" && typeof value.sessionId === "string" && value.sessionId !== "" && (value.refreshToken === undefined || typeof value.refreshToken === "string") && (jwtLifetimeMs(value.accessToken) !== undefined || typeof value.accessTokenExpiresAt === "string" && !Number.isNaN(Date.parse(value.accessTokenExpiresAt)));
528
825
  }
529
826
  function isStatefulSession(value) {
530
- return isRecord4(value) && typeof value.sessionId === "string" && value.sessionId !== "" && value.accessToken === undefined && value.accessTokenExpiresAt === undefined && value.refreshToken === undefined;
827
+ return isRecord5(value) && typeof value.sessionId === "string" && value.sessionId !== "" && value.accessToken === undefined && value.accessTokenExpiresAt === undefined && value.refreshToken === undefined;
531
828
  }
532
829
  function sessionIsGone(error) {
533
830
  return error.code.startsWith("session.") || error.code === "auth.unauthenticated" || error.code === "auth.user_banned";
@@ -553,13 +850,13 @@ function createSessionManager(options) {
553
850
  let refreshing = null;
554
851
  let backoff = null;
555
852
  let orphan = null;
556
- let undelivered = null;
853
+ let undelivered = [];
557
854
  let channel;
558
855
  let channelOpened = false;
559
856
  const endedSessions = new Set;
560
857
  let signingOut = 0;
561
858
  function setState(next) {
562
- const same = next.status === state.status && (next.status !== "signed-in" || state.status === "signed-in" && next.sessionId === state.sessionId && next.user === state.user);
859
+ const same = next.status === state.status && (next.status !== "signed-in" || state.status === "signed-in" && next.sessionId === state.sessionId && next.user === state.user && next.organizationId === state.organizationId);
563
860
  if (same) {
564
861
  return;
565
862
  }
@@ -616,7 +913,12 @@ function createSessionManager(options) {
616
913
  if (generation !== mine) {
617
914
  return;
618
915
  }
619
- setState({ status: "signed-in", sessionId: current.sessionId, user });
916
+ setState({
917
+ status: "signed-in",
918
+ sessionId: current.sessionId,
919
+ user,
920
+ organizationId: current.organizationId
921
+ });
620
922
  }
621
923
  function commit(issued) {
622
924
  generation += 1;
@@ -627,6 +929,7 @@ function createSessionManager(options) {
627
929
  tokens = {
628
930
  accessToken: null,
629
931
  sessionId: issued.sessionId,
932
+ organizationId: tokens?.sessionId === issued.sessionId ? tokens.organizationId : null,
630
933
  expiresAt: Number.MAX_SAFE_INTEGER,
631
934
  refreshAt: Number.MAX_SAFE_INTEGER
632
935
  };
@@ -635,6 +938,7 @@ function createSessionManager(options) {
635
938
  tokens = {
636
939
  accessToken,
637
940
  sessionId: issued.sessionId,
941
+ organizationId: organizationIdOf(accessToken),
638
942
  expiresAt: now + lifetime,
639
943
  refreshAt: now + lifetime - Math.min(ACCESS_TOKEN_EXPIRY_SKEW_MS, lifetime / 2)
640
944
  };
@@ -674,7 +978,7 @@ function createSessionManager(options) {
674
978
  return refreshToken;
675
979
  }
676
980
  function receive(data) {
677
- if (!isRecord4(data) || data.v !== CHANNEL_VERSION2) {
981
+ if (!isRecord5(data) || data.v !== CHANNEL_VERSION2) {
678
982
  return;
679
983
  }
680
984
  if (data.type === "signed-out") {
@@ -694,6 +998,7 @@ function createSessionManager(options) {
694
998
  tokens = {
695
999
  accessToken: data.accessToken,
696
1000
  sessionId: data.sessionId,
1001
+ organizationId: typeof data.organizationId === "string" && data.organizationId !== "" ? data.organizationId : null,
697
1002
  expiresAt: data.expiresAt,
698
1003
  refreshAt: data.refreshAt
699
1004
  };
@@ -803,6 +1108,9 @@ function createSessionManager(options) {
803
1108
  return { token: issued.accessToken ?? null, installed, failure: await persist(issued) };
804
1109
  }
805
1110
  async function runRefresh(explicit) {
1111
+ if (signingOut > 0) {
1112
+ return null;
1113
+ }
806
1114
  if (!explicit && backoff && environment.now() < backoff.until) {
807
1115
  throw backoff.error;
808
1116
  }
@@ -857,7 +1165,6 @@ function createSessionManager(options) {
857
1165
  throw clientError("response.invalid", options.messages());
858
1166
  }
859
1167
  openChannel();
860
- undelivered = null;
861
1168
  const installed = commit(issued);
862
1169
  const failure = await persist(issued);
863
1170
  await settle(installed);
@@ -895,19 +1202,29 @@ function createSessionManager(options) {
895
1202
  failure = clientError("storage.failed", options.messages(), cause);
896
1203
  }
897
1204
  }
898
- const newest = orphan ?? presented ?? undelivered;
1205
+ const newest = orphan ?? presented;
899
1206
  orphan = null;
900
- if (web || newest) {
1207
+ const owed = [...new Set([newest, ...undelivered].filter((t) => !!t))];
1208
+ if (web) {
901
1209
  try {
902
- await withLock(() => transport.call("signOut", { body: newest ? { refreshToken: newest } : {} }));
903
- undelivered = null;
1210
+ await withLock(() => transport.call("signOut", { body: {} }));
904
1211
  } catch (error) {
905
1212
  failure = error;
906
- if (!web && generation === mine) {
1213
+ }
1214
+ } else {
1215
+ const kept = [];
1216
+ for (const token of owed) {
1217
+ try {
1218
+ await withLock(() => transport.call("signOut", { body: { refreshToken: token } }));
1219
+ } catch (error) {
1220
+ failure = failure ?? error;
907
1221
  const over = isTulaError(error) && error.code.startsWith("session.");
908
- undelivered = over ? null : newest;
1222
+ if (!over) {
1223
+ kept.push(token);
1224
+ }
909
1225
  }
910
1226
  }
1227
+ undelivered = kept.slice(0, MAX_UNDELIVERED);
911
1228
  }
912
1229
  if (failure) {
913
1230
  throw failure;
@@ -996,6 +1313,42 @@ function createSessionManager(options) {
996
1313
  throw unauthenticated();
997
1314
  }
998
1315
  }
1316
+ let organizationEpoch = 0;
1317
+ async function setActiveOrganization(organizationId) {
1318
+ openChannel();
1319
+ organizationEpoch += 1;
1320
+ await refreshing?.catch(ignore);
1321
+ const { answer, sent } = await send("setActiveOrganization", { body: { organizationId } });
1322
+ if (!isActiveOrganization(answer)) {
1323
+ throw clientError("response.invalid", options.messages());
1324
+ }
1325
+ if (state.status === "signed-out" || tokens?.sessionId !== answer.sessionId) {
1326
+ throw unauthenticated();
1327
+ }
1328
+ if (answer.accessToken === undefined) {
1329
+ if (generation === sent && !refreshing && tokens) {
1330
+ tokens = { ...tokens, organizationId: answer.organizationId };
1331
+ post({ type: "session", ...tokens });
1332
+ if (state.status === "signed-in") {
1333
+ setState({ ...state, organizationId: answer.organizationId });
1334
+ }
1335
+ }
1336
+ return answer;
1337
+ }
1338
+ if (generation === sent && !refreshing) {
1339
+ await settle(commit({
1340
+ sessionId: answer.sessionId,
1341
+ accessToken: answer.accessToken,
1342
+ accessTokenExpiresAt: answer.accessTokenExpiresAt
1343
+ }));
1344
+ return answer;
1345
+ }
1346
+ await refreshing?.catch(ignore);
1347
+ if (tokens?.sessionId !== answer.sessionId || !await refresh(true)) {
1348
+ throw unauthenticated();
1349
+ }
1350
+ return answer;
1351
+ }
999
1352
  return {
1000
1353
  state: () => state,
1001
1354
  subscribe(listener) {
@@ -1020,6 +1373,16 @@ function createSessionManager(options) {
1020
1373
  },
1021
1374
  authorized,
1022
1375
  stepUp,
1376
+ setActiveOrganization,
1377
+ organizationEpoch: () => organizationEpoch,
1378
+ noteActiveOrganization(sessionId, organizationId, epoch) {
1379
+ if (epoch === organizationEpoch && tokens && tokens.accessToken === null && tokens.sessionId === sessionId) {
1380
+ tokens = { ...tokens, organizationId };
1381
+ if (state.status === "signed-in" && state.sessionId === sessionId) {
1382
+ setState({ ...state, organizationId });
1383
+ }
1384
+ }
1385
+ },
1023
1386
  expire() {
1024
1387
  if (tokens) {
1025
1388
  tokens = { ...tokens, refreshAt: 0 };
@@ -1029,11 +1392,11 @@ function createSessionManager(options) {
1029
1392
  }
1030
1393
 
1031
1394
  // src/flows.ts
1032
- function isRecord5(value) {
1395
+ function isRecord6(value) {
1033
1396
  return typeof value === "object" && value !== null && !Array.isArray(value);
1034
1397
  }
1035
1398
  function isUsableAttempt(value) {
1036
- if (!isRecord5(value) || typeof value.id !== "string" || value.id === "" || !isRecord5(value.step) || typeof value.step.status !== "string") {
1399
+ if (!isRecord6(value) || typeof value.id !== "string" || value.id === "" || !isRecord6(value.step) || typeof value.step.status !== "string") {
1037
1400
  return false;
1038
1401
  }
1039
1402
  return value.step.status === "complete" ? isSessionTokens(value.session) || isStatefulSession(value.session) : value.session === undefined;
@@ -1127,7 +1490,7 @@ function enrolmentActions(context, attempt, kind) {
1127
1490
  }),
1128
1491
  confirmTotpEnrolment: ({ code }) => attempt.exclusive(async (bound) => {
1129
1492
  const next = await context.transport.call(confirm, { ...bound, body: { code } });
1130
- const backupCodes = isRecord5(next) ? next.backupCodes : undefined;
1493
+ const backupCodes = isRecord6(next) ? next.backupCodes : undefined;
1131
1494
  if (!isCodes(backupCodes)) {
1132
1495
  throw clientError("response.invalid", context.messages());
1133
1496
  }
@@ -1146,7 +1509,7 @@ function secondFactorAction(context, attempt, operation) {
1146
1509
  const submit = async (bound, proof) => {
1147
1510
  const body = proof.method === "passkey" ? { method: "passkey", credential: proof.credential } : { method: proof.method, code: proof.code };
1148
1511
  const next = await context.transport.call(operation, { ...bound, body });
1149
- const remaining = isRecord5(next) ? next.backupCodesRemaining : undefined;
1512
+ const remaining = isRecord6(next) ? next.backupCodesRemaining : undefined;
1150
1513
  if (remaining !== undefined && !(Number.isInteger(remaining) && Number(remaining) >= 0)) {
1151
1514
  throw clientError("response.invalid", context.messages());
1152
1515
  }
@@ -1211,6 +1574,7 @@ async function signInFlow(context, started, first) {
1211
1574
  },
1212
1575
  attemptFirstFactor: (input) => attempt.step((bound) => transport.call("attemptSignInFirstFactor", { ...bound, body: input })),
1213
1576
  waitForEmailLink: (options) => waiting.wait(options?.signal),
1577
+ startSso: (input) => attempt.exclusive((bound) => startSso(context, bound, input)),
1214
1578
  discard() {
1215
1579
  attempt.discard();
1216
1580
  waiting.stop();
@@ -1315,377 +1679,116 @@ function emailLinkWait(context, attempt) {
1315
1679
  stopListening();
1316
1680
  if (channel) {
1317
1681
  channel.onmessage = null;
1318
- channel.close?.();
1319
- }
1320
- }
1321
- }
1322
- return {
1323
- wait(signal) {
1324
- if (attempt.finished()) {
1325
- return Promise.reject(attempt.refused("flow.invalid_step"));
1326
- }
1327
- if (signal?.aborted) {
1328
- return Promise.resolve(attempt.snapshot().step);
1329
- }
1330
- if (stopping) {
1331
- stopping = false;
1332
- resumed = true;
1333
- }
1334
- loop ??= run();
1335
- const running = loop;
1336
- return new Promise((resolve, reject) => {
1337
- const leave = () => {
1338
- callers.delete(release);
1339
- signal?.removeEventListener("abort", release);
1340
- };
1341
- const release = () => {
1342
- leave();
1343
- resolve(attempt.snapshot().step);
1344
- if (callers.size === 0) {
1345
- stopping = true;
1346
- wake?.();
1347
- }
1348
- };
1349
- callers.add(release);
1350
- signal?.addEventListener("abort", release);
1351
- running.then((step) => {
1352
- leave();
1353
- resolve(step);
1354
- }, (error) => {
1355
- leave();
1356
- reject(error);
1357
- });
1358
- });
1359
- },
1360
- stop
1361
- };
1362
- }
1363
- async function passwordResetFlow(context, started) {
1364
- const { transport } = context;
1365
- const attempt = createAttempt(context, started);
1366
- await attempt.accept(started);
1367
- return flowObject(attempt, {
1368
- submit: ({ code, password }) => attempt.step((bound) => transport.call("submitPasswordReset", { ...bound, body: { code, password } })),
1369
- resendCode: () => attempt.step((bound) => transport.call("resendPasswordResetCode", bound)),
1370
- ...secondFactorAction(context, attempt, "submitPasswordResetSecondFactor"),
1371
- ...enrolmentActions(context, attempt, "password_reset"),
1372
- discard: () => attempt.discard()
1373
- });
1374
- }
1375
- var PASSKEY_AUTOFILL_ROUND_MS = 240000;
1376
- async function askForPasskey(context, authenticator, options, request) {
1377
- if (!request.autofill) {
1378
- return authenticator.get(options, { signal: request.signal });
1379
- }
1380
- const round = new AbortController;
1381
- let lapsed = false;
1382
- const stop = () => round.abort();
1383
- request.signal?.addEventListener("abort", stop);
1384
- const cancel = context.environment.setTimer(() => {
1385
- lapsed = true;
1386
- round.abort();
1387
- }, PASSKEY_AUTOFILL_ROUND_MS);
1388
- try {
1389
- if (request.signal?.aborted) {
1390
- round.abort();
1391
- }
1392
- return await authenticator.get(options, { signal: round.signal, autofill: true });
1393
- } catch (error) {
1394
- if (lapsed && !request.signal?.aborted) {
1395
- return null;
1396
- }
1397
- throw error;
1398
- } finally {
1399
- cancel();
1400
- request.signal?.removeEventListener("abort", stop);
1401
- }
1402
- }
1403
- async function passkeySignIn(context, request = {}) {
1404
- const authenticator = context.passkeys();
1405
- for (;; ) {
1406
- const started = await context.transport.call("startPasskeySignIn", {});
1407
- if (!isPasskeySignInStart(started)) {
1408
- throw clientError("response.invalid", context.messages());
1409
- }
1410
- const credential = await askForPasskey(context, authenticator, started.options, request);
1411
- if (credential !== null) {
1412
- return signInFlow(context, started.attempt, (bound) => context.transport.call("submitSignInPasskey", { ...bound, body: { credential } }));
1413
- }
1414
- }
1415
- }
1416
- async function idTokenSignIn(context, provider) {
1417
- const started = await context.transport.call("startIdTokenSignIn", {
1418
- body: { provider }
1419
- });
1420
- if (!isRecord5(started) || typeof started.nonce !== "string" || !isUsableAttempt(started.attempt)) {
1421
- throw clientError("response.invalid", context.messages());
1422
- }
1423
- const { attempt } = started;
1424
- return {
1425
- nonce: started.nonce,
1426
- exchange: (idToken, name) => signInFlow(context, attempt, (bound) => context.transport.call("submitSignInIdToken", { ...bound, body: { ...name, idToken } }))
1427
- };
1428
- }
1429
-
1430
- // src/oauth.ts
1431
- import { ERROR_DEFINITIONS as ERROR_DEFINITIONS2 } from "@tula-tech/contract/error-codes";
1432
- import {
1433
- EMAIL_LINK_ATTEMPT_PARAM as EMAIL_LINK_ATTEMPT_PARAM2,
1434
- OAUTH_ERROR_PARAM,
1435
- OAUTH_TICKET_PARAM
1436
- } from "@tula-tech/contract/headers";
1437
- var OAUTH_BINDING_TTL_MS = 15 * 60000;
1438
- var OAUTH_TICKET_HOLD_MS = 60000;
1439
- var RETRYABLE = new Set([
1440
- "network.failed",
1441
- "network.timeout",
1442
- "rate_limited"
1443
- ]);
1444
- function isRetryableOAuthError(error) {
1445
- return isTulaError(error) && RETRYABLE.has(error.code);
1446
- }
1447
- var OAUTH_STORAGE_PREFIX = "tula.oauth.";
1448
- function isRecord6(value) {
1449
- return typeof value === "object" && value !== null && !Array.isArray(value);
1450
- }
1451
- function createOAuthStore(environment, scope) {
1452
- const storage = environment.tabStorage;
1453
- const keyOf = (attemptId) => `${OAUTH_STORAGE_PREFIX}${attemptId}`;
1454
- function guarded(work, fallback) {
1455
- try {
1456
- return work();
1457
- } catch {
1458
- return fallback;
1459
- }
1460
- }
1461
- function prune() {
1462
- if (!storage) {
1463
- return;
1464
- }
1465
- const stale = [];
1466
- for (let index = 0;index < storage.length; index++) {
1467
- const key = storage.key(index);
1468
- if (key?.startsWith(OAUTH_STORAGE_PREFIX)) {
1469
- const expiry = guarded(() => JSON.parse(storage.getItem(key) ?? "null")?.e, undefined);
1470
- if (typeof expiry !== "number" || expiry <= environment.now()) {
1471
- stale.push(key);
1472
- }
1473
- }
1474
- }
1475
- for (const key of stale) {
1476
- storage.removeItem(key);
1477
- }
1478
- }
1479
- return {
1480
- available: () => guarded(() => {
1481
- if (!storage) {
1482
- return false;
1483
- }
1484
- const probe = `${OAUTH_STORAGE_PREFIX}probe`;
1485
- storage.setItem(probe, "1");
1486
- storage.removeItem(probe);
1487
- return true;
1488
- }, false),
1489
- save: (attemptId, binding, intent) => guarded(() => {
1490
- if (!storage) {
1491
- return false;
1492
- }
1493
- prune();
1494
- storage.setItem(keyOf(attemptId), JSON.stringify({
1495
- b: binding,
1496
- e: environment.now() + OAUTH_BINDING_TTL_MS,
1497
- s: scope,
1498
- k: intent
1499
- }));
1500
- return true;
1501
- }, false),
1502
- read: (attemptId) => guarded(() => {
1503
- prune();
1504
- const entry = JSON.parse(storage?.getItem(keyOf(attemptId)) ?? "null");
1505
- return isRecord6(entry) && typeof entry.b === "string" && entry.s === scope && (entry.k === "sign_in" || entry.k === "link") ? { binding: entry.b, intent: entry.k } : null;
1506
- }, null),
1507
- remove(attemptId) {
1508
- guarded(() => storage?.removeItem(keyOf(attemptId)), undefined);
1509
- }
1510
- };
1511
- }
1512
- function readOAuthFragment(url) {
1513
- const at = url.indexOf("#");
1514
- if (at === -1) {
1515
- return null;
1516
- }
1517
- const fragment = new URLSearchParams(url.slice(at + 1));
1518
- const ticket = fragment.get(OAUTH_TICKET_PARAM);
1519
- const error = fragment.get(OAUTH_ERROR_PARAM);
1520
- if (!ticket && !error) {
1521
- return null;
1522
- }
1523
- const attemptId = fragment.get(EMAIL_LINK_ATTEMPT_PARAM2) ?? "";
1524
- for (const name of [OAUTH_TICKET_PARAM, OAUTH_ERROR_PARAM, EMAIL_LINK_ATTEMPT_PARAM2]) {
1525
- fragment.delete(name);
1526
- }
1527
- const rest = fragment.toString();
1528
- return {
1529
- attemptId,
1530
- ticket: ticket || null,
1531
- error: ticket ? null : error,
1532
- cleanUrl: `${url.slice(0, at)}${rest ? `#${rest}` : ""}`
1533
- };
1534
- }
1535
- function createOAuthTicketHolder() {
1536
- let held = null;
1537
- return {
1538
- get: () => held,
1539
- set(next) {
1540
- held = next;
1682
+ channel.close?.();
1683
+ }
1541
1684
  }
1542
- };
1543
- }
1544
- function discardOAuthCallback(context) {
1545
- const held = context.held.get();
1546
- if (held) {
1547
- context.held.set(null);
1548
- context.oauth.remove(held.attemptId);
1549
1685
  }
1686
+ return {
1687
+ wait(signal) {
1688
+ if (attempt.finished()) {
1689
+ return Promise.reject(attempt.refused("flow.invalid_step"));
1690
+ }
1691
+ if (signal?.aborted) {
1692
+ return Promise.resolve(attempt.snapshot().step);
1693
+ }
1694
+ if (stopping) {
1695
+ stopping = false;
1696
+ resumed = true;
1697
+ }
1698
+ loop ??= run();
1699
+ const running = loop;
1700
+ return new Promise((resolve, reject) => {
1701
+ const leave = () => {
1702
+ callers.delete(release);
1703
+ signal?.removeEventListener("abort", release);
1704
+ };
1705
+ const release = () => {
1706
+ leave();
1707
+ resolve(attempt.snapshot().step);
1708
+ if (callers.size === 0) {
1709
+ stopping = true;
1710
+ wake?.();
1711
+ }
1712
+ };
1713
+ callers.add(release);
1714
+ signal?.addEventListener("abort", release);
1715
+ running.then((step) => {
1716
+ leave();
1717
+ resolve(step);
1718
+ }, (error) => {
1719
+ leave();
1720
+ reject(error);
1721
+ });
1722
+ });
1723
+ },
1724
+ stop
1725
+ };
1550
1726
  }
1551
- function isLinkStart(value) {
1552
- return isRecord6(value) && typeof value.attemptId === "string" && value.attemptId !== "" && typeof value.authorizationUrl === "string" && typeof value.binding === "string";
1553
- }
1554
- function isIdentity(value) {
1555
- return isRecord6(value) && typeof value.id === "string" && typeof value.provider === "string";
1556
- }
1557
- function isIdentityList(value) {
1558
- return isRecord6(value) && Array.isArray(value.data) && value.data.every(isIdentity);
1727
+ async function passwordResetFlow(context, started) {
1728
+ const { transport } = context;
1729
+ const attempt = createAttempt(context, started);
1730
+ await attempt.accept(started);
1731
+ return flowObject(attempt, {
1732
+ submit: ({ code, password }) => attempt.step((bound) => transport.call("submitPasswordReset", { ...bound, body: { code, password } })),
1733
+ resendCode: () => attempt.step((bound) => transport.call("resendPasswordResetCode", bound)),
1734
+ ...secondFactorAction(context, attempt, "submitPasswordResetSecondFactor"),
1735
+ ...enrolmentActions(context, attempt, "password_reset"),
1736
+ discard: () => attempt.discard()
1737
+ });
1559
1738
  }
1560
- async function startOAuth(context, input, intent) {
1561
- const { environment, oauth, transport, session } = context;
1562
- discardOAuthCallback(context);
1563
- if (!oauth.available()) {
1564
- throw clientError("storage.failed", context.messages());
1739
+ var PASSKEY_AUTOFILL_ROUND_MS = 240000;
1740
+ async function askForPasskey(context, authenticator, options, request) {
1741
+ if (!request.autofill) {
1742
+ return authenticator.get(options, { signal: request.signal });
1565
1743
  }
1566
- const page = environment.page;
1567
- if (page && originOf(input.redirectUrl) !== originOf(page.url())) {
1568
- throw clientError("link.cross_origin", context.messages());
1744
+ const round = new AbortController;
1745
+ let lapsed = false;
1746
+ const stop = () => round.abort();
1747
+ request.signal?.addEventListener("abort", stop);
1748
+ const cancel = context.environment.setTimer(() => {
1749
+ lapsed = true;
1750
+ round.abort();
1751
+ }, PASSKEY_AUTOFILL_ROUND_MS);
1752
+ try {
1753
+ if (request.signal?.aborted) {
1754
+ round.abort();
1755
+ }
1756
+ return await authenticator.get(options, { signal: round.signal, autofill: true });
1757
+ } catch (error) {
1758
+ if (lapsed && !request.signal?.aborted) {
1759
+ return null;
1760
+ }
1761
+ throw error;
1762
+ } finally {
1763
+ cancel();
1764
+ request.signal?.removeEventListener("abort", stop);
1569
1765
  }
1570
- const body = { provider: input.provider, redirectUrl: input.redirectUrl };
1571
- let attemptId;
1572
- let started;
1573
- if (intent === "link") {
1574
- const answer = await session.authorized("startIdentityLink", { body });
1575
- if (!isLinkStart(answer)) {
1766
+ }
1767
+ async function passkeySignIn(context, request = {}) {
1768
+ const authenticator = context.passkeys();
1769
+ for (;; ) {
1770
+ const started = await context.transport.call("startPasskeySignIn", {});
1771
+ if (!isPasskeySignInStart(started)) {
1576
1772
  throw clientError("response.invalid", context.messages());
1577
1773
  }
1578
- attemptId = answer.attemptId;
1579
- started = answer;
1580
- } else {
1581
- const answer = await transport.call("startOAuthSignIn", { body });
1582
- if (!isRecord6(answer) || !isRecord6(answer.attempt) || typeof answer.attempt.id !== "string" || typeof answer.authorizationUrl !== "string" || typeof answer.binding !== "string") {
1583
- throw clientError("response.invalid", context.messages());
1774
+ const credential = await askForPasskey(context, authenticator, started.options, request);
1775
+ if (credential !== null) {
1776
+ return signInFlow(context, started.attempt, (bound) => context.transport.call("submitSignInPasskey", { ...bound, body: { credential } }));
1584
1777
  }
1585
- attemptId = answer.attempt.id;
1586
- started = { authorizationUrl: answer.authorizationUrl, binding: answer.binding };
1587
1778
  }
1588
- if (!/^https?:\/\//i.test(started.authorizationUrl)) {
1589
- throw clientError("response.invalid", context.messages());
1590
- }
1591
- if (!oauth.save(attemptId, started.binding, intent)) {
1592
- throw clientError("storage.failed", context.messages());
1593
- }
1594
- if (input.navigate !== false) {
1595
- page?.assign?.(started.authorizationUrl);
1596
- }
1597
- return { url: started.authorizationUrl };
1598
1779
  }
1599
- function originOf(url) {
1600
- try {
1601
- return new URL(url).origin;
1602
- } catch {
1603
- return null;
1780
+ async function idTokenSignIn(context, provider) {
1781
+ const started = await context.transport.call("startIdTokenSignIn", {
1782
+ body: { provider }
1783
+ });
1784
+ if (!isRecord6(started) || typeof started.nonce !== "string" || !isUsableAttempt(started.attempt)) {
1785
+ throw clientError("response.invalid", context.messages());
1604
1786
  }
1605
- }
1606
- function failed(code, messages) {
1607
- const known = Object.hasOwn(ERROR_DEFINITIONS2, code) ? code : "oauth.provider_error";
1787
+ const { attempt } = started;
1608
1788
  return {
1609
- status: "error",
1610
- code: known,
1611
- message: formatMessage(known, { messages })
1612
- };
1613
- }
1614
- async function handleOAuthCallback(context) {
1615
- const { environment, oauth, held, transport, session } = context;
1616
- const page = environment.page;
1617
- if (!page) {
1618
- return { status: "none" };
1619
- }
1620
- const arrived = readOAuthFragment(page.url());
1621
- let answer;
1622
- if (arrived) {
1623
- try {
1624
- page.replaceUrl(arrived.cleanUrl);
1625
- } catch {}
1626
- discardOAuthCallback(context);
1627
- if (arrived.error !== null || arrived.ticket === null) {
1628
- oauth.remove(arrived.attemptId);
1629
- return arrived.error !== null ? failed(arrived.error, context.messages()) : { status: "different_browser" };
1630
- }
1631
- answer = {
1632
- attemptId: arrived.attemptId,
1633
- ticket: arrived.ticket,
1634
- until: environment.now() + OAUTH_TICKET_HOLD_MS
1635
- };
1636
- } else {
1637
- const waiting = held.get();
1638
- if (!waiting) {
1639
- return { status: "none" };
1640
- }
1641
- if (waiting.until <= environment.now()) {
1642
- discardOAuthCallback(context);
1643
- return failed("oauth.ticket_invalid", context.messages());
1644
- }
1645
- answer = waiting;
1646
- }
1647
- const kept = oauth.read(answer.attemptId);
1648
- const settle = () => {
1649
- held.set(null);
1650
- oauth.remove(answer.attemptId);
1789
+ nonce: started.nonce,
1790
+ exchange: (idToken, name) => signInFlow(context, attempt, (bound) => context.transport.call("submitSignInIdToken", { ...bound, body: { ...name, idToken } }))
1651
1791
  };
1652
- if (!kept) {
1653
- settle();
1654
- return { status: "different_browser" };
1655
- }
1656
- held.set(answer);
1657
- const body = { ticket: answer.ticket, attemptId: answer.attemptId, binding: kept.binding };
1658
- try {
1659
- let outcome;
1660
- if (kept.intent === "link") {
1661
- const identity = await session.authorized("exchangeIdentityLinkTicket", { body });
1662
- if (!isIdentity(identity)) {
1663
- throw clientError("response.invalid", context.messages());
1664
- }
1665
- outcome = { status: "linked", identity };
1666
- } else {
1667
- const attempt = await transport.call("exchangeOAuthTicket", { body });
1668
- const complete = isRecord6(attempt) && isRecord6(attempt.step) && attempt.step.status === "complete";
1669
- outcome = {
1670
- status: complete ? "complete" : "needs_step",
1671
- flow: await signInFlow(context, attempt)
1672
- };
1673
- }
1674
- settle();
1675
- return outcome;
1676
- } catch (error) {
1677
- if (isRetryableOAuthError(error)) {
1678
- throw error;
1679
- }
1680
- settle();
1681
- if (!isTulaError(error) || error.status === 0) {
1682
- throw error;
1683
- }
1684
- if (error.code === "oauth.different_browser") {
1685
- return { status: "different_browser" };
1686
- }
1687
- return { status: "error", code: error.code, message: error.message };
1688
- }
1689
1792
  }
1690
1793
 
1691
1794
  // src/storage.ts
@@ -1719,6 +1822,8 @@ import {
1719
1822
 
1720
1823
  // src/generated/api.gen.ts
1721
1824
  var OPERATIONS = {
1825
+ acceptMyOrganizationInvitation: { method: "POST", path: "/v1/client/me/organization-invitations/{invitationId}/accept", session: true },
1826
+ acceptOrganizationInvitation: { method: "POST", path: "/v1/client/me/organization-invitations/accept", session: true },
1722
1827
  attemptSignInFirstFactor: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/first-factor/attempt", session: false },
1723
1828
  changeMyPassword: { method: "POST", path: "/v1/client/me/password", session: true },
1724
1829
  confirmPasswordResetTotpEnrolment: { method: "POST", path: "/v1/client/password-resets/{attemptId}/factor-enrolment/totp/confirm", session: false },
@@ -1726,7 +1831,14 @@ var OPERATIONS = {
1726
1831
  confirmSignUpTotpEnrolment: { method: "POST", path: "/v1/client/sign-ups/{attemptId}/factor-enrolment/totp/confirm", session: false },
1727
1832
  confirmSmsFactorEnrolment: { method: "POST", path: "/v1/client/me/factors/sms/confirm", session: true },
1728
1833
  confirmTotpEnrolment: { method: "POST", path: "/v1/client/me/factors/totp/confirm", session: true },
1834
+ createMyOrganization: { method: "POST", path: "/v1/client/me/organizations", session: true },
1835
+ createMyOrganizationSetupLink: { method: "POST", path: "/v1/client/me/organizations/{id}/setup-links", session: true },
1836
+ createMyOrganizationSsoConnection: { method: "POST", path: "/v1/client/me/organizations/{organizationId}/sso-connections", session: true },
1837
+ declineMyOrganizationInvitation: { method: "POST", path: "/v1/client/me/organization-invitations/{invitationId}/decline", session: true },
1838
+ declineOrganizationInvitation: { method: "POST", path: "/v1/client/me/organization-invitations/decline", session: true },
1729
1839
  deleteMyIdentity: { method: "DELETE", path: "/v1/client/me/identities/{identityId}", session: true },
1840
+ deleteMyOrganization: { method: "DELETE", path: "/v1/client/me/organizations/{organizationId}", session: true },
1841
+ deleteMyOrganizationSsoConnection: { method: "DELETE", path: "/v1/client/me/organizations/{organizationId}/sso-connections/{id}", session: true },
1730
1842
  disableSmsFactor: { method: "DELETE", path: "/v1/client/me/factors/sms", session: true },
1731
1843
  disableTotp: { method: "DELETE", path: "/v1/client/me/factors/totp", session: true },
1732
1844
  exchangeIdentityLinkTicket: { method: "POST", path: "/v1/client/me/identities/oauth/exchange", session: true },
@@ -1735,33 +1847,57 @@ var OPERATIONS = {
1735
1847
  getClientConfig: { method: "GET", path: "/v1/client/config", session: false },
1736
1848
  getMe: { method: "GET", path: "/v1/client/me", session: true },
1737
1849
  getMyFactors: { method: "GET", path: "/v1/client/me/factors", session: true },
1850
+ getMyOrganizationSsoConnection: { method: "GET", path: "/v1/client/me/organizations/{organizationId}/sso-connections/{id}", session: true },
1851
+ getMyOrganizationSsoConnectionTest: { method: "GET", path: "/v1/client/me/organizations/{organizationId}/sso-connections/{id}/tests/{testId}", session: true },
1852
+ getMyOrganizationSsoGroupRoles: { method: "GET", path: "/v1/client/me/organizations/{organizationId}/sso-connections/{id}/group-roles", session: true },
1738
1853
  getPasswordPolicy: { method: "GET", path: "/v1/client/password-policy", session: false },
1739
1854
  getPasswordResetSecondFactorPasskeyOptions: { method: "POST", path: "/v1/client/password-resets/{attemptId}/second-factor/passkey/options", session: false },
1740
1855
  getSignInSecondFactorPasskeyOptions: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/second-factor/passkey/options", session: false },
1741
1856
  getStepUpPasskeyOptions: { method: "POST", path: "/v1/client/sessions/step-up/passkey", session: true },
1857
+ inviteToMyOrganization: { method: "POST", path: "/v1/client/me/organizations/{organizationId}/invitations", session: true },
1858
+ joinOrganizationSuggestion: { method: "POST", path: "/v1/client/me/organizations/suggestions/{organizationId}/join", session: true },
1742
1859
  listMyIdentities: { method: "GET", path: "/v1/client/me/identities", session: true },
1860
+ listMyOrganizationDomains: { method: "GET", path: "/v1/client/me/organizations/{id}/domains", session: true },
1861
+ listMyOrganizationInvitations: { method: "GET", path: "/v1/client/me/organizations/{organizationId}/invitations", session: true },
1862
+ listMyOrganizationMembers: { method: "GET", path: "/v1/client/me/organizations/{organizationId}/members", session: true },
1863
+ listMyOrganizationRoles: { method: "GET", path: "/v1/client/me/organizations/{organizationId}/roles", session: true },
1864
+ listMyOrganizations: { method: "GET", path: "/v1/client/me/organizations", session: true },
1865
+ listMyOrganizationSetupLinks: { method: "GET", path: "/v1/client/me/organizations/{id}/setup-links", session: true },
1866
+ listMyOrganizationSsoConnections: { method: "GET", path: "/v1/client/me/organizations/{organizationId}/sso-connections", session: true },
1867
+ listMyOrganizationSuggestions: { method: "GET", path: "/v1/client/me/organizations/suggestions", session: true },
1743
1868
  listMyPasskeys: { method: "GET", path: "/v1/client/me/passkeys", session: true },
1869
+ listMyPendingOrganizationInvitations: { method: "GET", path: "/v1/client/me/organization-invitations", session: true },
1744
1870
  listSessions: { method: "GET", path: "/v1/client/sessions", session: true },
1745
1871
  preparePasswordResetSecondFactor: { method: "POST", path: "/v1/client/password-resets/{attemptId}/second-factor/prepare", session: false },
1746
1872
  prepareSignInFirstFactor: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/first-factor/prepare", session: false },
1747
1873
  prepareSignInSecondFactor: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/second-factor/prepare", session: false },
1874
+ previewOrganizationInvitation: { method: "POST", path: "/v1/client/organization-invitations/preview", session: false },
1875
+ refreshMyOrganizationSsoMetadata: { method: "POST", path: "/v1/client/me/organizations/{organizationId}/sso-connections/{id}/refresh-metadata", session: true },
1748
1876
  refreshSession: { method: "POST", path: "/v1/client/sessions/refresh", session: false },
1749
1877
  regenerateBackupCodes: { method: "POST", path: "/v1/client/me/factors/backup-codes", session: true },
1878
+ removeMyOrganizationDomain: { method: "DELETE", path: "/v1/client/me/organizations/{id}/domains/{domainId}", session: true },
1879
+ removeMyOrganizationMember: { method: "DELETE", path: "/v1/client/me/organizations/{organizationId}/members/{userId}", session: true },
1750
1880
  removePasskey: { method: "DELETE", path: "/v1/client/me/passkeys/{passkeyId}", session: true },
1751
1881
  removePhoneNumber: { method: "DELETE", path: "/v1/client/me/phone", session: true },
1752
1882
  renamePasskey: { method: "PATCH", path: "/v1/client/me/passkeys/{passkeyId}", session: true },
1883
+ replaceMyOrganizationSsoGroupRoles: { method: "PUT", path: "/v1/client/me/organizations/{organizationId}/sso-connections/{id}/group-roles", session: true },
1753
1884
  requestEmailAddressCode: { method: "POST", path: "/v1/client/me/email", session: true },
1754
1885
  requestPhoneCode: { method: "POST", path: "/v1/client/me/phone", session: true },
1755
1886
  resendPasswordResetCode: { method: "POST", path: "/v1/client/password-resets/{attemptId}/resend-code", session: false },
1756
1887
  resendSignInCode: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/resend-code", session: false },
1757
1888
  resendSignUpCode: { method: "POST", path: "/v1/client/sign-ups/{attemptId}/resend-code", session: false },
1889
+ revokeMyOrganizationInvitation: { method: "DELETE", path: "/v1/client/me/organizations/{organizationId}/invitations/{invitationId}", session: true },
1890
+ revokeMyOrganizationSetupLink: { method: "DELETE", path: "/v1/client/me/organizations/{id}/setup-links/{linkId}", session: true },
1758
1891
  revokeOtherSessions: { method: "POST", path: "/v1/client/sessions/revoke-others", session: true },
1759
1892
  revokeSession: { method: "DELETE", path: "/v1/client/sessions/{sessionId}", session: true },
1760
1893
  sendStepUpEmailCode: { method: "POST", path: "/v1/client/sessions/step-up/email-code", session: true },
1761
1894
  sendStepUpSmsCode: { method: "POST", path: "/v1/client/sessions/step-up/sms-code", session: true },
1895
+ setActiveOrganization: { method: "PUT", path: "/v1/client/me/organizations/active", session: true },
1762
1896
  signOut: { method: "POST", path: "/v1/client/sessions/sign-out", session: false },
1763
1897
  startIdentityLink: { method: "POST", path: "/v1/client/me/identities/oauth", session: true },
1764
1898
  startIdTokenSignIn: { method: "POST", path: "/v1/client/sign-ins/id-token", session: false },
1899
+ startMyOrganizationDomain: { method: "POST", path: "/v1/client/me/organizations/{id}/domains", session: true },
1900
+ startMyOrganizationSsoConnectionTest: { method: "POST", path: "/v1/client/me/organizations/{organizationId}/sso-connections/{id}/tests", session: true },
1765
1901
  startOAuthSignIn: { method: "POST", path: "/v1/client/sign-ins/oauth", session: false },
1766
1902
  startPasskeyRegistration: { method: "POST", path: "/v1/client/me/passkeys/options", session: true },
1767
1903
  startPasskeySignIn: { method: "POST", path: "/v1/client/sign-ins/passkey", session: false },
@@ -1772,6 +1908,7 @@ var OPERATIONS = {
1772
1908
  startSignUp: { method: "POST", path: "/v1/client/sign-ups", session: false },
1773
1909
  startSignUpTotpEnrolment: { method: "POST", path: "/v1/client/sign-ups/{attemptId}/factor-enrolment/totp", session: false },
1774
1910
  startSmsFactorEnrolment: { method: "POST", path: "/v1/client/me/factors/sms", session: true },
1911
+ startSsoSignIn: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/sso", session: false },
1775
1912
  startTotpEnrolment: { method: "POST", path: "/v1/client/me/factors/totp", session: true },
1776
1913
  stepUpSession: { method: "POST", path: "/v1/client/sessions/step-up", session: true },
1777
1914
  submitPasswordReset: { method: "POST", path: "/v1/client/password-resets/{attemptId}/password", session: false },
@@ -1781,7 +1918,12 @@ var OPERATIONS = {
1781
1918
  submitSignInPasskey: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/passkey", session: false },
1782
1919
  submitSignInPassword: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/password", session: false },
1783
1920
  submitSignInSecondFactor: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/second-factor", session: false },
1921
+ updateMyOrganization: { method: "PATCH", path: "/v1/client/me/organizations/{organizationId}", session: true },
1922
+ updateMyOrganizationDomain: { method: "PATCH", path: "/v1/client/me/organizations/{id}/domains/{domainId}", session: true },
1923
+ updateMyOrganizationMember: { method: "PATCH", path: "/v1/client/me/organizations/{organizationId}/members/{userId}", session: true },
1924
+ updateMyOrganizationSsoConnection: { method: "PATCH", path: "/v1/client/me/organizations/{organizationId}/sso-connections/{id}", session: true },
1784
1925
  verifyEmailAddress: { method: "POST", path: "/v1/client/me/email/verify", session: true },
1926
+ verifyMyOrganizationDomain: { method: "POST", path: "/v1/client/me/organizations/{id}/domains/{domainId}/verify", session: true },
1785
1927
  verifyPhoneNumber: { method: "POST", path: "/v1/client/me/phone/verify", session: true },
1786
1928
  verifySignInEmail: { method: "POST", path: "/v1/client/sign-ins/{attemptId}/verify-email", session: false },
1787
1929
  verifySignInLink: { method: "POST", path: "/v1/client/sign-ins/link", session: false },
@@ -1863,8 +2005,15 @@ function errorFromResponse(response, payload, messages) {
1863
2005
  retryAfterMs: readRetryAfterMs(response, params)
1864
2006
  });
1865
2007
  }
1866
- function buildUrl(baseUrl, path, params) {
1867
- return baseUrl + path.replace(/\{(\w+)\}/g, (_placeholder, name) => encodeURIComponent(params[name] ?? ""));
2008
+ function buildUrl(baseUrl, path, params, query = {}) {
2009
+ const search = new URLSearchParams;
2010
+ for (const [name, value] of Object.entries(query)) {
2011
+ if (value !== undefined) {
2012
+ search.set(name, String(value));
2013
+ }
2014
+ }
2015
+ const text = search.toString();
2016
+ return baseUrl + path.replace(/\{(\w+)\}/g, (_placeholder, name) => encodeURIComponent(params[name] ?? "")) + (text === "" ? "" : `?${text}`);
1868
2017
  }
1869
2018
  function createTransport(options) {
1870
2019
  let nonce;
@@ -1900,7 +2049,7 @@ function createTransport(options) {
1900
2049
  if (input.attemptSecret) {
1901
2050
  headers.set(FLOW_ATTEMPT_HEADER, input.attemptSecret);
1902
2051
  }
1903
- const url = buildUrl(options.baseUrl, route.path, input.params ?? {});
2052
+ const url = buildUrl(options.baseUrl, route.path, input.params ?? {}, input.query);
1904
2053
  const source = PROVEN.has(id) ? options.deviceKey : undefined;
1905
2054
  const controller = new AbortController;
1906
2055
  const timer = setTimeout(() => controller.abort(), input.timeoutMs ?? options.timeoutMs);
@@ -1917,6 +2066,7 @@ function createTransport(options) {
1917
2066
  headers,
1918
2067
  body: input.body === undefined ? undefined : JSON.stringify(input.body),
1919
2068
  signal: controller.signal,
2069
+ redirect: "manual",
1920
2070
  ...options.client === "web" && { credentials: "include" }
1921
2071
  }));
1922
2072
  if (options.deviceKey) {
@@ -1952,11 +2102,11 @@ function createTransport(options) {
1952
2102
  if (!response.ok) {
1953
2103
  throw errorFromResponse(response, payload, options.messages());
1954
2104
  }
1955
- if (payload === null) {
2105
+ if (response.status !== 204 && !isRecord7(payload)) {
1956
2106
  throw new TulaError({
1957
2107
  code: "response.invalid",
1958
2108
  message: formatMessage("response.invalid", { messages: options.messages() }),
1959
- status: response.status
2109
+ status: payload === null ? response.status : 0
1960
2110
  });
1961
2111
  }
1962
2112
  return payload;
@@ -1969,7 +2119,7 @@ var DEFAULT_TIMEOUT_MS = 15000;
1969
2119
  var PUBLISHABLE_KEY_PREFIX = "tula_pk_";
1970
2120
  var SECRET_KEY_PREFIX = "tula_sk_";
1971
2121
  var SESSION_PROFILE_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1972
- function normalizeBaseUrl(baseUrl) {
2122
+ function normalizeBaseUrl(baseUrl, allowInsecureHttp) {
1973
2123
  let url;
1974
2124
  try {
1975
2125
  url = new URL(baseUrl);
@@ -1979,6 +2129,10 @@ function normalizeBaseUrl(baseUrl) {
1979
2129
  if (url.protocol !== "https:" && url.protocol !== "http:") {
1980
2130
  throw new TypeError("createTulaClient: `baseUrl` must be an http(s) URL");
1981
2131
  }
2132
+ const host = url.hostname;
2133
+ if (url.protocol === "http:" && !allowInsecureHttp && host !== "localhost" && !host.endsWith(".localhost") && host !== "127.0.0.1" && host !== "[::1]") {
2134
+ throw new TypeError("createTulaClient: `baseUrl` is plain http to another machine; use https (or set `allowInsecureHttp` on a private network you trust)");
2135
+ }
1982
2136
  return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
1983
2137
  }
1984
2138
  function defaultClientKind() {
@@ -1989,7 +2143,7 @@ function createClient(options, environment) {
1989
2143
  if (typeof publishableKey !== "string" || !publishableKey.startsWith(PUBLISHABLE_KEY_PREFIX)) {
1990
2144
  throw new TypeError(typeof publishableKey === "string" && publishableKey.startsWith(SECRET_KEY_PREFIX) ? "createTulaClient: this is a secret key. Never put one in a client; use the publishable key" : "createTulaClient: `publishableKey` must be a publishable key (tula_pk_…)");
1991
2145
  }
1992
- const baseUrl = normalizeBaseUrl(options.baseUrl);
2146
+ const baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp);
1993
2147
  const client = options.client ?? defaultClientKind();
1994
2148
  if (client === "web" && options.storage) {
1995
2149
  throw new TypeError("createTulaClient: a `web` client keeps its refresh token in an httpOnly cookie and takes no `storage`");
@@ -2051,13 +2205,11 @@ function createClient(options, environment) {
2051
2205
  environment,
2052
2206
  links,
2053
2207
  scope,
2054
- passkeys
2055
- };
2056
- const oauth = {
2057
- ...flows,
2208
+ passkeys,
2058
2209
  oauth: createOAuthStore(environment, scope),
2059
2210
  held: createOAuthTicketHolder()
2060
2211
  };
2212
+ const oauth = flows;
2061
2213
  let config = null;
2062
2214
  function checked(answer, guard) {
2063
2215
  if (!guard(answer)) {
@@ -2166,6 +2318,122 @@ function createClient(options, environment) {
2166
2318
  },
2167
2319
  prepareStepUp: ({ method }) => receipt(method === "sms_code" ? "sendStepUpSmsCode" : "sendStepUpEmailCode", method)
2168
2320
  },
2321
+ organizations: {
2322
+ async list(query) {
2323
+ const epoch = session.organizationEpoch();
2324
+ const answer = await session.authorized("listMyOrganizations", { query });
2325
+ const now = session.state();
2326
+ if (now.status === "signed-in") {
2327
+ session.noteActiveOrganization(now.sessionId, answer.activeOrganizationId, epoch);
2328
+ }
2329
+ return answer;
2330
+ },
2331
+ create: (input) => session.authorized("createMyOrganization", { body: input }),
2332
+ setActive: (organizationId) => session.setActiveOrganization(organizationId),
2333
+ suggestions: () => session.authorized("listMyOrganizationSuggestions", {}),
2334
+ join: (organizationId) => session.authorized("joinOrganizationSuggestion", { params: { organizationId } }),
2335
+ update: (organizationId, input) => session.authorized("updateMyOrganization", { params: { organizationId }, body: input }),
2336
+ async remove(organizationId) {
2337
+ await session.authorized("deleteMyOrganization", { params: { organizationId } });
2338
+ },
2339
+ async leave(organizationId) {
2340
+ const now = session.state();
2341
+ const user = now.status === "signed-in" ? now.user : null;
2342
+ const userId = user?.id ?? (await asUser(() => session.authorized("getMe", {}))).id;
2343
+ await session.authorized("removeMyOrganizationMember", {
2344
+ params: { organizationId, userId }
2345
+ });
2346
+ },
2347
+ roles: (organizationId) => session.authorized("listMyOrganizationRoles", { params: { organizationId } }),
2348
+ members: {
2349
+ list: (organizationId) => session.authorized("listMyOrganizationMembers", { params: { organizationId } }),
2350
+ changeRole: (organizationId, userId, role) => session.authorized("updateMyOrganizationMember", {
2351
+ params: { organizationId, userId },
2352
+ body: { role }
2353
+ }),
2354
+ async remove(organizationId, userId) {
2355
+ await session.authorized("removeMyOrganizationMember", {
2356
+ params: { organizationId, userId }
2357
+ });
2358
+ }
2359
+ },
2360
+ domains: {
2361
+ list: (id) => session.authorized("listMyOrganizationDomains", { params: { id } }),
2362
+ start: (id, input) => session.authorized("startMyOrganizationDomain", { params: { id }, body: input }),
2363
+ verify: (id, domainId, code) => session.authorized("verifyMyOrganizationDomain", {
2364
+ params: { id, domainId },
2365
+ body: { code }
2366
+ }),
2367
+ update: (id, domainId, mode) => session.authorized("updateMyOrganizationDomain", {
2368
+ params: { id, domainId },
2369
+ body: { mode }
2370
+ }),
2371
+ async remove(id, domainId) {
2372
+ await session.authorized("removeMyOrganizationDomain", { params: { id, domainId } });
2373
+ }
2374
+ },
2375
+ sso: {
2376
+ list: (organizationId) => session.authorized("listMyOrganizationSsoConnections", { params: { organizationId } }),
2377
+ get: (organizationId, id) => session.authorized("getMyOrganizationSsoConnection", { params: { organizationId, id } }),
2378
+ create: (organizationId, input) => session.authorized("createMyOrganizationSsoConnection", {
2379
+ params: { organizationId },
2380
+ body: input
2381
+ }),
2382
+ update: (organizationId, id, input) => session.authorized("updateMyOrganizationSsoConnection", {
2383
+ params: { organizationId, id },
2384
+ body: input
2385
+ }),
2386
+ refreshMetadata: (organizationId, id) => session.authorized("refreshMyOrganizationSsoMetadata", {
2387
+ params: { organizationId, id }
2388
+ }),
2389
+ async remove(organizationId, id) {
2390
+ await session.authorized("deleteMyOrganizationSsoConnection", {
2391
+ params: { organizationId, id }
2392
+ });
2393
+ },
2394
+ groupRoles: (organizationId, id) => session.authorized("getMyOrganizationSsoGroupRoles", { params: { organizationId, id } }),
2395
+ setGroupRoles: (organizationId, id, mapping) => session.authorized("replaceMyOrganizationSsoGroupRoles", {
2396
+ params: { organizationId, id },
2397
+ body: mapping
2398
+ }),
2399
+ startTest: (organizationId, id, redirectUrl) => session.authorized("startMyOrganizationSsoConnectionTest", {
2400
+ params: { organizationId, id },
2401
+ body: { redirectUrl }
2402
+ }),
2403
+ testResult: (organizationId, id, testId) => session.authorized("getMyOrganizationSsoConnectionTest", {
2404
+ params: { organizationId, id, testId }
2405
+ }),
2406
+ setupLinks: (organizationId) => session.authorized("listMyOrganizationSetupLinks", { params: { id: organizationId } }),
2407
+ createSetupLink: (organizationId, input = {}) => session.authorized("createMyOrganizationSetupLink", {
2408
+ params: { id: organizationId },
2409
+ body: input
2410
+ }),
2411
+ async revokeSetupLink(organizationId, linkId) {
2412
+ await session.authorized("revokeMyOrganizationSetupLink", {
2413
+ params: { id: organizationId, linkId }
2414
+ });
2415
+ }
2416
+ },
2417
+ invitations: {
2418
+ list: (organizationId) => session.authorized("listMyOrganizationInvitations", { params: { organizationId } }),
2419
+ invite: (organizationId, input) => session.authorized("inviteToMyOrganization", { params: { organizationId }, body: input }),
2420
+ async revoke(organizationId, invitationId) {
2421
+ await session.authorized("revokeMyOrganizationInvitation", {
2422
+ params: { organizationId, invitationId }
2423
+ });
2424
+ },
2425
+ preview: (token) => transport.call("previewOrganizationInvitation", { body: { token } }),
2426
+ accept: (token) => session.authorized("acceptOrganizationInvitation", { body: { token } }),
2427
+ async decline(token) {
2428
+ await session.authorized("declineOrganizationInvitation", { body: { token } });
2429
+ },
2430
+ mine: () => session.authorized("listMyPendingOrganizationInvitations", {}),
2431
+ acceptMine: (invitationId) => session.authorized("acceptMyOrganizationInvitation", { params: { invitationId } }),
2432
+ async declineMine(invitationId) {
2433
+ await session.authorized("declineMyOrganizationInvitation", { params: { invitationId } });
2434
+ }
2435
+ }
2436
+ },
2169
2437
  user: {
2170
2438
  get: () => asUser(() => session.authorized("getMe", {})),
2171
2439
  email: {
@@ -2266,6 +2534,22 @@ function createClient(options, environment) {
2266
2534
  function createTulaClient(options) {
2267
2535
  return createClient(options, runtimeEnvironment());
2268
2536
  }
2537
+ // src/invitation.ts
2538
+ import { ORGANIZATION_INVITATION_TOKEN_PARAM } from "@tula-tech/contract/headers";
2539
+ function readInvitationLink(url) {
2540
+ const at = url.indexOf("#");
2541
+ if (at === -1) {
2542
+ return null;
2543
+ }
2544
+ const fragment = new URLSearchParams(url.slice(at + 1));
2545
+ const token = fragment.get(ORGANIZATION_INVITATION_TOKEN_PARAM);
2546
+ if (!token) {
2547
+ return null;
2548
+ }
2549
+ fragment.delete(ORGANIZATION_INVITATION_TOKEN_PARAM);
2550
+ const rest = fragment.toString();
2551
+ return { token, cleanUrl: `${url.slice(0, at)}${rest ? `#${rest}` : ""}` };
2552
+ }
2269
2553
  export {
2270
2554
  ACCESS_TOKEN_EXPIRY_SKEW_MS,
2271
2555
  DEFAULT_TIMEOUT_MS,
@@ -2285,6 +2569,7 @@ export {
2285
2569
  isStepUpRequired,
2286
2570
  isTulaError,
2287
2571
  memoryStorage,
2572
+ readInvitationLink,
2288
2573
  runtimeEnvironment,
2289
2574
  stepUpMethods
2290
2575
  };