@isomorph.ai/app-sdk 2.5.0 → 2.6.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
@@ -365,6 +365,33 @@ The calls go to the local App Gateway beside the runtime (`ISOMORPH_TRUSTED_GATE
365
365
 
366
366
  (1.17.0; data plane ADR 0057)
367
367
 
368
+ ## Public actions
369
+
370
+ One door with nobody signed in: a sign-up form, a feedback box, an inbound lead. An action that
371
+ exports `access = "public"` beside `expose = { description, input }` runs for a caller with no
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
374
+ enqueue inside it is refused with `PUBLIC_DENIED`. A public action cannot be trusted. The page
375
+ calls it with `isomorph.actions.invokePublic(name, input)`: a plain POST with no cookie and no
376
+ sign-in, answering the result or an `IsomorphError` with the gateway's category. The table it
377
+ writes says so in its row rule (`WITH CHECK (current_setting('harbour.user_id', true) = 'public:<app>')`).
378
+
379
+ ```ts
380
+ // actions/signup.ts
381
+ export const access = "public";
382
+ export const expose = { description: 'Sign a visitor up for the launch list', input: { type: 'object', properties: { email: { type: 'string' } }, required: ['email'] } };
383
+
384
+ export default async function signup(input: { email: string }) {
385
+ await isomorph.data.from('launch_signups').insert({ email: input.email });
386
+ return { ok: true };
387
+ }
388
+
389
+ // page
390
+ await isomorph.actions.invokePublic('signup', { email });
391
+ ```
392
+
393
+ (2.6.0)
394
+
368
395
  ## Browser read tools
369
396
 
370
397
  Install optional peers `ai` and `playwright-core@1.58.2`. Locally run `npx playwright-core install chromium --only-shell`; V2 packages Chromium in the existing app image. An existing system executable can be supplied as executablePath.
package/dist/index.d.ts CHANGED
@@ -38,10 +38,12 @@ export interface IsomorphUser {
38
38
  appUser?: unknown;
39
39
  /**
40
40
  * Who is calling: a `'person'` signed in through the browser or an MCP
41
- * client, or a `'service'` holding a credential the company's IT issued,
42
- * whose `id` and `name` are the credential's. Absent means a person.
41
+ * client, a `'service'` holding a credential the company's IT issued,
42
+ * whose `id` and `name` are the credential's, or `'public'`: nobody signed
43
+ * in, calling an action that exports `access = "public"` (`id` is
44
+ * `public:<app>`, no email, no groups). Absent means a person.
43
45
  */
44
- kind?: 'person' | 'service';
46
+ kind?: 'person' | 'service' | 'public';
45
47
  }
46
48
  /**
47
49
  * `export const expose = { description, input }` on an action, beside
@@ -2066,6 +2068,13 @@ export declare function createClient(options?: ClientOptions): {
2066
2068
  invoke: <T = unknown, P = string>(name: string, input: unknown, options?: {
2067
2069
  onProgress?: (step: P) => void;
2068
2070
  }) => Promise<T>;
2071
+ /**
2072
+ * Call an action that exports `access = "public"` with no sign-in: a
2073
+ * plain POST to `/_harbour/api/<name>` on the API host, no cookie, no
2074
+ * credential, no sign-in redirect. The result is the action's, the
2075
+ * error an `IsomorphError` with the gateway's category.
2076
+ */
2077
+ invokePublic: <T = unknown>(name: string, input: unknown) => Promise<T>;
2069
2078
  };
2070
2079
  jobs: {
2071
2080
  /** Select the owner's folder for a declared configurable file trigger. */
package/dist/index.js CHANGED
@@ -819,6 +819,26 @@ export function createClient(options = {}) {
819
819
  }
820
820
  return transport.json(path, { method: 'POST', body: JSON.stringify(input) }, options.onProgress);
821
821
  },
822
+ /**
823
+ * Call an action that exports `access = "public"` with no sign-in: a
824
+ * plain POST to `/_harbour/api/<name>` on the API host, no cookie, no
825
+ * credential, no sign-in redirect. The result is the action's, the
826
+ * error an `IsomorphError` with the gateway's category.
827
+ */
828
+ invokePublic: async (name, input) => {
829
+ const response = await transport.fetch(`${transport.baseUrl}/_harbour/api/${encodeURIComponent(name)}`, {
830
+ method: 'POST', credentials: 'omit', headers: { 'content-type': 'application/json' }, body: JSON.stringify(input ?? {}),
831
+ });
832
+ let body = {};
833
+ try {
834
+ body = await response.json();
835
+ }
836
+ catch { /* not JSON: the status says what happened */ }
837
+ if (!response.ok || body.error) {
838
+ throw new IsomorphError(body.error?.category ?? 'INTERNAL', body.error?.message ?? `the public action answered ${response.status}`, body.requestId, undefined, response.status);
839
+ }
840
+ return body.data;
841
+ },
822
842
  },
823
843
  jobs: {
824
844
  /** Select the owner's folder for a declared configurable file trigger. */
package/dist/jobs.d.ts CHANGED
@@ -58,6 +58,8 @@ export interface JobContext {
58
58
  }
59
59
  /** Whether the module says `export const <name> = true`, read without importing it; a module that does not parse does not. */
60
60
  export declare function exportsTrue(file: string, name: string): Promise<boolean>;
61
+ /** Whether the module says `export const <name> = <value>` with that one literal, read statically; nothing computed counts. */
62
+ export declare function exportsLiteral(file: string, name: string, value: string | boolean): Promise<boolean>;
61
63
  export declare function discoverJobs(root: string): Promise<JobDefinition[]>;
62
64
  type Handler = (...args: unknown[]) => unknown;
63
65
  export declare function loadHandler(root: string, dir: string, name: string): Promise<Handler>;
package/dist/jobs.js CHANGED
@@ -103,7 +103,11 @@ function parseModule(source) {
103
103
  return parse(stripTypeScriptTypes(source), { ecmaVersion: 'latest', sourceType: 'module' }).body;
104
104
  }
105
105
  /** Whether the module says `export const <name> = true`, read without importing it; a module that does not parse does not. */
106
- export async function exportsTrue(file, name) {
106
+ export function exportsTrue(file, name) {
107
+ return exportsLiteral(file, name, true);
108
+ }
109
+ /** Whether the module says `export const <name> = <value>` with that one literal, read statically; nothing computed counts. */
110
+ export async function exportsLiteral(file, name, value) {
107
111
  let statements;
108
112
  try {
109
113
  statements = parseModule(await readFile(file, 'utf8'));
@@ -114,7 +118,7 @@ export async function exportsTrue(file, name) {
114
118
  return statements.some(statement => statement.type === 'ExportNamedDeclaration'
115
119
  && statement.declaration?.type === 'VariableDeclaration' && statement.declaration.kind === 'const'
116
120
  && statement.declaration.declarations.some(declaration => declaration.id.type === 'Identifier' && declaration.id.name === name
117
- && declaration.init?.type === 'Literal' && declaration.init.value === true));
121
+ && declaration.init?.type === 'Literal' && declaration.init.value === value));
118
122
  }
119
123
  export async function discoverJobs(root) {
120
124
  const directory = join(root, JOBS_DIR);
package/dist/runtime.d.ts CHANGED
@@ -39,6 +39,8 @@ export interface RuntimeInventory {
39
39
  trusted: string[];
40
40
  /** The actions whose module says `export const expose = { description, input }`, by name: what the kit gate records as exposed (ADR 0100). */
41
41
  exposed: Record<string, ActionExposure>;
42
+ /** The actions whose module says `export const access = "public"`: the gateway runs them for callers with no sign-in (data plane ADR 0114). */
43
+ public: string[];
42
44
  jobs: JobDefinition[];
43
45
  }
44
46
  /** Every `actions/<name>.ts` and `jobs/<name>.ts` the app carries, by logical name. */
package/dist/runtime.js CHANGED
@@ -68,7 +68,7 @@ import { pathToFileURL } from 'node:url';
68
68
  import { createClient, gatewayRequest, IsomorphError } from './index.js';
69
69
  import { userGroupsFromHeader } from './user-groups.js';
70
70
  import { userRole } from './user-role.js';
71
- import { FILE_MIME_TYPES, providerFileId, actionExposure, discoverJobs, exportsTrue, invokeJob, loadHandler, JOBS_DIR } from './jobs.js';
71
+ import { FILE_MIME_TYPES, providerFileId, actionExposure, discoverJobs, exportsLiteral, exportsTrue, invokeJob, loadHandler, JOBS_DIR } from './jobs.js';
72
72
  import { WEBHOOK_ADAPTERS } from './webhook-adapters.js';
73
73
  import { LocalGateway } from './local-gateway.js';
74
74
  export { JOBS_DIR } from './jobs.js';
@@ -121,8 +121,9 @@ export async function discover(root) {
121
121
  const trusted = await Promise.all(actions.map((name) => exportsTrue(join(root, ACTIONS_DIR, `${name}.ts`), 'trusted')));
122
122
  const exposures = await Promise.all(actions.map((name) => actionExposure(join(root, ACTIONS_DIR, `${name}.ts`))));
123
123
  const exposed = Object.fromEntries(actions.flatMap((name, index) => exposures[index] ? [[name, exposures[index]]] : []));
124
+ const publicAccess = await Promise.all(actions.map((name) => exportsLiteral(join(root, ACTIONS_DIR, `${name}.ts`), 'access', 'public')));
124
125
  const jobs = await discoverJobs(root);
125
- return { actions, trusted: actions.filter((_, index) => trusted[index]), exposed, jobs };
126
+ return { actions, trusted: actions.filter((_, index) => trusted[index]), exposed, public: actions.filter((_, index) => publicAccess[index]), jobs };
126
127
  }
127
128
  // ---- Cron ------------------------------------------------------------------
128
129
  /** One field of a five-field cron expression: numbers, `*`, lists, ranges and steps, no names. */
@@ -321,7 +322,7 @@ export class Runs {
321
322
  }
322
323
  export class Runtime {
323
324
  options;
324
- inventory = { actions: [], trusted: [], exposed: {}, jobs: [] };
325
+ inventory = { actions: [], trusted: [], exposed: {}, public: [], jobs: [] };
325
326
  localGateway;
326
327
  running = new Set();
327
328
  timers = [];
@@ -728,8 +729,10 @@ export class Runtime {
728
729
  id: userId,
729
730
  // A service credential's call arrives with the credential as the verified user
730
731
  // (`service:<clientId>`, ADR 0101); the action sees it as `kind: 'service'`, the
731
- // shape `/_harbour/identity/current` already answers. Anything else is a person.
732
- ...(userId.startsWith('service:') ? { kind: 'service' } : {}),
732
+ // shape `/_harbour/identity/current` already answers. A public action's caller
733
+ // arrives as `public:<app>` with no email and no groups (data plane ADR 0114).
734
+ // Anything else is a person.
735
+ ...(userId.startsWith('service:') ? { kind: 'service' } : userId.startsWith('public:') ? { kind: 'public' } : {}),
733
736
  email: header(request, 'X-Harbour-Verified-User-Email'),
734
737
  groups: userGroupsFromHeader(header(request, 'X-Harbour-Verified-User-Groups')),
735
738
  role: userRole(header(request, 'X-Harbour-Verified-User-Role')),
@@ -887,7 +890,7 @@ export class Runtime {
887
890
  case PING_ACTION: {
888
891
  // `hooks` is what the local gateway's hooks entrance serves under `isomorph dev` (data plane ADR 0112).
889
892
  const hooks = this.inventory.jobs.flatMap((job) => job.trigger?.type === 'webhook' ? [{ job: job.name, provider: job.trigger.provider }] : []);
890
- const answer = { ok: true, mode: this.options.mode, actions: this.inventory.actions, trusted: this.inventory.trusted, exposed: this.inventory.exposed, jobs: this.inventory.jobs.map((job) => job.name), hooks };
893
+ const answer = { ok: true, mode: this.options.mode, actions: this.inventory.actions, trusted: this.inventory.trusted, exposed: this.inventory.exposed, public: this.inventory.public, jobs: this.inventory.jobs.map((job) => job.name), hooks };
891
894
  return this.options.mode === 'gate' ? { ...answer, loadFailures: await this.loadFailures() } : answer;
892
895
  }
893
896
  case ENQUEUE_ACTION: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",