@isomorph.ai/app-sdk 2.7.0 → 2.7.1

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
@@ -588,6 +588,14 @@ and no second GET is sent. When the refresh fails, `identity.current()`
588
588
  answers `null` and listeners hear `null`, as before. On the page's own origin
589
589
  the identity GET is never renewed (it is the renewal). (1.5.3)
590
590
 
591
+ When the renewal cannot restore the sign-in — the refresh failed, or the
592
+ resend was refused again — the session has ended (a platform operator's
593
+ session never renews; any session ends at its lifetime). That refusal's
594
+ message is `Your sign-in has ended. Reload the page to sign in again.`, which
595
+ you can show as is; its `category` (`AUTH_REQUIRED`), `status` (401),
596
+ `requestId` and `details` are unchanged, so branch on `category`. A 401 your
597
+ own action or a connected service gave keeps its own message. (2.7.1)
598
+
591
599
  ## External OTP users
592
600
 
593
601
  Use `user.id` for ownership and database row rules. External users can sign in with a phone number and have no email. `user.name`, `user.phone`, `user.email` and the `emailVerified`/`phoneVerified` flags describe their profile; an optional contact is not verified automatically. `user.directory` identifies `company` or `external`. Database transactions also expose `current_setting('harbour.directory', true)` when a row rule needs that distinction.
package/dist/index.js CHANGED
@@ -183,6 +183,18 @@ function renewableOnUnauthorized(path) {
183
183
  return false;
184
184
  return path.startsWith('/_harbour/') && path !== IDENTITY_PATH;
185
185
  }
186
+ // A browser call still refused after the renewal could not restore the
187
+ // identity (the API-host refresh failed, or the resend was refused again)
188
+ // means the person's sign-in has ended: a platform operator's session never
189
+ // renews (governance ADR 0178), and any session ends at its lifetime. The
190
+ // gateway's wire message is for engineers, and apps showed it verbatim to
191
+ // people mid-task (escplan, 2026-11-07), so that refusal reads as a plain
192
+ // instruction instead; category, status, requestId and details are kept.
193
+ // Only the identity refusal is reworded — the gateway's own sentence, or a
194
+ // 401 with no Isomorph error at all (an edge refusal). A 401 an action or a
195
+ // connected service gave keeps its own words.
196
+ const IDENTITY_REFUSAL = 'signed Isomorph identity is required';
197
+ const SIGN_IN_ENDED = 'Your sign-in has ended. Reload the page to sign in again.';
186
198
  class Transport {
187
199
  onUnauthorized;
188
200
  baseUrl;
@@ -207,7 +219,7 @@ class Transport {
207
219
  json(path, init = {}, onProgress) {
208
220
  return this.request(path, init, renewableOnUnauthorized(path) || (this.separateApiHost && path === IDENTITY_PATH), onProgress);
209
221
  }
210
- async request(path, init, renewOnUnauthorized, onProgress) {
222
+ async request(path, init, renewOnUnauthorized, onProgress, renewalTried = false) {
211
223
  const sentAt = this.renewal?.renewals() ?? 0;
212
224
  const action = currentAction();
213
225
  // (headers with an undefined value are removed so a multipart body can
@@ -245,13 +257,17 @@ class Transport {
245
257
  if (renewed && path === IDENTITY_PATH)
246
258
  return renewed;
247
259
  if (renewed !== false)
248
- return this.request(path, init, false, onProgress);
260
+ return this.request(path, init, false, onProgress, true);
261
+ renewalTried = true;
249
262
  }
250
263
  // Not before the resend: a page listening to identity.onChange would
251
264
  // otherwise see the user flicker to null while the edge was healing.
252
265
  this.onUnauthorized?.();
253
266
  }
254
- throw new IsomorphError(body?.error?.category ?? (response.status === 401 ? 'AUTH_REQUIRED' : response.status === 403 ? 'FORBIDDEN' : 'INTERNAL'), body?.error?.message ?? `Isomorph request failed with ${response.status}`, body?.requestId ?? response.headers.get('x-request-id') ?? undefined, body?.error?.details, response.status);
267
+ const category = body?.error?.category ?? (response.status === 401 ? 'AUTH_REQUIRED' : response.status === 403 ? 'FORBIDDEN' : 'INTERNAL');
268
+ const signInEnded = renewalTried && response.status === 401 && category === 'AUTH_REQUIRED'
269
+ && (body?.error?.message === undefined || body.error.message === IDENTITY_REFUSAL);
270
+ throw new IsomorphError(category, signInEnded ? SIGN_IN_ENDED : body?.error?.message ?? `Isomorph request failed with ${response.status}`, body?.requestId ?? response.headers.get('x-request-id') ?? undefined, body?.error?.details, response.status);
255
271
  }
256
272
  return (body && Object.prototype.hasOwnProperty.call(body, 'data') ? body.data : body);
257
273
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",