@isomorph.ai/app-sdk 1.17.0 → 1.18.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
@@ -172,6 +172,38 @@ export interface WarehouseViewReadRequest {
172
172
  limit?: number;
173
173
  };
174
174
  }
175
+ /** One row to append: every key an approved column of the alias; values typed by the catalog, `null` for any column. */
176
+ export type WarehouseAppendRow = Record<string, string | number | boolean | null>;
177
+ /**
178
+ * Land rows in the warehouse. The resource is an alias IT approved with access `append` (an alias reads or
179
+ * appends, never both). Up to 500 rows per call; a larger report is several calls, each with its own fresh key.
180
+ * On a lane without a warehouse binding the rows are validated and answered as a dry run; nothing is written.
181
+ */
182
+ export interface WarehouseTableAppendRequest {
183
+ operation: 'warehouse.table.append';
184
+ resource: string;
185
+ input: {
186
+ /** 1 to 500 rows. */
187
+ rows: WarehouseAppendRow[];
188
+ };
189
+ /** A fresh UUID per append, reused only to retry that same append. */
190
+ idempotencyKey: string;
191
+ }
192
+ /**
193
+ * The SMS a company-owned number received. The resource is a number alias of a `phone` connection. An app waiting
194
+ * for a one-time code polls this every few seconds with `since` set to when it asked, and reads the code out of
195
+ * the message body itself; there is no OTP concept on the platform.
196
+ */
197
+ export interface PhoneSmsListRequest {
198
+ operation: 'phone.sms.list';
199
+ resource: string;
200
+ input: {
201
+ /** ISO 8601 instant; only messages received at or after it are returned. */
202
+ since: string;
203
+ /** Only messages from this sender id or number, up to 32 characters. */
204
+ from?: string;
205
+ };
206
+ }
175
207
  /** Gmail runs as the signed-in person after their consent. Resources are approved mailbox aliases (`inbox`). */
