@flow-industries/id 0.7.0 → 0.8.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.
@@ -164,11 +164,24 @@ export function createFlow(options = {}) {
164
164
  return false;
165
165
  }
166
166
  }
167
- // Rotation is cross-tab-safe: the lock serializes refreshes across tabs of the
168
- // same origin, and refreshViaToken reads the token fresh from storage inside
169
- // the lock, so a second tab uses the already-rotated token (never a stale one
170
- // that would trip server-side reuse detection).
171
- const refreshJwt = () => withGuestLock(`flow.id.refresh:${host}`, refreshViaToken);
167
+ // Every refresh-token POST — boot, ensureGuest, and getToken — funnels through
168
+ // this one lock so two callers can't present the same stored token at once.
169
+ // Rotation is single-use: a token replayed after it rotated looks like theft,
170
+ // tripping server-side reuse detection and revoking the whole lineage (which
171
+ // would then 401 → clear → re-guest). The lock serializes refreshes across
172
+ // tabs of the same origin, and the guard short-circuits a caller that arrives
173
+ // after a concurrent holder already minted a fresh JWT, so no one re-presents
174
+ // an already-rotated token. refreshViaToken re-reads the token fresh from
175
+ // storage inside the lock, so a genuinely-second refresh uses the rotated one.
176
+ async function doRefresh() {
177
+ return withGuestLock(`flow.id.refresh:${host}`, async () => {
178
+ const current = store.getSnapshot().jwt;
179
+ if (current && !isExpiring(current))
180
+ return true;
181
+ return refreshViaToken();
182
+ });
183
+ }
184
+ const refreshJwt = doRefresh;
172
185
  // Dedupe concurrent refreshes: a burst of getToken() calls that all find the
173
186
  // cached token expired should trigger one mint, not one per call.
174
187
  let refreshInFlight = null;
@@ -220,7 +233,7 @@ export function createFlow(options = {}) {
220
233
  // existing session instead of minting a duplicate guest.
221
234
  if (store.getSnapshot().user)
222
235
  return true;
223
- if (await refreshViaToken())
236
+ if (await doRefresh())
224
237
  return true;
225
238
  try {
226
239
  const result = await getDialog().requestSilent(METHODS.guest, []);
@@ -249,7 +262,7 @@ export function createFlow(options = {}) {
249
262
  void (async () => {
250
263
  await restoreCredential(store);
251
264
  if (options.autoRestore !== false) {
252
- const restored = await refreshViaToken();
265
+ const restored = await doRefresh();
253
266
  // ensureGuest re-checks (refresh-first) under a cross-tab lock before
254
267
  // minting, so calling it after a failed refresh can't fork a visitor into
255
268
  // duplicate guests across tabs — the extra refresh is the dedup.
@@ -294,6 +307,17 @@ export function createFlow(options = {}) {
294
307
  accessKeyPrep = await accessKeyModule.prepareAccessKey(accessKeyOptions, chain.id);
295
308
  extraCapabilities = { accessKeyHash: accessKeyPrep.accessKeyHash };
296
309
  }
310
+ // When the current session is a guest, forward its access token so a
311
+ // sign-up upgrades that guest in place even where the guest cookie is
312
+ // unreadable (the iOS top-level sign-up popup). The server verifies it
313
+ // before honoring it, so an app that never becomes a guest sends nothing.
314
+ const snapshot = store.getSnapshot();
315
+ if (snapshot.user?.isGuest && snapshot.jwt) {
316
+ extraCapabilities = {
317
+ ...(extraCapabilities ?? {}),
318
+ guestToken: snapshot.jwt,
319
+ };
320
+ }
297
321
  const { session, webauthn, refreshToken } = await runLogin({
298
322
  dialog: dialogHost,
299
323
  store,
@@ -7,6 +7,13 @@ export type ConnectCapabilities = {
7
7
  accessKeyHash?: string;
8
8
  signInHeadless?: boolean;
9
9
  credentialId?: string;
10
+ /**
11
+ * The current guest's access JWT, forwarded so a sign-up upgrades that guest
12
+ * in place even where the guest cookie is unreadable (the iOS top-level
13
+ * sign-up popup). The server verifies it before letting it stand in for the
14
+ * cookie session; see the register endpoint.
15
+ */
16
+ guestToken?: string;
10
17
  };
11
18
  export type ConnectRequest = [{
12
19
  capabilities?: ConnectCapabilities;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "main": "./dist/sdk/client/index.js",
5
5
  "module": "./dist/sdk/client/index.js",
6
6
  "types": "./dist/sdk/client/index.d.ts",