@fourier-labs/harbour-contracts 0.69.0 → 0.71.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.
@@ -4,25 +4,67 @@ export const IntegrationExecuteInputRules = {
4
4
  "slack.channel.history": {
5
5
  idempotencyKey: false,
6
6
  fields: {
7
- limit: { kind: "integer", required: false, minimum: 1, maximum: 15, default: 15 },
7
+ limit: { kind: "integer", required: false, minimum: 1, maximum: 100, default: 15 },
8
+ cursor: { kind: "string", required: false, minLength: 1, maxLength: 256 },
9
+ oldest: { kind: "string", required: false, minLength: 1, maxLength: 64 },
10
+ latest: { kind: "string", required: false, minLength: 1, maxLength: 64 },
8
11
  },
9
12
  },
10
13
  "slack.message.post": {
11
14
  idempotencyKey: true,
12
15
  fields: {
13
16
  text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
17
+ threadId: { kind: "string", required: false, minLength: 0, pattern: "^[A-Za-z0-9._-]{1,64}$" },
18
+ format: { kind: "enum", required: false, values: ["plain", "mrkdwn"], default: "plain" },
19
+ },
20
+ },
21
+ "slack.message.update": {
22
+ idempotencyKey: false,
23
+ fields: {
24
+ messageId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9._-]{1,64}$" },
25
+ text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
26
+ format: { kind: "enum", required: false, values: ["plain", "mrkdwn"], default: "plain" },
27
+ },
28
+ },
29
+ "slack.message.delete": {
30
+ idempotencyKey: false,
31
+ fields: {
32
+ messageId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9._-]{1,64}$" },
33
+ },
34
+ },
35
+ "slack.dm.send": {
36
+ idempotencyKey: true,
37
+ fields: {
38
+ to: { kind: "string[]", required: true, minItems: 1, maxItems: 8, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
39
+ text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
40
+ format: { kind: "enum", required: false, values: ["plain", "mrkdwn"], default: "plain" },
41
+ },
42
+ },
43
+ "slack.user.lookup": {
44
+ idempotencyKey: false,
45
+ fields: {
46
+ email: { kind: "string", required: true, minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" },
14
47
  },
15
48
  },
16
49
  "gmail.thread.list": {
17
50
  idempotencyKey: false,
18
51
  fields: {
19
- limit: { kind: "integer", required: false, minimum: 1, maximum: 15, default: 15 },
52
+ limit: { kind: "integer", required: false, minimum: 1, maximum: 50, default: 15 },
53
+ q: { kind: "string", required: false, minLength: 1, maxLength: 256 },
54
+ labelIds: { kind: "string[]", required: false, minItems: 1, maxItems: 5, uniqueItems: true, item: { minLength: 1, maxLength: 64 }, default: ["INBOX"] },
55
+ pageToken: { kind: "string", required: false, minLength: 1, maxLength: 256 },
56
+ },
57
+ },
58
+ "gmail.thread.read": {
59
+ idempotencyKey: false,
60
+ fields: {
61
+ threadId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9._-]{1,64}$" },
20
62
  },
21
63
  },
22
64
  "gmail.message.read": {
23
65
  idempotencyKey: false,
24
66
  fields: {
25
- messageId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9_-]{1,64}$" },
67
+ messageId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9._-]{1,64}$" },
26
68
  },
27
69
  },
28
70
  "gmail.message.send": {
@@ -33,13 +75,41 @@ export const IntegrationExecuteInputRules = {
33
75
  bcc: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
34
76
  subject: { kind: "string", required: true, minLength: 1, maxLength: 256 },
35
77
  text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
78
+ html: { kind: "string", required: false, minLength: 1, maxLength: 65536 },
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 },
83
+ } },
36
84
  replyToMessageId: { kind: "string", required: false, minLength: 0, pattern: "^[A-Za-z0-9_-]{1,64}$" },
37
85
  },
38
86
  },
