@fourier-labs/harbour-contracts 0.73.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.
Files changed (33) hide show
  1. package/README.md +4 -4
  2. package/contract-release.json +26 -26
  3. package/dist/gen-ts/app-integrations-declaration.d.ts +15 -1
  4. package/dist/gen-ts/contract-release.d.ts +6 -6
  5. package/dist/gen-ts/contract-release.js +6 -6
  6. package/dist/gen-ts/data-grant-v2.d.ts +5 -5
  7. package/dist/gen-ts/data-source.d.ts +19 -2
  8. package/dist/gen-ts/integration-error-details.d.ts +2 -2
  9. package/dist/gen-ts/integration-execute-input-rules.d.ts +214 -0
  10. package/dist/gen-ts/integration-execute-input-rules.js +85 -0
  11. package/dist/gen-ts/integration-execute-request.d.ts +61 -6
  12. package/dist/gen-ts/integration-execute-result.d.ts +165 -1
  13. package/dist/gen-ts/integration-operations.d.ts +77 -0
  14. package/dist/gen-ts/integration-operations.js +99 -0
  15. package/dist/gen-ts/saas-connection.d.ts +22 -8
  16. package/gen-ts/app-integrations-declaration.ts +15 -1
  17. package/gen-ts/contract-release.ts +6 -6
  18. package/gen-ts/data-grant-v2.ts +15 -4
  19. package/gen-ts/data-source.ts +19 -2
  20. package/gen-ts/integration-error-details.ts +3 -2
  21. package/gen-ts/integration-execute-input-rules.ts +86 -1
  22. package/gen-ts/integration-execute-request.ts +71 -5
  23. package/gen-ts/integration-execute-result.ts +165 -1
  24. package/gen-ts/integration-operations.ts +99 -0
  25. package/gen-ts/saas-connection.ts +32 -7
  26. package/package.json +1 -1
  27. package/schema/app-integrations-declaration.schema.json +233 -2
  28. package/schema/data-grant-v2.schema.json +4 -4
  29. package/schema/data-source.schema.json +57 -2
  30. package/schema/integration-error-details.schema.json +3 -2
  31. package/schema/integration-execute-request.schema.json +563 -4
  32. package/schema/integration-execute-result.schema.json +558 -2
  33. package/schema/saas-connection.schema.json +35 -7
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Slack carries an installation and a channel map; Gmail is user-consent only.
2
+ * Slack carries an installation and a channel map; Gmail is user-consent only; Basecamp is user-consent only with an IT-pinned account and a project map in `channels`.
3
3
  */
4
- export type SaasProvider = 'slack' | 'gmail';
4
+ export type SaasProvider = 'slack' | 'gmail' | 'basecamp';
5
5
  /**
6
6
  * DISABLED stops every app call immediately (details.code CONNECTION_DISABLED).
7
7
  */
@@ -23,7 +23,7 @@ export type ExternalRecipientMode = 'block' | 'allowlist' | 'allow';
23
23
  */
24
24
  export type InstallationName = string;
25
25
  /**
26
- * Tenant-wide SaaS connection record (control table sk SAASCONN#<sourceId>, same tenant partition as database sources; alias uniqueness is enforced across both). One connection per tenant and provider: a single bot installation of the platform-owned provider app and a single logical channel map shared by every app environment (grants, requests and user consent links stay per environment). A Gmail connection has neither installation nor channels: user consent is the only step and its one resource is the fixed `inbox` alias. Contains references, never secrets: installation tokens live under their own token records. The pre-T25 per-environment shape (`clientId`, `clientSecretRef`, `environments`, `installations`) is deprecated: the control plane normalises such a stored record on read and never writes it.
26
+ * Tenant-wide SaaS connection record (control table sk SAASCONN#<sourceId>, same tenant partition as database sources; alias uniqueness is enforced across both). One connection per tenant and provider: a single bot installation of the platform-owned provider app and a single logical channel map shared by every app environment (grants, requests and user consent links stay per environment). A Gmail connection has neither installation nor channels: user consent is the only step and its one resource is the fixed `inbox` alias. A Basecamp connection (0.76.0, ADR 0022) has no installation either: IT pins the company's Basecamp `account` (chosen from the accounts IT's own Basecamp sign-in belongs to) and maps project aliases to project ids in `channels`; basecamp.project.list runs on the fixed `account` alias. Contains references, never secrets: installation tokens live under their own token records. The pre-T25 per-environment shape (`clientId`, `clientSecretRef`, `environments`, `installations`) is deprecated: the control plane normalises such a stored record on read and never writes it.
27
27
  */
