@isomorph.ai/app-sdk 1.17.0 → 1.19.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,49 @@ 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
+ }
207
+ /**
208
+ * The number a phone alias maps to (contracts 0.83.0, ADR 0029). The resource is a number alias of a `phone`
209
+ * connection, declared like `phone.sms.list`'s; the answer is the E.164 number IT mapped it to, so the app (or an
210
+ * agent it drives) can type it into a portal that signs in by mobile number. It takes no input: leave `input`
211
+ * out and the client sends `input: {}`, which the platform requires on the wire.
212
+ */
213
+ export interface PhoneNumberGetRequest {
214
+ operation: 'phone.number.get';
215
+ resource: string;
216
+ input?: Record<string, never>;
217
+ }
175
218
  /** Gmail runs as the signed-in person after their consent. Resources are approved mailbox aliases (`inbox`). */
176
219
  export interface GmailThreadListRequest {
177
220
  operation: 'gmail.thread.list';
@@ -497,7 +540,7 @@ export interface ServiceRequestResult {
497
540
  headers: Record<string, string>;
498
541
  body: unknown;
499
542
  }
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;
543
+ export type IntegrationRequest = ServiceRequest | SlackChannelHistoryRequest | SlackMessagePostRequest | SlackMessageUpdateRequest | SlackMessageDeleteRequest | SlackDmSendRequest | SlackUserLookupRequest | WarehouseViewReadRequest | WarehouseTableAppendRequest | PhoneSmsListRequest | PhoneNumberGetRequest | 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
544
  export type IntegrationOperation = IntegrationRequest['operation'];
502
545
  export interface SlackMessage {
503
546
  id: string;
@@ -534,6 +577,27 @@ export interface WarehouseViewReadResult {
534
577
  /** True when more rows matched than `limit`: the rows are incomplete, so never total or average them in code. */
535
578
  truncated: boolean;
536
579
  }
580
+ export interface WarehouseTableAppendResult {
581
+ /** How many rows landed (or, on a lane without a warehouse binding, would have). */
582
+ appended: number;
583
+ /** The write receipt's id; a retry with the same idempotencyKey answers the same receipt. */
584
+ jobId: string;
585
+ /** Present and true when the lane has no warehouse binding: the rows were validated, nothing was written. */
586
+ dryRun?: boolean;
587
+ }
588
+ export interface PhoneSmsMessage {
589
+ from: string;
590
+ body: string;
591
+ receivedAt: string;
592
+ }
593
+ /** Up to 50 messages received since `since`, newest first. */
594
+ export interface PhoneSmsListResult {
595
+ messages: PhoneSmsMessage[];
596
+ }
597
+ export interface PhoneNumberGetResult {
598
+ /** The E.164 number the alias maps to, e.g. `+919876543210`. */
599
+ number: string;
600
+ }
537
601
  export interface GmailThreadSummary {
538
602
  id: string;
539
603
  messageId: string;
@@ -753,7 +817,7 @@ export interface SharePointFileUploadResult {
753
817
  filename: string;
754
818
  webUrl: string;
755
819
  }
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;
820
+ 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 PhoneNumberGetRequest ? PhoneNumberGetResult : 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
821
  export type IntegrationConnectResult = {
758
822
  status: 'connected';
759
823
  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,14 +765,18 @@ 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
  }
759
774
  // Exactly the contract's fields go on the wire: metadata declares which
760
775
  // operations accept `mode`; identity and stray fields are not sent.
761
- const { operation, resource, input, idempotencyKey, mode } = request;
776
+ // phone.number.get takes no input, but the contract keeps `input`
777
+ // required on the wire (ADR 0029), so an omitted one goes out as {}.
778
+ const { operation, resource, input: given, idempotencyKey, mode } = request;
779
+ const input = given === undefined && operation === 'phone.number.get' ? {} : given;
762
780
  return transport.json('/_harbour/integrations/execute', {
763
781
  method: 'POST', body: JSON.stringify({ connection, operation, ...(operation !== 'service.request' ? { resource } : {}), ...(input !== undefined ? { input } : {}), ...(idempotencyKey !== undefined ? { idempotencyKey } : {}), ...(IntegrationOperations[operation]?.declaredMode && mode !== undefined ? { mode } : {}) }),
764
782
  });
@@ -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,27 @@ 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
+ };
800
+ readonly "phone.number.get": {
801
+ readonly idempotencyKey: false;
802
+ readonly fields: {};
803
+ };
774
804
  readonly "warehouse.view.read": {
775
805
  readonly idempotencyKey: false;
776
806
  readonly fields: {
@@ -880,6 +910,21 @@ export declare const IntegrationExecuteInputRules: {
880
910
  };
881
911
  };
882
912
  };
913
+ readonly "warehouse.table.append": {
914
+ readonly idempotencyKey: true;
915
+ readonly fields: {
916
+ readonly rows: {
917
+ readonly kind: "record[]";
918
+ readonly required: true;
919
+ readonly minItems: 1;
920
+ readonly maxItems: 500;
921
+ readonly minProperties: 1;
922
+ readonly maxProperties: 64;
923
+ readonly types: readonly ["string", "number", "boolean", "null"];
924
+ readonly maxLength: 4000;
925
+ };
926
+ };
927
+ };
883
928
  readonly "service.request": {
884
929
  readonly idempotencyKey: false;
885
930
  readonly fields: {
@@ -241,6 +241,17 @@ 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
+ },
251
+ "phone.number.get": {
252
+ idempotencyKey: false,
253
+ fields: {},
254
+ },
244
255
  "warehouse.view.read": {
245
256
  idempotencyKey: false,
246
257
  fields: {
@@ -263,6 +274,12 @@ export const IntegrationExecuteInputRules = {
263
274
  limit: { kind: "integer", required: false, minimum: 1, maximum: 1000, default: 100 },
264
275
  },
265
276
  },
277
+ "warehouse.table.append": {
278
+ idempotencyKey: true,
279
+ fields: {
280
+ rows: { kind: "record[]", required: true, minItems: 1, maxItems: 500, minProperties: 1, maxProperties: 64, types: ["string", "number", "boolean", "null"], maxLength: 4000 },
281
+ },
282
+ },
266
283
  "service.request": {
267
284
  idempotencyKey: false,
268
285
  fields: {
@@ -337,6 +354,19 @@ function checkFields(operation, prefix, fields, given, nullIsAbsent) {
337
354
  }
338
355
  return { ok: true, value: out };
339
356
  }
357
+ /** One record: a bounded count of free keys, every value one of the record's scalar types. */
358
+ function checkRecord(path, field, value) {
359
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
360
+ return refuse(path, `${path} must be a JSON object`);
361
+ const keys = Object.keys(value);
362
+ if (keys.length < field.minProperties || keys.length > field.maxProperties)
363
+ return refuse(path, `${path} must have ${field.minProperties} to ${field.maxProperties} properties`);
364
+ for (const key of keys) {
365
+ if (!isScalar(field.types, field.maxLength, value[key]))
366
+ return refuse(`${path}.${key}`, `${path}.${key} must be ${either(scalarShapes(field.types, field.maxLength))}`);
367
+ }
368
+ return { ok: true, value };
369
+ }
340
370
  function checkField(operation, path, name, field, value) {
341
371
  switch (field.kind) {
342
372
  case 'object':
@@ -347,15 +377,17 @@ function checkField(operation, path, name, field, value) {
347
377
  if (typeof value !== 'boolean')
348
378
  return refuse(path, `${path} must be a boolean`);
349
379
  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))}`);
380
+ case 'record':
381
+ return checkRecord(path, field, value);
382
+ case 'record[]': {
383
+ if (!Array.isArray(value) || value.length < field.minItems || value.length > field.maxItems)
384
+ return refuse(path, `${path} must list ${field.minItems} to ${field.maxItems} items`);
385
+ for (const [index, item] of value.entries()) {
386
+ if (item === null || typeof item !== 'object' || Array.isArray(item))
387
+ return refuse(path, `${path}[${index}] is not a valid ${name} entry`);
388
+ const entry = checkRecord(`${path}[${index}]`, field, item);
389
+ if (!entry.ok)
390
+ return entry;
359
391
  }
360
392
  return { ok: true, value };
361
393
  }
@@ -217,6 +217,20 @@ 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
+ };
227
+ readonly "phone.number.get": {
228
+ readonly kind: "saas";
229
+ readonly identities: readonly ["app"];
230
+ readonly readOnly: true;
231
+ readonly declaredMode: false;
232
+ readonly idempotencyKey: false;
233
+ };
220
234
  readonly "warehouse.view.read": {
221
235
  readonly kind: "database";
222
236
  readonly identities: readonly ["app"];
@@ -224,6 +238,13 @@ export declare const IntegrationOperations: {
224
238
  readonly declaredMode: false;
225
239
  readonly idempotencyKey: false;
226
240
  };
241
+ readonly "warehouse.table.append": {
242
+ readonly kind: "database";
243
+ readonly identities: readonly ["app"];
244
+ readonly readOnly: false;
245
+ readonly declaredMode: false;
246
+ readonly idempotencyKey: true;
247
+ };
227
248
  readonly "service.request": {
228
249
  readonly kind: "service";
229
250
  readonly identities: readonly ["app"];
@@ -285,6 +285,24 @@ 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
+ },
297
+ "phone.number.get": {
298
+ "kind": "saas",
299
+ "identities": [
300
+ "app"
301
+ ],
302
+ "readOnly": true,
303
+ "declaredMode": false,
304
+ "idempotencyKey": false
305
+ },
288
306
  "warehouse.view.read": {
289
307
  "kind": "database",
290
308
  "identities": [
@@ -294,6 +312,15 @@ export const IntegrationOperations = {
294
312
  "declaredMode": false,
295
313
  "idempotencyKey": false
296
314
  },
315
+ "warehouse.table.append": {
316
+ "kind": "database",
317
+ "identities": [
318
+ "app"
319
+ ],
320
+ "readOnly": false,
321
+ "declaredMode": false,
322
+ "idempotencyKey": true
323
+ },
297
324
  "service.request": {
298
325
  "kind": "service",
299
326
  "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.19.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",