@fourier-labs/harbour-contracts 0.103.0 → 0.105.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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schema": "harbour.contract-release/1.0",
3
- "repositoryVersion": "0.103.0.0",
4
- "publishVersion": "0.103.0",
5
- "publishTag": "v0.103.0",
6
- "schemasDigest": "sha256:834e5cbeb45c05b1631152db45981c272b9e2f34ff73509d22fc40a4f2a1d43a",
3
+ "repositoryVersion": "0.105.0.0",
4
+ "publishVersion": "0.105.0",
5
+ "publishTag": "v0.105.0",
6
+ "schemasDigest": "sha256:8844b241b17bcd774b50653cd753c379b432f5d67c49d69a36658bdfa1392c8e",
7
7
  "aiFixturesDigest": "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6",
8
- "generatedGoDigest": "sha256:b537d21916ff90f716f482eefc33371cf639ac969cadf69105fdd951c0805209",
9
- "generatedTypeScriptDigest": "sha256:937c0e54c5c268d3127adaec3265eb4dc6e21ca784301a60b69365dbe2fb4956",
8
+ "generatedGoDigest": "sha256:4773bbad6c269967b7c9b1619340e78be54565979dfa7128902a79700038313d",
9
+ "generatedTypeScriptDigest": "sha256:dd162dae0a43118ff104a9fec66bd48b834c90de90c8dfc6bac5c6a8933e94a4",
10
10
  "generatedExclusions": {
11
11
  "gen-go/contract_release.go": "contains the generated aggregate digests (self-reference)",
12
12
  "gen-ts/contract-release.ts": "contains the generated aggregate digests (self-reference)"
@@ -58,7 +58,7 @@
58
58
  "schema/identity-context.schema.json": "sha256:a0be3d3499b70f1641887ec66ab5b21ca5ab71063e9209290905738943a36e59",
59
59
  "schema/integration-connect-result.schema.json": "sha256:3f254b67ef739dc1fa395bdc8c4dc93359b16d481dba2a2d777aa5b6f34b7af8",
60
60
  "schema/integration-error-details.schema.json": "sha256:dd997865b6061c37e298a0ad8e7bcee6186422e04e3ccf63d24d64ef860036d3",
61
- "schema/integration-execute-request.schema.json": "sha256:f147e3e5b36d37b7c02442109ae31dda3e15f624c0628844c65e6ab2f55e618b",
61
+ "schema/integration-execute-request.schema.json": "sha256:c20616bcde10990be79cb40f4545c78e6d0c0bad4700098717ddf31ba43e536f",
62
62
  "schema/integration-execute-result.schema.json": "sha256:057b787c28b5f7339a95184fafa5deb7ce2c6441254ad8423ce3238c7dd6996c",
63
63
  "schema/internal-configuration-request.schema.json": "sha256:229d44a3458f68dceddfba802e5c1adcd95523a45af29b93b972706b88461a7d",
64
64
  "schema/kit-bundle-manifest.schema.json": "sha256:d8fc6ca9c30d791e785b021687e084e9171c0ea1a3a176d0f1582fcccf2c0e19",
@@ -114,11 +114,11 @@
114
114
  "gen-go/contracts_test.go": "sha256:56376287cb1548be1b35ab4c65d6f0b558c5d34885f7a47cb3d9e8d786607bf5",
115
115
  "gen-go/doc.go": "sha256:316d610ce568597a2ce6f38e9782e34fa5920f6f51ab0866e6d061b8a41cd822",
116
116
  "gen-go/error_category.go": "sha256:b2ddea22dcfa030f1a45e721e635485b0bd816cf1f38e7654628a653f5ed4583",
117
- "gen-go/integration_execute_input_rules.go": "sha256:7c516b0566367db217d1d3dab7a01bfd1188cda963ac2e9e094ec2a895039a1e",
118
- "gen-go/integration_execute_input_rules_test.go": "sha256:0b04eb79d07764796e72fc9f3407f5852d377e636af96f6cc5ee34e2691113ff",
117
+ "gen-go/integration_execute_input_rules.go": "sha256:fb789d2875e701d5b3c57fc7f4c6a2df46beb6daa1faf4ccd1f1d4b8bb1dbe2a",
118
+ "gen-go/integration_execute_input_rules_test.go": "sha256:9d4b541fbbd8ba08a18af627ac0fae035b6bdf72d78885687fc99bff5e9cde01",
119
119
  "gen-go/integration_operations.go": "sha256:a60ac73a7860b1b330b022ddde0c82e7ef146cc40ad22821013c8bdb73f9db28",
120
120
  "gen-go/schema_documents.go": "sha256:c19c0d540f602ee997a84b90e20702d9bc63ed7d27aec267e76c04655fc182e0",
121
- "gen-go/types.go": "sha256:0f5bbdc53a9b3cb1564b239f54d7dd2965713e1538d6b5c37b48703bd5c12b65",
121
+ "gen-go/types.go": "sha256:9086b46f8bb308e0090b7f42bcff8add777dde1f7a66a798b369d04b2adbc7f8",
122
122
  "gen-go/version_state.go": "sha256:4ef8abb126414c0025cf2c59bc9c79a74ef4a2ff89e8f316cbf05acc872371c1"
123
123
  },
