@superbuilders/primer-tives 4.0.4 → 4.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ bun add @superbuilders/primer-tives
19
19
 
20
20
  ## Version
21
21
 
22
- The current SDK version is `4.0.4`.
22
+ The current SDK version is `4.0.5`.
23
23
 
24
24
  ## Entrypoints
25
25
 
@@ -210,12 +210,40 @@ The SDK uses Primer's production runtime by default.
210
210
  | Shape | Semantics |
211
211
  | --- | --- |
212
212
  | `accessToken` present | `start` validates the token shape locally and returns `AccessTokenStartState`. This mode cannot return sign-in states. |
213
- | `accessToken` absent | `start` returns managed hosted-auth state or runtime state as `ManagedStartState`. This mode does not read browser storage and does not persist tokens. |
213
+ | `accessToken` absent | `start` uses managed hosted-auth mode. It reads and writes the SDK-managed browser session token cache documented below. |
214
214
 
215
215
  The public API exposes hosted auth as state behavior. `SignInRequiredState.login()` and `SignInFailedState.login()` are the sign-in transitions. `AuthUnavailableState` and `AuthConfigInvalidState` expose no login operation.
216
216
 
217
217
  When `subject` is present, it must be a concrete literal subject at the options declaration site. Do not pass a broad `Subject` value into `PrimerOptions`; narrow it in host control flow, then construct options with `satisfies PrimerOptionsWithManagedAuth<"math", ...>`, `satisfies PrimerOptionsWithAccessToken<"math", ...>`, or the corresponding literal subject variant.
218
218
 
219
+ ## Managed Auth Token Persistence
220
+
221
+ Managed hosted auth is designed for browser-only consumers that do not have their own server-side token broker. When `accessToken` is absent, PrimerTives owns a session-scoped browser token cache with one fixed storage location:
222
+
223
+ ```txt
224
+ sessionStorage["primer:access-token:<publishableKey>"]
225
+ ```
226
+
227
+ This is intentionally zero config. There is no storage selector and no persistence flag. Managed-auth mode always uses `globalThis.sessionStorage`; if `sessionStorage` is unavailable, `start` returns `AuthUnavailableState`.
228
+
229
+ The cache stores only the final Primer access token returned by hosted Timeback sign-in. OAuth transaction state, nonce, verifier, and callback validation state are not stored in browser storage; those remain server-managed by Primer.
230
+
231
+ Managed-auth startup behavior is:
232
+
233
+ | Situation | SDK behavior |
234
+ | --- | --- |
235
+ | Valid cached token exists | `start` uses it and enters the runtime without opening sign-in. |
236
+ | No cached token exists | `start` returns `SignInRequiredState`. |
237
+ | Cached token is malformed or expired | SDK clears the key and returns `SignInRequiredState`. |
238
+ | Hosted sign-in succeeds | SDK validates the returned token, stores it at the documented key, then starts the runtime. |
239
+ | Runtime rejects a managed cached token as expired or invalid | SDK clears the key and returns `SignInFailedState` so the app can ask the learner to sign in again. |
240
+
241
+ Access-token mode bypasses this cache entirely. If `accessToken` is present in `start` options, PrimerTives does not read, write, or clear `sessionStorage`.
242
+
243
+ The cached value is a bearer credential. Browser-only consumers get reload convenience from this default, but any script that can read the page can read the token. Host applications must maintain normal XSS protections and should use access-token mode if they need to own token storage themselves.
244
+
245
+ The canonical managed sign-in endpoint is `/api/auth/timeback/start`. Legacy `/auth/timeback` URLs are not part of the PrimerTives 4.0.5 contract.
246
+
219
247
  ## Auth Semantics
220
248
 
221
249
  An access token is expected to be JWS-shaped: it starts with `eyJ` and contains exactly two dots. If a provided token does not match that shape, `start` returns `FatalState` with `ErrMalformedAccessToken`.
