@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 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('trigger must be a literal object');
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('trigger must be a literal object');
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('trigger contains an invalid or repeated field');
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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",