176
208
  export interface GmailThreadListRequest {
177
209
  operation: 'gmail.thread.list';
@@ -497,7 +529,7 @@ export interface ServiceRequestResult {
497
529
  headers: Record<string, string>;
498
530
  body: unknown;
499
531
  }
500
- export type IntegrationRequest = ServiceRequest | SlackChannelHistoryRequest | SlackMessagePostRequest | SlackMessageUpdateRequest | SlackMessageDeleteRequest | SlackDmSendRequest | SlackUserLookupRequest | WarehouseViewReadRequest | GmailThreadListRequest | GmailThreadReadRequest | GmailMessageReadRequest | GmailAttachmentReadRequest | GmailMessageSendRequest | BasecampProjectListRequest | BasecampTodolistListRequest | BasecampTodoListRequest | BasecampTodoCreateRequest | BasecampTodoCompleteRequest | BasecampMessageListRequest | BasecampMessagePostRequest | BasecampCommentListRequest | BasecampCommentPostRequest | BasecampCampfirePostRequest | BasecampPeopleListRequest | SharePointListListRequest | SharePointItemListRequest | SharePointItemCreateRequest | SharePointItemUpdateRequest | SharePointDriveListRequest | SharePointFileListRequest | SharePointFileSearchRequest | SharePointFileReadRequest | SharePointFileUploadRequest;
532
+ export type IntegrationRequest = ServiceRequest | SlackChannelHistoryRequest | SlackMessagePostRequest | SlackMessageUpdateRequest | SlackMessageDeleteRequest | SlackDmSendRequest | SlackUserLookupRequest | WarehouseViewReadRequest | WarehouseTableAppendRequest | PhoneSmsListRequest | GmailThreadListRequest | GmailThreadReadRequest | GmailMessageReadRequest | GmailAttachmentReadRequest | GmailMessageSendRequest | BasecampProjectListRequest | BasecampTodolistListRequest | BasecampTodoListRequest | BasecampTodoCreateRequest | BasecampTodoCompleteRequest | BasecampMessageListRequest | BasecampMessagePostRequest | BasecampCommentListRequest | BasecampCommentPostRequest | BasecampCampfirePostRequest | BasecampPeopleListRequest | SharePointListListRequest | SharePointItemListRequest | SharePointItemCreateRequest | SharePointItemUpdateRequest | SharePointDriveListRequest | SharePointFileListRequest | SharePointFileSearchRequest | SharePointFileReadRequest | SharePointFileUploadRequest;
501
533
  export type IntegrationOperation = IntegrationRequest['operation'];
502
534
  export interface SlackMessage {
503
535
  id: string;
@@ -534,6 +566,23 @@ export interface WarehouseViewReadResult {
534
566
  /** True when more rows matched than `limit`: the rows are incomplete, so never total or average them in code. */
535
567
  truncated: boolean;
536
568
  }
569
+ export interface WarehouseTableAppendResult {
570
+ /** How many rows landed (or, on a lane without a warehouse binding, would have). */
571
+ appended: number;
572
+ /** The write receipt's id; a retry with the same idempotencyKey answers the same receipt. */
573
+ jobId: string;
574
+ /** Present and true when the lane has no warehouse binding: the rows were validated, nothing was written. */
575
+ dryRun?: boolean;
576
+ }
577
+ export interface PhoneSmsMessage {
578
+ from: string;
579
+ body: string;
580
+ receivedAt: string;
581
+ }
582
+ /** Up to 50 messages received since `since`, newest first. */
583
+ export interface PhoneSmsListResult {
584
+ messages: PhoneSmsMessage[];
585
+ }
537
586
  export interface GmailThreadSummary {
538
587
  id: string;
539
588
  messageId: string;
@@ -753,7 +802,7 @@ export interface SharePointFileUploadResult {
753
802
  filename: string;
754
803
  webUrl: string;
755
804
  }
756
- export type IntegrationResult<R extends IntegrationRequest> = R extends ServiceRequest ? ServiceRequestResult : R extends SlackChannelHistoryRequest ? SlackChannelHistoryResult : R extends SlackMessagePostRequest | SlackMessageUpdateRequest | SlackMessageDeleteRequest ? SlackMessagePostResult : R extends SlackDmSendRequest ? SlackDmSendResult : R extends SlackUserLookupRequest ? SlackUserLookupResult : R extends WarehouseViewReadRequest ? WarehouseViewReadResult : R extends GmailThreadListRequest ? GmailThreadListResult : R extends GmailThreadReadRequest ? GmailThreadReadResult : R extends GmailMessageReadRequest ? GmailMessageReadResult : R extends GmailAttachmentReadRequest ? GmailAttachmentReadResult : R extends GmailMessageSendRequest ? GmailMessageSendResult : R extends BasecampProjectListRequest ? BasecampProjectListResult : R extends BasecampTodolistListRequest ? BasecampTodolistListResult : R extends BasecampTodoListRequest ? BasecampTodoListResult : R extends BasecampMessageListRequest ? BasecampMessageListResult : R extends BasecampCommentListRequest ? BasecampCommentListResult : R extends BasecampPeopleListRequest ? BasecampPeopleListResult : R extends BasecampTodoCreateRequest | BasecampMessagePostRequest | BasecampCommentPostRequest | BasecampCampfirePostRequest ? BasecampRecordingResult : R extends BasecampTodoCompleteRequest ? BasecampTodoCompleteResult : R extends SharePointListListRequest ? SharePointListListResult : R extends SharePointItemListRequest ? SharePointItemListResult : R extends SharePointItemCreateRequest ? SharePointItemCreateResult : R extends SharePointItemUpdateRequest ? SharePointItemUpdateResult : R extends SharePointDriveListRequest ? SharePointDriveListResult : R extends SharePointFileListRequest | SharePointFileSearchRequest ? SharePointFileListResult : R extends SharePointFileReadRequest ? SharePointFileReadResult : R extends SharePointFileUploadRequest ? SharePointFileUploadResult : never;
805
+ export type IntegrationResult<R extends IntegrationRequest> = R extends ServiceRequest ? ServiceRequestResult : R extends SlackChannelHistoryRequest ? SlackChannelHistoryResult : R extends SlackMessagePostRequest | SlackMessageUpdateRequest | SlackMessageDeleteRequest ? SlackMessagePostResult : R extends SlackDmSendRequest ? SlackDmSendResult : R extends SlackUserLookupRequest ? SlackUserLookupResult : R extends WarehouseViewReadRequest ? WarehouseViewReadResult : R extends WarehouseTableAppendRequest ? WarehouseTableAppendResult : R extends PhoneSmsListRequest ? PhoneSmsListResult : R extends GmailThreadListRequest ? GmailThreadListResult : R extends GmailThreadReadRequest ? GmailThreadReadResult : R extends GmailMessageReadRequest ? GmailMessageReadResult : R extends GmailAttachmentReadRequest ? GmailAttachmentReadResult : R extends GmailMessageSendRequest ? GmailMessageSendResult : R extends BasecampProjectListRequest ? BasecampProjectListResult : R extends BasecampTodolistListRequest ? BasecampTodolistListResult : R extends BasecampTodoListRequest ? BasecampTodoListResult : R extends BasecampMessageListRequest ? BasecampMessageListResult : R extends BasecampCommentListRequest ? BasecampCommentListResult : R extends BasecampPeopleListRequest ? BasecampPeopleListResult : R extends BasecampTodoCreateRequest | BasecampMessagePostRequest | BasecampCommentPostRequest | BasecampCampfirePostRequest ? BasecampRecordingResult : R extends BasecampTodoCompleteRequest ? BasecampTodoCompleteResult : R extends SharePointListListRequest ? SharePointListListResult : R extends SharePointItemListRequest ? SharePointItemListResult : R extends SharePointItemCreateRequest ? SharePointItemCreateResult : R extends SharePointItemUpdateRequest ? SharePointItemUpdateResult : R extends SharePointDriveListRequest ? SharePointDriveListResult : R extends SharePointFileListRequest | SharePointFileSearchRequest ? SharePointFileListResult : R extends SharePointFileReadRequest ? SharePointFileReadResult : R extends SharePointFileUploadRequest ? SharePointFileUploadResult : never;
757
806
  export type IntegrationConnectResult = {
758
807
  status: 'connected';
759
808
  accountLabel: string;
package/dist/index.js CHANGED
@@ -30,6 +30,20 @@ function warehouseReadProblem({ columns, aggregates }) {
30
30
  return { field: 'columns', message: 'columns may use "*" only as the sole entry' };
31
31
  return undefined;
32
32
  }
33
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
34
+ // The append rules governance states in prose: 1 to 500 rows per call (a
35
+ // larger report is several calls) and a UUID idempotency key per call.
36
+ // Governance checks the columns and the value types against the catalog.
37
+ function warehouseAppendProblem({ input, idempotencyKey }) {
38
+ const rows = input?.rows;
39
+ if (!Array.isArray(rows) || rows.length === 0)
40
+ return { field: 'rows', message: 'a warehouse append needs 1 to 500 rows' };
41
+ if (rows.length > 500)
42
+ return { field: 'rows', message: 'a warehouse append takes at most 500 rows per call; send a larger report as several calls, each with its own idempotencyKey' };
43
+ if (typeof idempotencyKey !== 'string' || !UUID_PATTERN.test(idempotencyKey))
44
+ return { field: 'idempotencyKey', message: 'a warehouse append needs a fresh UUID idempotencyKey per call' };
45
+ return undefined;
46
+ }
33
47
  /** The AI reason code carried by an error, when it is a governed-AI refusal. */
34
48
  export function aiErrorCode(error) {
35
49
  const code = error instanceof IsomorphError ? error.details?.code : undefined;
@@ -751,8 +765,9 @@ export function createClient(options = {}) {
751
765
  // `isomorph check` (which runs retained journeys through this client)
752
766
  // and the deployed app fail the same way at build time, not at runtime.
753
767
  const checked = validateIntegrationExecuteInput(request.operation, request.input);
754
- const problem = !checked.ok ? checked.problem
755
- : request.operation === 'warehouse.view.read' ? warehouseReadProblem(request.input ?? {}) : undefined;
768
+ const problem = (request.operation === 'warehouse.table.append' ? warehouseAppendProblem(request) : undefined)
769
+ ?? (!checked.ok ? checked.problem
770
+ : request.operation === 'warehouse.view.read' ? warehouseReadProblem(request.input ?? {}) : undefined);
756
771
  if (problem) {
757
772
  return Promise.reject(new IsomorphError('VALIDATION_FAILED', problem.message, undefined, { code: 'INPUT_INVALID', field: problem.field }));
758
773
  }
@@ -63,6 +63,15 @@ export type IntegrationExecuteInputFieldRule = {
63
63
  maxProperties: number;
64
64
  types: readonly IntegrationExecuteInputRecordValueType[];
65
65
  maxLength?: number;
66
+ } | {
67
+ kind: 'record[]';
68
+ required: boolean;
69
+ minItems: number;
70
+ maxItems: number;
71
+ minProperties: number;
72
+ maxProperties: number;
73
+ types: readonly IntegrationExecuteInputRecordValueType[];
74
+ maxLength?: number;
66
75
  };
67
76
  /** The scalar types a `value` input accepts, alone or in its list form. */
68
77
  export type IntegrationExecuteInputValueType = 'string' | 'number' | 'boolean';
@@ -771,6 +780,23 @@ export declare const IntegrationExecuteInputRules: {
771
780
  };
772
781
  };
773
782
  };
783
+ readonly "phone.sms.list": {
784
+ readonly idempotencyKey: false;
785
+ readonly fields: {
786
+ readonly since: {
787
+ readonly kind: "string";
788
+ readonly required: true;
789
+ readonly minLength: 1;
790
+ readonly pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})$";
791
+ };
792
+ readonly from: {
793
+ readonly kind: "string";
794
+ readonly required: false;
795
+ readonly minLength: 0;
796
+ readonly maxLength: 32;
797
+ };
798
+ };
799
+ };
774
800
  readonly "warehouse.view.read": {
775
801
  readonly idempotencyKey: false;
776
802
  readonly fields: {
@@ -880,6 +906,21 @@ export declare const IntegrationExecuteInputRules: {
880
906
  };
881
907
  };
882
908
  };
909
+ readonly "warehouse.table.append": {
910
+ readonly idempotencyKey: true;
911
+ readonly fields: {
912
+ readonly rows: {
913
+ readonly kind: "record[]";
914
+ readonly required: true;
915
+ readonly minItems: 1;
916
+ readonly maxItems: 500;
917
+ readonly minProperties: 1;
918
+ readonly maxProperties: 64;
919
+ readonly types: readonly ["string", "number", "boolean", "null"];
920
+ readonly maxLength: 4000;
921
+ };
922
+ };
923
+ };
883
924
  readonly "service.request": {
884
925
  readonly idempotencyKey: false;
885
926
  readonly fields: {
@@ -241,6 +241,13 @@ export const IntegrationExecuteInputRules = {
241
241
  conflict: { kind: "enum", required: false, values: ["rename", "replace", "fail"], default: "rename" },
242
242
  },
243
243
  },
244
+ "phone.sms.list": {
245
+ idempotencyKey: false,
246
+ fields: {
247
+ since: { kind: "string", required: true, minLength: 1, pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})$" },
248
+ from: { kind: "string", required: false, minLength: 0, maxLength: 32 },
249
+ },
250
+ },
244
251
  "warehouse.view.read": {
245
252
  idempotencyKey: false,
246
253
  fields: {
@@ -263,6 +270,12 @@ export const IntegrationExecuteInputRules = {
263
270
  limit: { kind: "integer", required: false, minimum: 1, maximum: 1000, default: 100 },
264
271
  },
265
272
  },
273
+ "warehouse.table.append": {
274
+ idempotencyKey: true,
275
+ fields: {
276
+ rows: { kind: "record[]", required: true, minItems: 1, maxItems: 500, minProperties: 1, maxProperties: 64, types: ["string", "number", "boolean", "null"], maxLength: 4000 },
277
+ },
278
+ },
266
279
  "service.request": {
267
280
  idempotencyKey: false,
268
281
  fields: {
@@ -337,6 +350,19 @@ function checkFields(operation, prefix, fields, given, nullIsAbsent) {
337
350
  }
338
351
  return { ok: true, value: out };
339
352
  }
353
+ /** One record: a bounded count of free keys, every value one of the record's scalar types. */
354
+ function checkRecord(path, field, value) {
355
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
356
+ return refuse(path, `${path} must be a JSON object`);
357
+ const keys = Object.keys(value);
358
+ if (keys.length < field.minProperties || keys.length > field.maxProperties)
359
+ return refuse(path, `${path} must have ${field.minProperties} to ${field.maxProperties} properties`);
360
+ for (const key of keys) {
361
+ if (!isScalar(field.types, field.maxLength, value[key]))
362
+ return refuse(`${path}.${key}`, `${path}.${key} must be ${either(scalarShapes(field.types, field.maxLength))}`);
363
+ }
364
+ return { ok: true, value };
365
+ }
340
366
  function checkField(operation, path, name, field, value) {
341
367
  switch (field.kind) {
342
368
  case 'object':
@@ -347,15 +373,17 @@ function checkField(operation, path, name, field, value) {
347
373
  if (typeof value !== 'boolean')
348
374
  return refuse(path, `${path} must be a boolean`);
349
375
  return { ok: true, value };
350
- case 'record': {
351
- if (value === null || typeof value !== 'object' || Array.isArray(value))
352
- return refuse(path, `${path} must be a JSON object`);
353
- const keys = Object.keys(value);
354
- if (keys.length < field.minProperties || keys.length > field.maxProperties)
355
- return refuse(path, `${path} must have ${field.minProperties} to ${field.maxProperties} properties`);
356
- for (const key of keys) {
357
- if (!isScalar(field.types, field.maxLength, value[key]))
358
- return refuse(`${path}.${key}`, `${path}.${key} must be ${either(scalarShapes(field.types, field.maxLength))}`);
376
+ case 'record':
377
+ return checkRecord(path, field, value);
378
+ case 'record[]': {
379
+ if (!Array.isArray(value) || value.length < field.minItems || value.length > field.maxItems)
380
+ return refuse(path, `${path} must list ${field.minItems} to ${field.maxItems} items`);
381
+ for (const [index, item] of value.entries()) {
382
+ if (item === null || typeof item !== 'object' || Array.isArray(item))
383
+ return refuse(path, `${path}[${index}] is not a valid ${name} entry`);
384
+ const entry = checkRecord(`${path}[${index}]`, field, item);
385
+ if (!entry.ok)
386
+ return entry;
359
387
  }
360
388
  return { ok: true, value };
361
389
  }
@@ -217,6 +217,13 @@ export declare const IntegrationOperations: {
217
217
  readonly declaredMode: false;
218
218
  readonly idempotencyKey: true;
219
219
  };
220
+ readonly "phone.sms.list": {
221
+ readonly kind: "saas";
222
+ readonly identities: readonly ["app"];
223
+ readonly readOnly: true;
224
+ readonly declaredMode: false;
225
+ readonly idempotencyKey: false;
226
+ };
220
227
  readonly "warehouse.view.read": {
221
228
  readonly kind: "database";
222
229
  readonly identities: readonly ["app"];
@@ -224,6 +231,13 @@ export declare const IntegrationOperations: {
224
231
  readonly declaredMode: false;
225
232
  readonly idempotencyKey: false;
226
233
  };
234
+ readonly "warehouse.table.append": {
235
+ readonly kind: "database";
236
+ readonly identities: readonly ["app"];
237
+ readonly readOnly: false;
238
+ readonly declaredMode: false;
239
+ readonly idempotencyKey: true;
240
+ };
227
241
  readonly "service.request": {
228
242
  readonly kind: "service";
229
243
  readonly identities: readonly ["app"];
@@ -285,6 +285,15 @@ export const IntegrationOperations = {
285
285
  "declaredMode": false,
286
286
  "idempotencyKey": true
287
287
  },
288
+ "phone.sms.list": {
289
+ "kind": "saas",
290
+ "identities": [
291
+ "app"
292
+ ],
293
+ "readOnly": true,
294
+ "declaredMode": false,
295
+ "idempotencyKey": false
296
+ },
288
297
  "warehouse.view.read": {
289
298
  "kind": "database",
290
299
  "identities": [
@@ -294,6 +303,15 @@ export const IntegrationOperations = {
294
303
  "declaredMode": false,
295
304
  "idempotencyKey": false
296
305
  },
306
+ "warehouse.table.append": {
307
+ "kind": "database",
308
+ "identities": [
309
+ "app"
310
+ ],
311
+ "readOnly": false,
312
+ "declaredMode": false,
313
+ "idempotencyKey": true
314
+ },
297
315
  "service.request": {
298
316
  "kind": "service",
299
317
  "identities": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",