@fourier-labs/harbour-contracts 0.68.0 → 0.70.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.
package/README.md CHANGED
@@ -38,8 +38,8 @@ exist here first.
38
38
  | `binding-classification-subject.schema.json` | Complete value-free binding-name classification before version allocation | AI governance §5.7 / SEC-UP-01 |
39
39
  | `sanitized-binding-evidence.schema.json` | Exact-version sanitized runtime-binding allocation receipt | AI governance §5.7 / SEC-UP-02 |
40
40
  | `ai-pricing-revision`, `ai-metering-policy`, `ai-budget-ledger-event`, `ai-trace-event-v2` | Additive metering v2 records, including media dimensions and P5 trace attribution; v1 records remain readable | AI governance §9 |
41
- | `app-integrations-declaration.schema.json` | Committed `.isomorph/integrations.json` (`isomorph.app-integrations/2.0`): closed five-operation set (Slack read/post, Gmail read-only, warehouse view read) with fixed identity modes; owns the shared operation/identity/environment enums | Dev kit §4 |
42
- | `integration-execute-request`, `integration-execute-result`, `integration-connect-result`, `integration-error-details` | App SDK integrations wire contract on `/_harbour/integrations/*`: per-operation input bounds, the five result shapes, consent states and the closed `details.code` reason set | Dev kit §4, §8 |
41
+ | `app-integrations-declaration.schema.json` | Committed `.isomorph/integrations.json` (`isomorph.app-integrations/2.0`): closed six-operation set (Slack read/post, Gmail read/send, warehouse view read) with fixed identity modes; owns the shared operation/identity/environment enums | Dev kit §4 |
42
+ | `integration-execute-request`, `integration-execute-result`, `integration-connect-result`, `integration-error-details` | App SDK integrations wire contract on `/_harbour/integrations/*`: per-operation input bounds (a warehouse read filters, groups, aggregates and sorts in the warehouse and says when it was `truncated` — ADR 0017), the six result shapes, consent states and the closed `details.code` reason set | Dev kit §4, §8 |
43
43
  | `saas-connection.schema.json`, `data-grant-v2.schema.json` | Tenant SaaS connection record (one installation and channel map per tenant and provider) and the additive `harbour.data-grant/2.0` grant record with structured resources and provisioning lease/cleanup (status machine generated; legacy `data-grant` untouched) | Dev kit §5 |
44
44
  | `kit-bundle-manifest.schema.json`, `kit-lock.schema.json` | Published kit bundle identity (SDK tarball digest plus optional `sdk.version` and https `sdk.url` download location) and the committed `.isomorph/kit.lock.json` the CodeBuild SDK-vendoring gate compares against | Dev kit §3 |
45
45
  | `deploy-response.schema.json` | 202 Accepted body for `POST /v1/tenants/{tenant}/apps/{app}/deployments`, returned only after the authoritative QUEUED version exists and its SQS message is accepted | — |
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schema": "harbour.contract-release/1.0",
3
- "repositoryVersion": "0.68.0.0",
4
- "publishVersion": "0.68.0",
5
- "publishTag": "v0.68.0",
6
- "schemasDigest": "sha256:3860585b4a4a55334e788faca34018dfaa21bf7ee5fa42bb50b0a454a77047b3",
3
+ "repositoryVersion": "0.70.0.0",
4
+ "publishVersion": "0.70.0",
5
+ "publishTag": "v0.70.0",
6
+ "schemasDigest": "sha256:b3c430914fee8a4f33dc15051ec59a6a8bad9a84c31fda119b6d683eb523b094",
7
7
  "aiFixturesDigest": "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6",
