@superbuilders/primer-tives 4.0.5 → 4.1.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 (62) hide show
  1. package/README.md +12 -4
  2. package/dist/client/auth/access-token.d.ts +1 -1
  3. package/dist/client/auth/access-token.d.ts.map +1 -1
  4. package/dist/client/auth/hosted-popup.d.ts +1 -1
  5. package/dist/client/auth/hosted-popup.d.ts.map +1 -1
  6. package/dist/client/auth/provider.d.ts +2 -2
  7. package/dist/client/auth/provider.d.ts.map +1 -1
  8. package/dist/client/auth-state.d.ts +2 -2
  9. package/dist/client/auth-state.d.ts.map +1 -1
  10. package/dist/client/choice-state.d.ts +2 -2
  11. package/dist/client/choice-state.d.ts.map +1 -1
  12. package/dist/client/extended-text-state.d.ts +2 -2
  13. package/dist/client/extended-text-state.d.ts.map +1 -1
  14. package/dist/client/feedback-state.d.ts +4 -4
  15. package/dist/client/feedback-state.d.ts.map +1 -1
  16. package/dist/client/index.d.ts +2 -2
  17. package/dist/client/index.d.ts.map +1 -1
  18. package/dist/client/index.js +828 -812
  19. package/dist/client/index.js.map +24 -24
  20. package/dist/client/match-state.d.ts +2 -2
  21. package/dist/client/match-state.d.ts.map +1 -1
  22. package/dist/client/observation-state.d.ts +1 -1
  23. package/dist/client/observation-state.d.ts.map +1 -1
  24. package/dist/client/order-state.d.ts +2 -2
  25. package/dist/client/order-state.d.ts.map +1 -1
  26. package/dist/client/pci-state.d.ts +2 -2
  27. package/dist/client/pci-state.d.ts.map +1 -1
  28. package/dist/client/session-context.d.ts +2 -2
  29. package/dist/client/session-context.d.ts.map +1 -1
  30. package/dist/client/session.d.ts +4 -4
  31. package/dist/client/session.d.ts.map +1 -1
  32. package/dist/client/start.d.ts +3 -3
  33. package/dist/client/start.d.ts.map +1 -1
  34. package/dist/client/text-entry-state.d.ts +2 -2
  35. package/dist/client/text-entry-state.d.ts.map +1 -1
  36. package/dist/client/transport.d.ts +7 -6
  37. package/dist/client/transport.d.ts.map +1 -1
  38. package/dist/client/types.d.ts +8 -2
  39. package/dist/client/types.d.ts.map +1 -1
  40. package/dist/contracts/index.d.ts +4 -4
  41. package/dist/contracts/index.d.ts.map +1 -1
  42. package/dist/contracts/index.js +12 -12
  43. package/dist/contracts/index.js.map +6 -6
  44. package/dist/contracts/pci-schemas.d.ts +4 -0
  45. package/dist/contracts/pci-schemas.d.ts.map +1 -1
  46. package/dist/contracts/pci.d.ts +1 -1
  47. package/dist/contracts/pci.d.ts.map +1 -1
  48. package/dist/contracts/review.d.ts +1 -1
  49. package/dist/contracts/review.d.ts.map +1 -1
  50. package/dist/contracts/validation.d.ts +24 -2
  51. package/dist/contracts/validation.d.ts.map +1 -1
  52. package/dist/grade-level.d.ts +1 -1
  53. package/dist/grade-level.d.ts.map +1 -1
  54. package/dist/grade-level.js.map +1 -1
  55. package/dist/subject-pcis.d.ts +1 -1
  56. package/dist/subject-pcis.d.ts.map +1 -1
  57. package/dist/subject-pcis.js.map +2 -2
  58. package/dist/subject.d.ts +1 -1
  59. package/dist/subject.d.ts.map +1 -1
  60. package/dist/subject.js.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/package.json +1 -1
@@ -6461,14 +6461,14 @@ var FractionInputSubmissionDraft07Schema = {
6461
6461
  type: "object",
6462
6462
  additionalProperties: false,
6463
6463
  required: ["form", "whole"],
6464
- properties: { form: { const: "whole" }, whole: { type: "string" } }
6464
+ properties: { form: { type: "string", const: "whole" }, whole: { type: "string" } }
6465
6465
  },
