@isomorph.ai/app-sdk 2.8.2 → 2.9.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.
package/README.md CHANGED
@@ -370,12 +370,37 @@ The calls go to the local App Gateway beside the runtime (`ISOMORPH_TRUSTED_GATE
370
370
  One door with nobody signed in: a sign-up form, a feedback box, an inbound lead. An action that
371
371
  exports `access = "public"` beside `expose = { description, input }` runs for a caller with no
372
372
  sign-in as `public:<app>` (`ctx.user.kind === 'public'`, no email, no groups). It may only insert
373
- rows: a select, update, delete, file, integration, AI call, realtime, another action or a job
373
+ rows, or call a SQL function named `public_<name>` through `isomorph.data.rpc`: a select, update,
374
+ delete, any other function, file, integration, AI call, realtime, another action or a job
374
375
  enqueue inside it is refused with `PUBLIC_DENIED`. A public action cannot be trusted. The page
375
376
  calls it with `isomorph.actions.invokePublic(name, input)`: a plain POST with no cookie and no
376
377
  sign-in, answering the result or an `IsomorphError` with the gateway's category. The table it
377
378
  writes says so in its row rule (`WITH CHECK (current_setting('harbour.user_id', true) = 'public:<app>')`).
378
379
 
380
+ A page that shows one record by its link (a certificate, a ticket) reads it through one
381
+ `public_<name>` function the app declares `SECURITY DEFINER`, taking the link's token and
382
+ returning only what the page may show; the gateway runs it read-only. No public `SELECT` policy
383
+ is ever written.
384
+
385
+ ```sql
386
+ -- migrations/0012_public_certificate.sql
387
+ CREATE FUNCTION public_certificate(token text) RETURNS TABLE (holder text, course text, issued_on date)
388
+ LANGUAGE sql SECURITY DEFINER STABLE AS $$
389
+ SELECT holder, course, issued_on FROM certificates WHERE verify_token = token
390
+ $$;
391
+ ```
392
+
393
+ ```ts
394
+ // actions/verify-certificate.ts
395
+ export const access = "public";
396
+ export const expose = { description: 'Show the certificate a verification link points at', input: { type: 'object', properties: { token: { type: 'string' } }, required: ['token'] } };
397
+
398
+ export default async function verifyCertificate(input: { token: string }) {
399
+ const [certificate] = await isomorph.data.rpc<Array<{ holder: string; course: string; issued_on: string }>>('public_certificate', { token: input.token });
400
+ return certificate ?? null;
401
+ }
402
+ ```
403
+
379
404
  ```ts
380
405
  // actions/signup.ts
381
406
  export const access = "public";
@@ -390,7 +415,7 @@ export default async function signup(input: { email: string }) {
390
415
  await isomorph.actions.invokePublic('signup', { email });
391
416
  ```
392
417
 
393
- (2.6.0)
418
+ (2.6.0; public read by link 2.9.0, data plane ADR 0114 amendment 2026-11-07)
394
419
 
395
420
  ## Calling another app as the signed-in person
396
421
 
@@ -408,6 +433,10 @@ The other app (named by its address, as `appApiUrl` takes it) lists this one in
408
433
  trusted action; name a trusted action exactly. Its action sees the person in `ctx.user` (role `member`) and this app
409
434
  in `ctx.caller = { app, environment }` (`app` is the calling app's id; `local` under `isomorph dev` and
410
435
  `isomorph check`, where `app` is the calling address). `ctx.caller` is absent on its own pages, service and MCP calls.
436
+
437
+ Every action also sees `ctx.clientIp`: the caller's network address as the gateway in front of
438
+ the app saw it, for a sign-in record or a per-address rate limit (2.9.0). Nothing the browser
439
+ sends sets it.
411
440
  The call goes through this app's own gateway with the workload token and the person's context; a refusal is an
412
441
  `IsomorphError` with the gateway's category (`AUTH_REQUIRED`, `FORBIDDEN` when not listed, `NOT_FOUND`,
413
442
  `RATE_LIMITED`). It works only from an action a signed-in person started: a job keeps using a service credential.
package/dist/runtime.d.ts CHANGED
@@ -18,6 +18,11 @@ export interface ActionContext {
18
18
  tenant: string;
19
19
  app: string;
20
20
  requestId: string;
21
+ /**
22
+ * The caller's network address as the gateway saw it (`X-Harbour-Verified-Client-IP`): for a
23
+ * sign-in record or a rate limit. Absent under a runtime behind an older gateway.
24
+ */
25
+ clientIp?: string;
21
26
  /**
22
27
  * Reports a step to a page that invoked the action with `onProgress`; a
23
28
  * no-op otherwise. A string or a small JSON object; after 256 steps, or a
package/dist/runtime.js CHANGED
@@ -816,7 +816,8 @@ export class Runtime {
816
816
  response.write(line);
817
817
  };
818
818
  const caller = verifiedCaller(request);
819
- const context = { computerView: header(request, 'X-Harbour-Computer-View') === '1', signal: controller.signal, user, tenant, app, requestId: requestId ?? '', progress, transaction: (fn) => this.transaction(name, fn), ...(caller ? { caller } : {}) };
819
+ const clientIp = header(request, 'X-Harbour-Verified-Client-IP');
820
+ const context = { computerView: header(request, 'X-Harbour-Computer-View') === '1', signal: controller.signal, user, tenant, app, requestId: requestId ?? '', ...(clientIp ? { clientIp } : {}), progress, transaction: (fn) => this.transaction(name, fn), ...(caller ? { caller } : {}) };
820
821
  const limit = caller ? APP_CALL_MAX_BODY_BYTES : MAX_BODY_BYTES;
821
822
  const raw = await readBody(request, limit);
822
823
  if (raw === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "2.8.2",
3
+ "version": "2.9.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",