8
- "generatedGoDigest": "sha256:0c97fd145737bf8fba6de780187033f04e52fe524284cd95c084fcda38e31a28",
9
- "generatedTypeScriptDigest": "sha256:bd9927f250e96a91958f078ea29d1420ca3d6545ce93b2b2901033ac09e52827",
8
+ "generatedGoDigest": "sha256:05b383b27a90f3b3495a327e3da9ab6e13c5c2b047fc9311616ff6e7a283ccab",
9
+ "generatedTypeScriptDigest": "sha256:184aacb9eb73cec089f8e891b1840873ca9497eb91cf4d3b12cfa9f0a9f305d1",
10
10
  "generatedExclusions": {
11
11
  "gen-go/contract_release.go": "contains the generated aggregate digests (self-reference)",
12
12
  "gen-ts/contract-release.ts": "contains the generated aggregate digests (self-reference)"
@@ -54,8 +54,8 @@
54
54
  "schema/error-envelope.schema.json": "sha256:75dccda7dd16fdf8435deda53fc7dbcdc109b65114617a17f11763010c656f90",
55
55
  "schema/integration-connect-result.schema.json": "sha256:3f254b67ef739dc1fa395bdc8c4dc93359b16d481dba2a2d777aa5b6f34b7af8",
56
56
  "schema/integration-error-details.schema.json": "sha256:47ad7621c34b092c7e40f912a21e7ccbdc000b974cfb7fa0c9200463e029f8fb",
57
- "schema/integration-execute-request.schema.json": "sha256:7a3ebd7a9992a9390d05c9eea6b66762e437655afdca79b64ec992967172c325",
58
- "schema/integration-execute-result.schema.json": "sha256:de87c35fb4dfb16f7334cab111f247281fc2300dc27738871ed1f8397ece6753",
57
+ "schema/integration-execute-request.schema.json": "sha256:b0ad5a3f4836d1f724ee9c28c2e55748819004f37ece37e5711a46bbcc85f3a6",
58
+ "schema/integration-execute-result.schema.json": "sha256:ae0f17788d0933ee67e4f058d57484e192bc6d7d964ee01ec53b94b1777a0724",
59
59
  "schema/internal-configuration-request.schema.json": "sha256:229d44a3458f68dceddfba802e5c1adcd95523a45af29b93b972706b88461a7d",
60
60
  "schema/kit-bundle-manifest.schema.json": "sha256:4a9fcdd52ba0d55718ef4dbdab112b145ef90471d9356d4d3fb41e50fe9e400d",
61
61
  "schema/kit-lock.schema.json": "sha256:a48154d9471e49a893d928f7b0e1e0bc77708a4a7edce4fdf14073203e0e8987",
@@ -66,8 +66,8 @@
66
66
  "schema/saas-connection.schema.json": "sha256:82ed8018201800a95aa792bde1d22568e86b55b3be926aa094a260c8c78a58d7",
67
67
  "schema/sanitized-binding-evidence.schema.json": "sha256:1191ab88e2764df927e5f17837674cebebe068d480da2b2fdb390e589f96b51f",
68
68
  "schema/secret-reference.schema.json": "sha256:db4e3c2c6a2dbd72d7ea0f9c91cb3b2343a7e67b601996c6818b2ef08e440895",
69
- "schema/security-scan-input-v2.schema.json": "sha256:adf90e7e158e2a29145fa9696219dad82d294cac14614bcd10910a251ebe6940",
70
- "schema/security-verdict.schema.json": "sha256:6650c327b58431c6a567bbbf1e59bb9d4a8dc2f0ff2efc92ca4c2fd5b2cf28b6",
69
+ "schema/security-scan-input-v2.schema.json": "sha256:0d418342078c9b5046ea6f4bef3c2c57539285dfee99cc0850d4f0d03739ee13",
70
+ "schema/security-verdict.schema.json": "sha256:f4a1c109ed27779374c52daccb0a96589613db4f3cf030a8b0e4713bf1b3dbff",
71
71
  "schema/transform-parked-result.schema.json": "sha256:11729980d4c89d4a9e18f5f213232cd3359739c85626b36c20c2a37ba794c9af",
72
72
  "schema/transform-report.schema.json": "sha256:b9b4b09f88714b49d855f01ab9b611f34fb856447c0d063be409bb12a6c2700a",
73
73
  "schema/version-state.schema.json": "sha256:3b190007e2f8e3821c41ef11c6f5b226da7d5a18d34d4077f4618004846fd444",
@@ -109,10 +109,10 @@
109
109
  "gen-go/contracts_test.go": "sha256:56376287cb1548be1b35ab4c65d6f0b558c5d34885f7a47cb3d9e8d786607bf5",
110
110
  "gen-go/doc.go": "sha256:316d610ce568597a2ce6f38e9782e34fa5920f6f51ab0866e6d061b8a41cd822",
111
111
  "gen-go/error_category.go": "sha256:b2ddea22dcfa030f1a45e721e635485b0bd816cf1f38e7654628a653f5ed4583",
112
- "gen-go/integration_execute_input_rules.go": "sha256:52a405aa7a97f7b4e2ed3c314a7bd28b7698cc7883ee72ad4c027637be602ab9",
113
- "gen-go/integration_execute_input_rules_test.go": "sha256:c437a8406a2f062a0173b9419e4cb176fc6eb54e569506225d5162323f55b40b",
112
+ "gen-go/integration_execute_input_rules.go": "sha256:7465a20d79cfb82ed5be683eca901677066e9f28fc5355c643a1883a28c4793a",
113
+ "gen-go/integration_execute_input_rules_test.go": "sha256:0368dce6ee45d06cf8d8444afaa5d92449848187d8f391fffe69654ff1ca861b",
114
114
  "gen-go/schema_documents.go": "sha256:c19c0d540f602ee997a84b90e20702d9bc63ed7d27aec267e76c04655fc182e0",
115
- "gen-go/types.go": "sha256:e700fb22b4e2eb59f8531da4bdac154ac22991f9fd39e72b2f4483e1fa2e0264",
115
+ "gen-go/types.go": "sha256:096be357356c51cf346b3e4568014f501cad66c2a482bdb6fd349c9e23bbcea0",
116
116
  "gen-go/version_state.go": "sha256:4ef8abb126414c0025cf2c59bc9c79a74ef4a2ff89e8f316cbf05acc872371c1"
117
117
  },