6466
6466
  {
6467
6467
  type: "object",
6468
6468
  additionalProperties: false,
6469
6469
  required: ["form", "numerator", "denominator"],
6470
6470
  properties: {
6471
- form: { const: "proper" },
6471
+ form: { type: "string", const: "proper" },
6472
6472
  numerator: { type: "string" },
6473
6473
  denominator: { type: "string" }
6474
6474
  }
@@ -6478,7 +6478,7 @@ var FractionInputSubmissionDraft07Schema = {
6478
6478
  additionalProperties: false,
6479
6479
  required: ["form", "numerator", "denominator"],
6480
6480
  properties: {
6481
- form: { const: "improper" },
6481
+ form: { type: "string", const: "improper" },
6482
6482
  numerator: { type: "string" },
6483
6483
  denominator: { type: "string" }
6484
6484
  }
@@ -6488,7 +6488,7 @@ var FractionInputSubmissionDraft07Schema = {
6488
6488
  additionalProperties: false,
6489
6489
  required: ["form", "whole", "numerator", "denominator"],
6490
6490
  properties: {
6491
- form: { const: "mixed" },
6491
+ form: { type: "string", const: "mixed" },
6492
6492
  whole: { type: "string" },
6493
6493
  numerator: { type: "string" },
6494
6494
  denominator: { type: "string" }
@@ -6510,7 +6510,7 @@ var ChoiceSubmissionSchema = {
6510
6510
  additionalProperties: false,
6511
6511
  required: ["type", "selectedKeys"],
6512
6512
  properties: {
6513
- type: { const: "choice" },
6513
+ type: { type: "string", const: "choice" },
6514
6514
  selectedKeys: { type: "array", items: { type: "string" } }
6515
6515
  }
6516
6516
  };
@@ -6518,14 +6518,14 @@ var TextEntrySubmissionSchema = {
6518
6518
  type: "object",
6519
6519
  additionalProperties: false,
6520
6520
  required: ["type", "value"],
6521
- properties: { type: { const: "text-entry" }, value: { type: "string" } }
6521
+ properties: { type: { type: "string", const: "text-entry" }, value: { type: "string" } }
6522
6522
  };
6523
6523
  var ExtendedTextSubmissionSchema = {
6524
6524
  type: "object",
6525
6525
  additionalProperties: false,
6526
6526
  required: ["type", "values"],
6527
6527
  properties: {
6528
- type: { const: "extended-text" },
6528
+ type: { type: "string", const: "extended-text" },
6529
6529
  values: { type: "array", minItems: 1, items: { type: "string" } }
6530
6530
  }
6531
6531
  };
@@ -6534,7 +6534,7 @@ var OrderSubmissionSchema = {
6534
6534
  additionalProperties: false,
6535
6535
  required: ["type", "orderedKeys"],
6536
6536
  properties: {
6537
- type: { const: "order" },
6537
+ type: { type: "string", const: "order" },
6538
6538
  orderedKeys: { type: "array", items: { type: "string" } }
6539
6539
  }
6540
6540
  };
@@ -6543,7 +6543,7 @@ var MatchSubmissionSchema = {
6543
6543
  additionalProperties: false,
6544
6544
  required: ["type", "pairs"],
6545
6545
  properties: {
6546
- type: { const: "match" },
6546
+ type: { type: "string", const: "match" },
6547
6547
  pairs: { type: "array", items: MatchPairSchema }
6548
6548
  }
6549
6549
  };
@@ -6552,8 +6552,8 @@ var FractionInputPciSubmissionSchema = {
6552
6552
  additionalProperties: false,
6553
6553
  required: ["type", "pciId", "value"],
6554
6554
  properties: {
6555
- type: { const: "portable-custom" },
6556
- pciId: { const: "urn:primer:pci:fraction-input" },
6555
+ type: { type: "string", const: "portable-custom" },
6556
+ pciId: { type: "string", const: "urn:primer:pci:fraction-input" },
6557
6557
  value: FractionInputSubmissionDraft07Schema
6558
6558
  }
6559
6559
  };
@@ -6779,303 +6779,521 @@ function validateSubmissionForInteraction(interaction, submission) {
6779
6779
  function submissionValidationMessage(result) {
6780
6780
  return result.issues.join("; ");
6781
6781
  }
6782
- // src/client/transport.ts
6783
- import * as errors3 from "@superbuilders/errors";
6784
-
6785
- // src/version.ts
6786
- var SDK_VERSION = "4.0.5";
6787
- var NPM_PACKAGE_URL = "https://www.npmjs.com/package/@superbuilders/primer-tives";
6782
+ // src/client/auth/provider.ts
6783
+ import * as errors4 from "@superbuilders/errors";
6788
6784
 
6789
- // src/client/transport.ts
6790
- var ADVANCE_PATH = "/api/v0/advance";
6791
- function readStringField(value, key) {
6792
- if (!(key in value)) {
6793
- return;
6794
- }
6795
- const v = Reflect.get(value, key);
6796
- if (typeof v !== "string") {
6797
- return;
6785
+ // src/client/auth/access-token.ts
6786
+ import * as errors3 from "@superbuilders/errors";
6787
+ var ACCESS_TOKEN_PREFIX = "eyJ";
6788
+ var TOKEN_EXPIRY_SKEW_MS = 60000;
6789
+ var resolvedAccessTokenBrand = Symbol("primer resolved access token");
6790
+ function isMalformedJws(token) {
6791
+ if (!token.startsWith(ACCESS_TOKEN_PREFIX)) {
6792
+ return true;
6798
6793
  }
6799
- return v;
6794
+ const dotCount = token.split(".").length - 1;
6795
+ return dotCount !== 2;
6800
6796
  }
6801
- function parseAdvanceErrorBody(body) {
6802
- if (body.length === 0) {
6803
- return null;
6797
+ function paddedBase64(base64Url) {
6798
+ const normalized = base64Url.replaceAll("-", "+").replaceAll("_", "/");
6799
+ const remainder = normalized.length % 4;
6800
+ if (remainder === 0) {
6801
+ return normalized;
6804
6802
  }
6805
- const parsed = errors3.trySync(function parseJson() {
6806
- return JSON.parse(body);
6803
+ if (remainder === 2) {
6804
+ return `${normalized}==`;
6805
+ }
6806
+ if (remainder === 3) {
6807
+ return `${normalized}=`;
6808
+ }
6809
+ throw errors3.wrap(ErrMalformedAccessToken, "payload base64");
6810
+ }
6811
+ function decodeBase64UrlJson(segment) {
6812
+ const decoded = errors3.trySync(function decodePayload() {
6813
+ const json = globalThis.atob(paddedBase64(segment));
6814
+ return JSON.parse(json);
6807
6815
  });
6808
- if (parsed.error) {
6816
+ if (decoded.error) {
6817
+ throw errors3.wrap(ErrMalformedAccessToken, "payload decode");
6818
+ }
6819
+ return decoded.data;
6820
+ }
6821
+ function numericClaim(payload, claim) {
6822
+ if (typeof payload !== "object" || payload === null) {
6809
6823
  return null;
6810
6824
  }
6811
- const raw = parsed.data;
6812
- if (typeof raw !== "object" || raw === null) {
6825
+ const value = Reflect.get(payload, claim);
6826
+ if (typeof value !== "number" || !Number.isFinite(value)) {
6813
6827
  return null;
6814
6828
  }
6815
- const result = {};
6816
- const error = readStringField(raw, "error");
6817
- if (error !== undefined) {
6818
- result.error = error;
6829
+ return value;
6830
+ }
6831
+ function validateAccessTokenTimestamp(token, logger) {
6832
+ const segments = token.split(".");
6833
+ const payloadSegment = segments[1];
6834
+ if (payloadSegment === undefined || payloadSegment.length === 0) {
6835
+ logger.error("access token payload missing");
6836
+ throw errors3.wrap(ErrMalformedAccessToken, "payload missing");
6819
6837
  }
6820
- const detail = readStringField(raw, "detail");
6821
- if (detail !== undefined) {
6822
- result.detail = detail;
6838
+ const payload = decodeBase64UrlJson(payloadSegment);
6839
+ const exp = numericClaim(payload, "exp");
6840
+ if (exp === null) {
6841
+ logger.error("access token exp missing");
6842
+ throw errors3.wrap(ErrMalformedAccessToken, "exp missing");
6823
6843
  }
6824
- const minimumSdkVersion = readStringField(raw, "minimumSdkVersion");
6825
- if (minimumSdkVersion !== undefined) {
6826
- result.minimumSdkVersion = minimumSdkVersion;
6844
+ const expiresAtMs = exp * 1000;
6845
+ if (expiresAtMs <= Date.now() + TOKEN_EXPIRY_SKEW_MS) {
6846
+ logger.error("access token expired");
6847
+ throw ErrTokenExpired;
6827
6848
  }
6828
- const receivedSdkVersion = readStringField(raw, "receivedSdkVersion");
6829
- if (receivedSdkVersion !== undefined) {
6830
- result.receivedSdkVersion = receivedSdkVersion;
6849
+ }
6850
+ function resolveAccessToken(token, logger) {
6851
+ if (isMalformedJws(token)) {
6852
+ logger.error({ prefix: ACCESS_TOKEN_PREFIX }, "malformed access token");
6853
+ throw errors3.wrap(ErrMalformedAccessToken, `token must start with '${ACCESS_TOKEN_PREFIX}' and contain two dots`);
6831
6854
  }
6832
- const upgradeUrl = readStringField(raw, "upgradeUrl");
6833
- if (upgradeUrl !== undefined) {
6834
- result.upgradeUrl = upgradeUrl;
6855
+ validateAccessTokenTimestamp(token, logger);
6856
+ return { value: token, [resolvedAccessTokenBrand]: true };
6857
+ }
6858
+
6859
+ // src/client/auth/browser.ts
6860
+ var POPUP_TARGET = "primer-auth";
6861
+ var POPUP_FEATURES = "popup,width=480,height=720";
6862
+ function currentUrl(logger) {
6863
+ if (typeof globalThis.location === "undefined") {
6864
+ logger.error("auth location unavailable");
6865
+ throw ErrAuthUnavailable;
6866
+ }
6867
+ return new URL(globalThis.location.href);
6868
+ }
6869
+ function redirectUri(url) {
6870
+ return `${url.origin}${url.pathname}${url.search}`;
6871
+ }
6872
+ function randomClientState(logger) {
6873
+ if (typeof globalThis.crypto === "undefined") {
6874
+ logger.error("auth crypto unavailable");
6875
+ throw ErrAuthUnavailable;
6876
+ }
6877
+ const bytes = new Uint8Array(24);
6878
+ globalThis.crypto.getRandomValues(bytes);
6879
+ let result = "";
6880
+ for (const byte of bytes) {
6881
+ result += byte.toString(16).padStart(2, "0");
6835
6882
  }
6836
6883
  return result;
6837
6884
  }
6838
- function httpSentinel(status, body) {
6839
- if (status === 400) {
6840
- const parsed = parseAdvanceErrorBody(body);
6841
- if (parsed?.error === "sdk_upgrade_required") {
6842
- return ErrSdkUpgradeRequired;
6843
- }
6844
- return ErrBadRequest;
6885
+ function openAuthPopup(url, logger) {
6886
+ if (typeof globalThis.open === "undefined") {
6887
+ logger.error("auth popup api unavailable");
6888
+ throw ErrAuthUnavailable;
6845
6889
  }
6846
- if (status === 401) {
6847
- const parsed = parseAdvanceErrorBody(body);
6848
- if (parsed?.detail === "token_expired") {
6849
- return ErrTokenExpired;
6850
- }
6851
- return ErrInvalidAccessToken;
6890
+ const popup = globalThis.open(url, POPUP_TARGET, POPUP_FEATURES);
6891
+ if (popup === null) {
6892
+ logger.error("auth popup blocked");
6893
+ throw ErrAuthPopupBlocked;
6852
6894
  }
6853
- if (status === 403) {
6854
- return ErrForbidden;
6895
+ return popup;
6896
+ }
6897
+
6898
+ // src/client/auth/hosted-popup.ts
6899
+ var DEFAULT_POPUP_TIMEOUT_MS = 10 * 60 * 1000;
6900
+ var POPUP_POLL_MS = 250;
6901
+ var AUTH_MESSAGE_TYPE = "primer-tives.auth.result.v1";
6902
+ function hostedAuthUrl(config) {
6903
+ const logger = config.logger;
6904
+ if (!URL.canParse(config.origin)) {
6905
+ logger.error({ origin: config.origin }, "hosted auth origin invalid");
6906
+ throw ErrAuthConfigInvalid;
6855
6907
  }
6856
- if (status === 404) {
6857
- return ErrNotFound;
6908
+ const authUrl = new URL("/api/auth/timeback/start", config.origin);
6909
+ authUrl.searchParams.set("publishableKey", config.publishableKey);
6910
+ authUrl.searchParams.set("redirectUri", redirectUri(config.currentUrl));
6911
+ authUrl.searchParams.set("state", config.clientState);
6912
+ return authUrl.toString();
6913
+ }
6914
+ function isRecord(value) {
6915
+ return typeof value === "object" && value !== null;
6916
+ }
6917
+ function stringField(value, key) {
6918
+ const field = value[key];
6919
+ if (typeof field !== "string" || field.length === 0) {
6920
+ return null;
6858
6921
  }
6859
- if (status === 409) {
6860
- return ErrConflict;
6922
+ return field;
6923
+ }
6924
+ function readPopupMessage(event, popup, config, expectedOrigin) {
6925
+ if (event.source !== popup) {
6926
+ return { kind: "ignore" };
6861
6927
  }
6862
- if (status === 429) {
6863
- return ErrRateLimited;
6928
+ if (event.origin !== expectedOrigin) {
6929
+ return { kind: "ignore" };
6864
6930
  }
6865
- if (status === 502 || status === 503 || status === 504) {
6866
- return ErrServiceUnavailable;
6931
+ const data = event.data;
6932
+ if (!isRecord(data)) {
6933
+ return { kind: "ignore" };
6867
6934
  }
6868
- return ErrServerError;
6869
- }
6870
- function buildSdkUpgradeRequiredError(sentinel, text, logger) {
6871
- const parsed = parseAdvanceErrorBody(text);
6872
- if (parsed === null) {
6873
- logger.error({
6874
- receivedSdkVersion: null,
6875
- minimumSdkVersion: "<unknown>",
6876
- upgradeUrl: NPM_PACKAGE_URL
6877
- }, "sdk upgrade required");
6878
- const message2 = `<missing> < <unknown>; bump @superbuilders/primer-tives at ${NPM_PACKAGE_URL}`;
6879
- return errors3.wrap(sentinel, message2);
6935
+ const messageType = stringField(data, "type");
6936
+ if (messageType !== AUTH_MESSAGE_TYPE) {
6937
+ return { kind: "ignore" };
6880
6938
  }
6881
- const minimumSdkVersion = parsed.minimumSdkVersion === undefined ? "<unknown>" : parsed.minimumSdkVersion;
6882
- const receivedSdkVersion = parsed.receivedSdkVersion === undefined ? null : parsed.receivedSdkVersion;
6883
- const receivedDisplay = receivedSdkVersion === null ? "<missing>" : receivedSdkVersion;
6884
- const upgradeUrl = parsed.upgradeUrl === undefined ? NPM_PACKAGE_URL : parsed.upgradeUrl;
6885
- logger.error({
6886
- receivedSdkVersion,
6887
- minimumSdkVersion,
6888
- upgradeUrl
6889
- }, "sdk upgrade required");
6890
- const message = `${receivedDisplay} < ${minimumSdkVersion}; bump @superbuilders/primer-tives at ${upgradeUrl}`;
6891
- return errors3.wrap(sentinel, message);
6892
- }
6893
- function isAbortError(err) {
6894
- if (err instanceof DOMException && err.name === "AbortError") {
6895
- return true;
6939
+ const state = stringField(data, "state");
6940
+ if (state === null) {
6941
+ return { kind: "error", error: ErrAuthCallbackInvalid };
6896
6942
  }
6897
- if (err instanceof DOMException && err.name === "TimeoutError") {
6898
- return true;
6943
+ if (state !== config.clientState) {
6944
+ return { kind: "error", error: ErrAuthStateMismatch };
6899
6945
  }
6900
- return false;
6901
- }
6902
- function createTransport(tc) {
6903
- const fetchFn = tc.fetch ? tc.fetch : globalThis.fetch;
6904
- const logger = tc.logger;
6905
- const advanceUrl = new URL(ADVANCE_PATH, tc.origin).toString();
6906
- function transportSignal() {
6907
- if (tc.abort) {
6908
- return tc.abort.signal;
6909
- }
6910
- return;
6946
+ const status = stringField(data, "status");
6947
+ if (status === "error") {
6948
+ return { kind: "error", error: ErrAuthCallbackInvalid };
6911
6949
  }
6912
- async function transport(body) {
6913
- logger.debug({
6914
- intentKind: body.intent.kind,
6915
- subject: body.subject
6916
- }, "transport request");
6917
- const fetchResult = await fetchFn(advanceUrl, {
6918
- method: "POST",
6919
- mode: "cors",
6920
- credentials: "omit",
6921
- headers: {
6922
- "Content-Type": "application/json",
6923
- Authorization: `Bearer ${tc.accessToken.value}`,
6924
- "X-Primer-Publishable-Key": tc.publishableKey,
6925
- "X-Primer-SDK-Version": SDK_VERSION
6926
- },
6927
- body: JSON.stringify(body),
6928
- signal: transportSignal()
6929
- }).then(function ok(r) {
6930
- return { ok: true, response: r };
6931
- }, function fail(err) {
6932
- return { ok: false, error: err };
6933
- });
6934
- if (!fetchResult.ok) {
6935
- if (isAbortError(fetchResult.error)) {
6936
- logger.error({
6937
- intentKind: body.intent.kind
6938
- }, "transport timeout");
6939
- return { ok: false, error: errors3.wrap(ErrTimeout, fetchResult.error.message) };
6950
+ if (status !== "success") {
6951
+ return { kind: "error", error: ErrAuthCallbackInvalid };
6952
+ }
6953
+ const accessToken = stringField(data, "accessToken");
6954
+ if (accessToken === null) {
6955
+ return { kind: "error", error: ErrAuthCallbackInvalid };
6956
+ }
6957
+ return { kind: "success", accessToken };
6958
+ }
6959
+ async function waitForPopupMessage(popup, config, expectedOrigin) {
6960
+ let __stack = [];
6961
+ try {
6962
+ const logger = config.logger;
6963
+ const stack = __using(__stack, new AsyncDisposableStack, 1);
6964
+ stack.defer(function closePopup() {
6965
+ popup.close();
6966
+ });
6967
+ const result = await new Promise(function waitForMessage(resolve, reject) {
6968
+ let settled = false;
6969
+ function finishWithError(error) {
6970
+ if (settled) {
6971
+ return;
6972
+ }
6973
+ settled = true;
6974
+ reject(error);
6940
6975
  }
6941
- logger.error({
6942
- error: fetchResult.error
6943
- }, "transport network error");
6944
- return { ok: false, error: errors3.wrap(ErrNetwork, fetchResult.error.message) };
6945
- }
6946
- const res = fetchResult.response;
6947
- if (!res.ok) {
6948
- const text = await res.text().catch(function fallback() {
6949
- return "";
6976
+ function finishWithToken(token) {
6977
+ if (settled) {
6978
+ return;
6979
+ }
6980
+ settled = true;
6981
+ logger.debug("hosted auth popup completed");
6982
+ resolve(token);
6983
+ }
6984
+ const timeoutId = globalThis.setTimeout(function timeout() {
6985
+ logger.error("hosted auth popup timed out");
6986
+ finishWithError(ErrAuthCancelled);
6987
+ }, DEFAULT_POPUP_TIMEOUT_MS);
6988
+ stack.defer(function clearPopupTimeout() {
6989
+ globalThis.clearTimeout(timeoutId);
6950
6990
  });
6951
- const sentinel = httpSentinel(res.status, text);
6952
- if (errors3.is(sentinel, ErrSdkUpgradeRequired)) {
6953
- return { ok: false, error: buildSdkUpgradeRequiredError(sentinel, text, logger) };
6991
+ const closedPollId = globalThis.setInterval(function checkClosed() {
6992
+ if (!popup.closed) {
6993
+ return;
6994
+ }
6995
+ logger.error("hosted auth popup closed");
6996
+ finishWithError(ErrAuthCancelled);
6997
+ }, POPUP_POLL_MS);
6998
+ stack.defer(function clearClosedPoll() {
6999
+ globalThis.clearInterval(closedPollId);
7000
+ });
7001
+ function handleMessage(event) {
7002
+ const result2 = readPopupMessage(event, popup, config, expectedOrigin);
7003
+ if (result2.kind === "ignore") {
7004
+ return;
7005
+ }
7006
+ if (result2.kind === "error") {
7007
+ logger.error({ error: result2.error }, "hosted auth popup failed");
7008
+ finishWithError(result2.error);
7009
+ return;
7010
+ }
7011
+ finishWithToken(result2.accessToken);
6954
7012
  }
6955
- logger.error({
6956
- status: res.status,
6957
- body: text,
6958
- intentKind: body.intent.kind,
6959
- subject: body.subject
6960
- }, "transport http error");
6961
- return { ok: false, error: errors3.wrap(sentinel, text) };
6962
- }
6963
- const jsonResult = await res.json().then(function ok(data) {
6964
- return { ok: true, data };
6965
- }, function fail(err) {
6966
- return { ok: false, error: err };
7013
+ globalThis.addEventListener("message", handleMessage);
7014
+ stack.defer(function removeMessageListener() {
7015
+ globalThis.removeEventListener("message", handleMessage);
7016
+ });
6967
7017
  });
6968
- if (!jsonResult.ok) {
6969
- logger.error({
6970
- error: jsonResult.error
6971
- }, "transport json parse failed");
6972
- return { ok: false, error: errors3.wrap(ErrJsonParse, jsonResult.error.message) };
6973
- }
6974
- logger.debug({
6975
- intentKind: body.intent.kind
6976
- }, "transport success");
6977
- return { ok: true, data: jsonResult.data };
7018
+ return result;
7019
+ } catch (_catch) {
7020
+ var _err = _catch, _hasErr = 1;
7021
+ } finally {
7022
+ var _promise = __callDispose(__stack, _err, _hasErr);
7023
+ _promise && await _promise;
6978
7024
  }
6979
- return transport;
7025
+ }
7026
+ async function beginHostedPopup(config) {
7027
+ const logger = config.logger;
7028
+ const url = hostedAuthUrl(config);
7029
+ if (!URL.canParse(config.origin)) {
7030
+ logger.error({ origin: config.origin }, "hosted auth origin invalid");
7031
+ throw ErrAuthConfigInvalid;
7032
+ }
7033
+ const expectedOrigin = new URL(config.origin).origin;
7034
+ const popup = openAuthPopup(url, logger);
7035
+ return waitForPopupMessage(popup, config, expectedOrigin);
6980
7036
  }
6981
7037
 
6982
- // src/client/session.ts
6983
- import * as errors10 from "@superbuilders/errors";
6984
-
6985
- // src/client/consumed.ts
6986
- function poisonToJSON() {
6987
- throw ErrNotSerializable;
7038
+ // src/client/auth/storage.ts
7039
+ var MANAGED_AUTH_ACCESS_TOKEN_KEY_PREFIX = "primer:access-token";
7040
+ function managedAuthAccessTokenStorageKey(publishableKey) {
7041
+ return `${MANAGED_AUTH_ACCESS_TOKEN_KEY_PREFIX}:${publishableKey}`;
7042
+ }
7043
+ function managedAuthStorage(logger) {
7044
+ if (typeof globalThis.sessionStorage === "undefined") {
7045
+ logger.error("managed auth storage unavailable");
7046
+ throw ErrAuthUnavailable;
7047
+ }
7048
+ return globalThis.sessionStorage;
7049
+ }
7050
+ function loadManagedAuthAccessToken(storage, publishableKey) {
7051
+ const token = storage.getItem(managedAuthAccessTokenStorageKey(publishableKey));
7052
+ if (token === null || token.length === 0) {
7053
+ return null;
7054
+ }
7055
+ return token;
7056
+ }
7057
+ function storeManagedAuthAccessToken(storage, publishableKey, accessToken) {
7058
+ storage.setItem(managedAuthAccessTokenStorageKey(publishableKey), accessToken);
7059
+ }
7060
+ function clearManagedAuthAccessToken(storage, publishableKey) {
7061
+ storage.removeItem(managedAuthAccessTokenStorageKey(publishableKey));
6988
7062
  }
6989
7063
 
6990
- // src/client/choice-state.ts
6991
- import * as errors4 from "@superbuilders/errors";
6992
- function choiceState(ctx, body, stimulus, interaction, options, maxChoices, minChoices) {
6993
- let submitPending;
6994
- let submitKey;
6995
- let timeoutPending;
6996
- function beginSubmit(selectedKeys) {
6997
- const submission = { type: "choice", selectedKeys };
6998
- const key = JSON.stringify(submission);
6999
- if (timeoutPending) {
7000
- return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7001
- }
7002
- if (submitPending) {
7003
- if (submitKey === key) {
7004
- return submitPending;
7005
- }
7006
- return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit a different choice payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7007
- }
7008
- const validation = validateSubmissionForInteraction(interaction, submission);
7009
- if (!validation.ok) {
7010
- ctx.logger.error({ selectedKeys, issues: validation.issues }, "choice submit invalid");
7011
- return Promise.resolve(ctx.errored(errors4.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7012
- }
7013
- submitKey = key;
7014
- submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
7015
- submitPending = undefined;
7016
- submitKey = undefined;
7017
- });
7018
- return submitPending;
7064
+ // src/client/auth/provider.ts
7065
+ function resolveProvidedAccessToken(token, logger) {
7066
+ const result = errors4.trySync(function resolveProvidedToken() {
7067
+ return resolveAccessToken(token, logger);
7068
+ });
7069
+ if (result.error) {
7070
+ return { kind: "fatal", error: result.error };
7019
7071
  }
7020
- function beginTimeout() {
7021
- const intent = { kind: "timeout" };
7022
- if (submitPending) {
7023
- return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7024
- }
7025
- if (timeoutPending) {
7026
- return timeoutPending;
7072
+ return { kind: "resolved", accessToken: result.data };
7073
+ }
7074
+ function resolveHostedAccessToken(token, storage, options) {
7075
+ const result = errors4.trySync(function resolveHostedToken() {
7076
+ return resolveAccessToken(token, options.logger);
7077
+ });
7078
+ if (result.error) {
7079
+ return { kind: "sign-in-failed", error: result.error };
7080
+ }
7081
+ storeManagedAuthAccessToken(storage, options.publishableKey, token);
7082
+ return {
7083
+ kind: "resolved",
7084
+ accessToken: result.data,
7085
+ clearCachedAccessToken: function clearCachedAccessToken() {
7086
+ clearManagedAuthAccessToken(storage, options.publishableKey);
7027
7087
  }
7028
- timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
7029
- timeoutPending = undefined;
7030
- });
7031
- return timeoutPending;
7088
+ };
7089
+ }
7090
+ function resolveStoredAccessToken(storage, options) {
7091
+ const stored = loadManagedAuthAccessToken(storage, options.publishableKey);
7092
+ if (stored === null) {
7093
+ return { kind: "missing" };
7094
+ }
7095
+ const result = errors4.trySync(function resolveStoredToken() {
7096
+ return resolveAccessToken(stored, options.logger);
7097
+ });
7098
+ if (result.error) {
7099
+ clearManagedAuthAccessToken(storage, options.publishableKey);
7100
+ return { kind: "missing" };
7032
7101
  }
7033
7102
  return {
7034
- phase: "interaction",
7035
- kind: "choice",
7036
- body,
7037
- stimulus,
7038
- interaction,
7039
- options,
7040
- maxChoices,
7041
- minChoices,
7042
- submitChoice: beginSubmit,
7043
- timeout: beginTimeout,
7044
- toJSON: poisonToJSON
7103
+ kind: "resolved",
7104
+ accessToken: result.data,
7105
+ clearCachedAccessToken: function clearCachedAccessToken() {
7106
+ clearManagedAuthAccessToken(storage, options.publishableKey);
7107
+ }
7045
7108
  };
7046
7109
  }
7047
-
7048
- // src/client/extended-text-state.ts
7049
- import * as errors5 from "@superbuilders/errors";
7050
- function extendedTextState(ctx, body, stimulus, interaction) {
7051
- if (interaction.cardinality === "single") {
7052
- let submitText = function(value) {
7053
- const submission = { type: "extended-text", values: [value] };
7054
- const key = JSON.stringify(submission);
7055
- if (timeoutPending2) {
7056
- return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7057
- }
7058
- if (submitPending2) {
7059
- if (submitKey2 === key) {
7060
- return submitPending2;
7061
- }
7062
- return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7063
- }
7064
- const validation = validateSubmissionForInteraction(interaction, submission);
7065
- if (!validation.ok) {
7066
- ctx.logger.error({ value, issues: validation.issues }, "extended-text submit invalid");
7067
- return Promise.resolve(ctx.errored(errors5.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7068
- }
7069
- submitKey2 = key;
7070
- submitPending2 = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
7071
- submitPending2 = undefined;
7072
- submitKey2 = undefined;
7073
- });
7074
- return submitPending2;
7110
+ function resolveExistingAccessToken(options) {
7111
+ if (options.accessToken !== undefined) {
7112
+ return resolveProvidedAccessToken(options.accessToken, options.logger);
7113
+ }
7114
+ const storageResult = errors4.trySync(function readManagedAuthStorage() {
7115
+ return managedAuthStorage(options.logger);
7116
+ });
7117
+ if (storageResult.error) {
7118
+ return { kind: "auth-unavailable", error: storageResult.error };
7119
+ }
7120
+ return resolveStoredAccessToken(storageResult.data, options);
7121
+ }
7122
+ function authFailureResult(error) {
7123
+ if (errors4.is(error, ErrAuthConfigInvalid)) {
7124
+ return { kind: "auth-config-invalid", error };
7125
+ }
7126
+ if (errors4.is(error, ErrAuthUnavailable)) {
7127
+ return { kind: "auth-unavailable", error };
7128
+ }
7129
+ return { kind: "sign-in-failed", error };
7130
+ }
7131
+ async function beginHostedLogin(options) {
7132
+ const logger = options.logger;
7133
+ const contextResult = errors4.trySync(function readContext() {
7134
+ const storage = managedAuthStorage(logger);
7135
+ const url = currentUrl(logger);
7136
+ const clientState = randomClientState(logger);
7137
+ return { storage, url, clientState };
7138
+ });
7139
+ if (contextResult.error) {
7140
+ return authFailureResult(contextResult.error);
7141
+ }
7142
+ const context = contextResult.data;
7143
+ const accessTokenResult = await errors4.try(beginHostedPopup({
7144
+ origin: options.origin,
7145
+ publishableKey: options.publishableKey,
7146
+ currentUrl: context.url,
7147
+ clientState: context.clientState,
7148
+ logger
7149
+ }));
7150
+ if (accessTokenResult.error) {
7151
+ return authFailureResult(accessTokenResult.error);
7152
+ }
7153
+ return resolveHostedAccessToken(accessTokenResult.data, context.storage, options);
7154
+ }
7155
+
7156
+ // src/client/consumed.ts
7157
+ function poisonToJSON() {
7158
+ throw ErrNotSerializable;
7159
+ }
7160
+
7161
+ // src/client/auth-state.ts
7162
+ function pendingLogin(config) {
7163
+ let pending;
7164
+ function login() {
7165
+ if (pending !== undefined) {
7166
+ return pending;
7167
+ }
7168
+ pending = config.login().finally(function clearPending() {
7169
+ pending = undefined;
7170
+ });
7171
+ return pending;
7172
+ }
7173
+ return login;
7174
+ }
7175
+ function signInRequiredState(config) {
7176
+ return {
7177
+ phase: "sign-in-required",
7178
+ login: pendingLogin(config),
7179
+ toJSON: poisonToJSON
7180
+ };
7181
+ }
7182
+ function signInFailedState(config) {
7183
+ return {
7184
+ phase: "sign-in-failed",
7185
+ error: config.error,
7186
+ login: pendingLogin(config),
7187
+ toJSON: poisonToJSON
7188
+ };
7189
+ }
7190
+ function authUnavailableState(error) {
7191
+ return {
7192
+ phase: "auth-unavailable",
7193
+ error,
7194
+ toJSON: poisonToJSON
7195
+ };
7196
+ }
7197
+ function authConfigInvalidState(error) {
7198
+ return {
7199
+ phase: "auth-config-invalid",
7200
+ error,
7201
+ toJSON: poisonToJSON
7202
+ };
7203
+ }
7204
+
7205
+ // src/client/session.ts
7206
+ import * as errors11 from "@superbuilders/errors";
7207
+
7208
+ // src/client/choice-state.ts
7209
+ import * as errors5 from "@superbuilders/errors";
7210
+ function choiceState(ctx, body, stimulus, interaction, options, maxChoices, minChoices) {
7211
+ let submitPending;
7212
+ let submitKey;
7213
+ let timeoutPending;
7214
+ function beginSubmit(selectedKeys) {
7215
+ const submission = { type: "choice", selectedKeys };
7216
+ const key = JSON.stringify(submission);
7217
+ if (timeoutPending) {
7218
+ return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7219
+ }
7220
+ if (submitPending) {
7221
+ if (submitKey === key) {
7222
+ return submitPending;
7223
+ }
7224
+ return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit a different choice payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7225
+ }
7226
+ const validation = validateSubmissionForInteraction(interaction, submission);
7227
+ if (!validation.ok) {
7228
+ ctx.logger.error({ selectedKeys, issues: validation.issues }, "choice submit invalid");
7229
+ return Promise.resolve(ctx.errored(errors5.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7230
+ }
7231
+ submitKey = key;
7232
+ submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
7233
+ submitPending = undefined;
7234
+ submitKey = undefined;
7235
+ });
7236
+ return submitPending;
7237
+ }
7238
+ function beginTimeout() {
7239
+ const intent = { kind: "timeout" };
7240
+ if (submitPending) {
7241
+ return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7242
+ }
7243
+ if (timeoutPending) {
7244
+ return timeoutPending;
7245
+ }
7246
+ timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
7247
+ timeoutPending = undefined;
7248
+ });
7249
+ return timeoutPending;
7250
+ }
7251
+ return {
7252
+ phase: "interaction",
7253
+ kind: "choice",
7254
+ body,
7255
+ stimulus,
7256
+ interaction,
7257
+ options,
7258
+ maxChoices,
7259
+ minChoices,
7260
+ submitChoice: beginSubmit,
7261
+ timeout: beginTimeout,
7262
+ toJSON: poisonToJSON
7263
+ };
7264
+ }
7265
+
7266
+ // src/client/extended-text-state.ts
7267
+ import * as errors6 from "@superbuilders/errors";
7268
+ function extendedTextState(ctx, body, stimulus, interaction) {
7269
+ if (interaction.cardinality === "single") {
7270
+ let submitText = function(value) {
7271
+ const submission = { type: "extended-text", values: [value] };
7272
+ const key = JSON.stringify(submission);
7273
+ if (timeoutPending2) {
7274
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7275
+ }
7276
+ if (submitPending2) {
7277
+ if (submitKey2 === key) {
7278
+ return submitPending2;
7279
+ }
7280
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7281
+ }
7282
+ const validation = validateSubmissionForInteraction(interaction, submission);
7283
+ if (!validation.ok) {
7284
+ ctx.logger.error({ value, issues: validation.issues }, "extended-text submit invalid");
7285
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7286
+ }
7287
+ submitKey2 = key;
7288
+ submitPending2 = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
7289
+ submitPending2 = undefined;
7290
+ submitKey2 = undefined;
7291
+ });
7292
+ return submitPending2;
7075
7293
  }, timeout2 = function() {
7076
7294
  const intent = { kind: "timeout" };
7077
7295
  if (submitPending2) {
7078
- return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7296
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7079
7297
  }
7080
7298
  if (timeoutPending2) {
7081
7299
  return timeoutPending2;
@@ -7108,18 +7326,18 @@ function extendedTextState(ctx, body, stimulus, interaction) {
7108
7326
  const submission = { type: "extended-text", values };
7109
7327
  const key = JSON.stringify(submission);
7110
7328
  if (timeoutPending) {
7111
- return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7329
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7112
7330
  }
7113
7331
  if (submitPending) {
7114
7332
  if (submitKey === key) {
7115
7333
  return submitPending;
7116
7334
  }
7117
- return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7335
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7118
7336
  }
7119
7337
  const validation = validateSubmissionForInteraction(multi, submission);
7120
7338
  if (!validation.ok) {
7121
7339
  ctx.logger.error({ values, issues: validation.issues }, "extended-text submit invalid");
7122
- return Promise.resolve(ctx.errored(errors5.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7340
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7123
7341
  }
7124
7342
  submitKey = key;
7125
7343
  submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
@@ -7131,7 +7349,7 @@ function extendedTextState(ctx, body, stimulus, interaction) {
7131
7349
  function timeout() {
7132
7350
  const intent = { kind: "timeout" };
7133
7351
  if (submitPending) {
7134
- return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7352
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7135
7353
  }
7136
7354
  if (timeoutPending) {
7137
7355
  return timeoutPending;
@@ -7157,7 +7375,7 @@ function extendedTextState(ctx, body, stimulus, interaction) {
7157
7375
  }
7158
7376
 
7159
7377
  // src/client/feedback-state.ts
7160
- function feedbackState(ctx, body, stimulus, interaction, submission, isCorrect, feedbackContent, review) {
7378
+ function feedbackState(ctx, body, stimulus, interaction, submission, assessmentOutcome, feedbackContent, review) {
7161
7379
  let pending;
7162
7380
  return {
7163
7381
  phase: "feedback",
@@ -7165,7 +7383,7 @@ function feedbackState(ctx, body, stimulus, interaction, submission, isCorrect,
7165
7383
  stimulus,
7166
7384
  interaction,
7167
7385
  submission,
7168
- isCorrect,
7386
+ assessmentOutcome,
7169
7387
  feedbackContent,
7170
7388
  review,
7171
7389
  advance: function advance() {
@@ -7180,7 +7398,7 @@ function feedbackState(ctx, body, stimulus, interaction, submission, isCorrect,
7180
7398
  }
7181
7399
 
7182
7400
  // src/client/match-state.ts
7183
- import * as errors6 from "@superbuilders/errors";
7401
+ import * as errors7 from "@superbuilders/errors";
7184
7402
  function matchState(ctx, body, stimulus, interaction) {
7185
7403
  let submitPending;
7186
7404
  let submitKey;
@@ -7189,18 +7407,18 @@ function matchState(ctx, body, stimulus, interaction) {
7189
7407
  const submission = { type: "match", pairs };
7190
7408
  const key = JSON.stringify(submission);
7191
7409
  if (timeoutPending) {
7192
- return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7410
+ return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7193
7411
  }
7194
7412
  if (submitPending) {
7195
7413
  if (submitKey === key) {
7196
7414
  return submitPending;
7197
7415
  }
7198
- return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit a different match payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7416
+ return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot submit a different match payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7199
7417
  }
7200
7418
  const validation = validateSubmissionForInteraction(interaction, submission);
7201
7419
  if (!validation.ok) {
7202
7420
  ctx.logger.error({ pairs, issues: validation.issues }, "match submit invalid");
7203
- return Promise.resolve(ctx.errored(errors6.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7421
+ return Promise.resolve(ctx.errored(errors7.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7204
7422
  }
7205
7423
  submitKey = key;
7206
7424
  submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
@@ -7212,7 +7430,7 @@ function matchState(ctx, body, stimulus, interaction) {
7212
7430
  function timeout() {
7213
7431
  const intent = { kind: "timeout" };
7214
7432
  if (submitPending) {
7215
- return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7433
+ return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7216
7434
  }
7217
7435
  if (timeoutPending) {
7218
7436
  return timeoutPending;
@@ -7257,7 +7475,7 @@ function observationState(ctx, body, stimulus) {
7257
7475
  }
7258
7476
 
7259
7477
  // src/client/order-state.ts
7260
- import * as errors7 from "@superbuilders/errors";
7478
+ import * as errors8 from "@superbuilders/errors";
7261
7479
  function orderState(ctx, body, stimulus, interaction) {
7262
7480
  let submitPending;
7263
7481
  let submitKey;
@@ -7266,18 +7484,18 @@ function orderState(ctx, body, stimulus, interaction) {
7266
7484
  const submission = { type: "order", orderedKeys };
7267
7485
  const key = JSON.stringify(submission);
7268
7486
  if (timeoutPending) {
7269
- return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7487
+ return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7270
7488
  }
7271
7489
  if (submitPending) {
7272
7490
  if (submitKey === key) {
7273
7491
  return submitPending;
7274
7492
  }
7275
- return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot submit a different order payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7493
+ return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit a different order payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7276
7494
  }
7277
7495
  const validation = validateSubmissionForInteraction(interaction, submission);
7278
7496
  if (!validation.ok) {
7279
7497
  ctx.logger.error({ orderedKeys, issues: validation.issues }, "order submit invalid");
7280
- return Promise.resolve(ctx.errored(errors7.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7498
+ return Promise.resolve(ctx.errored(errors8.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7281
7499
  }
7282
7500
  submitKey = key;
7283
7501
  submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
@@ -7289,7 +7507,7 @@ function orderState(ctx, body, stimulus, interaction) {
7289
7507
  function timeout() {
7290
7508
  const intent = { kind: "timeout" };
7291
7509
  if (submitPending) {
7292
- return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7510
+ return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7293
7511
  }
7294
7512
  if (timeoutPending) {
7295
7513
  return timeoutPending;
@@ -7315,7 +7533,7 @@ function orderState(ctx, body, stimulus, interaction) {
7315
7533
  }
7316
7534
 
7317
7535
  // src/client/pci-state.ts
7318
- import * as errors8 from "@superbuilders/errors";
7536
+ import * as errors9 from "@superbuilders/errors";
7319
7537
  function pciInteractionState(ctx, body, stimulus, interaction) {
7320
7538
  let submitPending;
7321
7539
  let submitKey;
@@ -7325,13 +7543,13 @@ function pciInteractionState(ctx, body, stimulus, interaction) {
7325
7543
  const submission = { type: "portable-custom", pciId, value };
7326
7544
  const key = JSON.stringify(submission);
7327
7545
  if (timeoutPending) {
7328
- return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7546
+ return Promise.resolve(ctx.errored(errors9.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7329
7547
  }
7330
7548
  if (submitPending) {
7331
7549
  if (submitKey === key) {
7332
7550
  return submitPending;
7333
7551
  }
7334
- return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit a different pci payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7552
+ return Promise.resolve(ctx.errored(errors9.wrap(ErrConflict, "cannot submit a different pci payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7335
7553
  }
7336
7554
  ctx.logger.debug({ pciId }, "pci submit");
7337
7555
  submitKey = key;
@@ -7344,7 +7562,7 @@ function pciInteractionState(ctx, body, stimulus, interaction) {
7344
7562
  function timeout() {
7345
7563
  const intent = { kind: "timeout" };
7346
7564
  if (submitPending) {
7347
- return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7565
+ return Promise.resolve(ctx.errored(errors9.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7348
7566
  }
7349
7567
  if (timeoutPending) {
7350
7568
  return timeoutPending;
@@ -7370,7 +7588,7 @@ function pciInteractionState(ctx, body, stimulus, interaction) {
7370
7588
  }
7371
7589
 
7372
7590
  // src/client/text-entry-state.ts
7373
- import * as errors9 from "@superbuilders/errors";
7591
+ import * as errors10 from "@superbuilders/errors";
7374
7592
  function textEntryState(ctx, body, stimulus, interaction) {
7375
7593
  let submitPending;
7376
7594
  let submitKey;
@@ -7379,18 +7597,18 @@ function textEntryState(ctx, body, stimulus, interaction) {
7379
7597
  const submission = { type: "text-entry", value };
7380
7598
  const key = JSON.stringify(submission);
7381
7599
  if (timeoutPending) {
7382
- return Promise.resolve(ctx.errored(errors9.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7600
+ return Promise.resolve(ctx.errored(errors10.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
7383
7601
  }
7384
7602
  if (submitPending) {
7385
7603
  if (submitKey === key) {
7386
7604
  return submitPending;
7387
7605
  }
7388
- return Promise.resolve(ctx.errored(errors9.wrap(ErrConflict, "cannot submit a different text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7606
+ return Promise.resolve(ctx.errored(errors10.wrap(ErrConflict, "cannot submit a different text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
7389
7607
  }
7390
7608
  const validation = validateSubmissionForInteraction(interaction, submission);
7391
7609
  if (!validation.ok) {
7392
7610
  ctx.logger.error({ value, issues: validation.issues }, "text-entry submit invalid");
7393
- return Promise.resolve(ctx.errored(errors9.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7611
+ return Promise.resolve(ctx.errored(errors10.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
7394
7612
  }
7395
7613
  submitKey = key;
7396
7614
  submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
@@ -7402,7 +7620,7 @@ function textEntryState(ctx, body, stimulus, interaction) {
7402
7620
  function timeout() {
7403
7621
  const intent = { kind: "timeout" };
7404
7622
  if (submitPending) {
7405
- return Promise.resolve(ctx.errored(errors9.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7623
+ return Promise.resolve(ctx.errored(errors10.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
7406
7624
  }
7407
7625
  if (timeoutPending) {
7408
7626
  return timeoutPending;
@@ -7436,571 +7654,369 @@ var FATAL_SENTINELS = [
7436
7654
  ];
7437
7655
  function isFatalError(err) {
7438
7656
  for (const sentinel of FATAL_SENTINELS) {
7439
- if (errors10.is(err, sentinel)) {
7657
+ if (errors11.is(err, sentinel)) {
7440
7658
  return true;
7441
7659
  }
7442
7660
  }
7443
7661
  return false;
7444
7662
  }
7445
- function isRetriableError(err) {
7446
- return !errors10.is(err, ErrInvalidSubmission);
7447
- }
7448
- function makeSession(sc) {
7449
- const logger = sc.logger;
7450
- function resolve(result) {
7451
- switch (result.outcome) {
7452
- case "advanced":
7453
- return fromAdvanced(result.frame.body, result.frame.stimulus, result.frame.interaction);
7454
- case "submitted": {
7455
- const interaction = result.frame.interaction;
7456
- if (interaction === null) {
7457
- logger.error("submitted result without interaction");
7458
- return {
7459
- phase: "fatal",
7460
- error: errors10.wrap(ErrBadRequest, "submitted result missing interaction"),
7461
- retriable: false,
7462
- toJSON: poisonToJSON
7463
- };
7464
- }
7465
- return feedbackState(ctx, result.frame.body, result.frame.stimulus, interaction, result.submission, result.isCorrect, result.feedbackContent, result.review);
7466
- }
7467
- case "completed":
7468
- return { phase: "completed", toJSON: poisonToJSON };
7469
- }
7470
- }
7471
- function errored(error, failedPhase, intent) {
7472
- let pending;
7473
- const retriable = isRetriableError(error);
7474
- if (!retriable) {
7475
- return {
7476
- phase: "errored",
7477
- error,
7478
- retriable: false,
7479
- toJSON: poisonToJSON
7480
- };
7481
- }
7482
- const state = {
7483
- phase: "errored",
7484
- error,
7485
- retriable: true,
7486
- retry: function retry() {
7487
- if (pending) {
7488
- return pending;
7489
- }
7490
- logger.debug({ failedPhase }, "retrying from errored state");
7491
- pending = execute(intent, failedPhase);
7492
- return pending;
7493
- },
7494
- toJSON: poisonToJSON
7495
- };
7496
- return state;
7497
- }
7498
- async function execute(intent, phase) {
7499
- logger.debug({
7500
- phase,
7501
- intentKind: intent.kind,
7502
- subject: sc.subject
7503
- }, "session execute");
7504
- const body = {
7505
- subject: sc.subject,
7506
- intent
7507
- };
7508
- const result = await sc.transport(body);
7509
- if (!result.ok) {
7510
- if ((errors10.is(result.error, ErrTokenExpired) || errors10.is(result.error, ErrInvalidAccessToken)) && sc.reauthenticate !== null) {
7511
- logger.warn({ phase, intentKind: intent.kind }, "session token rejected");
7512
- return sc.reauthenticate(result.error);
7513
- }
7514
- if (isFatalError(result.error)) {
7515
- logger.error({
7516
- error: result.error,
7517
- phase,
7518
- intentKind: intent.kind
7519
- }, "fatal transport error");
7520
- return {
7521
- phase: "fatal",
7522
- error: result.error,
7523
- retriable: false,
7524
- toJSON: poisonToJSON
7525
- };
7526
- }
7527
- logger.warn({
7528
- error: result.error,
7529
- phase,
7530
- intentKind: intent.kind
7531
- }, "retriable transport error");
7532
- return errored(result.error, phase, intent);
7533
- }
7534
- const next = resolve(result.data);
7535
- logger.debug({
7536
- phase,
7537
- intentKind: intent.kind,
7538
- outcome: result.data.outcome,
7539
- nextPhase: next.phase
7540
- }, "session execute resolved");
7541
- return next;
7542
- }
7543
- function isPciSupported(pciId) {
7544
- for (const id of sc.supportedPcis) {
7545
- if (id === pciId) {
7546
- return true;
7547
- }
7548
- }
7549
- return false;
7550
- }
7551
- function fromAdvanced(body, stimulus, interaction) {
7552
- if (interaction === null) {
7553
- return observationState(ctx, body, stimulus);
7554
- }
7555
- if (interaction.type === "portable-custom") {
7556
- if (!isPciSupported(interaction.pciId)) {
7557
- logger.error({ pciId: interaction.pciId }, "unsupported pci in frame");
7558
- return {
7559
- phase: "fatal",
7560
- error: errors10.wrap(ErrUnsupportedPci, `pci '${interaction.pciId}'`),
7561
- retriable: false,
7562
- toJSON: poisonToJSON
7563
- };
7564
- }
7565
- }
7566
- return pendingInteractionState(body, stimulus, interaction);
7567
- }
7568
- function pendingInteractionState(body, stimulus, interaction) {
7569
- switch (interaction.type) {
7570
- case "choice":
7571
- return choiceState(ctx, body, stimulus, interaction, interaction.options, interaction.maxChoices, interaction.minChoices);
7572
- case "text-entry":
7573
- return textEntryState(ctx, body, stimulus, interaction);
7574
- case "extended-text":
7575
- return extendedTextState(ctx, body, stimulus, interaction);
7576
- case "order":
7577
- return orderState(ctx, body, stimulus, interaction);
7578
- case "match":
7579
- return matchState(ctx, body, stimulus, interaction);
7580
- case "portable-custom":
7581
- return pciInteractionState(ctx, body, stimulus, interaction);
7582
- }
7583
- }
7584
- const ctx = { logger, execute, errored };
7585
- return { execute };
7586
- }
7587
-
7588
- // src/client/auth/provider.ts
7589
- import * as errors12 from "@superbuilders/errors";
7590
-
7591
- // src/client/auth/browser.ts
7592
- var POPUP_TARGET = "primer-auth";
7593
- var POPUP_FEATURES = "popup,width=480,height=720";
7594
- function currentUrl(logger) {
7595
- if (typeof globalThis.location === "undefined") {
7596
- logger.error("auth location unavailable");
7597
- throw ErrAuthUnavailable;
7598
- }
7599
- return new URL(globalThis.location.href);
7600
- }
7601
- function redirectUri(url) {
7602
- return `${url.origin}${url.pathname}${url.search}`;
7603
- }
7604
- function randomClientState(logger) {
7605
- if (typeof globalThis.crypto === "undefined") {
7606
- logger.error("auth crypto unavailable");
7607
- throw ErrAuthUnavailable;
7608
- }
7609
- const bytes = new Uint8Array(24);
7610
- globalThis.crypto.getRandomValues(bytes);
7611
- let result = "";
7612
- for (const byte of bytes) {
7613
- result += byte.toString(16).padStart(2, "0");
7614
- }
7615
- return result;
7616
- }
7617
- function openAuthPopup(url, logger) {
7618
- if (typeof globalThis.open === "undefined") {
7619
- logger.error("auth popup api unavailable");
7620
- throw ErrAuthUnavailable;
7621
- }
7622
- const popup = globalThis.open(url, POPUP_TARGET, POPUP_FEATURES);
7623
- if (popup === null) {
7624
- logger.error("auth popup blocked");
7625
- throw ErrAuthPopupBlocked;
7626
- }
7627
- return popup;
7628
- }
7629
-
7630
- // src/client/auth/hosted-popup.ts
7631
- var DEFAULT_POPUP_TIMEOUT_MS = 10 * 60 * 1000;
7632
- var POPUP_POLL_MS = 250;
7633
- var AUTH_MESSAGE_TYPE = "primer-tives.auth.result.v1";
7634
- function hostedAuthUrl(config) {
7635
- const logger = config.logger;
7636
- if (!URL.canParse(config.origin)) {
7637
- logger.error({ origin: config.origin }, "hosted auth origin invalid");
7638
- throw ErrAuthConfigInvalid;
7639
- }
7640
- const authUrl = new URL("/api/auth/timeback/start", config.origin);
7641
- authUrl.searchParams.set("publishableKey", config.publishableKey);
7642
- authUrl.searchParams.set("redirectUri", redirectUri(config.currentUrl));
7643
- authUrl.searchParams.set("state", config.clientState);
7644
- return authUrl.toString();
7645
- }
7646
- function isRecord(value) {
7647
- return typeof value === "object" && value !== null;
7648
- }
7649
- function stringField(value, key) {
7650
- const field = value[key];
7651
- if (typeof field !== "string" || field.length === 0) {
7652
- return null;
7653
- }
7654
- return field;
7655
- }
7656
- function readPopupMessage(event, popup, config, expectedOrigin) {
7657
- if (event.source !== popup) {
7658
- return { kind: "ignore" };
7659
- }
7660
- if (event.origin !== expectedOrigin) {
7661
- return { kind: "ignore" };
7662
- }
7663
- const data = event.data;
7664
- if (!isRecord(data)) {
7665
- return { kind: "ignore" };
7666
- }
7667
- const messageType = stringField(data, "type");
7668
- if (messageType !== AUTH_MESSAGE_TYPE) {
7669
- return { kind: "ignore" };
7670
- }
7671
- const state = stringField(data, "state");
7672
- if (state === null) {
7673
- return { kind: "error", error: ErrAuthCallbackInvalid };
7674
- }
7675
- if (state !== config.clientState) {
7676
- return { kind: "error", error: ErrAuthStateMismatch };
7677
- }
7678
- const status = stringField(data, "status");
7679
- if (status === "error") {
7680
- return { kind: "error", error: ErrAuthCallbackInvalid };
7681
- }
7682
- if (status !== "success") {
7683
- return { kind: "error", error: ErrAuthCallbackInvalid };
7684
- }
7685
- const accessToken = stringField(data, "accessToken");
7686
- if (accessToken === null) {
7687
- return { kind: "error", error: ErrAuthCallbackInvalid };
7688
- }
7689
- return { kind: "success", accessToken };
7690
- }
7691
- async function waitForPopupMessage(popup, config, expectedOrigin) {
7692
- let __stack = [];
7693
- try {
7694
- const logger = config.logger;
7695
- const stack = __using(__stack, new AsyncDisposableStack, 1);
7696
- stack.defer(function closePopup() {
7697
- popup.close();
7698
- });
7699
- const result = await new Promise(function waitForMessage(resolve, reject) {
7700
- let settled = false;
7701
- function finishWithError(error) {
7702
- if (settled) {
7703
- return;
7704
- }
7705
- settled = true;
7706
- reject(error);
7707
- }
7708
- function finishWithToken(token) {
7709
- if (settled) {
7710
- return;
7711
- }
7712
- settled = true;
7713
- logger.debug("hosted auth popup completed");
7714
- resolve(token);
7715
- }
7716
- const timeoutId = globalThis.setTimeout(function timeout() {
7717
- logger.error("hosted auth popup timed out");
7718
- finishWithError(ErrAuthCancelled);
7719
- }, DEFAULT_POPUP_TIMEOUT_MS);
7720
- stack.defer(function clearPopupTimeout() {
7721
- globalThis.clearTimeout(timeoutId);
7722
- });
7723
- const closedPollId = globalThis.setInterval(function checkClosed() {
7724
- if (!popup.closed) {
7725
- return;
7726
- }
7727
- logger.error("hosted auth popup closed");
7728
- finishWithError(ErrAuthCancelled);
7729
- }, POPUP_POLL_MS);
7730
- stack.defer(function clearClosedPoll() {
7731
- globalThis.clearInterval(closedPollId);
7732
- });
7733
- function handleMessage(event) {
7734
- const result2 = readPopupMessage(event, popup, config, expectedOrigin);
7735
- if (result2.kind === "ignore") {
7736
- return;
7663
+ function isRetriableError(err) {
7664
+ return !errors11.is(err, ErrInvalidSubmission);
7665
+ }
7666
+ function makeSession(sc) {
7667
+ const logger = sc.logger;
7668
+ function resolve(result) {
7669
+ switch (result.outcome) {
7670
+ case "advanced":
7671
+ return fromAdvanced(result.frame.body, result.frame.stimulus, result.frame.interaction);
7672
+ case "submitted": {
7673
+ const interaction = result.frame.interaction;
7674
+ if (interaction === null) {
7675
+ logger.error("submitted result without interaction");
7676
+ return {
7677
+ phase: "fatal",
7678
+ error: errors11.wrap(ErrBadRequest, "submitted result missing interaction"),
7679
+ retriable: false,
7680
+ toJSON: poisonToJSON
7681
+ };
7737
7682
  }
7738
- if (result2.kind === "error") {
7739
- logger.error({ error: result2.error }, "hosted auth popup failed");
7740
- finishWithError(result2.error);
7741
- return;
7683
+ if (result.assessmentOutcome.value === "revisionRequested") {
7684
+ return fromAdvanced(result.frame.body, result.frame.stimulus, interaction);
7742
7685
  }
7743
- finishWithToken(result2.accessToken);
7686
+ return feedbackState(ctx, result.frame.body, result.frame.stimulus, interaction, result.submission, result.assessmentOutcome, result.feedbackContent, result.review);
7744
7687
  }
7745
- globalThis.addEventListener("message", handleMessage);
7746
- stack.defer(function removeMessageListener() {
7747
- globalThis.removeEventListener("message", handleMessage);
7748
- });
7749
- });
7750
- return result;
7751
- } catch (_catch) {
7752
- var _err = _catch, _hasErr = 1;
7753
- } finally {
7754
- var _promise = __callDispose(__stack, _err, _hasErr);
7755
- _promise && await _promise;
7688
+ case "completed":
7689
+ return { phase: "completed", toJSON: poisonToJSON };
7690
+ }
7756
7691
  }
7757
- }
7758
- async function beginHostedPopup(config) {
7759
- const logger = config.logger;
7760
- const url = hostedAuthUrl(config);
7761
- if (!URL.canParse(config.origin)) {
7762
- logger.error({ origin: config.origin }, "hosted auth origin invalid");
7763
- throw ErrAuthConfigInvalid;
7692
+ function errored(error, failedPhase, intent) {
7693
+ let pending;
7694
+ const retriable = isRetriableError(error);
7695
+ if (!retriable) {
7696
+ return {
7697
+ phase: "errored",
7698
+ error,
7699
+ retriable: false,
7700
+ toJSON: poisonToJSON
7701
+ };
7702
+ }
7703
+ const state = {
7704
+ phase: "errored",
7705
+ error,
7706
+ retriable: true,
7707
+ retry: function retry() {
7708
+ if (pending) {
7709
+ return pending;
7710
+ }
7711
+ logger.debug({ failedPhase }, "retrying from errored state");
7712
+ pending = execute(intent, failedPhase);
7713
+ return pending;
7714
+ },
7715
+ toJSON: poisonToJSON
7716
+ };
7717
+ return state;
7764
7718
  }
7765
- const expectedOrigin = new URL(config.origin).origin;
7766
- const popup = openAuthPopup(url, logger);
7767
- return waitForPopupMessage(popup, config, expectedOrigin);
7768
- }
7769
-
7770
- // src/client/auth/access-token.ts
7771
- import * as errors11 from "@superbuilders/errors";
7772
- var ACCESS_TOKEN_PREFIX = "eyJ";
7773
- var TOKEN_EXPIRY_SKEW_MS = 60000;
7774
- var resolvedAccessTokenBrand = Symbol("primer resolved access token");
7775
- function isMalformedJws(token) {
7776
- if (!token.startsWith(ACCESS_TOKEN_PREFIX)) {
7777
- return true;
7719
+ async function execute(intent, phase) {
7720
+ logger.debug({
7721
+ phase,
7722
+ intentKind: intent.kind,
7723
+ subject: sc.subject
7724
+ }, "session execute");
7725
+ const body = {
7726
+ subject: sc.subject,
7727
+ intent
7728
+ };
7729
+ const result = await sc.transport(body);
7730
+ if (!result.ok) {
7731
+ if ((errors11.is(result.error, ErrTokenExpired) || errors11.is(result.error, ErrInvalidAccessToken)) && sc.reauthenticate !== null) {
7732
+ logger.warn({ phase, intentKind: intent.kind }, "session token rejected");
7733
+ return sc.reauthenticate(result.error);
7734
+ }
7735
+ if (isFatalError(result.error)) {
7736
+ logger.error({
7737
+ error: result.error,
7738
+ phase,
7739
+ intentKind: intent.kind
7740
+ }, "fatal transport error");
7741
+ return {
7742
+ phase: "fatal",
7743
+ error: result.error,
7744
+ retriable: false,
7745
+ toJSON: poisonToJSON
7746
+ };
7747
+ }
7748
+ logger.warn({
7749
+ error: result.error,
7750
+ phase,
7751
+ intentKind: intent.kind
7752
+ }, "retriable transport error");
7753
+ return errored(result.error, phase, intent);
7754
+ }
7755
+ const next = resolve(result.data);
7756
+ logger.debug({
7757
+ phase,
7758
+ intentKind: intent.kind,
7759
+ outcome: result.data.outcome,
7760
+ nextPhase: next.phase
7761
+ }, "session execute resolved");
7762
+ return next;
7778
7763
  }
7779
- const dotCount = token.split(".").length - 1;
7780
- return dotCount !== 2;
7781
- }
7782
- function paddedBase64(base64Url) {
7783
- const normalized = base64Url.replaceAll("-", "+").replaceAll("_", "/");
7784
- const remainder = normalized.length % 4;
7785
- if (remainder === 0) {
7786
- return normalized;
7764
+ function isPciSupported(pciId) {
7765
+ for (const id of sc.supportedPcis) {
7766
+ if (id === pciId) {
7767
+ return true;
7768
+ }
7769
+ }
7770
+ return false;
7787
7771
  }
7788
- if (remainder === 2) {
7789
- return `${normalized}==`;
7772
+ function fromAdvanced(body, stimulus, interaction) {
7773
+ if (interaction === null) {
7774
+ return observationState(ctx, body, stimulus);
7775
+ }
7776
+ if (interaction.type === "portable-custom") {
7777
+ if (!isPciSupported(interaction.pciId)) {
7778
+ logger.error({ pciId: interaction.pciId }, "unsupported pci in frame");
7779
+ return {
7780
+ phase: "fatal",
7781
+ error: errors11.wrap(ErrUnsupportedPci, `pci '${interaction.pciId}'`),
7782
+ retriable: false,
7783
+ toJSON: poisonToJSON
7784
+ };
7785
+ }
7786
+ }
7787
+ return pendingInteractionState(body, stimulus, interaction);
7790
7788
  }
7791
- if (remainder === 3) {
7792
- return `${normalized}=`;
7789
+ function extendedTextInteractionState(body, stimulus, interaction) {
7790
+ if (!("cardinality" in interaction)) {
7791
+ logger.error("extended-text interaction is unsupported");
7792
+ return {
7793
+ phase: "fatal",
7794
+ error: errors11.wrap(ErrBadRequest, "extended-text interaction unsupported"),
7795
+ retriable: false,
7796
+ toJSON: poisonToJSON
7797
+ };
7798
+ }
7799
+ const extendedInteraction = interaction;
7800
+ return extendedTextState(ctx, body, stimulus, extendedInteraction);
7793
7801
  }
7794
- throw errors11.wrap(ErrMalformedAccessToken, "payload base64");
7795
- }
7796
- function decodeBase64UrlJson(segment) {
7797
- const decoded = errors11.trySync(function decodePayload() {
7798
- const json = globalThis.atob(paddedBase64(segment));
7799
- return JSON.parse(json);
7800
- });
7801
- if (decoded.error) {
7802
- throw errors11.wrap(ErrMalformedAccessToken, "payload decode");
7802
+ function pendingInteractionState(body, stimulus, interaction) {
7803
+ switch (interaction.type) {
7804
+ case "choice":
7805
+ return choiceState(ctx, body, stimulus, interaction, interaction.options, interaction.maxChoices, interaction.minChoices);
7806
+ case "text-entry":
7807
+ return textEntryState(ctx, body, stimulus, interaction);
7808
+ case "extended-text":
7809
+ return extendedTextInteractionState(body, stimulus, interaction);
7810
+ case "order":
7811
+ return orderState(ctx, body, stimulus, interaction);
7812
+ case "match":
7813
+ return matchState(ctx, body, stimulus, interaction);
7814
+ case "portable-custom":
7815
+ return pciInteractionState(ctx, body, stimulus, interaction);
7816
+ }
7803
7817
  }
7804
- return decoded.data;
7818
+ const ctx = { logger, execute, errored };
7819
+ return { execute };
7805
7820
  }
7806
- function numericClaim(payload, claim) {
7807
- if (typeof payload !== "object" || payload === null) {
7808
- return null;
7821
+
7822
+ // src/client/transport.ts
7823
+ import * as errors12 from "@superbuilders/errors";
7824
+
7825
+ // src/version.ts
7826
+ var SDK_VERSION = "4.1.0";
7827
+ var NPM_PACKAGE_URL = "https://www.npmjs.com/package/@superbuilders/primer-tives";
7828
+
7829
+ // src/client/transport.ts
7830
+ var ADVANCE_PATH = "/api/v0/advance";
7831
+ function readStringField(value, key) {
7832
+ if (!(key in value)) {
7833
+ return;
7809
7834
  }
7810
- const value = Reflect.get(payload, claim);
7811
- if (typeof value !== "number" || !Number.isFinite(value)) {
7812
- return null;
7835
+ const v = Reflect.get(value, key);
7836
+ if (typeof v !== "string") {
7837
+ return;
7813
7838
  }
7814
- return value;
7839
+ return v;
7815
7840
  }
7816
- function validateAccessTokenTimestamp(token, logger) {
7817
- const segments = token.split(".");
7818
- const payloadSegment = segments[1];
7819
- if (payloadSegment === undefined || payloadSegment.length === 0) {
7820
- logger.error("access token payload missing");
7821
- throw errors11.wrap(ErrMalformedAccessToken, "payload missing");
7841
+ function parseAdvanceErrorBody(body) {
7842
+ if (body.length === 0) {
7843
+ return null;
7822
7844
  }
7823
- const payload = decodeBase64UrlJson(payloadSegment);
7824
- const exp = numericClaim(payload, "exp");
7825
- if (exp === null) {
7826
- logger.error("access token exp missing");
7827
- throw errors11.wrap(ErrMalformedAccessToken, "exp missing");
7845
+ const parsed = errors12.trySync(function parseJson() {
7846
+ return JSON.parse(body);
7847
+ });
7848
+ if (parsed.error) {
7849
+ return null;
7828
7850
  }
7829
- const expiresAtMs = exp * 1000;
7830
- if (expiresAtMs <= Date.now() + TOKEN_EXPIRY_SKEW_MS) {
7831
- logger.error("access token expired");
7832
- throw ErrTokenExpired;
7851
+ const raw = parsed.data;
7852
+ if (typeof raw !== "object" || raw === null) {
7853
+ return null;
7833
7854
  }
7834
- }
7835
- function resolveAccessToken(token, logger) {
7836
- if (isMalformedJws(token)) {
7837
- logger.error({ prefix: ACCESS_TOKEN_PREFIX }, "malformed access token");
7838
- throw errors11.wrap(ErrMalformedAccessToken, `token must start with '${ACCESS_TOKEN_PREFIX}' and contain two dots`);
7855
+ const result = {};
7856
+ const error = readStringField(raw, "error");
7857
+ if (error !== undefined) {
7858
+ result.error = error;
7839
7859
  }
7840
- validateAccessTokenTimestamp(token, logger);
7841
- return { value: token, [resolvedAccessTokenBrand]: true };
7842
- }
7843
-
7844
- // src/client/auth/storage.ts
7845
- var MANAGED_AUTH_ACCESS_TOKEN_KEY_PREFIX = "primer:access-token";
7846
- function managedAuthAccessTokenStorageKey(publishableKey) {
7847
- return `${MANAGED_AUTH_ACCESS_TOKEN_KEY_PREFIX}:${publishableKey}`;
7848
- }
7849
- function managedAuthStorage(logger) {
7850
- if (typeof globalThis.sessionStorage === "undefined") {
7851
- logger.error("managed auth storage unavailable");
7852
- throw ErrAuthUnavailable;
7860
+ const detail = readStringField(raw, "detail");
7861
+ if (detail !== undefined) {
7862
+ result.detail = detail;
7853
7863
  }
7854
- return globalThis.sessionStorage;
7855
- }
7856
- function loadManagedAuthAccessToken(storage, publishableKey) {
7857
- const token = storage.getItem(managedAuthAccessTokenStorageKey(publishableKey));
7858
- if (token === null || token.length === 0) {
7859
- return null;
7864
+ const minimumSdkVersion = readStringField(raw, "minimumSdkVersion");
7865
+ if (minimumSdkVersion !== undefined) {
7866
+ result.minimumSdkVersion = minimumSdkVersion;
7860
7867
  }
7861
- return token;
7862
- }
7863
- function storeManagedAuthAccessToken(storage, publishableKey, accessToken) {
7864
- storage.setItem(managedAuthAccessTokenStorageKey(publishableKey), accessToken);
7865
- }
7866
- function clearManagedAuthAccessToken(storage, publishableKey) {
7867
- storage.removeItem(managedAuthAccessTokenStorageKey(publishableKey));
7868
- }
7869
-
7870
- // src/client/auth/provider.ts
7871
- function resolveProvidedAccessToken(token, logger) {
7872
- const result = errors12.trySync(function resolveProvidedToken() {
7873
- return resolveAccessToken(token, logger);
7874
- });
7875
- if (result.error) {
7876
- return { kind: "fatal", error: result.error };
7868
+ const receivedSdkVersion = readStringField(raw, "receivedSdkVersion");
7869
+ if (receivedSdkVersion !== undefined) {
7870
+ result.receivedSdkVersion = receivedSdkVersion;
7877
7871
  }
7878
- return { kind: "resolved", accessToken: result.data };
7872
+ const upgradeUrl = readStringField(raw, "upgradeUrl");
7873
+ if (upgradeUrl !== undefined) {
7874
+ result.upgradeUrl = upgradeUrl;
7875
+ }
7876
+ return result;
7879
7877
  }
7880
- function resolveHostedAccessToken(token, storage, options) {
7881
- const result = errors12.trySync(function resolveHostedToken() {
7882
- return resolveAccessToken(token, options.logger);
7883
- });
7884
- if (result.error) {
7885
- return { kind: "sign-in-failed", error: result.error };
7878
+ function httpSentinel(status, body) {
7879
+ if (status === 400) {
7880
+ const parsed = parseAdvanceErrorBody(body);
7881
+ if (parsed?.error === "sdk_upgrade_required") {
7882
+ return ErrSdkUpgradeRequired;
7883
+ }
7884
+ return ErrBadRequest;
7886
7885
  }
7887
- storeManagedAuthAccessToken(storage, options.publishableKey, token);
7888
- return {
7889
- kind: "resolved",
7890
- accessToken: result.data,
7891
- clearCachedAccessToken: function clearCachedAccessToken() {
7892
- clearManagedAuthAccessToken(storage, options.publishableKey);
7886
+ if (status === 401) {
7887
+ const parsed = parseAdvanceErrorBody(body);
7888
+ if (parsed?.detail === "token_expired") {
7889
+ return ErrTokenExpired;
7893
7890
  }
7894
- };
7895
- }
7896
- function resolveStoredAccessToken(storage, options) {
7897
- const stored = loadManagedAuthAccessToken(storage, options.publishableKey);
7898
- if (stored === null) {
7899
- return { kind: "missing" };
7891
+ return ErrInvalidAccessToken;
7900
7892
  }
7901
- const result = errors12.trySync(function resolveStoredToken() {
7902
- return resolveAccessToken(stored, options.logger);
7903
- });
7904
- if (result.error) {
7905
- clearManagedAuthAccessToken(storage, options.publishableKey);
7906
- return { kind: "missing" };
7893
+ if (status === 403) {
7894
+ return ErrForbidden;
7907
7895
  }
7908
- return {
7909
- kind: "resolved",
7910
- accessToken: result.data,
7911
- clearCachedAccessToken: function clearCachedAccessToken() {
7912
- clearManagedAuthAccessToken(storage, options.publishableKey);
7913
- }
7914
- };
7915
- }
7916
- function resolveExistingAccessToken(options) {
7917
- if (options.accessToken !== undefined) {
7918
- return resolveProvidedAccessToken(options.accessToken, options.logger);
7896
+ if (status === 404) {
7897
+ return ErrNotFound;
7919
7898
  }
7920
- const storageResult = errors12.trySync(function readManagedAuthStorage() {
7921
- return managedAuthStorage(options.logger);
7922
- });
7923
- if (storageResult.error) {
7924
- return { kind: "auth-unavailable", error: storageResult.error };
7899
+ if (status === 409) {
7900
+ return ErrConflict;
7925
7901
  }
7926
- return resolveStoredAccessToken(storageResult.data, options);
7927
- }
7928
- function authFailureResult(error) {
7929
- if (errors12.is(error, ErrAuthConfigInvalid)) {
7930
- return { kind: "auth-config-invalid", error };
7902
+ if (status === 429) {
7903
+ return ErrRateLimited;
7931
7904
  }
7932
- if (errors12.is(error, ErrAuthUnavailable)) {
7933
- return { kind: "auth-unavailable", error };
7905
+ if (status === 502 || status === 503 || status === 504) {
7906
+ return ErrServiceUnavailable;
7934
7907
  }
7935
- return { kind: "sign-in-failed", error };
7908
+ return ErrServerError;
7936
7909
  }
7937
- async function beginHostedLogin(options) {
7938
- const logger = options.logger;
7939
- const contextResult = errors12.trySync(function readContext() {
7940
- const storage = managedAuthStorage(logger);
7941
- const url = currentUrl(logger);
7942
- const clientState = randomClientState(logger);
7943
- return { storage, url, clientState };
7944
- });
7945
- if (contextResult.error) {
7946
- return authFailureResult(contextResult.error);
7910
+ function buildSdkUpgradeRequiredError(sentinel, text, logger) {
7911
+ const parsed = parseAdvanceErrorBody(text);
7912
+ if (parsed === null) {
7913
+ logger.error({
7914
+ receivedSdkVersion: null,
7915
+ minimumSdkVersion: "<unknown>",
7916
+ upgradeUrl: NPM_PACKAGE_URL
7917
+ }, "sdk upgrade required");
7918
+ const message2 = `<missing> < <unknown>; bump @superbuilders/primer-tives at ${NPM_PACKAGE_URL}`;
7919
+ return errors12.wrap(sentinel, message2);
7947
7920
  }
7948
- const context = contextResult.data;
7949
- const accessTokenResult = await errors12.try(beginHostedPopup({
7950
- origin: options.origin,
7951
- publishableKey: options.publishableKey,
7952
- currentUrl: context.url,
7953
- clientState: context.clientState,
7954
- logger
7955
- }));
7956
- if (accessTokenResult.error) {
7957
- return authFailureResult(accessTokenResult.error);
7921
+ const minimumSdkVersion = parsed.minimumSdkVersion === undefined ? "<unknown>" : parsed.minimumSdkVersion;
7922
+ const receivedSdkVersion = parsed.receivedSdkVersion === undefined ? null : parsed.receivedSdkVersion;
7923
+ const receivedDisplay = receivedSdkVersion === null ? "<missing>" : receivedSdkVersion;
7924
+ const upgradeUrl = parsed.upgradeUrl === undefined ? NPM_PACKAGE_URL : parsed.upgradeUrl;
7925
+ logger.error({
7926
+ receivedSdkVersion,
7927
+ minimumSdkVersion,
7928
+ upgradeUrl
7929
+ }, "sdk upgrade required");
7930
+ const message = `${receivedDisplay} < ${minimumSdkVersion}; bump @superbuilders/primer-tives at ${upgradeUrl}`;
7931
+ return errors12.wrap(sentinel, message);
7932
+ }
7933
+ function isAbortError(err) {
7934
+ if (err instanceof DOMException && err.name === "AbortError") {
7935
+ return true;
7958
7936
  }
7959
- return resolveHostedAccessToken(accessTokenResult.data, context.storage, options);
7937
+ if (err instanceof DOMException && err.name === "TimeoutError") {
7938
+ return true;
7939
+ }
7940
+ return false;
7960
7941
  }
7961
-
7962
- // src/client/auth-state.ts
7963
- function pendingLogin(config) {
7964
- let pending;
7965
- function login() {
7966
- if (pending !== undefined) {
7967
- return pending;
7942
+ function createTransport(tc) {
7943
+ const fetchFn = tc.fetch ? tc.fetch : globalThis.fetch;
7944
+ const logger = tc.logger;
7945
+ const advanceUrl = new URL(ADVANCE_PATH, tc.origin).toString();
7946
+ function transportSignal() {
7947
+ if (tc.abort) {
7948
+ return tc.abort.signal;
7968
7949
  }
7969
- pending = config.login().finally(function clearPending() {
7970
- pending = undefined;
7950
+ return;
7951
+ }
7952
+ async function transport(body) {
7953
+ logger.debug({
7954
+ intentKind: body.intent.kind,
7955
+ subject: body.subject
7956
+ }, "transport request");
7957
+ const fetchResult = await fetchFn(advanceUrl, {
7958
+ method: "POST",
7959
+ mode: "cors",
7960
+ credentials: "omit",
7961
+ headers: {
7962
+ "Content-Type": "application/json",
7963
+ Authorization: `Bearer ${tc.accessToken.value}`,
7964
+ "X-Primer-Publishable-Key": tc.publishableKey,
7965
+ "X-Primer-SDK-Version": SDK_VERSION
7966
+ },
7967
+ body: JSON.stringify(body),
7968
+ signal: transportSignal()
7969
+ }).then(function ok(r) {
7970
+ return { ok: true, response: r };
7971
+ }, function fail(err) {
7972
+ return { ok: false, error: err };
7971
7973
  });
7972
- return pending;
7974
+ if (!fetchResult.ok) {
7975
+ if (isAbortError(fetchResult.error)) {
7976
+ logger.error({
7977
+ intentKind: body.intent.kind
7978
+ }, "transport timeout");
7979
+ return { ok: false, error: errors12.wrap(ErrTimeout, fetchResult.error.message) };
7980
+ }
7981
+ logger.error({
7982
+ error: fetchResult.error
7983
+ }, "transport network error");
7984
+ return { ok: false, error: errors12.wrap(ErrNetwork, fetchResult.error.message) };
7985
+ }
7986
+ const res = fetchResult.response;
7987
+ if (!res.ok) {
7988
+ const text = await res.text().catch(function fallback() {
7989
+ return "";
7990
+ });
7991
+ const sentinel = httpSentinel(res.status, text);
7992
+ if (errors12.is(sentinel, ErrSdkUpgradeRequired)) {
7993
+ return { ok: false, error: buildSdkUpgradeRequiredError(sentinel, text, logger) };
7994
+ }
7995
+ logger.error({
7996
+ status: res.status,
7997
+ body: text,
7998
+ intentKind: body.intent.kind,
7999
+ subject: body.subject
8000
+ }, "transport http error");
8001
+ return { ok: false, error: errors12.wrap(sentinel, text) };
8002
+ }
8003
+ const jsonResult = await res.json().then(function ok(data) {
8004
+ return { ok: true, data };
8005
+ }, function fail(err) {
8006
+ return { ok: false, error: err };
8007
+ });
8008
+ if (!jsonResult.ok) {
8009
+ logger.error({
8010
+ error: jsonResult.error
8011
+ }, "transport json parse failed");
8012
+ return { ok: false, error: errors12.wrap(ErrJsonParse, jsonResult.error.message) };
8013
+ }
8014
+ logger.debug({
8015
+ intentKind: body.intent.kind
8016
+ }, "transport success");
8017
+ return { ok: true, data: jsonResult.data };
7973
8018
  }
7974
- return login;
7975
- }
7976
- function signInRequiredState(config) {
7977
- return {
7978
- phase: "sign-in-required",
7979
- login: pendingLogin(config),
7980
- toJSON: poisonToJSON
7981
- };
7982
- }
7983
- function signInFailedState(config) {
7984
- return {
7985
- phase: "sign-in-failed",
7986
- error: config.error,
7987
- login: pendingLogin(config),
7988
- toJSON: poisonToJSON
7989
- };
7990
- }
7991
- function authUnavailableState(error) {
7992
- return {
7993
- phase: "auth-unavailable",
7994
- error,
7995
- toJSON: poisonToJSON
7996
- };
7997
- }
7998
- function authConfigInvalidState(error) {
7999
- return {
8000
- phase: "auth-config-invalid",
8001
- error,
8002
- toJSON: poisonToJSON
8003
- };
8019
+ return transport;
8004
8020
  }
8005
8021
 
8006
8022
  // src/client/start.ts
@@ -8123,4 +8139,4 @@ export {
8123
8139
  start
8124
8140
  };
8125
8141
 
8126
- //# debugId=D2887B68B961EF6364756E2164756E21
8142
+ //# debugId=8BB175F09E303E4A64756E2164756E21