@isomorph.ai/app-sdk 2.6.2 → 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 +23 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +35 -3
- package/dist/runtime.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -533,6 +533,21 @@ await runtimeSecrets.delete(`supplier-key/${ctx.user.id}`);
|
|
|
533
533
|
|
|
534
534
|
(1.36.0, 1.36.1, 1.45.0; data plane ADR 0090)
|
|
535
535
|
|
|
536
|
+
## Another app's address and this app's environment
|
|
537
|
+
|
|
538
|
+
`appApiUrl(name)` is another app's API address in the environment the calling code runs in: its preview from preview, its production from production. `name` is that app's address name (the first part of its API host).
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
import { appApiUrl } from '@isomorph.ai/app-sdk';
|
|
542
|
+
const reply = await fetch(`${appApiUrl('ats')}/_harbour/api/list-openings`, { method: 'POST', headers, body });
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
- It is `https://<name>.<ISOMORPH_APPS_API_SUFFIX>`. Provision sets the suffix on a deployed app's actions and jobs from the app's own API host; `isomorph check` and `isomorph dev` set it from the app's preview address.
|
|
546
|
+
- A name that is not lower-case letters, digits and hyphens (one DNS label) throws `VALIDATION_FAILED`. Without the suffix (a browser, or a local run with no app address) it throws an `UNAVAILABLE` error whose `details.code` is `APP_API_ADDRESS_UNAVAILABLE`.
|
|
547
|
+
- `process.env.ISOMORPH_ENVIRONMENT` is `preview` or `production` on a deployed app, and `local` under `isomorph dev`.
|
|
548
|
+
|
|
549
|
+
(2.7.0; data plane ADR 2026-11-06, kit apps learn their environment and the apps' API suffix)
|
|
550
|
+
|
|
536
551
|
## Running and rollout
|
|
537
552
|
|
|
538
553
|
`isomorph dev` uses canned server-model fixtures. `dev --real` supplies an approved development connection to the action process. Hosted actions resolve their exact ACTIVE version key. The current V2 PLAN is published after LIVE; before key activation, an AI action fails and later actions retry. LIVE alone is not AI acceptance.
|
|
@@ -573,6 +588,14 @@ and no second GET is sent. When the refresh fails, `identity.current()`
|
|
|
573
588
|
answers `null` and listeners hear `null`, as before. On the page's own origin
|
|
574
589
|
the identity GET is never renewed (it is the renewal). (1.5.3)
|
|
575
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
|
+
|
|
576
599
|
## External OTP users
|
|
577
600
|
|
|
578
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.d.ts
CHANGED
|
@@ -2026,6 +2026,12 @@ declare class RealtimeChannelBuilder {
|
|
|
2026
2026
|
}
|
|
2027
2027
|
/** One request on the gateway with the client's headers and refusals; for the kit runtime's own calls. */
|
|
2028
2028
|
export declare function gatewayRequest<T>(options: ClientOptions, path: string, init?: RequestInit): Promise<T>;
|
|
2029
|
+
/**
|
|
2030
|
+
* Another app's API address in the environment this code runs in: its preview from preview, its production
|
|
2031
|
+
* from production. `name` is that app's address name. Server code only; the platform sets
|
|
2032
|
+
* ISOMORPH_APPS_API_SUFFIX on a deployed app and `isomorph dev` sets it locally.
|
|
2033
|
+
*/
|
|
2034
|
+
export declare function appApiUrl(name: string): string;
|
|
2029
2035
|
export declare function createClient(options?: ClientOptions): {
|
|
2030
2036
|
identity: {
|
|
2031
2037
|
current: () => Promise<IsomorphUser | null>;
|
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
|
-
|
|
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
|
}
|
|
@@ -435,6 +451,22 @@ class RealtimeChannelBuilder {
|
|
|
435
451
|
export function gatewayRequest(options, path, init) {
|
|
436
452
|
return new Transport(options).json(path, init);
|
|
437
453
|
}
|
|
454
|
+
const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
455
|
+
/**
|
|
456
|
+
* Another app's API address in the environment this code runs in: its preview from preview, its production
|
|
457
|
+
* from production. `name` is that app's address name. Server code only; the platform sets
|
|
458
|
+
* ISOMORPH_APPS_API_SUFFIX on a deployed app and `isomorph dev` sets it locally.
|
|
459
|
+
*/
|
|
460
|
+
export function appApiUrl(name) {
|
|
461
|
+
if (typeof name !== 'string' || !DNS_LABEL.test(name)) {
|
|
462
|
+
throw new IsomorphError('VALIDATION_FAILED', `${JSON.stringify(name)} is not an app address name: lower-case letters, digits and hyphens, at most 63, starting and ending with a letter or digit.`, undefined, { code: 'VALIDATION_FAILED' });
|
|
463
|
+
}
|
|
464
|
+
const suffix = typeof process === 'undefined' ? undefined : process.env.ISOMORPH_APPS_API_SUFFIX;
|
|
465
|
+
if (!suffix) {
|
|
466
|
+
throw new IsomorphError('UNAVAILABLE', `Another app's address is not known here. appApiUrl works in a deployed app's actions and jobs, and under isomorph dev for an app that has an address.`, undefined, { code: 'APP_API_ADDRESS_UNAVAILABLE' });
|
|
467
|
+
}
|
|
468
|
+
return `https://${name}.${suffix}`;
|
|
469
|
+
}
|
|
438
470
|
export function createClient(options = {}) {
|
|
439
471
|
const authListeners = new Set();
|
|
440
472
|
let currentUser;
|
package/dist/runtime.js
CHANGED
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
// ISOMORPH_JOBS_QUEUE_URL an enqueue sends the run to this SQS queue and
|
|
58
58
|
// the queue loop never runs here: the cell's
|
|
59
59
|
// Event Function works it (lambda-events-entry.ts)
|
|
60
|
+
// ISOMORPH_ENVIRONMENT preview or production, set by Provision (by the
|
|
61
|
+
// CLI under isomorph check and isomorph dev); the
|
|
62
|
+
// SDK itself never reads it
|
|
63
|
+
// ISOMORPH_APPS_API_SUFFIX the part of this app's API host after its own
|
|
64
|
+
// name, which appApiUrl puts after another app's
|
|
60
65
|
import { createHash, randomBytes } from 'node:crypto';
|
|
61
66
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
62
67
|
import { createServer } from 'node:http';
|