118
118
  "generatedTypeScript": {
@@ -163,9 +163,9 @@
163
163
  "gen-ts/index.ts": "sha256:8459b1cf136a0c822795154e2df2a28c848cddfadced329604be893ae6eef069",
164
164
  "gen-ts/integration-connect-result.ts": "sha256:4b10f169b6e1af364e9b418de21caa9fadbfb77b43cbff77d741cfae0b52180c",
165
165
  "gen-ts/integration-error-details.ts": "sha256:ad7ecec9dd4533a6e86b3508886a7d69937052f52105497285797f77106bcc6f",
166
- "gen-ts/integration-execute-input-rules.ts": "sha256:0da117ac9f3109462e5862d658edf0f50e4b71fdcbb675d3a9dcb08489bcadd0",
167
- "gen-ts/integration-execute-request.ts": "sha256:1008bc1c8b8fec41dd7241a9f6ccb31233473e34c5cf538c0125f4ee6f45d2db",
168
- "gen-ts/integration-execute-result.ts": "sha256:b7c9270abb0c12888e651aeb1a9927a658faa135ad6197be6dfbd46d942f2a0a",
166
+ "gen-ts/integration-execute-input-rules.ts": "sha256:6f97b347d822624cbb4d51dae5834c67696babdea04b4c3aec9361e2bb670886",
167
+ "gen-ts/integration-execute-request.ts": "sha256:fef0568ab5ffd9e021f3be4ee1178dec5ca1aa54d9799546699ae54b188d2c54",
168
+ "gen-ts/integration-execute-result.ts": "sha256:79d40eb1f6f75ea06ca69d62bef5a71b5ad0c46eb51ab312fedefd5d3f568cb2",
169
169
  "gen-ts/internal-configuration-request.ts": "sha256:7032437797473fa1c581d134adef2f3d4b4f4132f482cd17743e9d47097886bc",
170
170
  "gen-ts/kit-bundle-manifest.ts": "sha256:2777a268aef8787b81e71d154a7d05f6f5ded09d66f378211ff5493ddb885171",
171
171
  "gen-ts/kit-lock.ts": "sha256:00ceeb97e9b7d0187bec00728f5099fd34e775e9cc44c85c4714f7a3a4118ef6",
@@ -177,8 +177,8 @@
177
177
  "gen-ts/sanitized-binding-digest-helper.ts": "sha256:c47d7fdc20500e2e72c23fe39ecdd610fc397c47fcba51335ebec3c380fe6872",
178
178
  "gen-ts/sanitized-binding-evidence.ts": "sha256:f3fb7b1d7d2e0929eb0ea981eaf32db3104010647185f6ae59f67167d42538a9",
179
179
  "gen-ts/secret-reference.ts": "sha256:731fbcde7315cb4128d5b01bdbe6e6f248f13c7ad8dea04a3a4e927ac4890198",
180
- "gen-ts/security-scan-input-v2.ts": "sha256:ec703b8fe1734a623ce138adc54dc7acbe3b0880e469c70e8bf8f94d7ddb85c2",
181
- "gen-ts/security-verdict.ts": "sha256:c7c4c82183e7996941cee93a9cdb4dcaa8a4dab2d607ce29aab8683ee5ced8eb",
180
+ "gen-ts/security-scan-input-v2.ts": "sha256:cccbfc5dae0ea5a328e6b15525ac190a0aae522e3220dffe7e6c80c445d54610",
181
+ "gen-ts/security-verdict.ts": "sha256:beef7e4e23b385f3b18579f43bb4eb92b19aeef9e5089f3e9a149bf0067be4e8",
182
182
  "gen-ts/transform-parked-result.ts": "sha256:6279e27cee8d75c29f3ede45580fef1daa0ca300867a78e43ead8a7689ec613f",
183
183
  "gen-ts/transform-report.ts": "sha256:d83f1c34c4a082bd80bf9588b08e2fba3b7e25cd1855375ca3073ab9fafddfec",
184
184
  "gen-ts/version-state-machine.ts": "sha256:c67af3e86ac306743345b850dfd3ae5201c19d10c40e2c149cf8c38258320d5f",
@@ -1,7 +1,7 @@
1
- export declare const ContractRepositoryVersion: "0.68.0.0";
2
- export declare const ContractPublishVersion: "0.68.0";
3
- export declare const ContractPublishTag: "v0.68.0";
4
- export declare const ContractSchemasDigest: "sha256:3860585b4a4a55334e788faca34018dfaa21bf7ee5fa42bb50b0a454a77047b3";
1
+ export declare const ContractRepositoryVersion: "0.70.0.0";
2
+ export declare const ContractPublishVersion: "0.70.0";
3
+ export declare const ContractPublishTag: "v0.70.0";
4
+ export declare const ContractSchemasDigest: "sha256:b3c430914fee8a4f33dc15051ec59a6a8bad9a84c31fda119b6d683eb523b094";
5
5
  export declare const ContractAiFixturesDigest: "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6";
6
- export declare const ContractGeneratedGoDigest: "sha256:0c97fd145737bf8fba6de780187033f04e52fe524284cd95c084fcda38e31a28";
7
- export declare const ContractGeneratedTypeScriptDigest: "sha256:bd9927f250e96a91958f078ea29d1420ca3d6545ce93b2b2901033ac09e52827";
6
+ export declare const ContractGeneratedGoDigest: "sha256:05b383b27a90f3b3495a327e3da9ab6e13c5c2b047fc9311616ff6e7a283ccab";
7
+ export declare const ContractGeneratedTypeScriptDigest: "sha256:184aacb9eb73cec089f8e891b1840873ca9497eb91cf4d3b12cfa9f0a9f305d1";
@@ -1,9 +1,9 @@
1
1
  /* eslint-disable */
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
- export const ContractRepositoryVersion = "0.68.0.0";
4
- export const ContractPublishVersion = "0.68.0";
5
- export const ContractPublishTag = "v0.68.0";
6
- export const ContractSchemasDigest = "sha256:3860585b4a4a55334e788faca34018dfaa21bf7ee5fa42bb50b0a454a77047b3";
3
+ export const ContractRepositoryVersion = "0.70.0.0";
4
+ export const ContractPublishVersion = "0.70.0";
5
+ export const ContractPublishTag = "v0.70.0";
6
+ export const ContractSchemasDigest = "sha256:b3c430914fee8a4f33dc15051ec59a6a8bad9a84c31fda119b6d683eb523b094";
7
7
  export const ContractAiFixturesDigest = "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6";
8
- export const ContractGeneratedGoDigest = "sha256:0c97fd145737bf8fba6de780187033f04e52fe524284cd95c084fcda38e31a28";
9
- export const ContractGeneratedTypeScriptDigest = "sha256:bd9927f250e96a91958f078ea29d1420ca3d6545ce93b2b2901033ac09e52827";
8
+ export const ContractGeneratedGoDigest = "sha256:05b383b27a90f3b3495a327e3da9ab6e13c5c2b047fc9311616ff6e7a283ccab";
9
+ export const ContractGeneratedTypeScriptDigest = "sha256:184aacb9eb73cec089f8e891b1840873ca9497eb91cf4d3b12cfa9f0a9f305d1";
@@ -28,7 +28,30 @@ export type IntegrationExecuteInputFieldRule = {
28
28
  maxLength?: number;
29
29
  pattern?: string;
30
30
  };
31
+ } | {
32
+ kind: 'enum';
33
+ required: boolean;
34
+ values: readonly string[];
35
+ default?: string;
36
+ } | {
37
+ kind: 'object[]';
38
+ required: boolean;
39
+ minItems: number;
40
+ maxItems: number;
41
+ fields: Readonly<Record<string, IntegrationExecuteInputFieldRule>>;
42
+ } | {
43
+ kind: 'value';
44
+ required: boolean;
45
+ types: readonly IntegrationExecuteInputValueType[];
46
+ maxLength?: number;
47
+ list?: {
48
+ minItems: number;
49
+ maxItems: number;
50
+ types: readonly IntegrationExecuteInputValueType[];
51
+ };
31
52
  };