@@ -9,6 +9,7 @@ type AccessTokenResolverOptions = {
9
9
  type ResolvedAccessTokenResult = {
10
10
  readonly kind: "resolved";
11
11
  readonly accessToken: ResolvedAccessToken;
12
+ readonly clearCachedAccessToken?: () => void;
12
13
  };
13
14
  type MissingAccessTokenResult = {
14
15
  readonly kind: "missing";
@@ -17,7 +18,11 @@ type FatalAccessTokenResult = {
17
18
  readonly kind: "fatal";
18
19
  readonly error: Error;
19
20
  };
20
- type ExistingAccessTokenResult = ResolvedAccessTokenResult | MissingAccessTokenResult | FatalAccessTokenResult;
21
+ type AuthUnavailableResult = {
22
+ readonly kind: "auth-unavailable";
23
+ readonly error: Error;
24
+ };
25
+ type ExistingAccessTokenResult = ResolvedAccessTokenResult | MissingAccessTokenResult | AuthUnavailableResult | FatalAccessTokenResult;
21
26
  type HostedLoginResult = ResolvedAccessTokenResult | {
22
27
  readonly kind: "sign-in-failed";
23
28
  readonly error: Error;
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/client/auth/provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAGtE,OAAO,EAEN,KAAK,mBAAmB,EACxB,MAAM,sDAAsD,CAAA;AAG7D,KAAK,0BAA0B,GAAG;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;CAC7B,CAAA;AAED,KAAK,yBAAyB,GAAG;IAChC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAA;CACzC,CAAA;AAED,KAAK,wBAAwB,GAAG;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CACxB,CAAA;AAED,KAAK,sBAAsB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CACrB,CAAA;AAED,KAAK,yBAAyB,GAC3B,yBAAyB,GACzB,wBAAwB,GACxB,sBAAsB,CAAA;AAEzB,KAAK,iBAAiB,GACnB,yBAAyB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAC1D;IAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAA;AAyBlE,iBAAS,0BAA0B,CAClC,OAAO,EAAE,0BAA0B,GACjC,yBAAyB,CAK3B;AAYD,iBAAe,gBAAgB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwB/F;AAED,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,CAAA;AACvD,YAAY,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,CAAA"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/client/auth/provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAGtE,OAAO,EAEN,KAAK,mBAAmB,EACxB,MAAM,sDAAsD,CAAA;AAS7D,KAAK,0BAA0B,GAAG;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;CAC7B,CAAA;AAED,KAAK,yBAAyB,GAAG;IAChC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAA;IACzC,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAA;CAC5C,CAAA;AAED,KAAK,wBAAwB,GAAG;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CACxB,CAAA;AAED,KAAK,sBAAsB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CACrB,CAAA;AAED,KAAK,qBAAqB,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CACrB,CAAA;AAED,KAAK,yBAAyB,GAC3B,yBAAyB,GACzB,wBAAwB,GACxB,qBAAqB,GACrB,sBAAsB,CAAA;AAEzB,KAAK,iBAAiB,GACnB,yBAAyB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAC1D;IAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAA;AA4DlE,iBAAS,0BAA0B,CAClC,OAAO,EAAE,0BAA0B,GACjC,yBAAyB,CAW3B;AAYD,iBAAe,gBAAgB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAyB/F;AAED,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,CAAA;AACvD,YAAY,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,CAAA"}
@@ -0,0 +1,8 @@
1
+ import type { PrimerLogger } from "../../logger";
2
+ declare function managedAuthAccessTokenStorageKey(publishableKey: string): string;
3
+ declare function managedAuthStorage(logger: PrimerLogger): Storage;
4
+ declare function loadManagedAuthAccessToken(storage: Storage, publishableKey: string): string | null;
5
+ declare function storeManagedAuthAccessToken(storage: Storage, publishableKey: string, accessToken: string): void;
6
+ declare function clearManagedAuthAccessToken(storage: Storage, publishableKey: string): void;
7
+ export { clearManagedAuthAccessToken, loadManagedAuthAccessToken, managedAuthAccessTokenStorageKey, managedAuthStorage, storeManagedAuthAccessToken };
8
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/client/auth/storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAItE,iBAAS,gCAAgC,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,iBAAS,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAMzD;AAED,iBAAS,0BAA0B,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3F;AAED,iBAAS,2BAA2B,CACnC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,GACjB,IAAI,CAEN;AAED,iBAAS,2BAA2B,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAEnF;AAED,OAAO,EACN,2BAA2B,EAC3B,0BAA0B,EAC1B,gCAAgC,EAChC,kBAAkB,EAClB,2BAA2B,EAC3B,CAAA"}
@@ -6783,7 +6783,7 @@ function submissionValidationMessage(result) {
6783
6783
  import * as errors3 from "@superbuilders/errors";
6784
6784
 
6785
6785
  // src/version.ts
6786
- var SDK_VERSION = "4.0.4";
6786
+ var SDK_VERSION = "4.0.5";
6787
6787
  var NPM_PACKAGE_URL = "https://www.npmjs.com/package/@superbuilders/primer-tives";
6788
6788
 
6789
6789
  // src/client/transport.ts
@@ -7507,9 +7507,9 @@ function makeSession(sc) {
7507
7507
  };
7508
7508
  const result = await sc.transport(body);
7509
7509
  if (!result.ok) {
7510
- if (errors10.is(result.error, ErrTokenExpired) && sc.reauthenticate !== null) {
7511
- logger.warn({ phase, intentKind: intent.kind }, "session token expired");
7512
- return sc.reauthenticate();
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
7513
  }
7514
7514
  if (isFatalError(result.error)) {
7515
7515
  logger.error({
@@ -7841,6 +7841,32 @@ function resolveAccessToken(token, logger) {
7841
7841
  return { value: token, [resolvedAccessTokenBrand]: true };
7842
7842
  }
7843
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;
7853
+ }
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;
7860
+ }
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
+
7844
7870
  // src/client/auth/provider.ts
7845
7871
  function resolveProvidedAccessToken(token, logger) {
7846
7872
  const result = errors12.trySync(function resolveProvidedToken() {
@@ -7851,20 +7877,53 @@ function resolveProvidedAccessToken(token, logger) {
7851
7877
  }
7852
7878
  return { kind: "resolved", accessToken: result.data };
7853
7879
  }
7854
- function resolveHostedAccessToken(token, logger) {
7880
+ function resolveHostedAccessToken(token, storage, options) {
7855
7881
  const result = errors12.trySync(function resolveHostedToken() {
7856
- return resolveAccessToken(token, logger);
7882
+ return resolveAccessToken(token, options.logger);
7857
7883
  });
7858
7884
  if (result.error) {
7859
7885
  return { kind: "sign-in-failed", error: result.error };
7860
7886
  }
7861
- return { kind: "resolved", accessToken: result.data };
7887
+ storeManagedAuthAccessToken(storage, options.publishableKey, token);
7888
+ return {
7889
+ kind: "resolved",
7890
+ accessToken: result.data,
7891
+ clearCachedAccessToken: function clearCachedAccessToken() {
7892
+ clearManagedAuthAccessToken(storage, options.publishableKey);
7893
+ }
7894
+ };
7895
+ }
7896
+ function resolveStoredAccessToken(storage, options) {
7897
+ const stored = loadManagedAuthAccessToken(storage, options.publishableKey);
7898
+ if (stored === null) {
7899
+ return { kind: "missing" };
7900
+ }
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" };
7907
+ }
7908
+ return {
7909
+ kind: "resolved",
7910
+ accessToken: result.data,
7911
+ clearCachedAccessToken: function clearCachedAccessToken() {
7912
+ clearManagedAuthAccessToken(storage, options.publishableKey);
7913
+ }
7914
+ };
7862
7915
  }
7863
7916
  function resolveExistingAccessToken(options) {
7864
7917
  if (options.accessToken !== undefined) {
7865
7918
  return resolveProvidedAccessToken(options.accessToken, options.logger);
7866
7919
  }
7867
- return { kind: "missing" };
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 };
7925
+ }
7926
+ return resolveStoredAccessToken(storageResult.data, options);
7868
7927
  }
7869
7928
  function authFailureResult(error) {
7870
7929
  if (errors12.is(error, ErrAuthConfigInvalid)) {
@@ -7878,9 +7937,10 @@ function authFailureResult(error) {
7878
7937
  async function beginHostedLogin(options) {
7879
7938
  const logger = options.logger;
7880
7939
  const contextResult = errors12.trySync(function readContext() {
7940
+ const storage = managedAuthStorage(logger);
7881
7941
  const url = currentUrl(logger);
7882
7942
  const clientState = randomClientState(logger);
7883
- return { url, clientState };
7943
+ return { storage, url, clientState };
7884
7944
  });
7885
7945
  if (contextResult.error) {
7886
7946
  return authFailureResult(contextResult.error);
@@ -7896,7 +7956,7 @@ async function beginHostedLogin(options) {
7896
7956
  if (accessTokenResult.error) {
7897
7957
  return authFailureResult(accessTokenResult.error);
7898
7958
  }
7899
- return resolveHostedAccessToken(accessTokenResult.data, logger);
7959
+ return resolveHostedAccessToken(accessTokenResult.data, context.storage, options);
7900
7960
  }
7901
7961
 
7902
7962
  // src/client/auth-state.ts
@@ -7965,8 +8025,11 @@ function primerOrigin(origin) {
7965
8025
  }
7966
8026
  async function startRuntime(config, resolved) {
7967
8027
  let reauthenticate = null;
7968
- if (resolved.reauthenticate !== undefined) {
7969
- reauthenticate = resolved.reauthenticate;
8028
+ if (resolved.clearCachedAccessToken !== undefined) {
8029
+ reauthenticate = function reauthenticateManagedAuth(error) {
8030
+ resolved.clearCachedAccessToken?.();
8031
+ return Promise.resolve(makeSignInFailedState(config, error));
8032
+ };
7970
8033
  }
7971
8034
  const transport = createTransport({
7972
8035
  accessToken: resolved.accessToken,
@@ -8018,9 +8081,7 @@ async function loginAndStart(config) {
8018
8081
  }
8019
8082
  return startRuntime(config, {
8020
8083
  accessToken: result.accessToken,
8021
- reauthenticate: function reauthenticate() {
8022
- return Promise.resolve(makeSignInFailedState(config, ErrTokenExpired));
8023
- }
8084
+ clearCachedAccessToken: result.clearCachedAccessToken
8024
8085
  });
8025
8086
  }
8026
8087
  async function start(options) {
@@ -8050,6 +8111,9 @@ async function start(options) {
8050
8111
  toJSON: poisonToJSON
8051
8112
  };
8052
8113
  }
8114
+ if (accessToken.kind === "auth-unavailable") {
8115
+ return authUnavailableState(accessToken.error);
8116
+ }
8053
8117
  if (accessToken.kind === "missing") {
8054
8118
  return makeSignInRequiredState(config);
8055
8119
  }
@@ -8059,4 +8123,4 @@ export {
8059
8123
  start
8060
8124
  };
8061
8125
 
8062
- //# debugId=1270BDB9A874B19364756E2164756E21
8126
+ //# debugId=D2887B68B961EF6364756E2164756E21