87
+ "gmail.attachment.read": {
88
+ idempotencyKey: false,
89
+ fields: {
90
+ messageId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9._-]{1,64}$" },
91
+ attachmentId: { kind: "string", required: true, minLength: 1, maxLength: 512 },
92
+ },
93
+ },
39
94
  "warehouse.view.read": {
40
95
  idempotencyKey: false,
41
96
  fields: {
42
- columns: { kind: "string[]", required: true, minItems: 1, maxItems: 32, uniqueItems: false, item: { minLength: 0, pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$" } },
97
+ columns: { kind: "string[]", required: false, minItems: 1, maxItems: 32, uniqueItems: false, item: { minLength: 0, pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$" } },
98
+ where: { kind: "object[]", required: false, minItems: 1, maxItems: 16, fields: {
99
+ column: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$" },
100
+ op: { kind: "enum", required: true, values: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "isNull", "isNotNull"] },
101
+ value: { kind: "value", required: false, types: ["string", "number", "boolean"], maxLength: 256, list: { minItems: 1, maxItems: 100, types: ["string", "number"] } },
102
+ } },
103
+ groupBy: { kind: "string[]", required: false, minItems: 1, maxItems: 8, uniqueItems: false, item: { minLength: 0, pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$" } },
104
+ aggregates: { kind: "object[]", required: false, minItems: 1, maxItems: 16, fields: {
105
+ fn: { kind: "enum", required: true, values: ["count", "sum", "avg", "min", "max"] },
106
+ column: { kind: "string", required: true, minLength: 0, pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$" },
107
+ as: { kind: "string", required: true, minLength: 0, pattern: "^[a-z_][a-z0-9_]{0,62}$" },
108
+ } },
109
+ orderBy: { kind: "object[]", required: false, minItems: 1, maxItems: 8, fields: {
110
+ column: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$" },
111
+ direction: { kind: "enum", required: false, values: ["asc", "desc"], default: "asc" },
112
+ } },
43
113
  limit: { kind: "integer", required: false, minimum: 1, maximum: 1000, default: 100 },
44
114
  },
45
115
  },
@@ -60,55 +130,113 @@ function matches(pattern, value) {
60
130
  }
61
131
  return compiled.test(value);
62
132
  }
133
+ function refuse(field, message) { return { ok: false, problem: { field, message } }; }
134
+ const valueNouns = { string: ['a string', 'strings'], number: ['a number', 'numbers'], boolean: ['a boolean', 'booleans'] };
135
+ function either(parts) {
136
+ return parts.length > 1 ? `${parts.slice(0, -1).join(', ')} or ${parts[parts.length - 1]}` : parts.join('');
137
+ }
138
+ function isScalar(types, maxLength, value) {
139
+ if (typeof value === 'string')
140
+ return types.includes('string') && (maxLength === undefined || value.length <= maxLength);
141
+ if (typeof value === 'number')
142
+ return types.includes('number') && Number.isFinite(value);
143
+ return typeof value === 'boolean' && types.includes('boolean');
144
+ }
63
145
  /**
64
- * Validates one operation's input against the contract and returns it with the
65
- * contract's defaults applied. The message names the field and its allowed
66
- * range; it is the sentence the platform answers with, so a caller may show it.
146
+ * Checks one object's fields in rule order and applies defaults: the input
147
+ * itself (where null counts as absent), or one entry of an object[] input
148
+ * (where only a missing key is absent, so null is refused like any wrong value).
67
149
  */
68
- export function validateIntegrationExecuteInput(operation, input) {
69
- const rule = IntegrationExecuteInputRules[operation];
70
- if (!rule)
71
- return { ok: false, problem: { field: 'operation', message: `${operation} is not a declared integration operation` } };
72
- const raw = input === undefined ? {} : input;
73
- if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
74
- return { ok: false, problem: { field: 'input', message: 'input must be an object' } };
75
- const given = raw;
150
+ function checkFields(operation, prefix, fields, given, nullIsAbsent) {
76
151
  for (const name of Object.keys(given))
77
- if (!Object.prototype.hasOwnProperty.call(rule.fields, name))
78
- return { ok: false, problem: { field: name, message: `${name} is not an input of ${operation}` } };
152
+ if (!Object.prototype.hasOwnProperty.call(fields, name))
153
+ return refuse(prefix + name, `${prefix}${name} is not an input of ${operation}`);
79
154
  const out = {};
80
- for (const [name, field] of Object.entries(rule.fields)) {
155
+ for (const [name, field] of Object.entries(fields)) {
156
+ const path = prefix + name;
81
157
  const value = given[name];
82
- if (value === undefined || value === null) {
158
+ if (value === undefined || (nullIsAbsent && value === null)) {
83
159
  if (field.required)
84
- return { ok: false, problem: { field: name, message: `${name} is required for ${operation}` } };
85
- if (field.kind === 'integer' && field.default !== undefined)
160
+ return refuse(path, `${path} is required for ${operation}`);
161
+ if ((field.kind === 'integer' || field.kind === 'enum') && field.default !== undefined)
86
162
  out[name] = field.default;
163
+ if (field.kind === 'string[]' && field.default !== undefined)
164
+ out[name] = [...field.default];
87
165
  continue;
88
166
  }
89
- if (field.kind === 'integer') {
167
+ const checked = checkField(operation, path, name, field, value);
168
+ if (!checked.ok)
169
+ return checked;
170
+ out[name] = checked.value;
171
+ }
172
+ return { ok: true, value: out };
173
+ }
174
+ function checkField(operation, path, name, field, value) {
175
+ switch (field.kind) {
176
+ case 'integer':
90
177
  if (typeof value !== 'number' || !Number.isInteger(value) || value < field.minimum || value > field.maximum)
91
- return { ok: false, problem: { field: name, message: `${name} must be an integer between ${field.minimum} and ${field.maximum}` } };
92
- }
93
- else if (field.kind === 'string') {
178
+ return refuse(path, `${path} must be an integer between ${field.minimum} and ${field.maximum}`);
179
+ return { ok: true, value };
180
+ case 'string':
94
181
  if (typeof value !== 'string' || value.length < field.minLength || (field.maxLength !== undefined && value.length > field.maxLength))
95
- return { ok: false, problem: { field: name, message: field.maxLength !== undefined ? `${name} must be ${field.minLength} to ${field.maxLength} characters` : `${name} must be a string of at least ${field.minLength} characters` } };
182
+ return refuse(path, field.maxLength !== undefined ? `${path} must be ${field.minLength} to ${field.maxLength} characters` : `${path} must be a string of at least ${field.minLength} characters`);
96
183
  if (field.pattern && !matches(field.pattern, value))
97
- return { ok: false, problem: { field: name, message: `${name} must match ${field.pattern}` } };
98
- }
99
- else {
184
+ return refuse(path, `${path} must match ${field.pattern}`);
185
+ return { ok: true, value };
186
+ case 'enum':
187
+ if (typeof value !== 'string' || !field.values.includes(value))
188
+ return refuse(path, `${path} must be one of ${field.values.join(', ')}`);
189
+ return { ok: true, value };
190
+ case 'string[]': {
100
191
  if (!Array.isArray(value) || value.length < field.minItems || value.length > field.maxItems)
101
- return { ok: false, problem: { field: name, message: `${name} must list ${field.minItems} to ${field.maxItems} items` } };
192
+ return refuse(path, `${path} must list ${field.minItems} to ${field.maxItems} items`);
102
193
  const seen = new Set();
103
194
  for (const [index, item] of value.entries()) {
104
195
  if (typeof item !== 'string' || item.length < field.item.minLength || (field.item.maxLength !== undefined && item.length > field.item.maxLength) || (field.item.pattern !== undefined && !matches(field.item.pattern, item)))
105
- return { ok: false, problem: { field: name, message: `${name}[${index}] is not a valid ${name} entry` } };
196
+ return refuse(path, `${path}[${index}] is not a valid ${name} entry`);
106
197
  if (field.uniqueItems && seen.has(item))
107
- return { ok: false, problem: { field: name, message: `${name} must not repeat an entry` } };
198
+ return refuse(path, `${path} must not repeat an entry`);
108
199
  seen.add(item);
109
200
  }
201
+ return { ok: true, value };
202
+ }
203
+ case 'object[]': {
204
+ if (!Array.isArray(value) || value.length < field.minItems || value.length > field.maxItems)
205
+ return refuse(path, `${path} must list ${field.minItems} to ${field.maxItems} items`);
206
+ const entries = [];
207
+ for (const [index, item] of value.entries()) {
208
+ if (item === null || typeof item !== 'object' || Array.isArray(item))
209
+ return refuse(path, `${path}[${index}] is not a valid ${name} entry`);
210
+ const entry = checkFields(operation, `${path}[${index}].`, field.fields, item, false);
211
+ if (!entry.ok)
212
+ return entry;
213
+ entries.push(entry.value);
214
+ }
215
+ return { ok: true, value: entries };
216
+ }
217
+ case 'value': {
218
+ const { types, maxLength, list } = field;
219
+ if (isScalar(types, maxLength, value) || (list !== undefined && Array.isArray(value) && value.length >= list.minItems && value.length <= list.maxItems && value.every(item => isScalar(list.types, maxLength, item))))
220
+ return { ok: true, value };
221
+ const shapes = types.map(type => (type === 'string' && maxLength !== undefined ? `a string of at most ${maxLength} characters` : valueNouns[type][0]));
222
+ if (list !== undefined)
223
+ shapes.push(`a list of ${list.minItems} to ${list.maxItems} ${either(list.types.map(type => valueNouns[type][1]))}`);
224
+ return refuse(path, `${path} must be ${either(shapes)}`);
110
225
  }
111
- out[name] = value;
112
226
  }
113
- return { ok: true, input: out };
227
+ }
228
+ /**
229
+ * Validates one operation's input against the contract and returns it with the
230
+ * contract's defaults applied. The message names the field and its allowed
231
+ * range; it is the sentence the platform answers with, so a caller may show it.
232
+ */
233
+ export function validateIntegrationExecuteInput(operation, input) {
234
+ const rule = IntegrationExecuteInputRules[operation];
235
+ if (!rule)
236
+ return refuse('operation', `${operation} is not a declared integration operation`);
237
+ const raw = input === undefined ? {} : input;
238
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
239
+ return refuse('input', 'input must be an object');
240
+ const checked = checkFields(operation, '', rule.fields, raw, true);
241
+ return checked.ok ? { ok: true, input: checked.value } : checked;
114
242
  }
@@ -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 slack.message.post, and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A slack.message.post runs as the app (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 post does. Per-operation input bounds: slack.channel.history {limit? 1..15, default 15}; slack.message.post {text 1..4000 chars} and a required idempotencyKey retained across retries of one explicit Send; gmail.thread.list {limit? 1..15, default 15}; gmail.message.read {messageId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, replyToMessageId?} and a required idempotencyKey like slack.message.post (plain text only, no attachments); warehouse.view.read {columns 1..32, limit? 1..1000, default 100}. 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, 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), 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). 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), total deadline 10 s.
3
3
  */
4
4
  export type IntegrationExecuteRequest = {
5
5
  /**
@@ -7,26 +7,69 @@ export type IntegrationExecuteRequest = {
7
7
  */
8
8
  connection: string;
9
9
  /**
10
- * Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity. slack.message.post (0.53.0) runs in the mode the app declares: app (the company's Slack app, under the IT-approved per-app name) or user (the signed-in person, after their consent); since 0.55.0 each call names which of the approved modes it runs under.
10
+ * Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity; since 0.71.0 it may carry an HTML body and attachments. slack.message.post (0.53.0) runs in the mode the app declares: app (the company's Slack app, under the IT-approved per-app name) or user (the signed-in person, after their consent); since 0.55.0 each call names which of the approved modes it runs under. 0.71.0 (ADR 0018) adds slack.message.update and slack.message.delete (declared mode, like post), slack.dm.send (app mode: a direct message to 1..8 workspace members named by email), slack.user.lookup (app mode: email to Slack user id), gmail.thread.read (every message of one thread) and gmail.attachment.read (one attachment's bytes), and lets slack.channel.history run in app mode as well as user mode.
11
11
  */
12
- operation: 'slack.channel.history' | 'slack.message.post' | 'gmail.thread.list' | 'gmail.message.read' | 'gmail.message.send' | 'warehouse.view.read';
12
+ operation: 'slack.channel.history' | 'slack.message.post' | 'slack.message.update' | 'slack.message.delete' | 'slack.dm.send' | 'slack.user.lookup' | 'gmail.thread.list' | 'gmail.thread.read' | 'gmail.message.read' | 'gmail.message.send' | 'gmail.attachment.read' | 'warehouse.view.read';
13
13
  /**
14
14
  * Tenant-wide logical alias of a connection (governance sourceId) or of a resource under it.
15
15
  */
16
16
  resource: string;
17
17
  input: {
18
18
  /**
19
- * Read operations: how many items to return; the per-operation then-block sets the real ceiling (15 for slack.channel.history and gmail.thread.list, 1000 for warehouse.view.read) and the default.
19
+ * Read operations: how many items to return; the per-operation then-block sets the real ceiling (100 for slack.channel.history, 50 for gmail.thread.list, 1000 for warehouse.view.read) and the default.
20
20
  */
21
21
  limit?: number;
22
22
  /**
23
- * slack.message.post / gmail.message.send: the plain-text body, 1..4000 characters.
23
+ * slack.message.post / slack.message.update / slack.dm.send / gmail.message.send: the body, 1..4000 characters (plain text, or Slack mrkdwn when `format` says so).
24
24
  */
25
25
  text?: string;
26
26
  /**
27
- * gmail.message.read: the Gmail message id a gmail.thread.list result named.
27
+ * slack.message.post / slack.message.update / slack.dm.send (0.71.0): how Slack renders `text`; plain when absent.
28
+ */
29
+ format?: 'plain' | 'mrkdwn';
30
+ /**
31
+ * slack.message.post (0.71.0): the Slack message ts (a slack.channel.history message id) the post replies to, so it lands in that thread. gmail.thread.read: the Gmail thread id a gmail.thread.list result named.
32
+ */
33
+ threadId?: string;
34
+ /**
35
+ * gmail.message.read / gmail.attachment.read: the Gmail message id a gmail.thread.list or gmail.thread.read result named. slack.message.update / slack.message.delete (0.71.0): the Slack message ts a post or history result named.
28
36
  */
29
37
  messageId?: string;
38
+ /**
39
+ * gmail.attachment.read (0.71.0): the attachment id a gmail.message.read or gmail.thread.read result named.
40
+ */
41
+ attachmentId?: string;
42
+ /**
43
+ * slack.channel.history (0.71.0): the `nextCursor` of the previous page; absent reads the newest page.
44
+ */
45
+ cursor?: string;
46
+ /**
47
+ * slack.channel.history (0.71.0): only messages after this Slack ts.
48
+ */
49
+ oldest?: string;
50
+ /**
51
+ * slack.channel.history (0.71.0): only messages before this Slack ts.
52
+ */
53
+ latest?: string;
54
+ /**
55
+ * gmail.thread.list (0.71.0): a Gmail search query (the search-box syntax, e.g. `from:jane newer_than:7d`).
56
+ */
57
+ q?: string;
58
+ /**
59
+ * gmail.thread.list (0.71.0): the Gmail labels a thread must carry (all of them); INBOX when absent.
60
+ *
61
+ * @minItems 1
62
+ * @maxItems 5
63
+ */
64
+ labelIds?: string[];
65
+ /**
66
+ * gmail.thread.list (0.71.0): the `nextPageToken` of the previous page; absent reads the first page.
67
+ */
68
+ pageToken?: string;
69
+ /**
70
+ * slack.user.lookup (0.71.0): the workspace member's email address to resolve to a Slack user id.
71
+ */
72
+ email?: string;
30
73
  to?: EmailAddressList;
31
74
  cc?: EmailAddressList1;
32
75
  bcc?: EmailAddressList2;
@@ -39,19 +82,58 @@ export type IntegrationExecuteRequest = {
39
82
  */
40
83
  replyToMessageId?: string;
41
84
  /**
42
- * Explicit columns, or `*` alone when the app's approved resource grant allows all current and future columns.
85
+ * gmail.message.send (0.71.0): an HTML body sent beside `text` (which stays the plain-text alternative), at most 64 KiB.
86
+ */
87
+ html?: string;
88
+ /**
89
+ * gmail.message.send (0.71.0): files to attach, 1..5, each at most 2 MiB (2,796,203 base64 characters).
90
+ *
91
+ * @minItems 1
92
+ * @maxItems 5
93
+ */
94
+ attachments?: EmailAttachment[];
95
+ /**
96
+ * warehouse.view.read, a plain read: explicit columns, or `*` alone when the app's approved resource grant allows all current and future columns. An aggregate read names aggregates instead.
43
97
  *
44
98
  * @minItems 1
45
99
  * @maxItems 32
46
100
  */
47
101
  columns?: string[];
102
+ /**
103
+ * warehouse.view.read (0.70.0): filters the warehouse applies before it returns, grouping or limits rows. All of them must hold (AND); there is no OR and no LIKE.
104
+ *
105
+ * @minItems 1
106
+ * @maxItems 16
107
+ */
108
+ where?: WarehouseFilter[];
109
+ /**
110
+ * warehouse.view.read (0.70.0): columns (never `*`) that group the rows; needs aggregates. Each returned row is one group: its groupBy columns, then its aggregate aliases.
111
+ *
112
+ * @minItems 1
113
+ * @maxItems 8
114
+ */
115
+ groupBy?: string[];
116
+ /**
117
+ * warehouse.view.read (0.70.0): what an aggregate read computes, over each group or over every matching row when there is no groupBy.
118
+ *
119
+ * @minItems 1
120
+ * @maxItems 16
121
+ */
122
+ aggregates?: WarehouseAggregate[];
123
+ /**
124
+ * warehouse.view.read (0.70.0): sort keys, first key first. Without orderBy an aggregate read is sorted by its groupBy columns and a plain read is not sorted at all.
125
+ *
126
+ * @minItems 1
127
+ * @maxItems 8
128
+ */
129
+ orderBy?: WarehouseOrder[];
48
130
  };
49
131
  /**
50
132
  * UUID of one explicit Send action; required for slack.message.post and gmail.message.send only. The same key with the same payload and subject returns the recorded outcome for seven days.
51
133
  */
52
134
  idempotencyKey?: string;
53
135
  /**
54
- * slack.message.post only (0.55.0): which of the app's IT-approved modes this post runs under — app (the company's Slack app, under the grant's per-app name and the footer) or user (the signed-in person, after their consent; no name, no footer). Required when the app holds approved grants for both modes (else MODE_REQUIRED); may be omitted when it holds one, which is then used. A named mode without an approved grant is refused with MODE_NOT_GRANTED; governance never falls back to the other mode. Every other operation's mode is fixed, so the field is refused there.
136
+ * The declared-mode Slack operations only (slack.message.post since 0.55.0; slack.channel.history, slack.message.update and slack.message.delete since 0.71.0): which of the app's IT-approved modes this call runs under — app (the company's Slack app; a post goes out under the grant's per-app name and the footer) or user (the signed-in person, after their consent; no name, no footer). Required when the app holds approved grants for both modes (else MODE_REQUIRED); may be omitted when it holds one, which is then used. A named mode without an approved grant is refused with MODE_NOT_GRANTED; governance never falls back to the other mode. Every other operation's mode is fixed, so the field is refused there.
55
137
  */
56
138
  mode?: 'app' | 'user';
57
139
  };
@@ -60,7 +142,7 @@ export type IntegrationExecuteRequest = {
60
142
  */
61
143
  export type EmailAddress = string;
62
144
  /**
63
- * gmail.message.send: primary recipients.
145
+ * gmail.message.send: primary recipients. slack.dm.send (0.71.0): the workspace members to message, by email, 1..8; several open one group DM.
64
146
  *
65
147
  * @minItems 1
66
148
  * @maxItems 50
@@ -80,3 +162,70 @@ export type EmailAddressList1 = EmailAddress[];
80
162
  * @maxItems 50
81
163
  */
82
164
  export type EmailAddressList2 = EmailAddress[];
165
+ /**
166
+ * A filter comparison: = <> > >= < <= IN, NOT IN, IS NULL, IS NOT NULL. Word-like so every value is a valid generated constant.
167
+ */
168
+ export type WarehouseFilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'isNull' | 'isNotNull';
169
+ /**
170
+ * An aggregate function.
171
+ */
172
+ export type WarehouseAggregateFn = 'count' | 'sum' | 'avg' | 'min' | 'max';
173
+ /**
174
+ * Sort direction; asc when absent.
175
+ */
176
+ export type WarehouseOrderDirection = 'asc' | 'desc';
177
+ /**
178
+ * One file attached to a gmail.message.send (0.71.0), carried inline as base64. At most 2 MiB of bytes, which is 2,796,203 base64 characters.
179
+ */
180
+ export interface EmailAttachment {
181
+ /**
182
+ * The name the recipient sees.
183
+ */
184
+ filename: string;
185
+ /**
186
+ * The MIME type, e.g. application/pdf.
187
+ */
188
+ contentType: string;
189
+ /**
190
+ * The file bytes, standard base64.
191
+ */
192
+ dataBase64: string;
193
+ }
194
+ /**
195
+ * 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.
196
+ */
197
+ export interface WarehouseFilter {
198
+ /**
199
+ * The filtered column; never `*`.
200
+ */
201
+ column: string;
202
+ op: WarehouseFilterOp;
203
+ /**
204
+ * The value compared with: a string (a date or timestamp is its ISO string), a number, a boolean, or a list of strings or numbers for in/notIn. Never null; use isNull or isNotNull.
205
+ */
206
+ value?: string | number | boolean | (string | number)[];
207
+ }
208
+ /**
209
+ * One computed column of an aggregate warehouse.view.read (0.70.0): fn(column) AS as. `*` is a column only for count (a governance rule). The value follows the read's cell rules, so a 64-bit or decimal result may be a string.
210
+ */
211
+ export interface WarehouseAggregate {
212
+ fn: WarehouseAggregateFn;
213
+ /**
214
+ * A plain warehouse column identifier, or `*` as the sole selection when IT exposed all current and future columns.
215
+ */
216
+ column: string;
217
+ /**
218
+ * The name the computed value is returned under; unique in the read and never a groupBy column (governance rules).
219
+ */
220
+ as: string;
221
+ }
222
+ /**
223
+ * One warehouse.view.read sort key (0.70.0). A plain read sorts by a selected column (any column when it selects `*`); an aggregate read sorts by a groupBy column or an aggregate alias (governance rules).
224
+ */
225
+ export interface WarehouseOrder {
226
+ /**
227
+ * A column or an aggregate alias; never `*`.
228
+ */
229
+ column: string;
230
+ direction?: WarehouseOrderDirection;
231
+ }
@@ -1,17 +1,37 @@
1
1
  /**
2
- * The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the six operation results: slack.channel.history -> {messages, hasMore} (plain text/IDs/time, plus the display name and emoji icon a per-app-named app post carries — authorName/authorIcon, 0.55.0 — never the provider payload); slack.message.post -> {messageId}; gmail.thread.list -> {threads, hasMore} (newest inbox threads; each `messageId` is the thread's newest message, what gmail.message.read takes); gmail.message.read -> {id, threadId, subject, from, to, date, text, truncated} (headers plus the plain text with html stripped, cut at 16 KiB with `truncated` set); gmail.message.send -> {messageId, threadId} (the Gmail ids of the sent message and its thread); warehouse.view.read -> {rows} (decimal/64-bit values as strings, dates as ISO strings). An unknown send outcome is never a result; it is an error with details.code SEND_OUTCOME_UNKNOWN. Results over 1 MiB are rejected, not truncated.
2
+ * The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the operation results (twelve operations since 0.71.0, ADR 0018): slack.channel.history -> {messages, hasMore, nextCursor?} (plain text/IDs/time, plus the display name and emoji icon a per-app-named app post carries — authorName/authorIcon, 0.55.0 — never the provider payload; `nextCursor` when hasMore, to pass as the next read's `cursor`); slack.message.post, slack.message.update, slack.message.delete -> {messageId} (the Slack ts); slack.dm.send -> {messageId, channelId} (the ts and the DM channel it went to); slack.user.lookup -> {userId, displayName}; gmail.thread.list -> {threads, hasMore, nextPageToken?} (newest matching threads; each `messageId` is the thread's newest message, what gmail.message.read takes; `nextPageToken` when hasMore); gmail.thread.read -> {id, emails} (every message of the thread, oldest first, each shaped like a gmail.message.read result); gmail.message.read -> {id, threadId, subject, from, to, date, text, truncated, attachments?} (headers plus the plain text with html stripped, cut at 16 KiB with `truncated` set; `attachments` lists the files without their bytes, which gmail.attachment.read fetches by id); gmail.attachment.read -> {filename, mimeType, size, dataBase64}; gmail.message.send -> {messageId, threadId} (the Gmail ids of the sent message and its thread); warehouse.view.read -> {rows, truncated} (rows keyed by the selected columns, or for an aggregate read by the groupBy columns then the aggregate aliases; decimal/64-bit values as strings, dates as ISO strings; since 0.70.0 every server sends `truncated`, true when more rows matched than `limit` — the server reads limit+1 rows to know — so the rows are incomplete and must not be totalled or averaged; a server before 0.70.0 omitted it). An unknown send outcome is never a result; it is an error with details.code SEND_OUTCOME_UNKNOWN. A whole result over 1 MiB is rejected, never cut down to fit — except a gmail.attachment.read, whose single attachment is bounded by its own 2 MiB.
3
3
  */
4
4
  export type IntegrationExecuteResult = {
5
5
  /**
6
- * @maxItems 15
6
+ * @maxItems 100
7
7
  */
8
8
  messages?: ChannelMessage[];
9
9
  hasMore?: boolean;
10
+ /**
11
+ * slack.channel.history (0.71.0): the cursor of the next (older) page, present only when hasMore.
12
+ */
13
+ nextCursor?: string;
10
14
  messageId?: string;
11
15
  /**
12
- * @maxItems 15
16
+ * slack.dm.send (0.71.0): the Slack DM channel id the message went to.
17
+ */
18
+ channelId?: string;
19
+ /**
20
+ * slack.user.lookup (0.71.0): the Slack user id of the member with that email.
21
+ */
22
+ userId?: string;
23
+ /**
24
+ * slack.user.lookup (0.71.0): the member's display name, or real name when they set no display name; may be empty.
25
+ */
26
+ displayName?: string;
27
+ /**
28
+ * @maxItems 50
13
29
  */
14
30
  threads?: GmailThread[];
31
+ /**
32
+ * gmail.thread.list (0.71.0): the token of the next page, present only when hasMore.
33
+ */
34
+ nextPageToken?: string;
15
35
  id?: string;
16
36
  threadId?: string;
17
37
  subject?: string;
@@ -19,7 +39,32 @@ export type IntegrationExecuteResult = {
19
39
  to?: string;
20
40
  date?: string;
21
41
  text?: string;
42
+ /**
43
+ * gmail.message.read: the text was cut at 16 KiB. warehouse.view.read (0.70.0): more rows matched than the read's limit, so `rows` is incomplete.
44
+ */
22
45
  truncated?: boolean;
46
+ /**
47
+ * gmail.message.read (0.71.0): the message's attachments, without their bytes.
48
+ *
49
+ * @maxItems 100
50
+ */
51
+ attachments?: GmailAttachment[];
52
+ /**
53
+ * gmail.thread.read (0.71.0): every message of the thread, oldest first.
54
+ *
55
+ * @maxItems 100
56
+ */
57
+ emails?: GmailMessage[];
58
+ filename?: string;
59
+ mimeType?: string;
60
+ /**
61
+ * gmail.attachment.read (0.71.0): the attachment's size in bytes.
62
+ */
63
+ size?: number;
64
+ /**
65
+ * gmail.attachment.read (0.71.0): the attachment's bytes, standard base64, at most 2 MiB of bytes.
66
+ */
67
+ dataBase64?: string;
23
68
  /**
24
69
  * @maxItems 1000
25
70
  */
@@ -49,3 +94,32 @@ export interface GmailThread {
49
94
  date: string;
50
95
  snippet: string;
51
96
  }
97
+ /**
98
+ * One attachment of a Gmail message (0.71.0), named without its bytes; gmail.attachment.read fetches them by `id`.
99
+ */
100
+ export interface GmailAttachment {
101
+ id: string;
102
+ filename: string;
103
+ mimeType: string;
104
+ /**
105
+ * Bytes.
106
+ */
107
+ size: number;
108
+ }
109
+ /**
110
+ * One message of a gmail.thread.read result (0.71.0): the gmail.message.read shape.
111
+ */
112
+ export interface GmailMessage {
113
+ id: string;
114
+ threadId: string;
115
+ subject: string;
116
+ from: string;
117
+ to: string;
118
+ date: string;
119
+ text: string;
120
+ truncated: boolean;
121
+ /**
122
+ * @maxItems 100
123
+ */
124
+ attachments?: GmailAttachment[];
125
+ }