124
124
  "generatedTypeScript": {
@@ -173,8 +173,8 @@
173
173
  "gen-ts/index.ts": "sha256:5e903bb40fbf6599dc0d65171eaef7bc62b52d4cea090124a0c51ef876ad2909",
174
174
  "gen-ts/integration-connect-result.ts": "sha256:4b10f169b6e1af364e9b418de21caa9fadbfb77b43cbff77d741cfae0b52180c",
175
175
  "gen-ts/integration-error-details.ts": "sha256:ec082f2d302c1ef6c0bf768134c2bde4efa5fd5acf9cc0d3ccc4676e857ae43c",
176
- "gen-ts/integration-execute-input-rules.ts": "sha256:30623d79e0db8e239adffd061c0f9c21af7986b542230620c211eb5870110e1a",
177
- "gen-ts/integration-execute-request.ts": "sha256:c946d525774ff18125368f67f990fcb637749248fc810f88fed440177d31708f",
176
+ "gen-ts/integration-execute-input-rules.ts": "sha256:f82a02cd108037364a4c7ce91faf53dc5b8d9b6c5f3e2dbc7592f572ef9c7500",
177
+ "gen-ts/integration-execute-request.ts": "sha256:44eca4395eaf3597552f0c4405622bec0388bd3777d346a99d207d0bb5955bf0",
178
178
  "gen-ts/integration-execute-result.ts": "sha256:e4772c27615de03284f7686aa49f92ff83b79a12587292129783443cf3ca0f80",
179
179
  "gen-ts/integration-operations.ts": "sha256:fc468da58ab6177e755bb21b0907d7c0a510d6dd291476f0abc1c832c7820734",
180
180
  "gen-ts/internal-configuration-request.ts": "sha256:7032437797473fa1c581d134adef2f3d4b4f4132f482cd17743e9d47097886bc",
@@ -1,7 +1,7 @@
1
- export declare const ContractRepositoryVersion: "0.103.0.0";
2
- export declare const ContractPublishVersion: "0.103.0";
3
- export declare const ContractPublishTag: "v0.103.0";
4
- export declare const ContractSchemasDigest: "sha256:834e5cbeb45c05b1631152db45981c272b9e2f34ff73509d22fc40a4f2a1d43a";
1
+ export declare const ContractRepositoryVersion: "0.105.0.0";
2
+ export declare const ContractPublishVersion: "0.105.0";
3
+ export declare const ContractPublishTag: "v0.105.0";
4
+ export declare const ContractSchemasDigest: "sha256:8844b241b17bcd774b50653cd753c379b432f5d67c49d69a36658bdfa1392c8e";
5
5
  export declare const ContractAiFixturesDigest: "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6";
6
- export declare const ContractGeneratedGoDigest: "sha256:b537d21916ff90f716f482eefc33371cf639ac969cadf69105fdd951c0805209";
7
- export declare const ContractGeneratedTypeScriptDigest: "sha256:937c0e54c5c268d3127adaec3265eb4dc6e21ca784301a60b69365dbe2fb4956";
6
+ export declare const ContractGeneratedGoDigest: "sha256:4773bbad6c269967b7c9b1619340e78be54565979dfa7128902a79700038313d";
7
+ export declare const ContractGeneratedTypeScriptDigest: "sha256:dd162dae0a43118ff104a9fec66bd48b834c90de90c8dfc6bac5c6a8933e94a4";
@@ -1,9 +1,9 @@
1
1
  /* eslint-disable */
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
- export const ContractRepositoryVersion = "0.103.0.0";
4
- export const ContractPublishVersion = "0.103.0";
5
- export const ContractPublishTag = "v0.103.0";
6
- export const ContractSchemasDigest = "sha256:834e5cbeb45c05b1631152db45981c272b9e2f34ff73509d22fc40a4f2a1d43a";
3
+ export const ContractRepositoryVersion = "0.105.0.0";
4
+ export const ContractPublishVersion = "0.105.0";
5
+ export const ContractPublishTag = "v0.105.0";
6
+ export const ContractSchemasDigest = "sha256:8844b241b17bcd774b50653cd753c379b432f5d67c49d69a36658bdfa1392c8e";
7
7
  export const ContractAiFixturesDigest = "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6";
8
- export const ContractGeneratedGoDigest = "sha256:b537d21916ff90f716f482eefc33371cf639ac969cadf69105fdd951c0805209";
9
- export const ContractGeneratedTypeScriptDigest = "sha256:937c0e54c5c268d3127adaec3265eb4dc6e21ca784301a60b69365dbe2fb4956";
8
+ export const ContractGeneratedGoDigest = "sha256:4773bbad6c269967b7c9b1619340e78be54565979dfa7128902a79700038313d";
9
+ export const ContractGeneratedTypeScriptDigest = "sha256:dd162dae0a43118ff104a9fec66bd48b834c90de90c8dfc6bac5c6a8933e94a4";
@@ -284,7 +284,7 @@ export declare const IntegrationExecuteInputRules: {
284
284
  readonly fields: {
285
285
  readonly to: {
286
286
  readonly kind: "string[]";
287
- readonly required: true;
287
+ readonly required: false;
288
288
  readonly minItems: 1;
289
289
  readonly maxItems: 50;
290
290
  readonly uniqueItems: true;
@@ -342,21 +342,27 @@ export declare const IntegrationExecuteInputRules: {
342
342
  readonly minItems: 1;
343
343
  readonly maxItems: 5;
344
344
  readonly fields: {
345
+ readonly file: {
346
+ readonly kind: "string";
347
+ readonly required: false;
348
+ readonly minLength: 1;
349
+ readonly maxLength: 1024;
350
+ };
345
351
  readonly filename: {
346
352
  readonly kind: "string";
347
- readonly required: true;
353
+ readonly required: false;
348
354
  readonly minLength: 1;
349
355
  readonly maxLength: 255;
350
356
  };
351
357
  readonly contentType: {
352
358
  readonly kind: "string";
353
- readonly required: true;
359
+ readonly required: false;
354
360
  readonly minLength: 1;
355
361
  readonly maxLength: 128;
356
362
  };
357
363
  readonly dataBase64: {
358
364
  readonly kind: "string";
359
- readonly required: true;
365
+ readonly required: false;
360
366
  readonly minLength: 1;
361
367
  readonly maxLength: 2796203;
362
368
  };
@@ -368,6 +374,25 @@ export declare const IntegrationExecuteInputRules: {
368
374
  readonly minLength: 0;
369
375
  readonly pattern: "^[A-Za-z0-9_-]{1,64}$";
370
376
  };
377
+ readonly replyTo: {
378
+ readonly kind: "string";
379
+ readonly required: false;
380
+ readonly minLength: 3;
381
+ readonly maxLength: 254;
382
+ readonly pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$";
383
+ };
384
+ readonly fromName: {
385
+ readonly kind: "string";
386
+ readonly required: false;
387
+ readonly minLength: 1;
388
+ readonly maxLength: 120;
389
+ };
390
+ readonly inReplyTo: {
391
+ readonly kind: "string";
392
+ readonly required: false;
393
+ readonly minLength: 0;
394
+ readonly pattern: "^<[^\\s<>]{1,250}>$";
395
+ };
371
396
  };
372
397
  };
373
398
  readonly "gmail.attachment.read": {
@@ -1465,21 +1490,27 @@ export declare const IntegrationExecuteInputRules: {
1465
1490
  readonly minItems: 1;
1466
1491
  readonly maxItems: 5;
1467
1492
  readonly fields: {
1493
+ readonly file: {
1494
+ readonly kind: "string";
1495
+ readonly required: false;
1496
+ readonly minLength: 1;
1497
+ readonly maxLength: 1024;
1498
+ };
1468
1499
  readonly filename: {
1469
1500
  readonly kind: "string";
1470
- readonly required: true;
1501
+ readonly required: false;
1471
1502
  readonly minLength: 1;
1472
1503
  readonly maxLength: 255;
1473
1504
  };
1474
1505
  readonly contentType: {
1475
1506
  readonly kind: "string";
1476
- readonly required: true;
1507
+ readonly required: false;
1477
1508
  readonly minLength: 1;
1478
1509
  readonly maxLength: 128;
1479
1510
  };
1480
1511
  readonly dataBase64: {
1481
1512
  readonly kind: "string";
1482
- readonly required: true;
1513
+ readonly required: false;
1483
1514
  readonly minLength: 1;
1484
1515
  readonly maxLength: 2796203;
1485
1516
  };
@@ -1508,21 +1539,27 @@ export declare const IntegrationExecuteInputRules: {
1508
1539
  readonly minItems: 1;
1509
1540
  readonly maxItems: 5;
1510
1541
  readonly fields: {
1542
+ readonly file: {
1543
+ readonly kind: "string";
1544
+ readonly required: false;
1545
+ readonly minLength: 1;
1546
+ readonly maxLength: 1024;
1547
+ };
1511
1548
  readonly filename: {
1512
1549
  readonly kind: "string";
1513
- readonly required: true;
1550
+ readonly required: false;
1514
1551
  readonly minLength: 1;
1515
1552
  readonly maxLength: 255;
1516
1553
  };
1517
1554
  readonly contentType: {
1518
1555
  readonly kind: "string";
1519
- readonly required: true;
1556
+ readonly required: false;
1520
1557
  readonly minLength: 1;
1521
1558
  readonly maxLength: 128;
1522
1559
  };
1523
1560
  readonly dataBase64: {
1524
1561
  readonly kind: "string";
1525
- readonly required: true;
1562
+ readonly required: false;
1526
1563
  readonly minLength: 1;
1527
1564
  readonly maxLength: 2796203;
1528
1565
  };
@@ -70,18 +70,22 @@ export const IntegrationExecuteInputRules = {
70
70
  "gmail.message.send": {
71
71
  idempotencyKey: true,
72
72
  fields: {
73
- to: { kind: "string[]", required: true, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
73
+ to: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
74
74
  cc: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
75
75
  bcc: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
76
76
  subject: { kind: "string", required: true, minLength: 1, maxLength: 256 },
77
77
  text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
78
78
  html: { kind: "string", required: false, minLength: 1, maxLength: 65536 },
79
79
  attachments: { kind: "object[]", required: false, minItems: 1, maxItems: 5, fields: {
80
- filename: { kind: "string", required: true, minLength: 1, maxLength: 255 },
81
- contentType: { kind: "string", required: true, minLength: 1, maxLength: 128 },
82
- dataBase64: { kind: "string", required: true, minLength: 1, maxLength: 2796203 },
80
+ file: { kind: "string", required: false, minLength: 1, maxLength: 1024 },
81
+ filename: { kind: "string", required: false, minLength: 1, maxLength: 255 },
82
+ contentType: { kind: "string", required: false, minLength: 1, maxLength: 128 },
83
+ dataBase64: { kind: "string", required: false, minLength: 1, maxLength: 2796203 },
83
84
  } },
84
85
  replyToMessageId: { kind: "string", required: false, minLength: 0, pattern: "^[A-Za-z0-9_-]{1,64}$" },
86
+ replyTo: { kind: "string", required: false, minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" },
87
+ fromName: { kind: "string", required: false, minLength: 1, maxLength: 120 },
88
+ inReplyTo: { kind: "string", required: false, minLength: 0, pattern: "^<[^\\s<>]{1,250}>$" },
85
89
  },
86
90
  },
87
91
  "gmail.attachment.read": {
@@ -458,9 +462,10 @@ export const IntegrationExecuteInputRules = {
458
462
  text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
459
463
  html: { kind: "string", required: false, minLength: 1, maxLength: 65536 },
460
464
  attachments: { kind: "object[]", required: false, minItems: 1, maxItems: 5, fields: {
461
- filename: { kind: "string", required: true, minLength: 1, maxLength: 255 },
462
- contentType: { kind: "string", required: true, minLength: 1, maxLength: 128 },
463
- dataBase64: { kind: "string", required: true, minLength: 1, maxLength: 2796203 },
465
+ file: { kind: "string", required: false, minLength: 1, maxLength: 1024 },
466
+ filename: { kind: "string", required: false, minLength: 1, maxLength: 255 },
467
+ contentType: { kind: "string", required: false, minLength: 1, maxLength: 128 },
468
+ dataBase64: { kind: "string", required: false, minLength: 1, maxLength: 2796203 },
464
469
  } },
465
470
  },
466
471
  },
@@ -470,9 +475,10 @@ export const IntegrationExecuteInputRules = {
470
475
  messageId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9._=-]{1,256}$" },
471
476
  text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
472
477
  attachments: { kind: "object[]", required: false, minItems: 1, maxItems: 5, fields: {
473
- filename: { kind: "string", required: true, minLength: 1, maxLength: 255 },
474
- contentType: { kind: "string", required: true, minLength: 1, maxLength: 128 },
475
- dataBase64: { kind: "string", required: true, minLength: 1, maxLength: 2796203 },
478
+ file: { kind: "string", required: false, minLength: 1, maxLength: 1024 },
479
+ filename: { kind: "string", required: false, minLength: 1, maxLength: 255 },
480
+ contentType: { kind: "string", required: false, minLength: 1, maxLength: 128 },
481
+ dataBase64: { kind: "string", required: false, minLength: 1, maxLength: 2796203 },
476
482
  } },
477
483
  },
478
484
  },
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but the declared-mode Slack operations (slack.channel.history, slack.message.post, slack.message.update, slack.message.delete; 0.71.0 widened history), and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A declared-mode operation runs as the app (for a post: the IT-approved per-app name from the grant, plus a "Posted by <app> on Isomorph" footer) or as the signed-in person (no name, no footer) — the same input either way — and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a call does. Per-operation input bounds (0.71.0, ADR 0018): slack.channel.history {limit? 1..100, default 15, cursor?, oldest?, latest?}; slack.message.post {text 1..4000 chars, threadId?, format? plain|mrkdwn} and a required idempotencyKey retained across retries of one explicit Send; slack.message.update {messageId, text, format?}; slack.message.delete {messageId}; slack.dm.send {to 1..8 addresses, text, format?} (app mode only) with a required idempotencyKey; slack.user.lookup {email}; gmail.thread.list {limit? 1..50, default 15, q?, labelIds? default [INBOX], pageToken?}; gmail.thread.read {threadId}; gmail.message.read {messageId}; gmail.attachment.read {messageId, attachmentId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, html? <= 64 KiB, attachments? 1..5 of <= 2 MiB each as base64, replyToMessageId?} and a required idempotencyKey like slack.message.post; warehouse.view.read {columns? 1..32, where? 1..16 filters, groupBy? 1..8 columns, aggregates? 1..16, orderBy? 1..8, limit? 1..1000, default 100} (0.70.0: a plain read names columns, an aggregate read names aggregates and optionally groupBy; the cross-field rules — exactly one of columns or aggregates, groupBy needs aggregates, a filter value that fits its op, orderBy naming a returned column, every named column approved — are enforced by governance, not expressible here; ADR 0017). Basecamp (0.76.0, ADR 0022; every id a string of 1..20 digits, every operation user mode): basecamp.project.list {pageToken?, projectStatus? active|archived} on the fixed `account` alias; on a project alias basecamp.todolist.list {pageToken?}; basecamp.todo.list {todolistId, completed?, pageToken?}; basecamp.todo.create {todolistId, content 1..1000 (the to-do title), description? <= 16384, contentFormat? plain|html, dueOn? YYYY-MM-DD, assigneeIds? 1..20, notify?} with a required idempotencyKey; basecamp.todo.complete {todoId} (idempotent at Basecamp, so no key); basecamp.message.list {pageToken?}; basecamp.message.post {subject 1..256, content 1..65536, contentFormat?} with a required idempotencyKey; basecamp.comment.list {recordingId, pageToken?}; basecamp.comment.post {recordingId, content 1..16384, contentFormat?} with a required idempotencyKey; basecamp.campfire.post {content 1..4000, contentFormat?} with a required idempotencyKey; basecamp.people.list {pageToken?}. `pageToken` is the previous page's `nextPageToken` (a Basecamp page number as digits). SharePoint (0.78.0, ADR 0024; every id a Microsoft Graph id — GraphId, 1..256 characters — every operation user mode on a site alias): sharepoint.list.list {pageToken?}; sharepoint.item.list {listId, filter? 1..512 (an OData filter on `fields/...`), pageToken?}; sharepoint.item.create {listId, fields (1..32 columns, each a string <= 4000, a number, a boolean or null)} with a required idempotencyKey; sharepoint.item.update {listId, itemId, fields} (a PATCH, idempotent at Graph, so no key); sharepoint.drive.list {pageToken?}; sharepoint.file.list {driveId?, folderId?, pageToken?} (no driveId: the site's default drive; no folderId: its root); sharepoint.file.search {q 1..256, driveId?, pageToken?}; sharepoint.file.read {fileId, driveId?}; sharepoint.file.upload {filename 1..255, contentType, dataBase64 <= 2 MiB, driveId?, folderId?, conflict? rename|replace|fail} with a required idempotencyKey. A SharePoint `pageToken` is Graph's `$skiptoken` (up to 2048 characters; the shared field is that wide, the Gmail and Basecamp operations narrow it back to 256). Google Workspace and Microsoft 365 (0.86.0, ADR 0032; every operation user mode; the seven personal providers run on their fixed alias, teams.* on a channel alias): calendar.calendar.list {pageToken?}; calendar.event.list {calendarId?, timeMin, timeMax, q?, pageToken?} (RFC 3339 date-times with a zone); calendar.event.read {calendarId?, eventId}; calendar.event.create {calendarId?, title 1..256, start, end, timeZone?, allDay?, description? <= 8000, location? <= 512, attendees? 1..100 emails, notify?} with a required idempotencyKey; calendar.event.update {calendarId?, eventId, title?, start?, end?, timeZone?, description?, location?, attendees?, notify?} and calendar.event.cancel {calendarId?, eventId, notify?} (both idempotent at Google, so no key); drive.file.list {folderId?, pageToken?}; drive.file.search {q, pageToken?}; drive.file.read {fileId, exportAs? text|csv|pdf} (exportAs renders a Google Docs, Sheets or Slides file; a binary file is downloaded as is); drive.file.upload {filename, contentType, dataBase64 <= 2 MiB, folderId?, conflict?} and drive.folder.create {title, folderId?} with a required idempotencyKey; docs.document.read {documentId}; docs.document.create {title, text? <= 64 KiB} with a required idempotencyKey; docs.document.append {documentId, text <= 64 KiB} and docs.text.replace {documentId, replacements 1..64 of {find 1..512, replace <= 4000, matchCase?}} (no key: the app owns the document's state); sheets.spreadsheet.read {spreadsheetId}; sheets.values.read {spreadsheetId, range} (A1 notation, 1..256); sheets.values.append {spreadsheetId, range, values, valueInput? raw|user} with a required idempotencyKey; sheets.values.update {spreadsheetId, range, values, valueInput?} and sheets.values.clear {spreadsheetId, range} (no key); sheets.spreadsheet.create {title, sheets? 1..20 titles} with a required idempotencyKey (`values` is 1..1000 rows of 1..100 cells, each a string <= 4000, a number, a boolean or null); outlook.message.list {folder?, q?, pageToken?}; outlook.message.read {messageId}; outlook.attachment.read {messageId, attachmentId}; outlook.message.send {to, cc?, bcc?, subject, text 1..4000, html?, attachments?} and outlook.message.reply {messageId, text 1..4000, attachments?} (0.95.0: existing EmailAttachment bounds; recipients remain provider-resolved) with a required idempotencyKey like gmail.message.send; outlook.event.list {start, end, pageToken?}; outlook.event.read {eventId}; outlook.event.create (as calendar.event.create without calendarId) with a required idempotencyKey; outlook.event.update (as calendar.event.update without calendarId) and outlook.event.cancel {eventId, comment? <= 4000} (no key); onedrive.file.list, onedrive.file.search, onedrive.file.read {fileId}, onedrive.file.upload and onedrive.folder.create as the drive.* operations (Graph ids, no exportAs); excel.worksheet.list {fileId, driveId?}; excel.range.read {fileId, driveId?, worksheet, address? 1..64} (no address: the used range); excel.range.update {fileId, driveId?, worksheet, address, values} (no key: a PATCH); excel.table.list {fileId, driveId?, worksheet?}; excel.row.add {fileId, driveId?, tableName, values} and excel.worksheet.add {fileId, driveId?, title} with a required idempotencyKey; teams.message.list {pageToken?}; teams.message.post {text 1..4000, html?} and teams.reply.post {messageId, text, html?} with a required idempotencyKey. An Outlook or Teams `messageId` is a Graph id (the shared field widens to ^[A-Za-z0-9._=-]{1,256}$; the Gmail and Slack operations narrow it back to ^[A-Za-z0-9._-]{1,64}$ in their then-blocks, so their rules are unchanged), and `text` widens to 64 KiB for the Docs body (every other operation that takes it narrows it back to 4000). Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB except a gmail.message.send carrying attachments (governance sizes that route for the attachment bound) and a basecamp.message.post (its 64 KiB body; governance allows 80 KiB there) and a sharepoint.file.upload, drive.file.upload or onedrive.file.upload (its 2 MiB file as base64; governance sizes it like the Gmail attachment route), an outlook.message.send or outlook.message.reply with attachments (the Gmail route), a docs.document.create or docs.document.append (its 64 KiB body; governance allows 80 KiB there, as for basecamp.message.post) and a sheets.values.append, sheets.values.update, excel.range.update or excel.row.add (its 64 KiB `values`), total deadline 10 s.
2
+ * Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but the declared-mode Slack operations (slack.channel.history, slack.message.post, slack.message.update, slack.message.delete; 0.71.0 widened history), and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A declared-mode operation runs as the app (for a post: the IT-approved per-app name from the grant, plus a "Posted by <app> on Isomorph" footer) or as the signed-in person (no name, no footer) — the same input either way — and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a call does. Per-operation input bounds (0.71.0, ADR 0018): slack.channel.history {limit? 1..100, default 15, cursor?, oldest?, latest?}; slack.message.post {text 1..4000 chars, threadId?, format? plain|mrkdwn} and a required idempotencyKey retained across retries of one explicit Send; slack.message.update {messageId, text, format?}; slack.message.delete {messageId}; slack.dm.send {to 1..8 addresses, text, format?} (app mode only) with a required idempotencyKey; slack.user.lookup {email}; gmail.thread.list {limit? 1..50, default 15, q?, labelIds? default [INBOX], pageToken?}; gmail.thread.read {threadId}; gmail.message.read {messageId}; gmail.attachment.read {messageId, attachmentId}; gmail.message.send {to? 1..50 addresses (to+cc+bcc <= 50 in total; since 0.104.0 `to` may be absent when cc or bcc names a recipient — governance refuses a send with no recipient at all), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, html? <= 64 KiB, attachments? 1..5, each {filename, contentType, dataBase64} (inline, 2 MiB in total) or since 0.105.0 {file, filename?, contentType?} (a private app file by path, deployed apps only; 18 MiB in total), replyToMessageId?, replyTo? (one address), fromName? 1..120 chars, inReplyTo? (an RFC Message-ID; not with replyToMessageId)} and a required idempotencyKey like slack.message.post; warehouse.view.read {columns? 1..32, where? 1..16 filters, groupBy? 1..8 columns, aggregates? 1..16, orderBy? 1..8, limit? 1..1000, default 100} (0.70.0: a plain read names columns, an aggregate read names aggregates and optionally groupBy; the cross-field rules — exactly one of columns or aggregates, groupBy needs aggregates, a filter value that fits its op, orderBy naming a returned column, every named column approved — are enforced by governance, not expressible here; ADR 0017). Basecamp (0.76.0, ADR 0022; every id a string of 1..20 digits, every operation user mode): basecamp.project.list {pageToken?, projectStatus? active|archived} on the fixed `account` alias; on a project alias basecamp.todolist.list {pageToken?}; basecamp.todo.list {todolistId, completed?, pageToken?}; basecamp.todo.create {todolistId, content 1..1000 (the to-do title), description? <= 16384, contentFormat? plain|html, dueOn? YYYY-MM-DD, assigneeIds? 1..20, notify?} with a required idempotencyKey; basecamp.todo.complete {todoId} (idempotent at Basecamp, so no key); basecamp.message.list {pageToken?}; basecamp.message.post {subject 1..256, content 1..65536, contentFormat?} with a required idempotencyKey; basecamp.comment.list {recordingId, pageToken?}; basecamp.comment.post {recordingId, content 1..16384, contentFormat?} with a required idempotencyKey; basecamp.campfire.post {content 1..4000, contentFormat?} with a required idempotencyKey; basecamp.people.list {pageToken?}. `pageToken` is the previous page's `nextPageToken` (a Basecamp page number as digits). SharePoint (0.78.0, ADR 0024; every id a Microsoft Graph id — GraphId, 1..256 characters — every operation user mode on a site alias): sharepoint.list.list {pageToken?}; sharepoint.item.list {listId, filter? 1..512 (an OData filter on `fields/...`), pageToken?}; sharepoint.item.create {listId, fields (1..32 columns, each a string <= 4000, a number, a boolean or null)} with a required idempotencyKey; sharepoint.item.update {listId, itemId, fields} (a PATCH, idempotent at Graph, so no key); sharepoint.drive.list {pageToken?}; sharepoint.file.list {driveId?, folderId?, pageToken?} (no driveId: the site's default drive; no folderId: its root); sharepoint.file.search {q 1..256, driveId?, pageToken?}; sharepoint.file.read {fileId, driveId?}; sharepoint.file.upload {filename 1..255, contentType, dataBase64 <= 2 MiB, driveId?, folderId?, conflict? rename|replace|fail} with a required idempotencyKey. A SharePoint `pageToken` is Graph's `$skiptoken` (up to 2048 characters; the shared field is that wide, the Gmail and Basecamp operations narrow it back to 256). Google Workspace and Microsoft 365 (0.86.0, ADR 0032; every operation user mode; the seven personal providers run on their fixed alias, teams.* on a channel alias): calendar.calendar.list {pageToken?}; calendar.event.list {calendarId?, timeMin, timeMax, q?, pageToken?} (RFC 3339 date-times with a zone); calendar.event.read {calendarId?, eventId}; calendar.event.create {calendarId?, title 1..256, start, end, timeZone?, allDay?, description? <= 8000, location? <= 512, attendees? 1..100 emails, notify?} with a required idempotencyKey; calendar.event.update {calendarId?, eventId, title?, start?, end?, timeZone?, description?, location?, attendees?, notify?} and calendar.event.cancel {calendarId?, eventId, notify?} (both idempotent at Google, so no key); drive.file.list {folderId?, pageToken?}; drive.file.search {q, pageToken?}; drive.file.read {fileId, exportAs? text|csv|pdf} (exportAs renders a Google Docs, Sheets or Slides file; a binary file is downloaded as is); drive.file.upload {filename, contentType, dataBase64 <= 2 MiB, folderId?, conflict?} and drive.folder.create {title, folderId?} with a required idempotencyKey; docs.document.read {documentId}; docs.document.create {title, text? <= 64 KiB} with a required idempotencyKey; docs.document.append {documentId, text <= 64 KiB} and docs.text.replace {documentId, replacements 1..64 of {find 1..512, replace <= 4000, matchCase?}} (no key: the app owns the document's state); sheets.spreadsheet.read {spreadsheetId}; sheets.values.read {spreadsheetId, range} (A1 notation, 1..256); sheets.values.append {spreadsheetId, range, values, valueInput? raw|user} with a required idempotencyKey; sheets.values.update {spreadsheetId, range, values, valueInput?} and sheets.values.clear {spreadsheetId, range} (no key); sheets.spreadsheet.create {title, sheets? 1..20 titles} with a required idempotencyKey (`values` is 1..1000 rows of 1..100 cells, each a string <= 4000, a number, a boolean or null); outlook.message.list {folder?, q?, pageToken?}; outlook.message.read {messageId}; outlook.attachment.read {messageId, attachmentId}; outlook.message.send {to, cc?, bcc?, subject, text 1..4000, html?, attachments?} and outlook.message.reply {messageId, text 1..4000, attachments?} (0.95.0: existing EmailAttachment bounds; recipients remain provider-resolved) with a required idempotencyKey like gmail.message.send; outlook.event.list {start, end, pageToken?}; outlook.event.read {eventId}; outlook.event.create (as calendar.event.create without calendarId) with a required idempotencyKey; outlook.event.update (as calendar.event.update without calendarId) and outlook.event.cancel {eventId, comment? <= 4000} (no key); onedrive.file.list, onedrive.file.search, onedrive.file.read {fileId}, onedrive.file.upload and onedrive.folder.create as the drive.* operations (Graph ids, no exportAs); excel.worksheet.list {fileId, driveId?}; excel.range.read {fileId, driveId?, worksheet, address? 1..64} (no address: the used range); excel.range.update {fileId, driveId?, worksheet, address, values} (no key: a PATCH); excel.table.list {fileId, driveId?, worksheet?}; excel.row.add {fileId, driveId?, tableName, values} and excel.worksheet.add {fileId, driveId?, title} with a required idempotencyKey; teams.message.list {pageToken?}; teams.message.post {text 1..4000, html?} and teams.reply.post {messageId, text, html?} with a required idempotencyKey. An Outlook or Teams `messageId` is a Graph id (the shared field widens to ^[A-Za-z0-9._=-]{1,256}$; the Gmail and Slack operations narrow it back to ^[A-Za-z0-9._-]{1,64}$ in their then-blocks, so their rules are unchanged), and `text` widens to 64 KiB for the Docs body (every other operation that takes it narrows it back to 4000). Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB except a gmail.message.send carrying attachments (governance sizes that route for the attachment bound) and a basecamp.message.post (its 64 KiB body; governance allows 80 KiB there) and a sharepoint.file.upload, drive.file.upload or onedrive.file.upload (its 2 MiB file as base64; governance sizes it like the Gmail attachment route), an outlook.message.send or outlook.message.reply with attachments (the Gmail route), a docs.document.create or docs.document.append (its 64 KiB body; governance allows 80 KiB there, as for basecamp.message.post) and a sheets.values.append, sheets.values.update, excel.range.update or excel.row.add (its 64 KiB `values`), total deadline 10 s.
3
3
  */
4
4
  export type IntegrationExecuteRequest = {
5
5
  /**
@@ -81,12 +81,24 @@ export type IntegrationExecuteRequest = {
81
81
  * gmail.message.send: a Gmail message id (from gmail.thread.list / gmail.message.read) the email replies to; the send joins that thread.
82
82
  */
83
83
  replyToMessageId?: string;
84
+ /**
85
+ * One bare email address (no display name).
86
+ */
87
+ replyTo?: string;
88
+ /**
89
+ * gmail.message.send (0.104.0): the display name shown beside the sending mailbox's address (From: "Name" <address>), one line of 1..120 characters. The address itself is always the mailbox that sends.
90
+ */
91
+ fromName?: string;
92
+ /**
93
+ * gmail.message.send (0.104.0): the RFC 5322 Message-ID (angle brackets included) of a message this email answers when that message is not in the sending mailbox; sets In-Reply-To and References so the recipient's mail client threads it. Not accepted together with replyToMessageId.
94
+ */
95
+ inReplyTo?: string;
84
96
  /**
85
97
  * gmail.message.send (0.71.0): an HTML body sent beside `text` (which stays the plain-text alternative), at most 64 KiB. outlook.message.send / teams.message.post / teams.reply.post (0.86.0): an HTML body beside `text`, the same way.
86
98
  */
87
99
  html?: string;
88
100
  /**
89
- * gmail.message.send (0.71.0): files to attach, 1..5, each at most 2 MiB (2,796,203 base64 characters). outlook.message.send (0.86.0): the same, 1..5.
101
+ * gmail.message.send (0.71.0): files to attach, 1..5; inline ones (`dataBase64`) total at most 2 MiB, and since 0.105.0 one may instead name a private app file (`file`), 18 MiB in total. outlook.message.send (0.86.0): inline files only, 1..5.
90
102
  *
91
103
  * @minItems 1
92
104
  * @maxItems 5
@@ -449,21 +461,25 @@ export type WarehouseAggregateFn = 'count' | 'sum' | 'avg' | 'min' | 'max';
449
461
  */
450
462
  export type WarehouseOrderDirection = 'asc' | 'desc';
451
463
  /**
452
- * One file attached to a gmail.message.send (0.71.0) or an outlook.message.send (0.86.0), carried inline as base64. At most 2 MiB of bytes, which is 2,796,203 base64 characters.
464
+ * One file attached to a gmail.message.send (0.71.0) or an outlook.message.send (0.86.0). Either carried inline as base64 — `filename`, `contentType` and `dataBase64`, at most 2 MiB of bytes, which is 2,796,203 base64 characters — or, on a gmail.message.send from a deployed app (0.105.0), named by `file`: the path of a private file of the app (isomorph.files), which the app's gateway checks the caller may read and hands to governance as a short-lived link, so no file bytes ride the request; `filename` and `contentType` then default to the file's own. Exactly one of `dataBase64` and `file`: a cross-field rule governance enforces (no field is required here). The Outlook sends take the inline form only.
453
465
  */
454
466
  export interface EmailAttachment {
467
+ /**
468
+ * gmail.message.send (0.105.0): the path of one of the app's private files, as isomorph.files names it (e.g. letters/offer.pdf). The inline and referenced attachments of one email total at most 18 MiB decoded.
469
+ */
470
+ file?: string;
455
471
  /**
456
472
  * The name the recipient sees.
457
473
  */
458
- filename: string;
474
+ filename?: string;
459
475
  /**
460
476
  * The MIME type, e.g. application/pdf.
461
477
  */
462
- contentType: string;
478
+ contentType?: string;
463
479
  /**
464
480
  * The file bytes, standard base64.
465
481
  */
466
- dataBase64: string;
482
+ dataBase64?: string;
467
483
  }
468
484
  /**
469
485
  * One warehouse.view.read filter (0.70.0): <column> <op> <value>. The value is always bound as a query parameter, never written into the SQL. Which value an op takes is a governance rule: isNull/isNotNull take none; in/notIn take a non-empty list of one scalar type; every other op takes one scalar, and a boolean only with eq/ne.
@@ -1,10 +1,10 @@
1
1
  /* eslint-disable */
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
- export const ContractRepositoryVersion = "0.103.0.0" as const;
5
- export const ContractPublishVersion = "0.103.0" as const;
6
- export const ContractPublishTag = "v0.103.0" as const;
7
- export const ContractSchemasDigest = "sha256:834e5cbeb45c05b1631152db45981c272b9e2f34ff73509d22fc40a4f2a1d43a" as const;
4
+ export const ContractRepositoryVersion = "0.105.0.0" as const;
5
+ export const ContractPublishVersion = "0.105.0" as const;
6
+ export const ContractPublishTag = "v0.105.0" as const;
7
+ export const ContractSchemasDigest = "sha256:8844b241b17bcd774b50653cd753c379b432f5d67c49d69a36658bdfa1392c8e" as const;
8
8
  export const ContractAiFixturesDigest = "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6" as const;
9
- export const ContractGeneratedGoDigest = "sha256:b537d21916ff90f716f482eefc33371cf639ac969cadf69105fdd951c0805209" as const;
10
- export const ContractGeneratedTypeScriptDigest = "sha256:937c0e54c5c268d3127adaec3265eb4dc6e21ca784301a60b69365dbe2fb4956" as const;
9
+ export const ContractGeneratedGoDigest = "sha256:4773bbad6c269967b7c9b1619340e78be54565979dfa7128902a79700038313d" as const;
10
+ export const ContractGeneratedTypeScriptDigest = "sha256:dd162dae0a43118ff104a9fec66bd48b834c90de90c8dfc6bac5c6a8933e94a4" as const;
@@ -99,18 +99,22 @@ export const IntegrationExecuteInputRules = {
99
99
  "gmail.message.send": {
100
100
  idempotencyKey: true,
101
101
  fields: {
102
- to: { kind: "string[]", required: true, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
102
+ to: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
103
103
  cc: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
104
104
  bcc: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
105
105
  subject: { kind: "string", required: true, minLength: 1, maxLength: 256 },
106
106
  text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
107
107
  html: { kind: "string", required: false, minLength: 1, maxLength: 65536 },
108
108
  attachments: { kind: "object[]", required: false, minItems: 1, maxItems: 5, fields: {
109
- filename: { kind: "string", required: true, minLength: 1, maxLength: 255 },
110
- contentType: { kind: "string", required: true, minLength: 1, maxLength: 128 },
111
- dataBase64: { kind: "string", required: true, minLength: 1, maxLength: 2796203 },
109
+ file: { kind: "string", required: false, minLength: 1, maxLength: 1024 },
110
+ filename: { kind: "string", required: false, minLength: 1, maxLength: 255 },
111
+ contentType: { kind: "string", required: false, minLength: 1, maxLength: 128 },
112
+ dataBase64: { kind: "string", required: false, minLength: 1, maxLength: 2796203 },
112
113
  } },
113
114
  replyToMessageId: { kind: "string", required: false, minLength: 0, pattern: "^[A-Za-z0-9_-]{1,64}$" },
115
+ replyTo: { kind: "string", required: false, minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" },
116
+ fromName: { kind: "string", required: false, minLength: 1, maxLength: 120 },
117
+ inReplyTo: { kind: "string", required: false, minLength: 0, pattern: "^<[^\\s<>]{1,250}>$" },
114
118
  },
115
119
  },
116
120
  "gmail.attachment.read": {
@@ -487,9 +491,10 @@ export const IntegrationExecuteInputRules = {
487
491
  text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
488
492
  html: { kind: "string", required: false, minLength: 1, maxLength: 65536 },
489
493
  attachments: { kind: "object[]", required: false, minItems: 1, maxItems: 5, fields: {
490
- filename: { kind: "string", required: true, minLength: 1, maxLength: 255 },
491
- contentType: { kind: "string", required: true, minLength: 1, maxLength: 128 },
492
- dataBase64: { kind: "string", required: true, minLength: 1, maxLength: 2796203 },
494
+ file: { kind: "string", required: false, minLength: 1, maxLength: 1024 },
495
+ filename: { kind: "string", required: false, minLength: 1, maxLength: 255 },
496
+ contentType: { kind: "string", required: false, minLength: 1, maxLength: 128 },
497
+ dataBase64: { kind: "string", required: false, minLength: 1, maxLength: 2796203 },
493
498
  } },
494
499
  },
495
500
  },
@@ -499,9 +504,10 @@ export const IntegrationExecuteInputRules = {
499
504
  messageId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9._=-]{1,256}$" },
500
505
  text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
501
506
  attachments: { kind: "object[]", required: false, minItems: 1, maxItems: 5, fields: {
502
- filename: { kind: "string", required: true, minLength: 1, maxLength: 255 },
503
- contentType: { kind: "string", required: true, minLength: 1, maxLength: 128 },
504
- dataBase64: { kind: "string", required: true, minLength: 1, maxLength: 2796203 },
507
+ file: { kind: "string", required: false, minLength: 1, maxLength: 1024 },
508
+ filename: { kind: "string", required: false, minLength: 1, maxLength: 255 },
509
+ contentType: { kind: "string", required: false, minLength: 1, maxLength: 128 },
510
+ dataBase64: { kind: "string", required: false, minLength: 1, maxLength: 2796203 },
505
511
  } },
506
512
  },
507
513
  },
@@ -2,7 +2,7 @@
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
4
  /**
5
- * Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but the declared-mode Slack operations (slack.channel.history, slack.message.post, slack.message.update, slack.message.delete; 0.71.0 widened history), and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A declared-mode operation runs as the app (for a post: the IT-approved per-app name from the grant, plus a "Posted by <app> on Isomorph" footer) or as the signed-in person (no name, no footer) — the same input either way — and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a call does. Per-operation input bounds (0.71.0, ADR 0018): slack.channel.history {limit? 1..100, default 15, cursor?, oldest?, latest?}; slack.message.post {text 1..4000 chars, threadId?, format? plain|mrkdwn} and a required idempotencyKey retained across retries of one explicit Send; slack.message.update {messageId, text, format?}; slack.message.delete {messageId}; slack.dm.send {to 1..8 addresses, text, format?} (app mode only) with a required idempotencyKey; slack.user.lookup {email}; gmail.thread.list {limit? 1..50, default 15, q?, labelIds? default [INBOX], pageToken?}; gmail.thread.read {threadId}; gmail.message.read {messageId}; gmail.attachment.read {messageId, attachmentId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, html? <= 64 KiB, attachments? 1..5 of <= 2 MiB each as base64, replyToMessageId?} and a required idempotencyKey like slack.message.post; warehouse.view.read {columns? 1..32, where? 1..16 filters, groupBy? 1..8 columns, aggregates? 1..16, orderBy? 1..8, limit? 1..1000, default 100} (0.70.0: a plain read names columns, an aggregate read names aggregates and optionally groupBy; the cross-field rules — exactly one of columns or aggregates, groupBy needs aggregates, a filter value that fits its op, orderBy naming a returned column, every named column approved — are enforced by governance, not expressible here; ADR 0017). Basecamp (0.76.0, ADR 0022; every id a string of 1..20 digits, every operation user mode): basecamp.project.list {pageToken?, projectStatus? active|archived} on the fixed `account` alias; on a project alias basecamp.todolist.list {pageToken?}; basecamp.todo.list {todolistId, completed?, pageToken?}; basecamp.todo.create {todolistId, content 1..1000 (the to-do title), description? <= 16384, contentFormat? plain|html, dueOn? YYYY-MM-DD, assigneeIds? 1..20, notify?} with a required idempotencyKey; basecamp.todo.complete {todoId} (idempotent at Basecamp, so no key); basecamp.message.list {pageToken?}; basecamp.message.post {subject 1..256, content 1..65536, contentFormat?} with a required idempotencyKey; basecamp.comment.list {recordingId, pageToken?}; basecamp.comment.post {recordingId, content 1..16384, contentFormat?} with a required idempotencyKey; basecamp.campfire.post {content 1..4000, contentFormat?} with a required idempotencyKey; basecamp.people.list {pageToken?}. `pageToken` is the previous page's `nextPageToken` (a Basecamp page number as digits). SharePoint (0.78.0, ADR 0024; every id a Microsoft Graph id — GraphId, 1..256 characters — every operation user mode on a site alias): sharepoint.list.list {pageToken?}; sharepoint.item.list {listId, filter? 1..512 (an OData filter on `fields/...`), pageToken?}; sharepoint.item.create {listId, fields (1..32 columns, each a string <= 4000, a number, a boolean or null)} with a required idempotencyKey; sharepoint.item.update {listId, itemId, fields} (a PATCH, idempotent at Graph, so no key); sharepoint.drive.list {pageToken?}; sharepoint.file.list {driveId?, folderId?, pageToken?} (no driveId: the site's default drive; no folderId: its root); sharepoint.file.search {q 1..256, driveId?, pageToken?}; sharepoint.file.read {fileId, driveId?}; sharepoint.file.upload {filename 1..255, contentType, dataBase64 <= 2 MiB, driveId?, folderId?, conflict? rename|replace|fail} with a required idempotencyKey. A SharePoint `pageToken` is Graph's `$skiptoken` (up to 2048 characters; the shared field is that wide, the Gmail and Basecamp operations narrow it back to 256). Google Workspace and Microsoft 365 (0.86.0, ADR 0032; every operation user mode; the seven personal providers run on their fixed alias, teams.* on a channel alias): calendar.calendar.list {pageToken?}; calendar.event.list {calendarId?, timeMin, timeMax, q?, pageToken?} (RFC 3339 date-times with a zone); calendar.event.read {calendarId?, eventId}; calendar.event.create {calendarId?, title 1..256, start, end, timeZone?, allDay?, description? <= 8000, location? <= 512, attendees? 1..100 emails, notify?} with a required idempotencyKey; calendar.event.update {calendarId?, eventId, title?, start?, end?, timeZone?, description?, location?, attendees?, notify?} and calendar.event.cancel {calendarId?, eventId, notify?} (both idempotent at Google, so no key); drive.file.list {folderId?, pageToken?}; drive.file.search {q, pageToken?}; drive.file.read {fileId, exportAs? text|csv|pdf} (exportAs renders a Google Docs, Sheets or Slides file; a binary file is downloaded as is); drive.file.upload {filename, contentType, dataBase64 <= 2 MiB, folderId?, conflict?} and drive.folder.create {title, folderId?} with a required idempotencyKey; docs.document.read {documentId}; docs.document.create {title, text? <= 64 KiB} with a required idempotencyKey; docs.document.append {documentId, text <= 64 KiB} and docs.text.replace {documentId, replacements 1..64 of {find 1..512, replace <= 4000, matchCase?}} (no key: the app owns the document's state); sheets.spreadsheet.read {spreadsheetId}; sheets.values.read {spreadsheetId, range} (A1 notation, 1..256); sheets.values.append {spreadsheetId, range, values, valueInput? raw|user} with a required idempotencyKey; sheets.values.update {spreadsheetId, range, values, valueInput?} and sheets.values.clear {spreadsheetId, range} (no key); sheets.spreadsheet.create {title, sheets? 1..20 titles} with a required idempotencyKey (`values` is 1..1000 rows of 1..100 cells, each a string <= 4000, a number, a boolean or null); outlook.message.list {folder?, q?, pageToken?}; outlook.message.read {messageId}; outlook.attachment.read {messageId, attachmentId}; outlook.message.send {to, cc?, bcc?, subject, text 1..4000, html?, attachments?} and outlook.message.reply {messageId, text 1..4000, attachments?} (0.95.0: existing EmailAttachment bounds; recipients remain provider-resolved) with a required idempotencyKey like gmail.message.send; outlook.event.list {start, end, pageToken?}; outlook.event.read {eventId}; outlook.event.create (as calendar.event.create without calendarId) with a required idempotencyKey; outlook.event.update (as calendar.event.update without calendarId) and outlook.event.cancel {eventId, comment? <= 4000} (no key); onedrive.file.list, onedrive.file.search, onedrive.file.read {fileId}, onedrive.file.upload and onedrive.folder.create as the drive.* operations (Graph ids, no exportAs); excel.worksheet.list {fileId, driveId?}; excel.range.read {fileId, driveId?, worksheet, address? 1..64} (no address: the used range); excel.range.update {fileId, driveId?, worksheet, address, values} (no key: a PATCH); excel.table.list {fileId, driveId?, worksheet?}; excel.row.add {fileId, driveId?, tableName, values} and excel.worksheet.add {fileId, driveId?, title} with a required idempotencyKey; teams.message.list {pageToken?}; teams.message.post {text 1..4000, html?} and teams.reply.post {messageId, text, html?} with a required idempotencyKey. An Outlook or Teams `messageId` is a Graph id (the shared field widens to ^[A-Za-z0-9._=-]{1,256}$; the Gmail and Slack operations narrow it back to ^[A-Za-z0-9._-]{1,64}$ in their then-blocks, so their rules are unchanged), and `text` widens to 64 KiB for the Docs body (every other operation that takes it narrows it back to 4000). Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB except a gmail.message.send carrying attachments (governance sizes that route for the attachment bound) and a basecamp.message.post (its 64 KiB body; governance allows 80 KiB there) and a sharepoint.file.upload, drive.file.upload or onedrive.file.upload (its 2 MiB file as base64; governance sizes it like the Gmail attachment route), an outlook.message.send or outlook.message.reply with attachments (the Gmail route), a docs.document.create or docs.document.append (its 64 KiB body; governance allows 80 KiB there, as for basecamp.message.post) and a sheets.values.append, sheets.values.update, excel.range.update or excel.row.add (its 64 KiB `values`), total deadline 10 s.
5
+ * Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but the declared-mode Slack operations (slack.channel.history, slack.message.post, slack.message.update, slack.message.delete; 0.71.0 widened history), and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A declared-mode operation runs as the app (for a post: the IT-approved per-app name from the grant, plus a "Posted by <app> on Isomorph" footer) or as the signed-in person (no name, no footer) — the same input either way — and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a call does. Per-operation input bounds (0.71.0, ADR 0018): slack.channel.history {limit? 1..100, default 15, cursor?, oldest?, latest?}; slack.message.post {text 1..4000 chars, threadId?, format? plain|mrkdwn} and a required idempotencyKey retained across retries of one explicit Send; slack.message.update {messageId, text, format?}; slack.message.delete {messageId}; slack.dm.send {to 1..8 addresses, text, format?} (app mode only) with a required idempotencyKey; slack.user.lookup {email}; gmail.thread.list {limit? 1..50, default 15, q?, labelIds? default [INBOX], pageToken?}; gmail.thread.read {threadId}; gmail.message.read {messageId}; gmail.attachment.read {messageId, attachmentId}; gmail.message.send {to? 1..50 addresses (to+cc+bcc <= 50 in total; since 0.104.0 `to` may be absent when cc or bcc names a recipient — governance refuses a send with no recipient at all), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, html? <= 64 KiB, attachments? 1..5, each {filename, contentType, dataBase64} (inline, 2 MiB in total) or since 0.105.0 {file, filename?, contentType?} (a private app file by path, deployed apps only; 18 MiB in total), replyToMessageId?, replyTo? (one address), fromName? 1..120 chars, inReplyTo? (an RFC Message-ID; not with replyToMessageId)} and a required idempotencyKey like slack.message.post; warehouse.view.read {columns? 1..32, where? 1..16 filters, groupBy? 1..8 columns, aggregates? 1..16, orderBy? 1..8, limit? 1..1000, default 100} (0.70.0: a plain read names columns, an aggregate read names aggregates and optionally groupBy; the cross-field rules — exactly one of columns or aggregates, groupBy needs aggregates, a filter value that fits its op, orderBy naming a returned column, every named column approved — are enforced by governance, not expressible here; ADR 0017). Basecamp (0.76.0, ADR 0022; every id a string of 1..20 digits, every operation user mode): basecamp.project.list {pageToken?, projectStatus? active|archived} on the fixed `account` alias; on a project alias basecamp.todolist.list {pageToken?}; basecamp.todo.list {todolistId, completed?, pageToken?}; basecamp.todo.create {todolistId, content 1..1000 (the to-do title), description? <= 16384, contentFormat? plain|html, dueOn? YYYY-MM-DD, assigneeIds? 1..20, notify?} with a required idempotencyKey; basecamp.todo.complete {todoId} (idempotent at Basecamp, so no key); basecamp.message.list {pageToken?}; basecamp.message.post {subject 1..256, content 1..65536, contentFormat?} with a required idempotencyKey; basecamp.comment.list {recordingId, pageToken?}; basecamp.comment.post {recordingId, content 1..16384, contentFormat?} with a required idempotencyKey; basecamp.campfire.post {content 1..4000, contentFormat?} with a required idempotencyKey; basecamp.people.list {pageToken?}. `pageToken` is the previous page's `nextPageToken` (a Basecamp page number as digits). SharePoint (0.78.0, ADR 0024; every id a Microsoft Graph id — GraphId, 1..256 characters — every operation user mode on a site alias): sharepoint.list.list {pageToken?}; sharepoint.item.list {listId, filter? 1..512 (an OData filter on `fields/...`), pageToken?}; sharepoint.item.create {listId, fields (1..32 columns, each a string <= 4000, a number, a boolean or null)} with a required idempotencyKey; sharepoint.item.update {listId, itemId, fields} (a PATCH, idempotent at Graph, so no key); sharepoint.drive.list {pageToken?}; sharepoint.file.list {driveId?, folderId?, pageToken?} (no driveId: the site's default drive; no folderId: its root); sharepoint.file.search {q 1..256, driveId?, pageToken?}; sharepoint.file.read {fileId, driveId?}; sharepoint.file.upload {filename 1..255, contentType, dataBase64 <= 2 MiB, driveId?, folderId?, conflict? rename|replace|fail} with a required idempotencyKey. A SharePoint `pageToken` is Graph's `$skiptoken` (up to 2048 characters; the shared field is that wide, the Gmail and Basecamp operations narrow it back to 256). Google Workspace and Microsoft 365 (0.86.0, ADR 0032; every operation user mode; the seven personal providers run on their fixed alias, teams.* on a channel alias): calendar.calendar.list {pageToken?}; calendar.event.list {calendarId?, timeMin, timeMax, q?, pageToken?} (RFC 3339 date-times with a zone); calendar.event.read {calendarId?, eventId}; calendar.event.create {calendarId?, title 1..256, start, end, timeZone?, allDay?, description? <= 8000, location? <= 512, attendees? 1..100 emails, notify?} with a required idempotencyKey; calendar.event.update {calendarId?, eventId, title?, start?, end?, timeZone?, description?, location?, attendees?, notify?} and calendar.event.cancel {calendarId?, eventId, notify?} (both idempotent at Google, so no key); drive.file.list {folderId?, pageToken?}; drive.file.search {q, pageToken?}; drive.file.read {fileId, exportAs? text|csv|pdf} (exportAs renders a Google Docs, Sheets or Slides file; a binary file is downloaded as is); drive.file.upload {filename, contentType, dataBase64 <= 2 MiB, folderId?, conflict?} and drive.folder.create {title, folderId?} with a required idempotencyKey; docs.document.read {documentId}; docs.document.create {title, text? <= 64 KiB} with a required idempotencyKey; docs.document.append {documentId, text <= 64 KiB} and docs.text.replace {documentId, replacements 1..64 of {find 1..512, replace <= 4000, matchCase?}} (no key: the app owns the document's state); sheets.spreadsheet.read {spreadsheetId}; sheets.values.read {spreadsheetId, range} (A1 notation, 1..256); sheets.values.append {spreadsheetId, range, values, valueInput? raw|user} with a required idempotencyKey; sheets.values.update {spreadsheetId, range, values, valueInput?} and sheets.values.clear {spreadsheetId, range} (no key); sheets.spreadsheet.create {title, sheets? 1..20 titles} with a required idempotencyKey (`values` is 1..1000 rows of 1..100 cells, each a string <= 4000, a number, a boolean or null); outlook.message.list {folder?, q?, pageToken?}; outlook.message.read {messageId}; outlook.attachment.read {messageId, attachmentId}; outlook.message.send {to, cc?, bcc?, subject, text 1..4000, html?, attachments?} and outlook.message.reply {messageId, text 1..4000, attachments?} (0.95.0: existing EmailAttachment bounds; recipients remain provider-resolved) with a required idempotencyKey like gmail.message.send; outlook.event.list {start, end, pageToken?}; outlook.event.read {eventId}; outlook.event.create (as calendar.event.create without calendarId) with a required idempotencyKey; outlook.event.update (as calendar.event.update without calendarId) and outlook.event.cancel {eventId, comment? <= 4000} (no key); onedrive.file.list, onedrive.file.search, onedrive.file.read {fileId}, onedrive.file.upload and onedrive.folder.create as the drive.* operations (Graph ids, no exportAs); excel.worksheet.list {fileId, driveId?}; excel.range.read {fileId, driveId?, worksheet, address? 1..64} (no address: the used range); excel.range.update {fileId, driveId?, worksheet, address, values} (no key: a PATCH); excel.table.list {fileId, driveId?, worksheet?}; excel.row.add {fileId, driveId?, tableName, values} and excel.worksheet.add {fileId, driveId?, title} with a required idempotencyKey; teams.message.list {pageToken?}; teams.message.post {text 1..4000, html?} and teams.reply.post {messageId, text, html?} with a required idempotencyKey. An Outlook or Teams `messageId` is a Graph id (the shared field widens to ^[A-Za-z0-9._=-]{1,256}$; the Gmail and Slack operations narrow it back to ^[A-Za-z0-9._-]{1,64}$ in their then-blocks, so their rules are unchanged), and `text` widens to 64 KiB for the Docs body (every other operation that takes it narrows it back to 4000). Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB except a gmail.message.send carrying attachments (governance sizes that route for the attachment bound) and a basecamp.message.post (its 64 KiB body; governance allows 80 KiB there) and a sharepoint.file.upload, drive.file.upload or onedrive.file.upload (its 2 MiB file as base64; governance sizes it like the Gmail attachment route), an outlook.message.send or outlook.message.reply with attachments (the Gmail route), a docs.document.create or docs.document.append (its 64 KiB body; governance allows 80 KiB there, as for basecamp.message.post) and a sheets.values.append, sheets.values.update, excel.range.update or excel.row.add (its 64 KiB `values`), total deadline 10 s.
6
6
  */
7
7
  export type IntegrationExecuteRequest = {
8
8
  /**
@@ -180,12 +180,24 @@ export type IntegrationExecuteRequest = {
180
180
  * gmail.message.send: a Gmail message id (from gmail.thread.list / gmail.message.read) the email replies to; the send joins that thread.
181
181
  */
182
182
  replyToMessageId?: string;
183
+ /**
184
+ * One bare email address (no display name).
185
+ */
186
+ replyTo?: string;
187
+ /**
188
+ * gmail.message.send (0.104.0): the display name shown beside the sending mailbox's address (From: "Name" <address>), one line of 1..120 characters. The address itself is always the mailbox that sends.
189
+ */
190
+ fromName?: string;
191
+ /**
192
+ * gmail.message.send (0.104.0): the RFC 5322 Message-ID (angle brackets included) of a message this email answers when that message is not in the sending mailbox; sets In-Reply-To and References so the recipient's mail client threads it. Not accepted together with replyToMessageId.
193
+ */
194
+ inReplyTo?: string;
183
195
  /**
184
196
  * gmail.message.send (0.71.0): an HTML body sent beside `text` (which stays the plain-text alternative), at most 64 KiB. outlook.message.send / teams.message.post / teams.reply.post (0.86.0): an HTML body beside `text`, the same way.
185
197
  */
186
198
  html?: string;
187
199
  /**
188
- * gmail.message.send (0.71.0): files to attach, 1..5, each at most 2 MiB (2,796,203 base64 characters). outlook.message.send (0.86.0): the same, 1..5.
200
+ * gmail.message.send (0.71.0): files to attach, 1..5; inline ones (`dataBase64`) total at most 2 MiB, and since 0.105.0 one may instead name a private app file (`file`), 18 MiB in total. outlook.message.send (0.86.0): inline files only, 1..5.
189
201
  *
190
202
  * @minItems 1
191
203
  * @maxItems 5
@@ -549,21 +561,25 @@ export type WarehouseAggregateFn = 'count' | 'sum' | 'avg' | 'min' | 'max';
549
561
  export type WarehouseOrderDirection = 'asc' | 'desc';
550
562
 
551
563
  /**
552
- * One file attached to a gmail.message.send (0.71.0) or an outlook.message.send (0.86.0), carried inline as base64. At most 2 MiB of bytes, which is 2,796,203 base64 characters.
564
+ * One file attached to a gmail.message.send (0.71.0) or an outlook.message.send (0.86.0). Either carried inline as base64 — `filename`, `contentType` and `dataBase64`, at most 2 MiB of bytes, which is 2,796,203 base64 characters — or, on a gmail.message.send from a deployed app (0.105.0), named by `file`: the path of a private file of the app (isomorph.files), which the app's gateway checks the caller may read and hands to governance as a short-lived link, so no file bytes ride the request; `filename` and `contentType` then default to the file's own. Exactly one of `dataBase64` and `file`: a cross-field rule governance enforces (no field is required here). The Outlook sends take the inline form only.
553
565
  */
554
566
  export interface EmailAttachment {
567
+ /**
568
+ * gmail.message.send (0.105.0): the path of one of the app's private files, as isomorph.files names it (e.g. letters/offer.pdf). The inline and referenced attachments of one email total at most 18 MiB decoded.
569
+ */
570
+ file?: string;
555
571
  /**
556
572
  * The name the recipient sees.
557
573
  */
558
- filename: string;
574
+ filename?: string;
559
575
  /**
560
576
  * The MIME type, e.g. application/pdf.
561
577
  */
562
- contentType: string;
578
+ contentType?: string;
563
579
  /**
564
580
  * The file bytes, standard base64.
565
581
  */
566
- dataBase64: string;
582
+ dataBase64?: string;
567
583
  }
568
584
  /**
569
585
  * One warehouse.view.read filter (0.70.0): <column> <op> <value>. The value is always bound as a query parameter, never written into the SQL. Which value an op takes is a governance rule: isNull/isNotNull take none; in/notIn take a non-empty list of one scalar type; every other op takes one scalar, and a boolean only with eq/ne.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourier-labs/harbour-contracts",
3
- "version": "0.103.0",
3
+ "version": "0.105.0",
4
4
  "description": "Single source of truth for every data shape that crosses a boundary between Isomorph components.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/integration-execute-request.schema.json",
4
4
  "title": "IntegrationExecuteRequest",
5
- "description": "Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but the declared-mode Slack operations (slack.channel.history, slack.message.post, slack.message.update, slack.message.delete; 0.71.0 widened history), and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A declared-mode operation runs as the app (for a post: the IT-approved per-app name from the grant, plus a \"Posted by <app> on Isomorph\" footer) or as the signed-in person (no name, no footer) \u2014 the same input either way \u2014 and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a call does. Per-operation input bounds (0.71.0, ADR 0018): slack.channel.history {limit? 1..100, default 15, cursor?, oldest?, latest?}; slack.message.post {text 1..4000 chars, threadId?, format? plain|mrkdwn} and a required idempotencyKey retained across retries of one explicit Send; slack.message.update {messageId, text, format?}; slack.message.delete {messageId}; slack.dm.send {to 1..8 addresses, text, format?} (app mode only) with a required idempotencyKey; slack.user.lookup {email}; gmail.thread.list {limit? 1..50, default 15, q?, labelIds? default [INBOX], pageToken?}; gmail.thread.read {threadId}; gmail.message.read {messageId}; gmail.attachment.read {messageId, attachmentId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, html? <= 64 KiB, attachments? 1..5 of <= 2 MiB each as base64, replyToMessageId?} and a required idempotencyKey like slack.message.post; warehouse.view.read {columns? 1..32, where? 1..16 filters, groupBy? 1..8 columns, aggregates? 1..16, orderBy? 1..8, limit? 1..1000, default 100} (0.70.0: a plain read names columns, an aggregate read names aggregates and optionally groupBy; the cross-field rules \u2014 exactly one of columns or aggregates, groupBy needs aggregates, a filter value that fits its op, orderBy naming a returned column, every named column approved \u2014 are enforced by governance, not expressible here; ADR 0017). Basecamp (0.76.0, ADR 0022; every id a string of 1..20 digits, every operation user mode): basecamp.project.list {pageToken?, projectStatus? active|archived} on the fixed `account` alias; on a project alias basecamp.todolist.list {pageToken?}; basecamp.todo.list {todolistId, completed?, pageToken?}; basecamp.todo.create {todolistId, content 1..1000 (the to-do title), description? <= 16384, contentFormat? plain|html, dueOn? YYYY-MM-DD, assigneeIds? 1..20, notify?} with a required idempotencyKey; basecamp.todo.complete {todoId} (idempotent at Basecamp, so no key); basecamp.message.list {pageToken?}; basecamp.message.post {subject 1..256, content 1..65536, contentFormat?} with a required idempotencyKey; basecamp.comment.list {recordingId, pageToken?}; basecamp.comment.post {recordingId, content 1..16384, contentFormat?} with a required idempotencyKey; basecamp.campfire.post {content 1..4000, contentFormat?} with a required idempotencyKey; basecamp.people.list {pageToken?}. `pageToken` is the previous page's `nextPageToken` (a Basecamp page number as digits). SharePoint (0.78.0, ADR 0024; every id a Microsoft Graph id \u2014 GraphId, 1..256 characters \u2014 every operation user mode on a site alias): sharepoint.list.list {pageToken?}; sharepoint.item.list {listId, filter? 1..512 (an OData filter on `fields/...`), pageToken?}; sharepoint.item.create {listId, fields (1..32 columns, each a string <= 4000, a number, a boolean or null)} with a required idempotencyKey; sharepoint.item.update {listId, itemId, fields} (a PATCH, idempotent at Graph, so no key); sharepoint.drive.list {pageToken?}; sharepoint.file.list {driveId?, folderId?, pageToken?} (no driveId: the site's default drive; no folderId: its root); sharepoint.file.search {q 1..256, driveId?, pageToken?}; sharepoint.file.read {fileId, driveId?}; sharepoint.file.upload {filename 1..255, contentType, dataBase64 <= 2 MiB, driveId?, folderId?, conflict? rename|replace|fail} with a required idempotencyKey. A SharePoint `pageToken` is Graph's `$skiptoken` (up to 2048 characters; the shared field is that wide, the Gmail and Basecamp operations narrow it back to 256). Google Workspace and Microsoft 365 (0.86.0, ADR 0032; every operation user mode; the seven personal providers run on their fixed alias, teams.* on a channel alias): calendar.calendar.list {pageToken?}; calendar.event.list {calendarId?, timeMin, timeMax, q?, pageToken?} (RFC 3339 date-times with a zone); calendar.event.read {calendarId?, eventId}; calendar.event.create {calendarId?, title 1..256, start, end, timeZone?, allDay?, description? <= 8000, location? <= 512, attendees? 1..100 emails, notify?} with a required idempotencyKey; calendar.event.update {calendarId?, eventId, title?, start?, end?, timeZone?, description?, location?, attendees?, notify?} and calendar.event.cancel {calendarId?, eventId, notify?} (both idempotent at Google, so no key); drive.file.list {folderId?, pageToken?}; drive.file.search {q, pageToken?}; drive.file.read {fileId, exportAs? text|csv|pdf} (exportAs renders a Google Docs, Sheets or Slides file; a binary file is downloaded as is); drive.file.upload {filename, contentType, dataBase64 <= 2 MiB, folderId?, conflict?} and drive.folder.create {title, folderId?} with a required idempotencyKey; docs.document.read {documentId}; docs.document.create {title, text? <= 64 KiB} with a required idempotencyKey; docs.document.append {documentId, text <= 64 KiB} and docs.text.replace {documentId, replacements 1..64 of {find 1..512, replace <= 4000, matchCase?}} (no key: the app owns the document's state); sheets.spreadsheet.read {spreadsheetId}; sheets.values.read {spreadsheetId, range} (A1 notation, 1..256); sheets.values.append {spreadsheetId, range, values, valueInput? raw|user} with a required idempotencyKey; sheets.values.update {spreadsheetId, range, values, valueInput?} and sheets.values.clear {spreadsheetId, range} (no key); sheets.spreadsheet.create {title, sheets? 1..20 titles} with a required idempotencyKey (`values` is 1..1000 rows of 1..100 cells, each a string <= 4000, a number, a boolean or null); outlook.message.list {folder?, q?, pageToken?}; outlook.message.read {messageId}; outlook.attachment.read {messageId, attachmentId}; outlook.message.send {to, cc?, bcc?, subject, text 1..4000, html?, attachments?} and outlook.message.reply {messageId, text 1..4000, attachments?} (0.95.0: existing EmailAttachment bounds; recipients remain provider-resolved) with a required idempotencyKey like gmail.message.send; outlook.event.list {start, end, pageToken?}; outlook.event.read {eventId}; outlook.event.create (as calendar.event.create without calendarId) with a required idempotencyKey; outlook.event.update (as calendar.event.update without calendarId) and outlook.event.cancel {eventId, comment? <= 4000} (no key); onedrive.file.list, onedrive.file.search, onedrive.file.read {fileId}, onedrive.file.upload and onedrive.folder.create as the drive.* operations (Graph ids, no exportAs); excel.worksheet.list {fileId, driveId?}; excel.range.read {fileId, driveId?, worksheet, address? 1..64} (no address: the used range); excel.range.update {fileId, driveId?, worksheet, address, values} (no key: a PATCH); excel.table.list {fileId, driveId?, worksheet?}; excel.row.add {fileId, driveId?, tableName, values} and excel.worksheet.add {fileId, driveId?, title} with a required idempotencyKey; teams.message.list {pageToken?}; teams.message.post {text 1..4000, html?} and teams.reply.post {messageId, text, html?} with a required idempotencyKey. An Outlook or Teams `messageId` is a Graph id (the shared field widens to ^[A-Za-z0-9._=-]{1,256}$; the Gmail and Slack operations narrow it back to ^[A-Za-z0-9._-]{1,64}$ in their then-blocks, so their rules are unchanged), and `text` widens to 64 KiB for the Docs body (every other operation that takes it narrows it back to 4000). Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB except a gmail.message.send carrying attachments (governance sizes that route for the attachment bound) and a basecamp.message.post (its 64 KiB body; governance allows 80 KiB there) and a sharepoint.file.upload, drive.file.upload or onedrive.file.upload (its 2 MiB file as base64; governance sizes it like the Gmail attachment route), an outlook.message.send or outlook.message.reply with attachments (the Gmail route), a docs.document.create or docs.document.append (its 64 KiB body; governance allows 80 KiB there, as for basecamp.message.post) and a sheets.values.append, sheets.values.update, excel.range.update or excel.row.add (its 64 KiB `values`), total deadline 10 s.",
5
+ "description": "Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but the declared-mode Slack operations (slack.channel.history, slack.message.post, slack.message.update, slack.message.delete; 0.71.0 widened history), and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A declared-mode operation runs as the app (for a post: the IT-approved per-app name from the grant, plus a \"Posted by <app> on Isomorph\" footer) or as the signed-in person (no name, no footer) \u2014 the same input either way \u2014 and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a call does. Per-operation input bounds (0.71.0, ADR 0018): slack.channel.history {limit? 1..100, default 15, cursor?, oldest?, latest?}; slack.message.post {text 1..4000 chars, threadId?, format? plain|mrkdwn} and a required idempotencyKey retained across retries of one explicit Send; slack.message.update {messageId, text, format?}; slack.message.delete {messageId}; slack.dm.send {to 1..8 addresses, text, format?} (app mode only) with a required idempotencyKey; slack.user.lookup {email}; gmail.thread.list {limit? 1..50, default 15, q?, labelIds? default [INBOX], pageToken?}; gmail.thread.read {threadId}; gmail.message.read {messageId}; gmail.attachment.read {messageId, attachmentId}; gmail.message.send {to? 1..50 addresses (to+cc+bcc <= 50 in total; since 0.104.0 `to` may be absent when cc or bcc names a recipient \u2014 governance refuses a send with no recipient at all), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, html? <= 64 KiB, attachments? 1..5, each {filename, contentType, dataBase64} (inline, 2 MiB in total) or since 0.105.0 {file, filename?, contentType?} (a private app file by path, deployed apps only; 18 MiB in total), replyToMessageId?, replyTo? (one address), fromName? 1..120 chars, inReplyTo? (an RFC Message-ID; not with replyToMessageId)} and a required idempotencyKey like slack.message.post; warehouse.view.read {columns? 1..32, where? 1..16 filters, groupBy? 1..8 columns, aggregates? 1..16, orderBy? 1..8, limit? 1..1000, default 100} (0.70.0: a plain read names columns, an aggregate read names aggregates and optionally groupBy; the cross-field rules \u2014 exactly one of columns or aggregates, groupBy needs aggregates, a filter value that fits its op, orderBy naming a returned column, every named column approved \u2014 are enforced by governance, not expressible here; ADR 0017). Basecamp (0.76.0, ADR 0022; every id a string of 1..20 digits, every operation user mode): basecamp.project.list {pageToken?, projectStatus? active|archived} on the fixed `account` alias; on a project alias basecamp.todolist.list {pageToken?}; basecamp.todo.list {todolistId, completed?, pageToken?}; basecamp.todo.create {todolistId, content 1..1000 (the to-do title), description? <= 16384, contentFormat? plain|html, dueOn? YYYY-MM-DD, assigneeIds? 1..20, notify?} with a required idempotencyKey; basecamp.todo.complete {todoId} (idempotent at Basecamp, so no key); basecamp.message.list {pageToken?}; basecamp.message.post {subject 1..256, content 1..65536, contentFormat?} with a required idempotencyKey; basecamp.comment.list {recordingId, pageToken?}; basecamp.comment.post {recordingId, content 1..16384, contentFormat?} with a required idempotencyKey; basecamp.campfire.post {content 1..4000, contentFormat?} with a required idempotencyKey; basecamp.people.list {pageToken?}. `pageToken` is the previous page's `nextPageToken` (a Basecamp page number as digits). SharePoint (0.78.0, ADR 0024; every id a Microsoft Graph id \u2014 GraphId, 1..256 characters \u2014 every operation user mode on a site alias): sharepoint.list.list {pageToken?}; sharepoint.item.list {listId, filter? 1..512 (an OData filter on `fields/...`), pageToken?}; sharepoint.item.create {listId, fields (1..32 columns, each a string <= 4000, a number, a boolean or null)} with a required idempotencyKey; sharepoint.item.update {listId, itemId, fields} (a PATCH, idempotent at Graph, so no key); sharepoint.drive.list {pageToken?}; sharepoint.file.list {driveId?, folderId?, pageToken?} (no driveId: the site's default drive; no folderId: its root); sharepoint.file.search {q 1..256, driveId?, pageToken?}; sharepoint.file.read {fileId, driveId?}; sharepoint.file.upload {filename 1..255, contentType, dataBase64 <= 2 MiB, driveId?, folderId?, conflict? rename|replace|fail} with a required idempotencyKey. A SharePoint `pageToken` is Graph's `$skiptoken` (up to 2048 characters; the shared field is that wide, the Gmail and Basecamp operations narrow it back to 256). Google Workspace and Microsoft 365 (0.86.0, ADR 0032; every operation user mode; the seven personal providers run on their fixed alias, teams.* on a channel alias): calendar.calendar.list {pageToken?}; calendar.event.list {calendarId?, timeMin, timeMax, q?, pageToken?} (RFC 3339 date-times with a zone); calendar.event.read {calendarId?, eventId}; calendar.event.create {calendarId?, title 1..256, start, end, timeZone?, allDay?, description? <= 8000, location? <= 512, attendees? 1..100 emails, notify?} with a required idempotencyKey; calendar.event.update {calendarId?, eventId, title?, start?, end?, timeZone?, description?, location?, attendees?, notify?} and calendar.event.cancel {calendarId?, eventId, notify?} (both idempotent at Google, so no key); drive.file.list {folderId?, pageToken?}; drive.file.search {q, pageToken?}; drive.file.read {fileId, exportAs? text|csv|pdf} (exportAs renders a Google Docs, Sheets or Slides file; a binary file is downloaded as is); drive.file.upload {filename, contentType, dataBase64 <= 2 MiB, folderId?, conflict?} and drive.folder.create {title, folderId?} with a required idempotencyKey; docs.document.read {documentId}; docs.document.create {title, text? <= 64 KiB} with a required idempotencyKey; docs.document.append {documentId, text <= 64 KiB} and docs.text.replace {documentId, replacements 1..64 of {find 1..512, replace <= 4000, matchCase?}} (no key: the app owns the document's state); sheets.spreadsheet.read {spreadsheetId}; sheets.values.read {spreadsheetId, range} (A1 notation, 1..256); sheets.values.append {spreadsheetId, range, values, valueInput? raw|user} with a required idempotencyKey; sheets.values.update {spreadsheetId, range, values, valueInput?} and sheets.values.clear {spreadsheetId, range} (no key); sheets.spreadsheet.create {title, sheets? 1..20 titles} with a required idempotencyKey (`values` is 1..1000 rows of 1..100 cells, each a string <= 4000, a number, a boolean or null); outlook.message.list {folder?, q?, pageToken?}; outlook.message.read {messageId}; outlook.attachment.read {messageId, attachmentId}; outlook.message.send {to, cc?, bcc?, subject, text 1..4000, html?, attachments?} and outlook.message.reply {messageId, text 1..4000, attachments?} (0.95.0: existing EmailAttachment bounds; recipients remain provider-resolved) with a required idempotencyKey like gmail.message.send; outlook.event.list {start, end, pageToken?}; outlook.event.read {eventId}; outlook.event.create (as calendar.event.create without calendarId) with a required idempotencyKey; outlook.event.update (as calendar.event.update without calendarId) and outlook.event.cancel {eventId, comment? <= 4000} (no key); onedrive.file.list, onedrive.file.search, onedrive.file.read {fileId}, onedrive.file.upload and onedrive.folder.create as the drive.* operations (Graph ids, no exportAs); excel.worksheet.list {fileId, driveId?}; excel.range.read {fileId, driveId?, worksheet, address? 1..64} (no address: the used range); excel.range.update {fileId, driveId?, worksheet, address, values} (no key: a PATCH); excel.table.list {fileId, driveId?, worksheet?}; excel.row.add {fileId, driveId?, tableName, values} and excel.worksheet.add {fileId, driveId?, title} with a required idempotencyKey; teams.message.list {pageToken?}; teams.message.post {text 1..4000, html?} and teams.reply.post {messageId, text, html?} with a required idempotencyKey. An Outlook or Teams `messageId` is a Graph id (the shared field widens to ^[A-Za-z0-9._=-]{1,256}$; the Gmail and Slack operations narrow it back to ^[A-Za-z0-9._-]{1,64}$ in their then-blocks, so their rules are unchanged), and `text` widens to 64 KiB for the Docs body (every other operation that takes it narrows it back to 4000). Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB except a gmail.message.send carrying attachments (governance sizes that route for the attachment bound) and a basecamp.message.post (its 64 KiB body; governance allows 80 KiB there) and a sharepoint.file.upload, drive.file.upload or onedrive.file.upload (its 2 MiB file as base64; governance sizes it like the Gmail attachment route), an outlook.message.send or outlook.message.reply with attachments (the Gmail route), a docs.document.create or docs.document.append (its 64 KiB body; governance allows 80 KiB there, as for basecamp.message.post) and a sheets.values.append, sheets.values.update, excel.range.update or excel.row.add (its 64 KiB `values`), total deadline 10 s.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": [
@@ -128,6 +128,21 @@
128
128
  "pattern": "^[A-Za-z0-9_-]{1,64}$",
129
129
  "description": "gmail.message.send: a Gmail message id (from gmail.thread.list / gmail.message.read) the email replies to; the send joins that thread."
130
130
  },
131
+ "replyTo": {
132
+ "$ref": "#/definitions/EmailAddress",
133
+ "description": "gmail.message.send (0.104.0): the one address a recipient's reply goes to (the Reply-To header), when it is not the sending mailbox."
134
+ },
135
+ "fromName": {
136
+ "type": "string",
137
+ "minLength": 1,
138
+ "maxLength": 120,
139
+ "description": "gmail.message.send (0.104.0): the display name shown beside the sending mailbox's address (From: \"Name\" <address>), one line of 1..120 characters. The address itself is always the mailbox that sends."
140
+ },
141
+ "inReplyTo": {
142
+ "type": "string",
143
+ "pattern": "^<[^\\s<>]{1,250}>$",
144
+ "description": "gmail.message.send (0.104.0): the RFC 5322 Message-ID (angle brackets included) of a message this email answers when that message is not in the sending mailbox; sets In-Reply-To and References so the recipient's mail client threads it. Not accepted together with replyToMessageId."
145
+ },
131
146
  "html": {
132
147
  "type": "string",
133
148
  "minLength": 1,
@@ -141,7 +156,7 @@
141
156
  "items": {
142
157
  "$ref": "#/definitions/EmailAttachment"
143
158
  },
144
- "description": "gmail.message.send (0.71.0): files to attach, 1..5, each at most 2 MiB (2,796,203 base64 characters). outlook.message.send (0.86.0): the same, 1..5."
159
+ "description": "gmail.message.send (0.71.0): files to attach, 1..5; inline ones (`dataBase64`) total at most 2 MiB, and since 0.105.0 one may instead name a private app file (`file`), 18 MiB in total. outlook.message.send (0.86.0): inline files only, 1..5."
145
160
  },
146
161
  "todolistId": {
147
162
  "$ref": "app-integrations-declaration.schema.json#/definitions/BasecampId",
@@ -1403,11 +1418,13 @@
1403
1418
  "text",
1404
1419
  "html",
1405
1420
  "attachments",
1406
- "replyToMessageId"
1421
+ "replyToMessageId",
1422
+ "replyTo",
1423
+ "fromName",
1424
+ "inReplyTo"
1407
1425
  ]
1408
1426
  },
1409
1427
  "required": [
1410
- "to",
1411
1428
  "subject",
1412
1429
  "text"
1413
1430
  ],
@@ -4266,13 +4283,14 @@
4266
4283
  "EmailAttachment": {
4267
4284
  "type": "object",
4268
4285
  "additionalProperties": false,
4269
- "required": [
4270
- "filename",
4271
- "contentType",
4272
- "dataBase64"
4273
- ],
4274
- "description": "One file attached to a gmail.message.send (0.71.0) or an outlook.message.send (0.86.0), carried inline as base64. At most 2 MiB of bytes, which is 2,796,203 base64 characters.",
4286
+ "description": "One file attached to a gmail.message.send (0.71.0) or an outlook.message.send (0.86.0). Either carried inline as base64 \u2014 `filename`, `contentType` and `dataBase64`, at most 2 MiB of bytes, which is 2,796,203 base64 characters \u2014 or, on a gmail.message.send from a deployed app (0.105.0), named by `file`: the path of a private file of the app (isomorph.files), which the app's gateway checks the caller may read and hands to governance as a short-lived link, so no file bytes ride the request; `filename` and `contentType` then default to the file's own. Exactly one of `dataBase64` and `file`: a cross-field rule governance enforces (no field is required here). The Outlook sends take the inline form only.",
4275
4287
  "properties": {
4288
+ "file": {
4289
+ "type": "string",
4290
+ "minLength": 1,
4291
+ "maxLength": 1024,
4292
+ "description": "gmail.message.send (0.105.0): the path of one of the app's private files, as isomorph.files names it (e.g. letters/offer.pdf). The inline and referenced attachments of one email total at most 18 MiB decoded."
4293
+ },
4276
4294
  "filename": {
4277
4295
  "type": "string",
4278
4296
  "minLength": 1,