@fourier-labs/harbour-contracts 0.50.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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 NOT a caller field: it is fixed by the operation and enforced from the approved grant; any identity/userId/scope field is rejected by additionalProperties. 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}. 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 NOT a caller field: it is fixed by the operation (declared per app for slack.message.post) and enforced from the 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 Harbour" footer) or as the signed-in person (no name, no footer), whichever mode the app declared and IT approved — the same input either way. 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.
3
3
  */
4
4
  export type IntegrationExecuteRequest = {
5
5
  /**
@@ -7,7 +7,7 @@ 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.
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).
11
11
  */
12
12
  operation: 'slack.channel.history' | 'slack.message.post' | 'gmail.thread.list' | 'gmail.message.read' | 'gmail.message.send' | 'warehouse.view.read';
13
13
  /**
@@ -15,7 +15,13 @@ export type IntegrationExecuteRequest = {
15
15
  */
16
16
  resource: string;
17
17
  input: {
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.
20
+ */
18
21
  limit?: number;
22
+ /**
23
+ * slack.message.post / gmail.message.send: the plain-text body, 1..4000 characters.
24
+ */
19
25
  text?: string;
20
26
  /**
21
27
  * gmail.message.read: the Gmail message id a gmail.thread.list result named.
@@ -93,6 +93,12 @@ export interface SaasInstallation {
93
93
  botUserId: string;
94
94
  status: SaasInstallationStatus;
95
95
  installedAt: string;
96
+ /**
97
+ * The bot scopes Slack granted at installation (0.53.0). Absent on installations recorded before it, which therefore lack chat:write.customize: per-app display names need a reinstall.
98
+ *
99
+ * @maxItems 64
100
+ */
101
+ scopes?: string[];
96
102
  }
97
103
  export interface DevelopmentPreapproval {
98
104
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It also carries validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run. An unsupported report carries only application.unsupported and no plan or fixtures.
2
+ * Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It may also carry validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run; the deployment plane reads the workspace fixture sidecar, so the report's copy is optional. An unsupported report carries only application.unsupported and no plan or fixtures.
3
3
  */
4
4
  export type TransformReport = {
5
5
  /**
@@ -8,23 +8,33 @@ export type TransformReport = {
8
8
  * @minItems 1
9
9
  */
10
10
  applied_playbooks?: string[];
11
+ /**
12
+ * Optional. The deployed value is a platform constant supplied to the build, not a report field, so a deployable report may omit it and one that still carries it stays valid.
13
+ */
11
14
  static_rebuild_required?: boolean;
12
15
  analysis?: {
13
16
  deployable: boolean;
14
- needsTransformation: boolean;
17
+ /**
18
+ * Whether the attempt changed the tree. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
19
+ */
20
+ needsTransformation?: boolean;
15
21
  /**
16
22
  * @maxItems 64
17
23
  */
18
24
  blockers: string[];
19
25
  /**
26
+ * Paths the attempt touched. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
27
+ *
20
28
  * @minItems 1
21
29
  * @maxItems 1024
22
30
  */
23
- surfaces: string[];
31
+ surfaces?: string[];
24
32
  /**
33
+ * Optional file-and-line citations for why the transformation was needed. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.
34
+ *
25
35
  * @maxItems 1024
26
36
  */
27
- evidence: {
37
+ evidence?: {
28
38
  path: string;
29
39
  line: number;
30
40
  reason: string;
@@ -32,29 +42,46 @@ export type TransformReport = {
32
42
  };
33
43
  database?: {
34
44
  mode: 'none' | 'sql';
35
- changeType: 'none' | 'code_only' | 'schema_only' | 'code_and_schema';
36
- requiresCodeChanges: boolean;
37
- requiresSchemaMigration: boolean;
38
45
  /**
46
+ * Classification of the changed paths. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
47
+ */
48
+ changeType?: 'none' | 'code_only' | 'schema_only' | 'code_and_schema';
49
+ /**
50
+ * Whether any changed path sits outside the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
51
+ */
52
+ requiresCodeChanges?: boolean;
53
+ /**
54
+ * Whether the Aurora bundle grew past its seeded prefix. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
55
+ */
56
+ requiresSchemaMigration?: boolean;
57
+ /**
58
+ * Migration files in the Aurora bundle. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
59
+ *
39
60
  * @maxItems 128
40
61
  */
41
- migrationPaths: string[];
62
+ migrationPaths?: string[];
42
63
  /**
64
+ * Changed paths on the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
65
+ *
43
66
  * @maxItems 512
44
67
  */
45
- changedFiles: string[];
68
+ changedFiles?: string[];
46
69
  /**
70
+ * Optional file-and-line citations for the database findings. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.
71
+ *
47
72
  * @maxItems 128
48
73
  */
49
- evidence: {
74
+ evidence?: {
50
75
  path: string;
51
76
  line: number;
52
77
  reason: string;
53
78
  }[];
54
79
  /**
80
+ * Optional. The platform's only structural decline is application.unsupported; nothing reads this list, so a deployable report may omit it and one that still carries it stays valid.
81
+ *
55
82
  * @maxItems 64
56
83
  */
57
- unsupportedFeatures: string[];
84
+ unsupportedFeatures?: string[];
58
85
  };
59
86
  /**
60
87
  * Agent-authored preservation (smoke) plan for this attempt. Required whenever the report is deployable; forbidden on an unsupported report. Every test must be grounded in files the agent actually read or changed (evidence) and expressed as tenant-relative browser or HTTP steps the validation cell can execute without secrets. Structural bounds live here; per-action required fields, at least one assert* step per test, one to three happy_path tests, unique ids, the {{runId}} substitution and path safety are enforced semantically by the deployment plane.
@@ -72,12 +99,12 @@ export type TransformReport = {
72
99
  kind: 'happy_path' | 'boundary_check';
73
100
  name: string;
74
101
  /**
75
- * Transformation boundaries this test claims to cover.
102
+ * Transformation boundaries this test claims to cover. Optional: Harbour derives them from the test's own evidence paths, so a deployable report may omit them and one that still carries them stays valid.
76
103
  *
77
104
  * @minItems 1
78
105
  * @maxItems 8
79
106
  */
80
- boundaries: ('application' | 'routing' | 'database' | 'authentication' | 'runtime_dependency' | 'files' | 'provider' | 'background_work')[];
107
+ boundaries?: ('application' | 'routing' | 'database' | 'authentication' | 'runtime_dependency' | 'files' | 'provider' | 'background_work')[];
81
108
  /**
82
109
  * Files (and optionally lines) in the transformed workspace that ground this test. Paths must exist after transformation.
83
110
  *
@@ -147,7 +174,10 @@ export type TransformReport = {
147
174
  * @maxItems 8
148
175
  */
149
176
  uncovered: {
150
- boundary: 'application' | 'routing' | 'database' | 'authentication' | 'runtime_dependency' | 'files' | 'provider' | 'background_work';
177
+ /**
178
+ * Optional: Harbour derives the uncovered boundaries from the changed paths and the covered set, so a deployable report may omit this and one that still carries it stays valid.
179
+ */
180
+ boundary?: 'application' | 'routing' | 'database' | 'authentication' | 'runtime_dependency' | 'files' | 'provider' | 'background_work';
151
181
  reason: 'not-observable-in-validation-cell' | 'no-safe-fixture' | 'no-grounded-surface';
152
182
  detail?: string;
153
183
  }[];
@@ -224,7 +254,13 @@ export type TransformReport = {
224
254
  kind: 'sql';
225
255
  path: string;
226
256
  } | null;
257
+ /**
258
+ * Optional: determined by topology (static topology iff static build), so a deployable report may omit it and one that still carries it stays valid.
259
+ */
227
260
  buildKind?: 'container' | 'static';
261
+ /**
262
+ * Optional: determined by the build output kinds, so a deployable report may omit it and one that still carries it stays valid.
263
+ */
228
264
  topology?: 'static' | 'backend' | 'frontend_and_backend';
229
265
  entrypoint?: string;
230
266
  build?: {
@@ -248,6 +284,9 @@ export type TransformReport = {
248
284
  route: string;
249
285
  }[];
250
286
  };
287
+ /**
288
+ * Optional: both members are platform constants, so a deployable report may omit this object and one that still carries it stays valid.
289
+ */
251
290
  basePath?: {
252
291
  required: boolean;
253
292
  environmentVariable: 'HARBOUR_BASE_PATH';
@@ -262,6 +301,8 @@ export type TransformReport = {
262
301
  reason: string;
263
302
  }[];
264
303
  /**
304
+ * Optional: a deployable plan requires this empty, so a deployable report may omit it and one that still carries it (empty) stays valid.
305
+ *
265
306
  * @maxItems 64
266
307
  */
267
308
  unsupportedFeatures?: string[];
@@ -11,13 +11,17 @@ export type IntegrationAlias = string;
11
11
  * @maxItems 64
12
12
  */
13
13
  export type ResourceNames = IntegrationAlias[];
14
+ /**
15
+ * Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time.
16
+ */
17
+ export type IntegrationIdentityMode = 'app' | 'user';
14
18
  /**
15
19
  * A plain column identifier of a warehouse view.
16
20
  */
17
21
  export type ColumnName = string;
18
22
 
19
23
  /**
20
- * Committed `.harbour/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 (five operations) and each operation carries a fixed identity mode: slack.channel.history=user, slack.message.post=app, gmail.thread.list=user, gmail.message.read=user, warehouse.view.read=app. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read. Gmail is read-only and user-mode only; its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind enums referenced by the other integration schemas.
24
+ * Committed `.harbour/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 (six operations). Each operation carries a fixed identity mode — slack.channel.history=user, gmail.thread.list=user, gmail.message.read=user, gmail.message.send=user, warehouse.view.read=app — except slack.message.post, whose mode the app DECLARES: `app` posts as the company's Slack app (the one audited bot identity, under the IT-approved per-app display name when the connection declares `presentation`), `user` posts as the signed-in person after their own consent. The mode is never chosen at call time: governance runs the call under the approved grant of the declared mode. 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 plus one send); its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind / IntegrationPresentation definitions referenced by the other integration schemas.
21
25
  */
22
26
  export interface AppIntegrationsDeclaration {
23
27
  schema: 'harbour.app-integrations/2.0';
@@ -27,21 +31,38 @@ export interface AppIntegrationsDeclaration {
27
31
  }
28
32
  export interface DeclaredConnection {
29
33
  kind: IntegrationConnectionKind;
34
+ presentation?: IntegrationPresentation;
30
35
  operations: {
31
36
  'slack.channel.history'?: UserModeChannelOperation;
32
- 'slack.message.post'?: AppModeChannelOperation;
37
+ 'slack.message.post'?: DeclaredModeChannelOperation;
33
38
  'gmail.thread.list'?: UserModeChannelOperation;
34
39
  'gmail.message.read'?: UserModeChannelOperation;
35
40
  'gmail.message.send'?: UserModeChannelOperation;
36
41
  'warehouse.view.read'?: AppModeViewOperation;
37
42
  };
38
43
  }
44
+ /**
45
+ * saas connections only (Slack): how this app's app-mode posts are presented; meaningless for every other operation.
46
+ */
47
+ export interface IntegrationPresentation {
48
+ /**
49
+ * 1..80 characters of plain text: not blank, no control characters, no @-mentions (`@name`, `<@id>`). Must not impersonate a person: IT sees it before approving.
50
+ */
51
+ displayName: string;
52
+ /**
53
+ * A Slack emoji shortcode such as `:sandwich:`; absent keeps the bot's own icon.
54
+ */
55
+ iconEmoji?: string;
56
+ }
39
57
  export interface UserModeChannelOperation {
40
58
  identity: 'user';
41
59
  resources: ResourceNames;
42
60
  }
43
- export interface AppModeChannelOperation {
44
- identity: 'app';
61
+ /**
62
+ * slack.message.post: the app declares the mode — app (posts as the company's Slack app) or user (posts as the signed-in person).
63
+ */
64
+ export interface DeclaredModeChannelOperation {
65
+ identity: IntegrationIdentityMode;
45
66
  resources: ResourceNames;
46
67
  }
47
68
  export interface AppModeViewOperation {
@@ -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.50.0.0" as const;
5
- export const ContractPublishVersion = "0.50.0" as const;
6
- export const ContractPublishTag = "v0.50.0" as const;
7
- export const ContractSchemasDigest = "sha256:c77dcbf9daad9c66141e9cb8215760a098afd1bf3e665e247f6a4df8db2bcd4f" as const;
4
+ export const ContractRepositoryVersion = "0.53.0.0" as const;
5
+ export const ContractPublishVersion = "0.53.0" as const;
6
+ export const ContractPublishTag = "v0.53.0" as const;
7
+ export const ContractSchemasDigest = "sha256:c4d7c9642842bf3709003f447fb22429ce3573acabaa2da8279e0fc399e8d237" as const;
8
8
  export const ContractAiFixturesDigest = "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6" as const;
9
- export const ContractGeneratedGoDigest = "sha256:545d8b23ab712e75285ea962851bcf8f08f5baeb018bea6f7ba4e1ca2fe80ed0" as const;
10
- export const ContractGeneratedTypeScriptDigest = "sha256:1e47c3956a3468531690693ff50ff7947a798af01e01d25659401f727cb934c4" as const;
9
+ export const ContractGeneratedGoDigest = "sha256:8f3c3281a76792a9480ca1f7bcbad820db1908efffb29072fcdb0a8f99dbd208" as const;
10
+ export const ContractGeneratedTypeScriptDigest = "sha256:82a083862e500a06df3550e694221cff9dcb98461d5174889c374713f961040c" 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. `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, 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';
@@ -27,7 +27,7 @@ export interface DataGrantV2 {
27
27
  */
28
28
  sourceId: string;
29
29
  /**
30
- * Whose authority an operation runs under. Fixed per operation; callers never choose it.
30
+ * Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time.
31
31
  */
32
32
  mode: 'app' | 'user';
33
33
  kind: 'saas' | 'database';
@@ -69,6 +69,7 @@ export interface DataGrantV2 {
69
69
  * The one open access request attached to this grant, if any.
70
70
  */
71
71
  pendingRequestId?: string;
72
+ presentation?: IntegrationPresentation;
72
73
  provisioning: DataGrantV2Provisioning;
73
74
  /**
74
75
  * Database grants: secretref://tenant/{t}/app/{a}/env/{env}/source/{s}. Set only by a successful publish and removed by cleanup.
@@ -82,7 +83,7 @@ export interface DataGrantV2 {
82
83
  updatedAt: string;
83
84
  /**
84
85
  * @deprecated
85
- * Whose authority an operation runs under. Fixed per operation; callers never choose it.
86
+ * Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time.
86
87
  */
87
88
  identityMode?: 'app' | 'user';
88
89
  /**
@@ -107,6 +108,19 @@ export interface IntegrationResource {
107
108
  */
108
109
  columns?: string[];
109
110
  }
111
+ /**
112
+ * App-mode Slack grants only: how the app's posts are presented, as IT approved it. Absent means the posts carry the bot's own name.
113
+ */
114
+ interface IntegrationPresentation {
115
+ /**
116
+ * 1..80 characters of plain text: not blank, no control characters, no @-mentions (`@name`, `<@id>`). Must not impersonate a person: IT sees it before approving.
117
+ */
118
+ displayName: string;
119
+ /**
120
+ * A Slack emoji shortcode such as `:sandwich:`; absent keeps the bot's own icon.
121
+ */
122
+ iconEmoji?: string;
123
+ }
110
124
  export interface DataGrantV2Provisioning {
111
125
  status: DataGrantV2ProvisioningStatus;
112
126
  /**
package/gen-ts/index.ts CHANGED
@@ -46,6 +46,7 @@ export * from './error-category.js';
46
46
  export * from './error-envelope.js';
47
47
  export * from './integration-connect-result.js';
48
48
  export * from './integration-error-details.js';
49
+ export * from './integration-execute-input-rules.js';
49
50
  export * from './integration-execute-request.js';
50
51
  export * from './integration-execute-result.js';
51
52
  export * from './kit-bundle-manifest.js';
@@ -0,0 +1,119 @@
1
+ /* eslint-disable */
2
+ // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
+
4
+ /**
5
+ * Per-operation input rules of integration-execute-request: derived from each
6
+ * operation's then-block (propertyNames.enum, required, per-field overrides).
7
+ * The governance execute route, the app SDK and the kit's local check validate
8
+ * with validateIntegrationExecuteInput below, so a request is refused with the
9
+ * same message wherever it is made.
10
+ */
11
+ export type IntegrationExecuteInputFieldRule =
12
+ | { kind: 'integer'; required: boolean; minimum: number; maximum: number; default?: number }
13
+ | { kind: 'string'; required: boolean; minLength: number; maxLength?: number; pattern?: string }
14
+ | { kind: 'string[]'; required: boolean; minItems: number; maxItems: number; uniqueItems: boolean; item: { minLength: number; maxLength?: number; pattern?: string } };
15
+
16
+ export type IntegrationExecuteOperationRule = { idempotencyKey: boolean; fields: Record<string, IntegrationExecuteInputFieldRule> };
17
+
18
+ export const IntegrationExecuteInputRules = {
19
+ "slack.channel.history": {
20
+ idempotencyKey: false,
21
+ fields: {
22
+ limit: { kind: "integer", required: false, minimum: 1, maximum: 15, default: 15 },
23
+ },
24
+ },
25
+ "slack.message.post": {
26
+ idempotencyKey: true,
27
+ fields: {
28
+ text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
29
+ },
30
+ },
31
+ "gmail.thread.list": {
32
+ idempotencyKey: false,
33
+ fields: {
34
+ limit: { kind: "integer", required: false, minimum: 1, maximum: 15, default: 15 },
35
+ },
36
+ },
37
+ "gmail.message.read": {
38
+ idempotencyKey: false,
39
+ fields: {
40
+ messageId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9_-]{1,64}$" },
41
+ },
42
+ },
43
+ "gmail.message.send": {
44
+ idempotencyKey: true,
45
+ fields: {
46
+ to: { kind: "string[]", required: true, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
47
+ cc: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
48
+ bcc: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
49
+ subject: { kind: "string", required: true, minLength: 1, maxLength: 256 },
50
+ text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
51
+ replyToMessageId: { kind: "string", required: false, minLength: 0, pattern: "^[A-Za-z0-9_-]{1,64}$" },
52
+ },
53
+ },
54
+ "warehouse.view.read": {
55
+ idempotencyKey: false,
56
+ fields: {
57
+ columns: { kind: "string[]", required: true, minItems: 1, maxItems: 32, uniqueItems: false, item: { minLength: 0, pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$" } },
58
+ limit: { kind: "integer", required: false, minimum: 1, maximum: 1000, default: 100 },
59
+ },
60
+ },
61
+ } as const satisfies Record<string, IntegrationExecuteOperationRule>;
62
+
63
+ export type IntegrationExecuteOperation = keyof typeof IntegrationExecuteInputRules;
64
+
65
+ /** The upper bound of one integer input as a literal type: integrationExecuteInputMaximum('slack.channel.history', 'limit') is 15. */
66
+ export function integrationExecuteInputMaximum<O extends IntegrationExecuteOperation, F extends keyof (typeof IntegrationExecuteInputRules)[O]['fields']>(operation: O, field: F): Extract<(typeof IntegrationExecuteInputRules)[O]['fields'][F], { maximum: number }>['maximum'] {
67
+ const rule = (IntegrationExecuteInputRules[operation].fields as Record<string, IntegrationExecuteInputFieldRule>)[field as string];
68
+ if (!rule || rule.kind !== 'integer') throw new Error(`${String(operation)}.input.${String(field)} has no integer bound`);
69
+ return rule.maximum as never;
70
+ }
71
+
72
+ export type IntegrationExecuteInputProblem = { field: string; message: string };
73
+ export type IntegrationExecuteInputValidation = { ok: true; input: Record<string, unknown> } | { ok: false; problem: IntegrationExecuteInputProblem };
74
+
75
+ const patterns = new Map<string, RegExp>();
76
+ function matches(pattern: string, value: string): boolean {
77
+ let compiled = patterns.get(pattern);
78
+ if (!compiled) { compiled = new RegExp(pattern, 'u'); patterns.set(pattern, compiled); }
79
+ return compiled.test(value);
80
+ }
81
+
82
+ /**
83
+ * Validates one operation's input against the contract and returns it with the
84
+ * contract's defaults applied. The message names the field and its allowed
85
+ * range; it is the sentence the platform answers with, so a caller may show it.
86
+ */
87
+ export function validateIntegrationExecuteInput(operation: string, input: unknown): IntegrationExecuteInputValidation {
88
+ const rule = (IntegrationExecuteInputRules as Record<string, IntegrationExecuteOperationRule>)[operation];
89
+ if (!rule) return { ok: false, problem: { field: 'operation', message: `${operation} is not a declared integration operation` } };
90
+ const raw = input === undefined ? {} : input;
91
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, problem: { field: 'input', message: 'input must be an object' } };
92
+ const given = raw as Record<string, unknown>;
93
+ for (const name of Object.keys(given)) if (!Object.prototype.hasOwnProperty.call(rule.fields, name)) return { ok: false, problem: { field: name, message: `${name} is not an input of ${operation}` } };
94
+ const out: Record<string, unknown> = {};
95
+ for (const [name, field] of Object.entries(rule.fields)) {
96
+ const value = given[name];
97
+ if (value === undefined || value === null) {
98
+ if (field.required) return { ok: false, problem: { field: name, message: `${name} is required for ${operation}` } };
99
+ if (field.kind === 'integer' && field.default !== undefined) out[name] = field.default;
100
+ continue;
101
+ }
102
+ if (field.kind === 'integer') {
103
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < field.minimum || value > field.maximum) return { ok: false, problem: { field: name, message: `${name} must be an integer between ${field.minimum} and ${field.maximum}` } };
104
+ } else if (field.kind === 'string') {
105
+ if (typeof value !== 'string' || value.length < field.minLength || (field.maxLength !== undefined && value.length > field.maxLength)) 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` } };
106
+ if (field.pattern && !matches(field.pattern, value)) return { ok: false, problem: { field: name, message: `${name} must match ${field.pattern}` } };
107
+ } else {
108
+ if (!Array.isArray(value) || value.length < field.minItems || value.length > field.maxItems) return { ok: false, problem: { field: name, message: `${name} must list ${field.minItems} to ${field.maxItems} items` } };
109
+ const seen = new Set<string>();
110
+ for (const [index, item] of value.entries()) {
111
+ 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))) return { ok: false, problem: { field: name, message: `${name}[${index}] is not a valid ${name} entry` } };
112
+ if (field.uniqueItems && seen.has(item)) return { ok: false, problem: { field: name, message: `${name} must not repeat an entry` } };
113
+ seen.add(item);
114
+ }
115
+ }
116
+ out[name] = value;
117
+ }
118
+ return { ok: true, input: out };
119
+ }
@@ -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 NOT a caller field: it is fixed by the operation and enforced from the approved grant; any identity/userId/scope field is rejected by additionalProperties. 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}. Transport limits (not expressible here): request <= 16 KiB, 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 NOT a caller field: it is fixed by the operation (declared per app for slack.message.post) and enforced from the 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 Harbour" footer) or as the signed-in person (no name, no footer), whichever mode the app declared and IT approved — the same input either way. 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.
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 plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity.
13
+ * 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).
14
14
  */
15
15
  operation:
16
16
  | 'slack.channel.history'
@@ -24,7 +24,13 @@ export type IntegrationExecuteRequest = {
24
24
  */
25
25
  resource: string;
26
26
  input: {
27
+ /**
28
+ * 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.
29
+ */
27
30
  limit?: number;
31
+ /**
32
+ * slack.message.post / gmail.message.send: the plain-text body, 1..4000 characters.
33
+ */
28
34
  text?: string;
29
35
  /**
30
36
  * gmail.message.read: the Gmail message id a gmail.thread.list result named.
@@ -104,6 +104,12 @@ export interface SaasInstallation {
104
104
  botUserId: string;
105
105
  status: SaasInstallationStatus;
106
106
  installedAt: string;
107
+ /**
108
+ * The bot scopes Slack granted at installation (0.53.0). Absent on installations recorded before it, which therefore lack chat:write.customize: per-app display names need a reinstall.
109
+ *
110
+ * @maxItems 64
111
+ */
112
+ scopes?: string[];
107
113
  }
108
114
  export interface DevelopmentPreapproval {
109
115
  /**