@fourier-labs/harbour-contracts 0.74.0 → 0.76.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.
@@ -12,7 +12,7 @@ export type DataGrantV2ProvisioningStatus = 'NONE' | 'PENDING' | 'FAILED' | 'DON
12
12
  export type DataGrantV2CleanupStatus = 'PENDING' | 'FAILED' | 'DONE';
13
13
 
14
14
  /**
15
- * An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON ["v2",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. An app-mode Slack grant may carry `presentation` (0.53.0): the IT-approved display name and icon the app's posts go out under, copied from the approved request and read by every post — never from the call. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.
15
+ * An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON ["v2",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, a Basecamp project alias or the fixed Basecamp `account` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. An app-mode Slack grant may carry `presentation` (0.53.0): the IT-approved display name and icon the app's posts go out under, copied from the approved request and read by every post — never from the call. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.
16
16
  */
17
17
  export interface DataGrantV2 {
18
18
  schema: 'harbour.data-grant/2.0';
@@ -33,9 +33,9 @@ export interface DataGrantV2 {
33
33
  kind: 'saas' | 'database' | 'service';
34
34
  status: DataGrantV2Status;
35
35
  /**
36
- * Approved operations; each must match this grant's mode. Empty until the first approval. At most the eleven SaaS operations (0.71.0); a database grant holds one.
36
+ * Approved operations; each must match this grant's mode. Empty until the first approval. At most the twenty-two SaaS operations (0.76.0: eleven Slack and Gmail, eleven Basecamp); a database grant holds one.
37
37
  *
38
- * @maxItems 11
38
+ * @maxItems 22
39
39
  */
40
40
  operations: (
41
41
  | 'slack.channel.history'
@@ -49,6 +49,17 @@ export interface DataGrantV2 {
49
49
  | 'gmail.message.read'
50
50
  | 'gmail.message.send'
51
51
  | 'gmail.attachment.read'
52
+ | 'basecamp.project.list'
53
+ | 'basecamp.todolist.list'
54
+ | 'basecamp.todo.list'
55
+ | 'basecamp.todo.create'
56
+ | 'basecamp.todo.complete'
57
+ | 'basecamp.message.list'
58
+ | 'basecamp.message.post'
59
+ | 'basecamp.comment.list'
60
+ | 'basecamp.comment.post'
61
+ | 'basecamp.campfire.post'
62
+ | 'basecamp.people.list'
52
63
  | 'warehouse.view.read'
53
64
  | 'service.request'
54
65
  )[];
@@ -100,7 +111,7 @@ export interface DataGrantV2 {
100
111
  installation?: string;
101
112
  }
102
113
  /**
103
- * One approved logical resource: a channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with the approved columns.
114
+ * One approved logical resource: a Slack channel alias, the fixed Gmail `inbox` alias, a Basecamp project alias or the fixed Basecamp `account` alias (0.76.0), or a warehouse view alias with the approved columns.
104
115
  */
105
116
  export interface IntegrationResource {
106
117
  /**
@@ -15,7 +15,8 @@ export type IntegrationExecuteInputFieldRule =
15
15
  | { kind: 'string[]'; required: boolean; minItems: number; maxItems: number; uniqueItems: boolean; item: { minLength: number; maxLength?: number; pattern?: string }; default?: readonly string[] }
16
16
  | { kind: 'enum'; required: boolean; values: readonly string[]; default?: string }
17
17
  | { kind: 'object[]'; required: boolean; minItems: number; maxItems: number; fields: Readonly<Record<string, IntegrationExecuteInputFieldRule>> }
18
- | { kind: 'value'; required: boolean; types: readonly IntegrationExecuteInputValueType[]; maxLength?: number; list?: { minItems: number; maxItems: number; types: readonly IntegrationExecuteInputValueType[] } };
18
+ | { kind: 'value'; required: boolean; types: readonly IntegrationExecuteInputValueType[]; maxLength?: number; list?: { minItems: number; maxItems: number; types: readonly IntegrationExecuteInputValueType[] } }
19
+ | { kind: 'boolean'; required: boolean };
19
20
 
20
21
  /** The scalar types a `value` input accepts, alone or in its list form. */
21
22
  export type IntegrationExecuteInputValueType = 'string' | 'number' | 'boolean';
@@ -113,6 +114,87 @@ export const IntegrationExecuteInputRules = {
113
114
  attachmentId: { kind: "string", required: true, minLength: 1, maxLength: 512 },
114
115
  },
115
116
  },
117
+ "basecamp.project.list": {
118
+ idempotencyKey: false,
119
+ fields: {
120
+ pageToken: { kind: "string", required: false, minLength: 1, maxLength: 256 },
121
+ projectStatus: { kind: "enum", required: false, values: ["active", "archived"] },
122
+ },
123
+ },
124
+ "basecamp.todolist.list": {
125
+ idempotencyKey: false,
126
+ fields: {
127
+ pageToken: { kind: "string", required: false, minLength: 1, maxLength: 256 },
128
+ },
129
+ },
130
+ "basecamp.todo.list": {
131
+ idempotencyKey: false,
132
+ fields: {
133
+ todolistId: { kind: "string", required: true, minLength: 0, pattern: "^[0-9]{1,20}$" },
134
+ completed: { kind: "boolean", required: false },
135
+ pageToken: { kind: "string", required: false, minLength: 1, maxLength: 256 },
136
+ },
137
+ },
138
+ "basecamp.todo.create": {
139
+ idempotencyKey: true,
140
+ fields: {
141
+ todolistId: { kind: "string", required: true, minLength: 0, pattern: "^[0-9]{1,20}$" },
142
+ content: { kind: "string", required: true, minLength: 1, maxLength: 1000 },
143
+ description: { kind: "string", required: false, minLength: 1, maxLength: 16384 },
144
+ contentFormat: { kind: "enum", required: false, values: ["plain", "html"], default: "plain" },
145
+ dueOn: { kind: "string", required: false, minLength: 0, pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
146
+ assigneeIds: { kind: "string[]", required: false, minItems: 1, maxItems: 20, uniqueItems: true, item: { minLength: 0, pattern: "^[0-9]{1,20}$" } },
147
+ notify: { kind: "boolean", required: false },
148
+ },
149
+ },
150
+ "basecamp.todo.complete": {
151
+ idempotencyKey: false,
152
+ fields: {
153
+ todoId: { kind: "string", required: true, minLength: 0, pattern: "^[0-9]{1,20}$" },
154
+ },
155
+ },
156
+ "basecamp.message.list": {
157
+ idempotencyKey: false,
158
+ fields: {
159
+ pageToken: { kind: "string", required: false, minLength: 1, maxLength: 256 },
160
+ },
161
+ },
162
+ "basecamp.message.post": {
163
+ idempotencyKey: true,
164
+ fields: {
165
+ subject: { kind: "string", required: true, minLength: 1, maxLength: 256 },
166
+ content: { kind: "string", required: true, minLength: 1, maxLength: 65536 },
167
+ contentFormat: { kind: "enum", required: false, values: ["plain", "html"], default: "plain" },
168
+ },
169
+ },
170
+ "basecamp.comment.list": {
171
+ idempotencyKey: false,
172
+ fields: {
173
+ recordingId: { kind: "string", required: true, minLength: 0, pattern: "^[0-9]{1,20}$" },
174
+ pageToken: { kind: "string", required: false, minLength: 1, maxLength: 256 },
175
+ },
176
+ },
177
+ "basecamp.comment.post": {
178
+ idempotencyKey: true,
179
+ fields: {
180
+ recordingId: { kind: "string", required: true, minLength: 0, pattern: "^[0-9]{1,20}$" },
181
+ content: { kind: "string", required: true, minLength: 1, maxLength: 16384 },
182
+ contentFormat: { kind: "enum", required: false, values: ["plain", "html"], default: "plain" },
183
+ },
184
+ },
185
+ "basecamp.campfire.post": {
186
+ idempotencyKey: true,
187
+ fields: {
188
+ content: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
189
+ contentFormat: { kind: "enum", required: false, values: ["plain", "html"], default: "plain" },
190
+ },
191
+ },
192
+ "basecamp.people.list": {
193
+ idempotencyKey: false,
194
+ fields: {
195
+ pageToken: { kind: "string", required: false, minLength: 1, maxLength: 256 },
196
+ },
197
+ },
116
198
  "warehouse.view.read": {
117
199
  idempotencyKey: false,
118
200
  fields: {
@@ -209,6 +291,9 @@ function checkField(operation: string, path: string, name: string, field: Integr
209
291
  case 'object':
210
292
  if (value === null || typeof value !== 'object' || Array.isArray(value)) return refuse(path, `${path} must be a JSON object`);
211
293
  return { ok: true, value };
294
+ case 'boolean':
295
+ if (typeof value !== 'boolean') return refuse(path, `${path} must be a boolean`);
296
+ return { ok: true, value };
212
297
  case 'integer':
213
298
  if (typeof value !== 'number' || !Number.isInteger(value) || value < field.minimum || value > field.maximum) return refuse(path, `${path} must be an integer between ${field.minimum} and ${field.maximum}`);
214
299
  return { ok: true, value };
@@ -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). 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.
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). 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), total deadline 10 s.
6
6
  */
7
7
  export type IntegrationExecuteRequest = {
8
8
  /**
@@ -10,7 +10,7 @@ export type IntegrationExecuteRequest = {
10
10
  */
11
11
  connection: string;
12
12
  /**
13
- * 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.
13
+ * Closed operation set. Extending it is a contracts change. 0.76.0 (ADR 0022) adds the eleven Basecamp operations, every one user mode only like gmail.*: basecamp.project.list (the projects of the company's pinned Basecamp account; its resource is the fixed `account` alias), and on one mapped project (the resource is a project alias, a key of the connection's `channels` map) basecamp.todolist.list, basecamp.todo.list, basecamp.todo.create (a receipted write: explicit action, idempotencyKey), basecamp.todo.complete (idempotent at Basecamp, no key), basecamp.message.list, basecamp.message.post, basecamp.comment.list, basecamp.comment.post, basecamp.campfire.post (the three posts are receipted writes) and basecamp.people.list. 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.
14
14
  */
15
15
  operation:
16
16
  | 'slack.channel.history'
@@ -24,6 +24,17 @@ export type IntegrationExecuteRequest = {
24
24
  | 'gmail.message.read'
25
25
  | 'gmail.message.send'
26
26
  | 'gmail.attachment.read'
27
+ | 'basecamp.project.list'
28
+ | 'basecamp.todolist.list'
29
+ | 'basecamp.todo.list'
30
+ | 'basecamp.todo.create'
31
+ | 'basecamp.todo.complete'
32
+ | 'basecamp.message.list'
33
+ | 'basecamp.message.post'
34
+ | 'basecamp.comment.list'
35
+ | 'basecamp.comment.post'
36
+ | 'basecamp.campfire.post'
37
+ | 'basecamp.people.list'
27
38
  | 'warehouse.view.read'
28
39
  | 'service.request';
29
40
  /**
@@ -79,7 +90,7 @@ export type IntegrationExecuteRequest = {
79
90
  */
80
91
  labelIds?: string[];
81
92
  /**
82
- * gmail.thread.list (0.71.0): the `nextPageToken` of the previous page; absent reads the first page.
93
+ * gmail.thread.list (0.71.0) and every basecamp.*.list operation (0.76.0): the `nextPageToken` of the previous page; absent reads the first page.
83
94
  */
84
95
  pageToken?: string;
85
96
  /**
@@ -90,7 +101,7 @@ export type IntegrationExecuteRequest = {
90
101
  cc?: EmailAddressList1;
91
102
  bcc?: EmailAddressList2;
92
103
  /**
93
- * gmail.message.send: one-line subject (no line breaks).
104
+ * gmail.message.send: one-line subject (no line breaks). basecamp.message.post (0.76.0): the message's title.
94
105
  */
95
106
  subject?: string;
96
107
  /**
@@ -108,6 +119,53 @@ export type IntegrationExecuteRequest = {
108
119
  * @maxItems 5
109
120
  */
110
121
  attachments?: EmailAttachment[];
122
+ /**
123
+ * basecamp.todo.list / basecamp.todo.create (0.76.0): the to-do list, as a basecamp.todolist.list result named it.
124
+ */
125
+ todolistId?: string;
126
+ /**
127
+ * basecamp.todo.complete (0.76.0): the to-do, as a basecamp.todo.list or basecamp.todo.create result named it.
128
+ */
129
+ todoId?: string;
130
+ /**
131
+ * basecamp.comment.list / basecamp.comment.post (0.76.0): the Basecamp recording commented on — a message, a to-do or any other id a basecamp.* result named.
132
+ */
133
+ recordingId?: string;
134
+ /**
135
+ * basecamp.todo.create (0.76.0): the to-do's title, 1..1000 characters. basecamp.message.post: the body, at most 64 KiB; basecamp.comment.post: at most 16 KiB; basecamp.campfire.post: at most 4000 characters. Plain text unless `contentFormat` is html.
136
+ */
137
+ content?: string;
138
+ /**
139
+ * basecamp.todo.create / basecamp.message.post / basecamp.comment.post / basecamp.campfire.post (0.76.0): how `content` (and a to-do's `description`) is read; plain when absent.
140
+ */
141
+ contentFormat?: 'plain' | 'html';
142
+ /**
143
+ * basecamp.todo.create (0.76.0): the to-do's notes, at most 16 KiB, read the way `contentFormat` says.
144
+ */
145
+ description?: string;
146
+ /**
147
+ * basecamp.todo.create (0.76.0): the due date as YYYY-MM-DD; absent leaves the to-do undated.
148
+ */
149
+ dueOn?: string;
150
+ /**
151
+ * basecamp.todo.create (0.76.0): the people assigned, 1..20, by the ids a basecamp.people.list result named.
152
+ *
153
+ * @minItems 1
154
+ * @maxItems 20
155
+ */
156
+ assigneeIds?: string[];
157
+ /**
158
+ * basecamp.todo.create (0.76.0): whether Basecamp notifies the assignees; false when absent.
159
+ */
160
+ notify?: boolean;
161
+ /**
162
+ * basecamp.todo.list (0.76.0): true lists the completed to-dos instead of the open ones; absent lists the open ones.
163
+ */
164
+ completed?: boolean;
165
+ /**
166
+ * basecamp.project.list (0.76.0): which projects to list; active when absent.
167
+ */
168
+ projectStatus?: 'active' | 'archived';
111
169
  /**
112
170
  * 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.
113
171
  *
@@ -159,7 +217,7 @@ export type IntegrationExecuteRequest = {
159
217
  };
160
218
  };
161
219
  /**
162
- * 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.
220
+ * UUID of one explicit Send action; required for the sends (slack.message.post, slack.dm.send, gmail.message.send) and the receipted Basecamp writes (basecamp.todo.create, basecamp.message.post, basecamp.comment.post, basecamp.campfire.post; 0.76.0), refused everywhere else. The same key with the same payload and subject returns the recorded outcome for seven days.
163
221
  */
164
222
  idempotencyKey?: string;
165
223
  /**
@@ -2,7 +2,7 @@
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
4
  /**
5
- * 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.
5
+ * The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the operation results (twenty-four operations since 0.76.0, ADR 0018 and ADR 0022): 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). Basecamp (0.76.0, ADR 0022; every id a string of digits, every page at most 100 items, `nextPageToken` only when hasMore, text cut at 16 KiB like Gmail): basecamp.project.list -> {projects, hasMore, nextPageToken?}; basecamp.todolist.list -> {todolists, hasMore, nextPageToken?}; basecamp.todo.list -> {todos, hasMore, nextPageToken?}; basecamp.message.list -> {boardMessages, hasMore, nextPageToken?} (`boardMessages`, not `messages`, which is the Slack history shape); basecamp.comment.list -> {comments, hasMore, nextPageToken?}; basecamp.people.list -> {people, hasMore, nextPageToken?}; basecamp.todo.create, basecamp.message.post, basecamp.comment.post, basecamp.campfire.post -> {id, appUrl} (the new recording and its Basecamp page); basecamp.todo.complete -> {id, completed} with completed always true. 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.
6
6
  */
7
7
  export type IntegrationExecuteResult = {
8
8
  /**
@@ -78,6 +78,50 @@ export type IntegrationExecuteResult = {
78
78
  rows?: {
79
79
  [k: string]: unknown;
80
80
  }[];
81
+ /**
82
+ * basecamp.project.list (0.76.0): one page of the pinned account's projects the signed-in person can see.
83
+ *
84
+ * @maxItems 100
85
+ */
86
+ projects?: BasecampProject[];
87
+ /**
88
+ * basecamp.todolist.list (0.76.0): one page of the project's to-do lists.
89
+ *
90
+ * @maxItems 100
91
+ */
92
+ todolists?: BasecampTodolist[];
93
+ /**
94
+ * basecamp.todo.list (0.76.0): one page of the list's open to-dos, or its completed ones when the read said `completed`.
95
+ *
96
+ * @maxItems 100
97
+ */
98
+ todos?: BasecampTodo[];
99
+ /**
100
+ * basecamp.message.list (0.76.0): one page of the project's message board, newest first. Named apart from `messages`, the Slack history shape.
101
+ *
102
+ * @maxItems 100
103
+ */
104
+ boardMessages?: BasecampMessage[];
105
+ /**
106
+ * basecamp.comment.list (0.76.0): one page of the recording's comments, oldest first.
107
+ *
108
+ * @maxItems 100
109
+ */
110
+ comments?: BasecampComment[];
111
+ /**
112
+ * basecamp.people.list (0.76.0): one page of the people on the project.
113
+ *
114
+ * @maxItems 100
115
+ */
116
+ people?: BasecampPerson[];
117
+ /**
118
+ * basecamp.todo.create / basecamp.message.post / basecamp.comment.post / basecamp.campfire.post (0.76.0): the Basecamp page of the new recording, for a person to open.
119
+ */
120
+ appUrl?: string;
121
+ /**
122
+ * basecamp.todo.complete (0.76.0): always true; the to-do is complete whether this call or an earlier one completed it.
123
+ */
124
+ completed?: boolean;
81
125
  status?: number;
82
126
  headers?: {
83
127
  [k: string]: string;
@@ -136,3 +180,119 @@ export interface GmailMessage {
136
180
  */
137
181
  attachments?: GmailAttachment[];
138
182
  }
183
+ /**
184
+ * One Basecamp project of a basecamp.project.list result (0.76.0). `id` is what IT maps a project alias to on the connection.
185
+ */
186
+ export interface BasecampProject {
187
+ /**
188
+ * A Basecamp id (0.76.0): an account, project, to-do list, to-do, recording or person, as a string of 1..20 digits — how every basecamp.* result names one and every basecamp.* input takes one back.
189
+ */
190
+ id: string;
191
+ name: string;
192
+ description?: string;
193
+ appUrl: string;
194
+ /**
195
+ * @maxItems 32
196
+ */
197
+ tools: BasecampTool[];
198
+ }
199
+ /**
200
+ * One enabled tool of a Basecamp project (0.76.0), from its dock: `name` is Basecamp's kind (todoset, message_board, chat, schedule, vault, questionnaire, inbox, kanban_board), `title` what the project calls it.
201
+ */
202
+ export interface BasecampTool {
203
+ /**
204
+ * A Basecamp id (0.76.0): an account, project, to-do list, to-do, recording or person, as a string of 1..20 digits — how every basecamp.* result names one and every basecamp.* input takes one back.
205
+ */
206
+ id: string;
207
+ name: string;
208
+ title: string;
209
+ }
210
+ /**
211
+ * One to-do list of a basecamp.todolist.list result (0.76.0); `id` is what basecamp.todo.list and basecamp.todo.create take as `todolistId`.
212
+ */
213
+ export interface BasecampTodolist {
214
+ /**
215
+ * A Basecamp id (0.76.0): an account, project, to-do list, to-do, recording or person, as a string of 1..20 digits — how every basecamp.* result names one and every basecamp.* input takes one back.
216
+ */
217
+ id: string;
218
+ name: string;
219
+ description?: string;
220
+ /**
221
+ * Basecamp's `completed_ratio`, e.g. `3/10`.
222
+ */
223
+ completedRatio: string;
224
+ appUrl: string;
225
+ }
226
+ /**
227
+ * One to-do of a basecamp.todo.list result (0.76.0): `content` is its title, `description` its notes as plain text (empty when none), each cut at 16 KiB.
228
+ */
229
+ export interface BasecampTodo {
230
+ /**
231
+ * A Basecamp id (0.76.0): an account, project, to-do list, to-do, recording or person, as a string of 1..20 digits — how every basecamp.* result names one and every basecamp.* input takes one back.
232
+ */
233
+ id: string;
234
+ content: string;
235
+ description: string;
236
+ completed: boolean;
237
+ dueOn?: string;
238
+ /**
239
+ * @maxItems 100
240
+ */
241
+ assignees: BasecampPersonRef[];
242
+ appUrl: string;
243
+ createdAt: string;
244
+ }
245
+ /**
246
+ * A person named on a Basecamp recording (0.76.0): an author or an assignee.
247
+ */
248
+ export interface BasecampPersonRef {
249
+ /**
250
+ * A Basecamp id (0.76.0): an account, project, to-do list, to-do, recording or person, as a string of 1..20 digits — how every basecamp.* result names one and every basecamp.* input takes one back.
251
+ */
252
+ id: string;
253
+ name: string;
254
+ }
255
+ /**
256
+ * One message-board post of a basecamp.message.list result (0.76.0); `content` is plain text cut at 16 KiB; `id` is a recordingId for basecamp.comment.list and basecamp.comment.post.
257
+ */
258
+ export interface BasecampMessage {
259
+ /**
260
+ * A Basecamp id (0.76.0): an account, project, to-do list, to-do, recording or person, as a string of 1..20 digits — how every basecamp.* result names one and every basecamp.* input takes one back.
261
+ */
262
+ id: string;
263
+ subject: string;
264
+ content: string;
265
+ author: BasecampPersonRef;
266
+ createdAt: string;
267
+ appUrl: string;
268
+ commentsCount: number;
269
+ }
270
+ /**
271
+ * One comment of a basecamp.comment.list result (0.76.0); `content` is plain text cut at 16 KiB.
272
+ */
273
+ export interface BasecampComment {
274
+ /**
275
+ * A Basecamp id (0.76.0): an account, project, to-do list, to-do, recording or person, as a string of 1..20 digits — how every basecamp.* result names one and every basecamp.* input takes one back.
276
+ */
277
+ id: string;
278
+ content: string;
279
+ author: BasecampPersonRef;
280
+ createdAt: string;
281
+ appUrl: string;
282
+ }
283
+ /**
284
+ * One person of a basecamp.people.list result (0.76.0); `id` is what basecamp.todo.create takes in `assigneeIds`. `email` and `title` when Basecamp shows them.
285
+ */
286
+ export interface BasecampPerson {
287
+ /**
288
+ * A Basecamp id (0.76.0): an account, project, to-do list, to-do, recording or person, as a string of 1..20 digits — how every basecamp.* result names one and every basecamp.* input takes one back.
289
+ */
290
+ id: string;
291
+ name: string;
292
+ email?: string;
293
+ title?: string;
294
+ /**
295
+ * Whether the person is an administrator of the Basecamp account.
296
+ */
297
+ admin: boolean;
298
+ }
@@ -106,6 +106,105 @@ export const IntegrationOperations = {
106
106
  "declaredMode": false,
107
107
  "idempotencyKey": false
108
108
  },
109
+ "basecamp.project.list": {
110
+ "kind": "saas",
111
+ "identities": [
112
+ "user"
113
+ ],
114
+ "readOnly": true,
115
+ "declaredMode": false,
116
+ "idempotencyKey": false
117
+ },
118
+ "basecamp.todolist.list": {
119
+ "kind": "saas",
120
+ "identities": [
121
+ "user"
122
+ ],
123
+ "readOnly": true,
124
+ "declaredMode": false,
125
+ "idempotencyKey": false
126
+ },
127
+ "basecamp.todo.list": {
128
+ "kind": "saas",
129
+ "identities": [
130
+ "user"
131
+ ],
132
+ "readOnly": true,
133
+ "declaredMode": false,
134
+ "idempotencyKey": false
135
+ },
136
+ "basecamp.todo.create": {
137
+ "kind": "saas",
138
+ "identities": [
139
+ "user"
140
+ ],
141
+ "readOnly": false,
142
+ "declaredMode": false,
143
+ "idempotencyKey": true
144
+ },
145
+ "basecamp.todo.complete": {
146
+ "kind": "saas",
147
+ "identities": [
148
+ "user"
149
+ ],
150
+ "readOnly": false,
151
+ "declaredMode": false,
152
+ "idempotencyKey": false
153
+ },
154
+ "basecamp.message.list": {
155
+ "kind": "saas",
156
+ "identities": [
157
+ "user"
158
+ ],
159
+ "readOnly": true,
160
+ "declaredMode": false,
161
+ "idempotencyKey": false
162
+ },
163
+ "basecamp.message.post": {
164
+ "kind": "saas",
165
+ "identities": [
166
+ "user"
167
+ ],
168
+ "readOnly": false,
169
+ "declaredMode": false,
170
+ "idempotencyKey": true
171
+ },
172
+ "basecamp.comment.list": {
173
+ "kind": "saas",
174
+ "identities": [
175
+ "user"
176
+ ],
177
+ "readOnly": true,
178
+ "declaredMode": false,
179
+ "idempotencyKey": false
180
+ },
181
+ "basecamp.comment.post": {
182
+ "kind": "saas",
183
+ "identities": [
184
+ "user"
185
+ ],
186
+ "readOnly": false,
187
+ "declaredMode": false,
188
+ "idempotencyKey": true
189
+ },
190
+ "basecamp.campfire.post": {
191
+ "kind": "saas",
192
+ "identities": [
193
+ "user"
194
+ ],
195
+ "readOnly": false,
196
+ "declaredMode": false,
197
+ "idempotencyKey": true
198
+ },
199
+ "basecamp.people.list": {
200
+ "kind": "saas",
201
+ "identities": [
202
+ "user"
203
+ ],
204
+ "readOnly": true,
205
+ "declaredMode": false,
206
+ "idempotencyKey": false
207
+ },
109
208
  "warehouse.view.read": {
110
209
  "kind": "database",
111
210
  "identities": [