@isomorph.ai/app-sdk 2.4.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 +28 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +20 -0
- package/dist/jobs.d.ts +2 -0
- package/dist/jobs.js +6 -2
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +11 -6
- package/package.json +1 -1
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.
|
|
@@ -396,7 +423,7 @@ const computerUse = createComputerUseTool({ runKey: ctx.runId ?? `my-job/${ctx.s
|
|
|
396
423
|
// Add { computerUse } to generateText's tools. It returns { status: 'done' | 'failed', summary, finalUrl, downloads }.
|
|
397
424
|
```
|
|
398
425
|
|
|
399
|
-
-
|
|
426
|
+
- Hosted, the tool posts to its App Gateway (`ISOMORPH_GATEWAY_URL`, `/_harbour/computer-use/...`) with the workload token; the gateway and governance carry the call into Harbour's account (data plane ADR 0103). Only a runtime with no gateway URL calls the task API at `ISOMORPH_COMPUTER_USE_URL` directly (region `ISOMORPH_COMPUTER_USE_REGION`, default `us-east-1`), signed with SigV4 as its own role; Provision no longer sets those (ADR 0110).
|
|
400
427
|
- It starts the task with `POST /tasks`, then checks `POST /tasks/check` every 5 s until the task is `done`, `failed` or `waiting_for_input`. Waiting returns immediately; end the app job rather than holding a worker while a person signs in.
|
|
401
428
|
- It calls `progress(step count, note)` on every new step and at least once a minute, so the job's lease does not run out.
|
|
402
429
|
- When `cancelled()` turns true it sends `POST /tasks/cancel` once and keeps checking until the task has `failed`.
|
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,
|
|
42
|
-
* whose `id` and `name` are the credential's
|
|
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
|
|
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 ===
|
|
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.
|
|
732
|
-
|
|
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')),
|
|
@@ -885,7 +888,9 @@ export class Runtime {
|
|
|
885
888
|
const owner = context.user.id;
|
|
886
889
|
switch (name) {
|
|
887
890
|
case PING_ACTION: {
|
|
888
|
-
|
|
891
|
+
// `hooks` is what the local gateway's hooks entrance serves under `isomorph dev` (data plane ADR 0112).
|
|
892
|
+
const hooks = this.inventory.jobs.flatMap((job) => job.trigger?.type === 'webhook' ? [{ job: job.name, provider: job.trigger.provider }] : []);
|
|
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 };
|
|
889
894
|
return this.options.mode === 'gate' ? { ...answer, loadFailures: await this.loadFailures() } : answer;
|
|
890
895
|
}
|
|
891
896
|
case ENQUEUE_ACTION: {
|