53
+ /** The scalar types a `value` input accepts, alone or in its list form. */
54
+ export type IntegrationExecuteInputValueType = 'string' | 'number' | 'boolean';
32
55
  export type IntegrationExecuteOperationRule = {
33
56
  idempotencyKey: boolean;
34
57
  fields: Record<string, IntegrationExecuteInputFieldRule>;
@@ -144,7 +167,7 @@ export declare const IntegrationExecuteInputRules: {
144
167
  readonly fields: {
145
168
  readonly columns: {
146
169
  readonly kind: "string[]";
147
- readonly required: true;
170
+ readonly required: false;
148
171
  readonly minItems: 1;
149
172
  readonly maxItems: 32;
150
173
  readonly uniqueItems: false;
@@ -153,6 +176,92 @@ export declare const IntegrationExecuteInputRules: {
153
176
  readonly pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$";
154
177
  };
155
178
  };
179
+ readonly where: {
180
+ readonly kind: "object[]";
181
+ readonly required: false;
182
+ readonly minItems: 1;
183
+ readonly maxItems: 16;
184
+ readonly fields: {
185
+ readonly column: {
186
+ readonly kind: "string";
187
+ readonly required: true;
188
+ readonly minLength: 0;
189
+ readonly pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$";
190
+ };
191
+ readonly op: {
192
+ readonly kind: "enum";
193
+ readonly required: true;
194
+ readonly values: readonly ["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "isNull", "isNotNull"];
195
+ };
196
+ readonly value: {
197
+ readonly kind: "value";
198
+ readonly required: false;
199
+ readonly types: readonly ["string", "number", "boolean"];
200
+ readonly maxLength: 256;
201
+ readonly list: {
202
+ readonly minItems: 1;
203
+ readonly maxItems: 100;
204
+ readonly types: readonly ["string", "number"];
205
+ };
206
+ };
207
+ };
208
+ };
209
+ readonly groupBy: {
210
+ readonly kind: "string[]";
211
+ readonly required: false;
212
+ readonly minItems: 1;
213
+ readonly maxItems: 8;
214
+ readonly uniqueItems: false;
215
+ readonly item: {
216
+ readonly minLength: 0;
217
+ readonly pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$";
218
+ };
219
+ };
220
+ readonly aggregates: {
221
+ readonly kind: "object[]";
222
+ readonly required: false;
223
+ readonly minItems: 1;
224
+ readonly maxItems: 16;
225
+ readonly fields: {
226
+ readonly fn: {
227
+ readonly kind: "enum";
228
+ readonly required: true;
229
+ readonly values: readonly ["count", "sum", "avg", "min", "max"];
230
+ };
231
+ readonly column: {
232
+ readonly kind: "string";
233
+ readonly required: true;
234
+ readonly minLength: 0;
235
+ readonly pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$";
236
+ };
237
+ readonly as: {
238
+ readonly kind: "string";
239
+ readonly required: true;
240
+ readonly minLength: 0;
241
+ readonly pattern: "^[a-z_][a-z0-9_]{0,62}$";
242
+ };
243
+ };
244
+ };
245
+ readonly orderBy: {
246
+ readonly kind: "object[]";
247
+ readonly required: false;
248
+ readonly minItems: 1;
249
+ readonly maxItems: 8;
250
+ readonly fields: {
251
+ readonly column: {
252
+ readonly kind: "string";
253
+ readonly required: true;
254
+ readonly minLength: 0;
255
+ readonly pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$";
256
+ };
257
+ readonly direction: {
258
+ readonly kind: "enum";
259
+ readonly required: false;
260
+ readonly values: readonly ["asc", "desc"];
261
+ readonly default: "asc";
262
+ };
263
+ };
264
+ };
156
265
  readonly limit: {
157
266
  readonly kind: "integer";
158
267
  readonly required: false;
@@ -39,7 +39,22 @@ export const IntegrationExecuteInputRules = {
39
39
  "warehouse.view.read": {
40
40
  idempotencyKey: false,
41
41
  fields: {
42
- columns: { kind: "string[]", required: true, minItems: 1, maxItems: 32, uniqueItems: false, item: { minLength: 0, pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$" } },
42
+ columns: { kind: "string[]", required: false, minItems: 1, maxItems: 32, uniqueItems: false, item: { minLength: 0, pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$" } },
43
+ where: { kind: "object[]", required: false, minItems: 1, maxItems: 16, fields: {
44
+ column: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$" },
45
+ op: { kind: "enum", required: true, values: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "isNull", "isNotNull"] },
46
+ value: { kind: "value", required: false, types: ["string", "number", "boolean"], maxLength: 256, list: { minItems: 1, maxItems: 100, types: ["string", "number"] } },
47
+ } },
48
+ groupBy: { kind: "string[]", required: false, minItems: 1, maxItems: 8, uniqueItems: false, item: { minLength: 0, pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$" } },
49
+ aggregates: { kind: "object[]", required: false, minItems: 1, maxItems: 16, fields: {
50
+ fn: { kind: "enum", required: true, values: ["count", "sum", "avg", "min", "max"] },
51
+ column: { kind: "string", required: true, minLength: 0, pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$" },
52
+ as: { kind: "string", required: true, minLength: 0, pattern: "^[a-z_][a-z0-9_]{0,62}$" },
53
+ } },
54
+ orderBy: { kind: "object[]", required: false, minItems: 1, maxItems: 8, fields: {
55
+ column: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$" },
56
+ direction: { kind: "enum", required: false, values: ["asc", "desc"], default: "asc" },
57
+ } },
43
58
  limit: { kind: "integer", required: false, minimum: 1, maximum: 1000, default: 100 },
44
59
  },
45
60
  },
@@ -60,55 +75,111 @@ function matches(pattern, value) {
60
75
  }
61
76
  return compiled.test(value);
62
77
  }
78
+ function refuse(field, message) { return { ok: false, problem: { field, message } }; }
79
+ const valueNouns = { string: ['a string', 'strings'], number: ['a number', 'numbers'], boolean: ['a boolean', 'booleans'] };
80
+ function either(parts) {
81
+ return parts.length > 1 ? `${parts.slice(0, -1).join(', ')} or ${parts[parts.length - 1]}` : parts.join('');
82
+ }
83
+ function isScalar(types, maxLength, value) {
84
+ if (typeof value === 'string')
85
+ return types.includes('string') && (maxLength === undefined || value.length <= maxLength);
86
+ if (typeof value === 'number')
87
+ return types.includes('number') && Number.isFinite(value);
88
+ return typeof value === 'boolean' && types.includes('boolean');
89
+ }
63
90
  /**
64
- * Validates one operation's input against the contract and returns it with the
65
- * contract's defaults applied. The message names the field and its allowed
66
- * range; it is the sentence the platform answers with, so a caller may show it.
91
+ * Checks one object's fields in rule order and applies defaults: the input
92
+ * itself (where null counts as absent), or one entry of an object[] input
93
+ * (where only a missing key is absent, so null is refused like any wrong value).
67
94
  */
68
- export function validateIntegrationExecuteInput(operation, input) {
69
- const rule = IntegrationExecuteInputRules[operation];
70
- if (!rule)
71
- return { ok: false, problem: { field: 'operation', message: `${operation} is not a declared integration operation` } };
72
- const raw = input === undefined ? {} : input;
73
- if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
74
- return { ok: false, problem: { field: 'input', message: 'input must be an object' } };
75
- const given = raw;
95
+ function checkFields(operation, prefix, fields, given, nullIsAbsent) {
76
96
  for (const name of Object.keys(given))
77
- if (!Object.prototype.hasOwnProperty.call(rule.fields, name))
78
- return { ok: false, problem: { field: name, message: `${name} is not an input of ${operation}` } };
97
+ if (!Object.prototype.hasOwnProperty.call(fields, name))
98
+ return refuse(prefix + name, `${prefix}${name} is not an input of ${operation}`);
79
99
  const out = {};
80
- for (const [name, field] of Object.entries(rule.fields)) {
100
+ for (const [name, field] of Object.entries(fields)) {
101
+ const path = prefix + name;
81
102
  const value = given[name];
82
- if (value === undefined || value === null) {
103
+ if (value === undefined || (nullIsAbsent && value === null)) {
83
104
  if (field.required)
84
- return { ok: false, problem: { field: name, message: `${name} is required for ${operation}` } };
85
- if (field.kind === 'integer' && field.default !== undefined)
105
+ return refuse(path, `${path} is required for ${operation}`);
106
+ if ((field.kind === 'integer' || field.kind === 'enum') && field.default !== undefined)
86
107
  out[name] = field.default;
87
108
  continue;
88
109
  }
89
- if (field.kind === 'integer') {
110
+ const checked = checkField(operation, path, name, field, value);
111
+ if (!checked.ok)
112
+ return checked;
113
+ out[name] = checked.value;
114
+ }
115
+ return { ok: true, value: out };
116
+ }
117
+ function checkField(operation, path, name, field, value) {
118
+ switch (field.kind) {
119
+ case 'integer':
90
120
  if (typeof value !== 'number' || !Number.isInteger(value) || value < field.minimum || value > field.maximum)
91
- return { ok: false, problem: { field: name, message: `${name} must be an integer between ${field.minimum} and ${field.maximum}` } };
92
- }
93
- else if (field.kind === 'string') {
121
+ return refuse(path, `${path} must be an integer between ${field.minimum} and ${field.maximum}`);
122
+ return { ok: true, value };
123
+ case 'string':
94
124
  if (typeof value !== 'string' || value.length < field.minLength || (field.maxLength !== undefined && value.length > field.maxLength))
95
- return { ok: false, problem: { field: name, message: field.maxLength !== undefined ? `${name} must be ${field.minLength} to ${field.maxLength} characters` : `${name} must be a string of at least ${field.minLength} characters` } };
125
+ return refuse(path, field.maxLength !== undefined ? `${path} must be ${field.minLength} to ${field.maxLength} characters` : `${path} must be a string of at least ${field.minLength} characters`);
96
126
  if (field.pattern && !matches(field.pattern, value))
97
- return { ok: false, problem: { field: name, message: `${name} must match ${field.pattern}` } };
98
- }
99
- else {
127
+ return refuse(path, `${path} must match ${field.pattern}`);
128
+ return { ok: true, value };
129
+ case 'enum':
130
+ if (typeof value !== 'string' || !field.values.includes(value))
131
+ return refuse(path, `${path} must be one of ${field.values.join(', ')}`);
132
+ return { ok: true, value };
133
+ case 'string[]': {
100
134
  if (!Array.isArray(value) || value.length < field.minItems || value.length > field.maxItems)
101
- return { ok: false, problem: { field: name, message: `${name} must list ${field.minItems} to ${field.maxItems} items` } };
135
+ return refuse(path, `${path} must list ${field.minItems} to ${field.maxItems} items`);
102
136
  const seen = new Set();
103
137
  for (const [index, item] of value.entries()) {
104
138
  if (typeof item !== 'string' || item.length < field.item.minLength || (field.item.maxLength !== undefined && item.length > field.item.maxLength) || (field.item.pattern !== undefined && !matches(field.item.pattern, item)))
105
- return { ok: false, problem: { field: name, message: `${name}[${index}] is not a valid ${name} entry` } };
139
+ return refuse(path, `${path}[${index}] is not a valid ${name} entry`);
106
140
  if (field.uniqueItems && seen.has(item))
107
- return { ok: false, problem: { field: name, message: `${name} must not repeat an entry` } };
141
+ return refuse(path, `${path} must not repeat an entry`);
108
142
  seen.add(item);
109
143
  }
144
+ return { ok: true, value };
145
+ }
146
+ case 'object[]': {
147
+ if (!Array.isArray(value) || value.length < field.minItems || value.length > field.maxItems)
148
+ return refuse(path, `${path} must list ${field.minItems} to ${field.maxItems} items`);
149
+ const entries = [];
150
+ for (const [index, item] of value.entries()) {
151
+ if (item === null || typeof item !== 'object' || Array.isArray(item))
152
+ return refuse(path, `${path}[${index}] is not a valid ${name} entry`);
153
+ const entry = checkFields(operation, `${path}[${index}].`, field.fields, item, false);
154
+ if (!entry.ok)
155
+ return entry;
156
+ entries.push(entry.value);
157
+ }
158
+ return { ok: true, value: entries };
159
+ }
160
+ case 'value': {
161
+ const { types, maxLength, list } = field;
162
+ if (isScalar(types, maxLength, value) || (list !== undefined && Array.isArray(value) && value.length >= list.minItems && value.length <= list.maxItems && value.every(item => isScalar(list.types, maxLength, item))))
163
+ return { ok: true, value };
164
+ const shapes = types.map(type => (type === 'string' && maxLength !== undefined ? `a string of at most ${maxLength} characters` : valueNouns[type][0]));
165
+ if (list !== undefined)
166
+ shapes.push(`a list of ${list.minItems} to ${list.maxItems} ${either(list.types.map(type => valueNouns[type][1]))}`);
167
+ return refuse(path, `${path} must be ${either(shapes)}`);
110
168
  }
111
- out[name] = value;
112
169
  }
113
- return { ok: true, input: out };
170
+ }
171
+ /**
172
+ * Validates one operation's input against the contract and returns it with the
173
+ * contract's defaults applied. The message names the field and its allowed
174
+ * range; it is the sentence the platform answers with, so a caller may show it.
175
+ */
176
+ export function validateIntegrationExecuteInput(operation, input) {
177
+ const rule = IntegrationExecuteInputRules[operation];
178
+ if (!rule)
179
+ return refuse('operation', `${operation} is not a declared integration operation`);
180
+ const raw = input === undefined ? {} : input;
181
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
182
+ return refuse('input', 'input must be an object');
183
+ const checked = checkFields(operation, '', rule.fields, raw, true);
184
+ return checked.ok ? { ok: true, input: checked.value } : checked;
114
185
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but slack.message.post, and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A slack.message.post runs as the app (the IT-approved per-app name from the grant, plus a "Posted by <app> on Isomorph" footer) or as the signed-in person (no name, no footer) — the same input either way — and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a post does. Per-operation input bounds: slack.channel.history {limit? 1..15, default 15}; slack.message.post {text 1..4000 chars} and a required idempotencyKey retained across retries of one explicit Send; gmail.thread.list {limit? 1..15, default 15}; gmail.message.read {messageId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, replyToMessageId?} and a required idempotencyKey like slack.message.post (plain text only, no attachments); warehouse.view.read {columns 1..32, limit? 1..1000, default 100}. Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB, total deadline 10 s.
2
+ * Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but slack.message.post, and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A slack.message.post runs as the app (the IT-approved per-app name from the grant, plus a "Posted by <app> on Isomorph" footer) or as the signed-in person (no name, no footer) — the same input either way — and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a post does. Per-operation input bounds: slack.channel.history {limit? 1..15, default 15}; slack.message.post {text 1..4000 chars} and a required idempotencyKey retained across retries of one explicit Send; gmail.thread.list {limit? 1..15, default 15}; gmail.message.read {messageId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, replyToMessageId?} and a required idempotencyKey like slack.message.post (plain text only, no attachments); warehouse.view.read {columns? 1..32, 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, total deadline 10 s.
3
3
  */
4
4
  export type IntegrationExecuteRequest = {
5
5
  /**
@@ -39,12 +39,40 @@ export type IntegrationExecuteRequest = {
39
39
  */
40
40
  replyToMessageId?: string;
41
41
  /**
42
- * Explicit columns, or `*` alone when the app's approved resource grant allows all current and future columns.
42
+ * warehouse.view.read, a plain read: explicit columns, or `*` alone when the app's approved resource grant allows all current and future columns. An aggregate read names aggregates instead.
43
43
  *
44
44
  * @minItems 1
45
45
  * @maxItems 32
46
46
  */
47
47
  columns?: string[];
48
+ /**
49
+ * warehouse.view.read (0.70.0): filters the warehouse applies before it returns, grouping or limits rows. All of them must hold (AND); there is no OR and no LIKE.
50
+ *
51
+ * @minItems 1
52
+ * @maxItems 16
53
+ */
54
+ where?: WarehouseFilter[];
55
+ /**
56
+ * warehouse.view.read (0.70.0): columns (never `*`) that group the rows; needs aggregates. Each returned row is one group: its groupBy columns, then its aggregate aliases.
57
+ *
58
+ * @minItems 1
59
+ * @maxItems 8
60
+ */
61
+ groupBy?: string[];
62
+ /**
63
+ * warehouse.view.read (0.70.0): what an aggregate read computes, over each group or over every matching row when there is no groupBy.
64
+ *
65
+ * @minItems 1
66
+ * @maxItems 16
67
+ */
68
+ aggregates?: WarehouseAggregate[];
69
+ /**
70
+ * warehouse.view.read (0.70.0): sort keys, first key first. Without orderBy an aggregate read is sorted by its groupBy columns and a plain read is not sorted at all.
71
+ *
72
+ * @minItems 1
73
+ * @maxItems 8
74
+ */
75
+ orderBy?: WarehouseOrder[];
48
76
  };
49
77
  /**
50
78
  * 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.
@@ -80,3 +108,53 @@ export type EmailAddressList1 = EmailAddress[];
80
108
  * @maxItems 50
81
109
  */
82
110
  export type EmailAddressList2 = EmailAddress[];
111
+ /**
112
+ * A filter comparison: = <> > >= < <= IN, NOT IN, IS NULL, IS NOT NULL. Word-like so every value is a valid generated constant.
113
+ */
114
+ export type WarehouseFilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'isNull' | 'isNotNull';
115
+ /**
116
+ * An aggregate function.
117
+ */
118
+ export type WarehouseAggregateFn = 'count' | 'sum' | 'avg' | 'min' | 'max';
119
+ /**
120
+ * Sort direction; asc when absent.
121
+ */
122
+ export type WarehouseOrderDirection = 'asc' | 'desc';
123
+ /**
124
+ * One warehouse.view.read filter (0.70.0): <column> <op> <value>. The value is always bound as a query parameter, never written into the SQL. Which value an op takes is a governance rule: isNull/isNotNull take none; in/notIn take a non-empty list of one scalar type; every other op takes one scalar, and a boolean only with eq/ne.
125
+ */
126
+ export interface WarehouseFilter {
127
+ /**
128
+ * The filtered column; never `*`.
129
+ */
130
+ column: string;
131
+ op: WarehouseFilterOp;
132
+ /**
133
+ * The value compared with: a string (a date or timestamp is its ISO string), a number, a boolean, or a list of strings or numbers for in/notIn. Never null; use isNull or isNotNull.
134
+ */
135
+ value?: string | number | boolean | (string | number)[];
136
+ }
137
+ /**
138
+ * One computed column of an aggregate warehouse.view.read (0.70.0): fn(column) AS as. `*` is a column only for count (a governance rule). The value follows the read's cell rules, so a 64-bit or decimal result may be a string.
139
+ */
140
+ export interface WarehouseAggregate {
141
+ fn: WarehouseAggregateFn;
142
+ /**
143
+ * A plain warehouse column identifier, or `*` as the sole selection when IT exposed all current and future columns.
144
+ */
145
+ column: string;
146
+ /**
147
+ * The name the computed value is returned under; unique in the read and never a groupBy column (governance rules).
148
+ */
149
+ as: string;
150
+ }
151
+ /**
152
+ * One warehouse.view.read sort key (0.70.0). A plain read sorts by a selected column (any column when it selects `*`); an aggregate read sorts by a groupBy column or an aggregate alias (governance rules).
153
+ */
154
+ export interface WarehouseOrder {
155
+ /**
156
+ * A column or an aggregate alias; never `*`.
157
+ */
158
+ column: string;
159
+ direction?: WarehouseOrderDirection;
160
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the six operation results: slack.channel.history -> {messages, hasMore} (plain text/IDs/time, plus the display name and emoji icon a per-app-named app post carries — authorName/authorIcon, 0.55.0 — never the provider payload); slack.message.post -> {messageId}; gmail.thread.list -> {threads, hasMore} (newest inbox threads; each `messageId` is the thread's newest message, what gmail.message.read takes); gmail.message.read -> {id, threadId, subject, from, to, date, text, truncated} (headers plus the plain text with html stripped, cut at 16 KiB with `truncated` set); gmail.message.send -> {messageId, threadId} (the Gmail ids of the sent message and its thread); warehouse.view.read -> {rows} (decimal/64-bit values as strings, dates as ISO strings). An unknown send outcome is never a result; it is an error with details.code SEND_OUTCOME_UNKNOWN. Results over 1 MiB are rejected, not truncated.
2
+ * The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the six operation results: slack.channel.history -> {messages, hasMore} (plain text/IDs/time, plus the display name and emoji icon a per-app-named app post carries — authorName/authorIcon, 0.55.0 — never the provider payload); slack.message.post -> {messageId}; gmail.thread.list -> {threads, hasMore} (newest inbox threads; each `messageId` is the thread's newest message, what gmail.message.read takes); gmail.message.read -> {id, threadId, subject, from, to, date, text, truncated} (headers plus the plain text with html stripped, cut at 16 KiB with `truncated` set); gmail.message.send -> {messageId, threadId} (the Gmail ids of the sent message and its thread); warehouse.view.read -> {rows, 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.
3
3
  */
4
4
  export type IntegrationExecuteResult = {
5
5
  /**
@@ -19,6 +19,9 @@ export type IntegrationExecuteResult = {
19
19
  to?: string;
20
20
  date?: string;
21
21
  text?: string;
22
+ /**
23
+ * gmail.message.read: the text was cut at 16 KiB. warehouse.view.read (0.70.0): more rows matched than the read's limit, so `rows` is incomplete.
24
+ */
22
25
  truncated?: boolean;
23
26
  /**
24
27
  * @maxItems 1000
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Input to the Security Assurance machine, second shape, replacing security-scan-input.schema.json. V1 scanned a deployed commit located through the deployment state; v2 scans exactly one thing: an uploaded package that governance validated at source intake, identified by the zip sha256 the stager verifies before extraction and by the tree digest governance computed from the unzipped bytes (`sourceDigestOf`, `sha256:`-prefixed), which keys the governance scan record and the check report on the laptop. Commit-mode and ARTIFACT-phase inputs are deliberately absent: the console's later on-demand scan uses this same package path against the deploy upload, and the Transform lane runs the scanner in-process; an image scan, if it returns, is an additive v3. The policy profile is named by the caller when it knows better than the data classification (the kit lane names BASELINE); `profile` and `classification` are mutually exclusive, and both absent still defaults to STANDARD with `classificationDefaulted: true`. Nothing here carries a scanner version: the scanner stamps its own into verdict provenance, and governance decides validity by comparing that against its MINIMUM_SCANNER_VERSION. `org`/`app` are the tenant and app ids for kit apps; `env` stays required because the evidence key carries it (governance passes `preview` for a package scan; promotion never re-scans). The `execution` block names the tenant's execution-account scanner, invoke role and artifacts bucket, resolved by governance the way admission resolves them for the deploy machine.
2
+ * Input to the Security Assurance machine, second shape, replacing security-scan-input.schema.json. V1 scanned a deployed commit located through the deployment state; v2 scans exactly one thing: an uploaded package that governance validated at source intake, identified by the zip sha256 the stager verifies before extraction and by the tree digest governance computed from the unzipped bytes (`sourceDigestOf`, `sha256:`-prefixed), which keys the governance scan record and the check report on the laptop. Commit-mode and ARTIFACT-phase inputs are deliberately absent: the console's later on-demand scan uses this same package path against the deploy upload, and the Transform lane runs the scanner in-process; an image scan, if it returns, is an additive v3. The caller always names the policy profile (governance names BASELINE for the kit lane); there is no classification input and no default profile. Nothing here carries a scanner version: the scanner stamps its own into verdict provenance, and governance decides validity by comparing that against its MINIMUM_SCANNER_VERSION. `org`/`app` are the tenant and app ids for kit apps; `env` stays required because the evidence key carries it (governance passes `preview` for a package scan; promotion never re-scans). The `execution` block names the tenant's execution-account scanner, invoke role and artifacts bucket, resolved by governance the way admission resolves them for the deploy machine.
3
3
  */
4
4
  export interface SecurityScanInputV2 {
5
5
  schema: 'harbour.security-scan-input/2.0';
@@ -18,13 +18,9 @@ export interface SecurityScanInputV2 {
18
18
  package: PackageSource;
19
19
  execution: Execution;
20
20
  /**
21
- * Policy profile named by the caller. Takes the place of deriving one from `classification`; the verdict echoes the profile evaluated and stamps `classificationDefaulted: false`. Governance names BASELINE for kit-lane scans; the default when nothing is named is STANDARD.
21
+ * Policy profile the machine evaluates the evidence against; the verdict echoes it. Governance names BASELINE for kit-lane scans.
22
22
  */
23
- profile?: 'BASELINE' | 'STANDARD' | 'HIGH' | 'CRITICAL';
24
- /**
25
- * Data classification, mapped to a profile as in v1 (GREEN → BASELINE, YELLOW → STANDARD, ORANGE → HIGH, RED → CRITICAL). Not allowed beside `profile`. Absent with no `profile` means STANDARD and `classificationDefaulted: true`; an invalid value fails closed.
26
- */
27
- classification?: 'GREEN' | 'YELLOW' | 'ORANGE' | 'RED';
23
+ profile: 'BASELINE' | 'STANDARD' | 'HIGH' | 'CRITICAL';
28
24
  }
29
25
  /**
30
26
  * The bytes `isomorph secure` (or a later console re-run against a deploy upload) put through source intake.