@isomorph.ai/app-sdk 2.2.0 → 2.3.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/dist/index.d.ts +17 -0
- package/dist/jobs.d.ts +3 -0
- package/dist/jobs.js +36 -6
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +6 -4
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,23 @@ export interface IsomorphUser {
|
|
|
36
36
|
*/
|
|
37
37
|
role: IsomorphUserRole;
|
|
38
38
|
appUser?: unknown;
|
|
39
|
+
/**
|
|
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.
|
|
43
|
+
*/
|
|
44
|
+
kind?: 'person' | 'service';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `export const expose = { description, input }` on an action, beside
|
|
48
|
+
* `export const trusted = true`: the action may be called from outside the
|
|
49
|
+
* app by a service credential or an MCP client. `description` is read by
|
|
50
|
+
* people and agents; `input` is a JSON Schema object literal the platform
|
|
51
|
+
* validates the request body against. Literal only.
|
|
52
|
+
*/
|
|
53
|
+
export interface ActionExposure {
|
|
54
|
+
description: string;
|
|
55
|
+
input: Record<string, unknown>;
|
|
39
56
|
}
|
|
40
57
|
export interface IsomorphResult<T> {
|
|
41
58
|
data: T;
|
package/dist/jobs.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ActionExposure } from './index.js';
|
|
1
2
|
export declare const JOBS_DIR = "jobs";
|
|
2
3
|
export interface EmailArrivalTrigger {
|
|
3
4
|
type: 'email.arrived';
|
|
@@ -25,6 +26,8 @@ export interface JobDefinition {
|
|
|
25
26
|
schedule?: string;
|
|
26
27
|
trigger?: EventTrigger;
|
|
27
28
|
}
|
|
29
|
+
/** The action's `export const expose = { description, input }`, read without importing it: undefined when the module has none, does not parse, or declares anything but a literal object of that shape. */
|
|
30
|
+
export declare function actionExposure(file: string): Promise<ActionExposure | undefined>;
|
|
28
31
|
export declare function eventTrigger(value: unknown): EventTrigger;
|
|
29
32
|
export interface JobContext {
|
|
30
33
|
scheduledAt: string;
|
package/dist/jobs.js
CHANGED
|
@@ -8,25 +8,55 @@ export const JOBS_DIR = 'jobs';
|
|
|
8
8
|
const LOGICAL_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
9
9
|
export const FILE_MIME_TYPES = ['application/pdf', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'image/png', 'image/jpeg'];
|
|
10
10
|
export const providerFileId = (value) => typeof value === 'string' && /^[A-Za-z0-9!._,=-]{1,256}$/.test(value);
|
|
11
|
-
function literalObject(node) {
|
|
11
|
+
function literalObject(node, what = 'trigger') {
|
|
12
12
|
const value = node;
|
|
13
13
|
if (value?.type === 'Literal')
|
|
14
14
|
return value.value;
|
|
15
15
|
if (value?.type === 'ArrayExpression')
|
|
16
|
-
return value.elements.map(literalObject);
|
|
16
|
+
return value.elements.map(element => literalObject(element, what));
|
|
17
17
|
if (value?.type !== 'ObjectExpression')
|
|
18
|
-
throw new Error(
|
|
18
|
+
throw new Error(`${what} must be a literal object`);
|
|
19
19
|
const result = {};
|
|
20
20
|
for (const property of value.properties ?? []) {
|
|
21
21
|
if (property.type !== 'Property' || property.computed || property.kind !== 'init' || property.method)
|
|
22
|
-
throw new Error(
|
|
22
|
+
throw new Error(`${what} must be a literal object`);
|
|
23
23
|
const key = property.key.name ?? property.key.value;
|
|
24
24
|
if (typeof key !== 'string' || Object.hasOwn(result, key))
|
|
25
|
-
throw new Error(
|
|
26
|
-
Object.defineProperty(result, key, { value: literalObject(property.value), enumerable: true });
|
|
25
|
+
throw new Error(`${what} contains an invalid or repeated field`);
|
|
26
|
+
Object.defineProperty(result, key, { value: literalObject(property.value, what), enumerable: true });
|
|
27
27
|
}
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
|
+
const plainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
31
|
+
/** The action's `export const expose = { description, input }`, read without importing it: undefined when the module has none, does not parse, or declares anything but a literal object of that shape. */
|
|
32
|
+
export async function actionExposure(file) {
|
|
33
|
+
let statements;
|
|
34
|
+
try {
|
|
35
|
+
statements = parseModule(await readFile(file, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
for (const statement of statements) {
|
|
41
|
+
if (statement.type !== 'ExportNamedDeclaration' || statement.declaration?.type !== 'VariableDeclaration' || statement.declaration.kind !== 'const')
|
|
42
|
+
continue;
|
|
43
|
+
for (const declaration of statement.declaration.declarations) {
|
|
44
|
+
if (declaration.id.type !== 'Identifier' || declaration.id.name !== 'expose')
|
|
45
|
+
continue;
|
|
46
|
+
let value;
|
|
47
|
+
try {
|
|
48
|
+
value = literalObject(declaration.init, 'expose');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (!plainObject(value) || Object.keys(value).some(key => key !== 'description' && key !== 'input') || typeof value.description !== 'string' || !plainObject(value.input))
|
|
54
|
+
return undefined;
|
|
55
|
+
return { description: value.description, input: value.input };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
30
60
|
export function eventTrigger(value) {
|
|
31
61
|
if (value?.type === 'file.created') {
|
|
32
62
|
const t = value;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
-
import { type IsomorphClient, type IsomorphUser, type JobRun, type JobRunSummary } from './index.js';
|
|
2
|
+
import { type ActionExposure, type IsomorphClient, type IsomorphUser, type JobRun, type JobRunSummary } from './index.js';
|
|
3
3
|
import { type JobDefinition } from './jobs.js';
|
|
4
4
|
import { type LocalGatewayOptions } from './local-gateway.js';
|
|
5
5
|
export { JOBS_DIR, type JobContext } from './jobs.js';
|
|
@@ -37,6 +37,8 @@ export interface RuntimeInventory {
|
|
|
37
37
|
actions: string[];
|
|
38
38
|
/** The actions whose module says `export const trusted = true`: they act as the app on behalf of the caller. */
|
|
39
39
|
trusted: string[];
|
|
40
|
+
/** The actions whose module says `export const expose = { description, input }`, by name: what the kit gate records as exposed (ADR 0100). */
|
|
41
|
+
exposed: Record<string, ActionExposure>;
|
|
40
42
|
jobs: JobDefinition[];
|
|
41
43
|
}
|
|
42
44
|
/** 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, discoverJobs, exportsTrue, invokeJob, loadHandler, JOBS_DIR } from './jobs.js';
|
|
71
|
+
import { FILE_MIME_TYPES, providerFileId, actionExposure, discoverJobs, exportsTrue, invokeJob, loadHandler, JOBS_DIR } from './jobs.js';
|
|
72
72
|
import { LocalGateway } from './local-gateway.js';
|
|
73
73
|
export { JOBS_DIR } from './jobs.js';
|
|
74
74
|
const actionIdentity = new AsyncLocalStorage();
|
|
@@ -118,8 +118,10 @@ export async function discover(root) {
|
|
|
118
118
|
};
|
|
119
119
|
const actions = (await list(ACTIONS_DIR)).filter((name) => !name.startsWith(RESERVED_PREFIX));
|
|
120
120
|
const trusted = await Promise.all(actions.map((name) => exportsTrue(join(root, ACTIONS_DIR, `${name}.ts`), 'trusted')));
|
|
121
|
+
const exposures = await Promise.all(actions.map((name) => actionExposure(join(root, ACTIONS_DIR, `${name}.ts`))));
|
|
122
|
+
const exposed = Object.fromEntries(actions.flatMap((name, index) => exposures[index] ? [[name, exposures[index]]] : []));
|
|
121
123
|
const jobs = await discoverJobs(root);
|
|
122
|
-
return { actions, trusted: actions.filter((_, index) => trusted[index]), jobs };
|
|
124
|
+
return { actions, trusted: actions.filter((_, index) => trusted[index]), exposed, jobs };
|
|
123
125
|
}
|
|
124
126
|
// ---- Cron ------------------------------------------------------------------
|
|
125
127
|
/** One field of a five-field cron expression: numbers, `*`, lists, ranges and steps, no names. */
|
|
@@ -313,7 +315,7 @@ export class Runs {
|
|
|
313
315
|
}
|
|
314
316
|
export class Runtime {
|
|
315
317
|
options;
|
|
316
|
-
inventory = { actions: [], trusted: [], jobs: [] };
|
|
318
|
+
inventory = { actions: [], trusted: [], exposed: {}, jobs: [] };
|
|
317
319
|
localGateway;
|
|
318
320
|
running = new Set();
|
|
319
321
|
timers = [];
|
|
@@ -826,7 +828,7 @@ export class Runtime {
|
|
|
826
828
|
const owner = context.user.id;
|
|
827
829
|
switch (name) {
|
|
828
830
|
case PING_ACTION: {
|
|
829
|
-
const answer = { ok: true, mode: this.options.mode, actions: this.inventory.actions, trusted: this.inventory.trusted, jobs: this.inventory.jobs.map((job) => job.name) };
|
|
831
|
+
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) };
|
|
830
832
|
return this.options.mode === 'gate' ? { ...answer, loadFailures: await this.loadFailures() } : answer;
|
|
831
833
|
}
|
|
832
834
|
case ENQUEUE_ACTION: {
|