28
28
  export interface SaasConnection {
29
29
  schema: 'harbour.saas-connection/1.0';
@@ -41,22 +41,23 @@ export interface SaasConnection {
41
41
  */
42
42
  version: number;
43
43
  installation?: SaasInstallation;
44
+ account?: BasecampAccount;
44
45
  /**
45
- * Logical channel alias -> provider channel ID, shared by every environment. Always empty for Gmail.
46
+ * Logical alias → provider resource id: a Slack channel id or a Basecamp project id. Always empty for Gmail.
46
47
  */
47
48
  channels: {
48
49
  /**
49
- * Provider channel ID; only public, non-archived channels are valid.
50
+ * A Slack channel id (public, non-archived) or a Basecamp project id (digits).
50
51
  */
51
52
  [k: string]: string;
52
53
  };
53
54
  /**
54
- * Operations IT allows apps to request on this connection: the provider's own SaaS operations (Slack: slack.channel.history, slack.message.post, slack.message.update, slack.message.delete, slack.dm.send, slack.user.lookup; Gmail: gmail.thread.list, gmail.thread.read, gmail.message.read, gmail.message.send, gmail.attachment.read). Never warehouse.view.read.
55
+ * Operations IT allows apps to request on this connection: the provider's own SaaS operations (Slack: slack.channel.history, slack.message.post, slack.message.update, slack.message.delete, slack.dm.send, slack.user.lookup; Gmail: gmail.thread.list, gmail.thread.read, gmail.message.read, gmail.message.send, gmail.attachment.read; Basecamp (0.76.0): basecamp.project.list, basecamp.todolist.list, basecamp.todo.list, basecamp.todo.create, basecamp.todo.complete, basecamp.message.list, basecamp.message.post, basecamp.comment.list, basecamp.comment.post, basecamp.campfire.post, basecamp.people.list). Never warehouse.view.read.
55
56
  *
56
57
  * @minItems 1
57
- * @maxItems 11
58
+ * @maxItems 22
58
59
  */
59
- operations: ('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' | 'service.request')[];
60
+ operations: ('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' | 'basecamp.project.list' | 'basecamp.todolist.list' | 'basecamp.todo.list' | 'basecamp.todo.create' | 'basecamp.todo.complete' | 'basecamp.message.list' | 'basecamp.message.post' | 'basecamp.comment.list' | 'basecamp.comment.post' | 'basecamp.campfire.post' | 'basecamp.people.list' | 'warehouse.view.read' | 'service.request')[];
60
61
  developmentPreapproval: DevelopmentPreapproval;
61
62
  oauthClient?: SaasOauthClient;
62
63
  userModeApproval?: UserModeApproval;
@@ -120,6 +121,19 @@ export interface SaasInstallation {
120
121
  */
121
122
  botName?: string;
122
123
  }
124
+ /**
125
+ * The pinned Basecamp account (0.76.0, Basecamp only): the one account, among those IT's own Basecamp sign-in belongs to, that every user token on this connection must also belong to and that every project id in `channels` lives in. Absent until IT chooses it; governance refuses every basecamp.* call until then.
126
+ */
127
+ export interface BasecampAccount {
128
+ /**
129
+ * The Basecamp account id, the first path segment of the API base URL.
130
+ */
131
+ id: string;
132
+ /**
133
+ * The account's name as Basecamp shows it, for IT to recognise.
134
+ */
135
+ name: string;
136
+ }
123
137
  export interface DevelopmentPreapproval {
124
138
  /**
125
139
  * Auto-approve development requests within the allowlist.
@@ -16,6 +16,17 @@ export type DeclaredConnection = {
16
16
  'gmail.message.read'?: UserModeChannelOperation;
17
17
  'gmail.message.send'?: UserModeChannelOperation;
18
18
  'gmail.attachment.read'?: UserModeChannelOperation;
19
+ 'basecamp.project.list'?: UserModeChannelOperation;
20
+ 'basecamp.todolist.list'?: UserModeChannelOperation;
21
+ 'basecamp.todo.list'?: UserModeChannelOperation;
22
+ 'basecamp.todo.create'?: UserModeChannelOperation;
23
+ 'basecamp.todo.complete'?: UserModeChannelOperation;
24
+ 'basecamp.message.list'?: UserModeChannelOperation;
25
+ 'basecamp.message.post'?: UserModeChannelOperation;
26
+ 'basecamp.comment.list'?: UserModeChannelOperation;
27
+ 'basecamp.comment.post'?: UserModeChannelOperation;
28
+ 'basecamp.campfire.post'?: UserModeChannelOperation;
29
+ 'basecamp.people.list'?: UserModeChannelOperation;
19
30
  'warehouse.view.read'?: AppModeViewOperation;
20
31
  'service.request'?: ServiceOperation;
21
32
  };
@@ -40,7 +51,7 @@ export type ResourceNames = IntegrationAlias[];
40
51
  export type ColumnName = string;
41
52
 
42
53
  /**
43
- * Committed `.isomorph/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (thirteen operations since 0.73.0, ADR 0018). Each operation carries a fixed identity mode — slack.dm.send=app, slack.user.lookup=app, every gmail.* operation=user, warehouse.view.read=app — except the declared-mode Slack operations (slack.message.post, and since 0.71.0 slack.channel.history, slack.message.update and slack.message.delete), whose mode the app DECLARES: `app` acts as the company's Slack app (the one audited bot identity; a post goes out under the IT-approved per-app display name when the connection declares `presentation`), `user` acts as the signed-in person after their own consent (reads what they can read, edits what they wrote). The declared mode is the one the app requests access for; an app may hold approved grants for both modes after two requests, and each declared-mode call names which approved mode it runs under (`mode` on the execute request, 0.55.0) — a mode IT did not approve is refused, never swapped for the other. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read; `presentation` is a `saas` connection's optional per-app display name and icon for app-mode Slack posts, shown to IT at approval time and applied only from the approved grant. Gmail is user-mode only (reads, one send); its single resource is the fixed `inbox` alias. slack.dm.send and slack.user.lookup address people by email rather than a channel, so their single resource is the fixed `workspace` alias (the connection's installed workspace), the way Gmail's is `inbox`. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind / IntegrationPresentation definitions referenced by the other integration schemas.
54
+ * Committed `.isomorph/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (twenty-four operations since 0.76.0, ADR 0018 and ADR 0022). Each operation carries a fixed identity mode — slack.dm.send=app, slack.user.lookup=app, every gmail.* operation=user, every basecamp.* operation=user, warehouse.view.read=app, service.request=app — except the declared-mode Slack operations (slack.message.post, and since 0.71.0 slack.channel.history, slack.message.update and slack.message.delete), whose mode the app DECLARES: `app` acts as the company's Slack app (the one audited bot identity; a post goes out under the IT-approved per-app display name when the connection declares `presentation`), `user` acts as the signed-in person after their own consent (reads what they can read, edits what they wrote). The declared mode is the one the app requests access for; an app may hold approved grants for both modes after two requests, and each declared-mode call names which approved mode it runs under (`mode` on the execute request, 0.55.0) — a mode IT did not approve is refused, never swapped for the other. A `saas` connection may declare only the Slack, Gmail and Basecamp operations and a `database` connection only warehouse.view.read; `presentation` is a `saas` connection's optional per-app display name and icon for app-mode Slack posts, shown to IT at approval time and applied only from the approved grant. Gmail is user-mode only (reads, one send); its single resource is the fixed `inbox` alias. Basecamp (0.76.0, ADR 0022) is user-mode only too: basecamp.project.list runs on the fixed `account` alias (the company's pinned Basecamp account) and every other basecamp.* operation on a project alias IT mapped in the connection's `channels` map, so Basecamp itself refuses an id outside that project. slack.dm.send and slack.user.lookup address people by email rather than a channel, so their single resource is the fixed `workspace` alias (the connection's installed workspace), the way Gmail's is `inbox`. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind / IntegrationPresentation definitions referenced by the other integration schemas.
44
55
  */
45
56
  export interface AppIntegrationsDeclaration {
46
57
  schema: 'isomorph.app-integrations/2.0';
@@ -75,6 +86,9 @@ export interface AppModeChannelOperation {
75
86
  identity: 'app';
76
87
  resources: ResourceNames;
77
88
  }
89
+ /**
90
+ * Every gmail.* operation (resource: the fixed `inbox` alias) and, since 0.76.0, every basecamp.* operation (resource: a project alias from the connection's `channels` map, or the fixed `account` alias for basecamp.project.list): user mode only — the app acts as the signed-in person after their own consent.
91
+ */
78
92
  export interface UserModeChannelOperation {
79
93
  identity: 'user';
80
94
  resources: ResourceNames;
@@ -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.73.0.0" as const;
5
- export const ContractPublishVersion = "0.73.0" as const;
6
- export const ContractPublishTag = "v0.73.0" as const;
7
- export const ContractSchemasDigest = "sha256:c0f37c59c7c2decb4fb62d96596b787628483bdcdb09756c3ea286385d31a5f5" as const;
4
+ export const ContractRepositoryVersion = "0.76.0.0" as const;
5
+ export const ContractPublishVersion = "0.76.0" as const;
6
+ export const ContractPublishTag = "v0.76.0" as const;
7
+ export const ContractSchemasDigest = "sha256:20ed0df1c63b7aec22d74c3582b1e6c5d36a974005bfe656f03f8f746b371fa7" as const;
8
8
  export const ContractAiFixturesDigest = "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6" as const;
9
- export const ContractGeneratedGoDigest = "sha256:eb98ee80133c91e8562b95d92388c5bb637a06ae8d97a0827f528cd6decde8c9" as const;
10
- export const ContractGeneratedTypeScriptDigest = "sha256:ad539efcc9e4a7521f9526dd6304215f6b764be64bd40cd598ec023f6bbb4583" as const;
9
+ export const ContractGeneratedGoDigest = "sha256:db5f27b48a5e6b3706ac0173ebe37ec26deed375be46646b592da35a3aebcce4" as const;
10
+ export const ContractGeneratedTypeScriptDigest = "sha256:9467bd533c557df0b263789b8585f0201abd9f536506ec28400e245be18dae42" as const;
@@ -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
  /**
@@ -2,9 +2,9 @@
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
4
  /**
5
- * Closed connector set for the Data-Access MVP.
5
+ * Closed connector set for the Data-Access MVP. trino (0.74.0): Trino/Presto-compatible engines (Trino, Presto, Starburst, Galaxy) over the Trino HTTP statement protocol; the endpoint reuses host, port and database (as the Trino catalog) with no connector-specific fields.
6
6
  */
7
- export type DataSourceConnector = 'postgres' | 'redshift' | 'bigquery' | 'databricks' | 'mysql';
7
+ export type DataSourceConnector = 'postgres' | 'redshift' | 'bigquery' | 'databricks' | 'mysql' | 'trino';
8
8
  /**
9
9
  * byo = admin brought existing infrastructure; harbour_managed = Isomorph provisioned it.
10
10
  */
@@ -54,6 +54,13 @@ export interface DataSource {
54
54
  */
55
55
  allowValidation?: boolean;
56
56
  status: DataSourceStatus;
57
+ /**
58
+ * 0.74.0: per-environment copies of the source. A read for an environment that has a copy here uses that copy's endpoint and credentialRef; production always uses the top-level endpoint and credentialRef. A development read with no development copy is answered from the app's sample rows (integration-execute-request.sampleRows), never from the source; a preview read with no preview copy uses the production endpoint. A laptop never reads the production endpoint.
59
+ */
60
+ environments?: {
61
+ development?: DataSourceEnvironmentCopy;
62
+ preview?: DataSourceEnvironmentCopy;
63
+ };
57
64
  }
58
65
  /**
59
66
  * Connector endpoint coordinates. host is always present; database vs project/workspace depends on the connector family.
@@ -80,3 +87,13 @@ export interface DataSourceEndpoint {
80
87
  */
81
88
  workspace?: string;
82
89
  }
90
+ /**
91
+ * One environment's copy of a source: the same endpoint and credentialRef shapes as the top-level fields, for the same connector.
92
+ */
93
+ export interface DataSourceEnvironmentCopy {
94
+ endpoint: DataSourceEndpoint;
95
+ /**
96
+ * secretref URI naming the platform-held credential for this copy. The referenced value never crosses this boundary.
97
+ */
98
+ credentialRef: string;
99
+ }
@@ -7,7 +7,7 @@
7
7
  export type IntegrationErrorCategory =
8
8
  'AUTH_REQUIRED' | 'FORBIDDEN' | 'NOT_FOUND' | 'CONFLICT' | 'VALIDATION_FAILED' | 'RATE_LIMITED' | 'UNAVAILABLE';
9
9
  /**
10
- * Stable reason codes. SEND_OUTCOME_UNKNOWN: dispatch happened but the outcome is unknown; never re-post automatically. SEND_KEY_REUSED: the idempotency key was seen with a different payload or subject. GRANT_DENIED: IT denied the request. TOKEN_REFRESH_PENDING: another call holds the refresh lease; retry shortly. REQUEST_PENDING: a different request for this grant is still open. INPUT_INVALID: the operation input failed its bounds. IDEMPOTENCY_KEY_REQUIRED: a send without a UUID idempotencyKey. SEND_IN_PROGRESS: a retry arrived while the first send of that key is still running. CONNECTION_NOT_FOUND / APP_NOT_FOUND / GRANT_NOT_FOUND: the named record does not exist. AUTH_REQUIRED: no verified caller. UNAVAILABLE: the governance dependency (store, provider seam) is unreachable. MODE_NOT_GRANTED (403 FORBIDDEN, 0.55.0): a slack.message.post named a mode (app or user) IT has not approved for this app; the post is never served under the other mode. MODE_REQUIRED (400 VALIDATION_FAILED, 0.55.0): the app holds approved grants for both modes and the post named neither.
10
+ * Stable reason codes. SEND_OUTCOME_UNKNOWN: dispatch happened but the outcome is unknown; never re-post automatically. SEND_KEY_REUSED: the idempotency key was seen with a different payload or subject. GRANT_DENIED: IT denied the request. TOKEN_REFRESH_PENDING: another call holds the refresh lease; retry shortly. REQUEST_PENDING: a different request for this grant is still open. INPUT_INVALID: the operation input failed its bounds. IDEMPOTENCY_KEY_REQUIRED: a send without a UUID idempotencyKey. SEND_IN_PROGRESS: a retry arrived while the first send of that key is still running. CONNECTION_NOT_FOUND / APP_NOT_FOUND / GRANT_NOT_FOUND: the named record does not exist. AUTH_REQUIRED: no verified caller. UNAVAILABLE: the governance dependency (store, provider seam) is unreachable. MODE_NOT_GRANTED (403 FORBIDDEN, 0.55.0): a slack.message.post named a mode (app or user) IT has not approved for this app; the post is never served under the other mode. MODE_REQUIRED (400 VALIDATION_FAILED, 0.55.0): the app holds approved grants for both modes and the post named neither. SAMPLE_ROWS_NOT_ALLOWED (400 VALIDATION_FAILED, 0.74.0): an execute request carried sampleRows on any route but the development one. A sample value that does not parse as the column's recorded type is INPUT_INVALID, naming the column and the row.
11
11
  */
12
12
  export type IntegrationErrorCode =
13
13
  | 'USER_CONNECTION_REQUIRED'
@@ -39,7 +39,8 @@ export type IntegrationErrorCode =
39
39
  | 'AUTH_REQUIRED'
40
40
  | 'UNAVAILABLE'
41
41
  | 'MODE_NOT_GRANTED'
42
- | 'MODE_REQUIRED';
42
+ | 'MODE_REQUIRED'
43
+ | 'SAMPLE_ROWS_NOT_ALLOWED';
43
44
 
44
45
  /**
45
46
  * The `error` member of an integration failure response `{error, requestId}` on the execute/connect routes; the SDK maps it onto IsomorphError(category, message, details). `category` is the app-SDK IsomorphErrorCategory (not the deployment pipeline's dot-path error-category registry), with the HTTP status fixed per category: 401 AUTH_REQUIRED, 403 FORBIDDEN (denied/revoked/expired), 404 NOT_FOUND (unknown app/connection/resource), 409 CONFLICT (pending provisioning/consent/concurrent request), 400 or 413 VALIDATION_FAILED (invalid input / size overflow), 429 RATE_LIMITED (+ Retry-After header), 503 UNAVAILABLE. `details.code` is the closed stable reason set; `message` is human-facing and never carries provider error bodies, tokens or SQL.
@@ -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,13 +217,21 @@ 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
  /**
166
224
  * 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.
167
225
  */
168
226
  mode?: 'app' | 'user';
227
+ /**
228
+ * Development only (0.74.0). Rows the kit dev runtime attaches for the resource being read, from the sample-row file the agent wrote into the app's .isomorph/ folder. Governance serves them for a development warehouse.view.read when the source has no development copy (data-source.environments.development): it parses each value by the resource's recorded column type and refuses a value that does not parse, naming the column and the row. The field is refused with SAMPLE_ROWS_NOT_ALLOWED on any route but the development one, so a preview or production read never carries made-up data. Not part of the per-operation input rules; the dev runtime adds it beside the app's request.
229
+ *
230
+ * @maxItems 50
231
+ */
232
+ sampleRows?: {
233
+ [k: string]: string | number | boolean | null;
234
+ }[];
169
235
  };
170
236
  /**
171
237
  * One bare email address (no display name).