@fourier-labs/harbour-contracts 0.45.0 → 0.47.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 (39) hide show
  1. package/README.md +4 -4
  2. package/contract-release.json +28 -28
  3. package/dist/gen-ts/ai-policy-snapshot.d.ts +19 -0
  4. package/dist/gen-ts/ai-runtime-contract.d.ts +1 -1
  5. package/dist/gen-ts/ai-trace-event.d.ts +4 -1
  6. package/dist/gen-ts/app-integrations-declaration.d.ts +8 -2
  7. package/dist/gen-ts/contract-release.d.ts +6 -6
  8. package/dist/gen-ts/contract-release.js +6 -6
  9. package/dist/gen-ts/data-grant-v2.d.ts +60 -16
  10. package/dist/gen-ts/integration-error-details.d.ts +2 -2
  11. package/dist/gen-ts/integration-execute-request.d.ts +6 -2
  12. package/dist/gen-ts/integration-execute-result.d.ts +21 -1
  13. package/dist/gen-ts/kit-bundle-manifest.d.ts +8 -0
  14. package/dist/gen-ts/kit-lock.d.ts +8 -0
  15. package/dist/gen-ts/saas-connection.d.ts +80 -29
  16. package/dist/resolver/index.d.ts +7 -1
  17. package/gen-ts/ai-policy-snapshot.ts +19 -0
  18. package/gen-ts/ai-runtime-contract.ts +1 -1
  19. package/gen-ts/ai-trace-event.ts +4 -1
  20. package/gen-ts/app-integrations-declaration.ts +8 -2
  21. package/gen-ts/contract-release.ts +6 -6
  22. package/gen-ts/data-grant-v2.ts +62 -16
  23. package/gen-ts/integration-error-details.ts +10 -2
  24. package/gen-ts/integration-execute-request.ts +7 -2
  25. package/gen-ts/integration-execute-result.ts +21 -1
  26. package/gen-ts/kit-bundle-manifest.ts +8 -0
  27. package/gen-ts/kit-lock.ts +8 -0
  28. package/gen-ts/saas-connection.ts +82 -29
  29. package/package.json +1 -1
  30. package/schema/ai-policy-snapshot.schema.json +13 -1
  31. package/schema/ai-runtime-contract.schema.json +1 -1
  32. package/schema/ai-trace-event.schema.json +7 -1
  33. package/schema/app-integrations-declaration.schema.json +19 -5
  34. package/schema/data-grant-v2.schema.json +102 -23
  35. package/schema/integration-error-details.schema.json +10 -2
  36. package/schema/integration-execute-request.schema.json +27 -6
  37. package/schema/integration-execute-result.schema.json +34 -1
  38. package/schema/kit-bundle-manifest.schema.json +9 -2
  39. package/schema/saas-connection.schema.json +97 -41
@@ -1,63 +1,104 @@
1
1
  /* eslint-disable */
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
+ /**
5
+ * Slack carries an installation and a channel map; Gmail is user-consent only and read-only.
6
+ */
7
+ export type SaasProvider = 'slack' | 'gmail';
4
8
  /**
5
9
  * DISABLED stops every app call immediately (details.code CONNECTION_DISABLED).
6
10
  */
7
11
  export type SaasConnectionStatus = 'ACTIVE' | 'DISABLED';
12
+ export type SaasInstallationStatus = 'installed' | 'reconnect_required' | 'revoked';
13
+ /**
14
+ */
8
15
  export type InstallationName = string;
9
16
 
10
17
  /**
11
- * Tenant-wide SaaS connection record (control table sk SAASCONN#<sourceId>, same tenant partition as database sources; alias uniqueness is enforced across both). IT maps each environment to a provider installation and logical channel aliases to channel IDs; apps only ever see the aliases. Contains references, never secrets: the client secret lives in Secrets Manager under clientSecretRef and installation tokens under their own token records.
18
+ * 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.
12
19
  */
13
20
  export interface SaasConnection {
21
+ schema: 'harbour.saas-connection/1.0';
22
+ tenantId: string;
14
23
  /**
15
24
  * Tenant-wide logical alias of a connection (governance sourceId) or of a resource under it.
16
25
  */
17
26
  sourceId: string;
18
27
  kind: 'saas';
19
- provider: 'slack';
28
+ provider: SaasProvider;
20
29
  displayName: string;
21
30
  status: SaasConnectionStatus;
22
31
  /**
23
32
  * Optimistic-concurrency version; every write is conditional on it.
24
33
  */
25
34
  version: number;
26
- clientId: string;
35
+ installation?: SaasInstallation;
27
36
  /**
28
- * secretref://tenant/{tenantId}/saas/{sourceId}/client
37
+ * Logical channel alias -> provider channel ID, shared by every environment. Always empty for Gmail.
29
38
  */
30
- clientSecretRef?: string;
31
- environments: {
32
- development?: SaasEnvironmentMapping;
33
- preview?: SaasEnvironmentMapping;
34
- production?: SaasEnvironmentMapping;
39
+ channels: {
40
+ /**
41
+ * Provider channel ID; only public, non-archived channels are valid.
42
+ */
43
+ [k: string]: string;
35
44
  };
36
45
  /**
37
- * Operations IT allows apps to request on this connection.
46
+ * Operations IT allows apps to request on this connection: the provider's own SaaS operations (Slack: slack.channel.history, slack.message.post; Gmail: gmail.thread.list, gmail.message.read). Never warehouse.view.read.
38
47
  *
39
48
  * @minItems 1
40
- * @maxItems 2
49
+ * @maxItems 4
41
50
  */
42
- operations: ('slack.channel.history' | 'slack.message.post')[];
51
+ operations: (
52
+ 'slack.channel.history' | 'slack.message.post' | 'gmail.thread.list' | 'gmail.message.read' | 'warehouse.view.read'
53
+ )[];
43
54
  developmentPreapproval: DevelopmentPreapproval;
44
55
  /**
45
- * Provider installations keyed by the installation name environments point at.
56
+ * Lower-cased builder emails excluded from development preapproval.
57
+ *
58
+ * @maxItems 256
59
+ */
60
+ blockedDevelopmentBuilders?: string[];
61
+ createdAt: string;
62
+ updatedAt: string;
63
+ /**
64
+ * @deprecated
65
+ * Deprecated (pre-T25 tenant-owned provider client). Dropped on read; never written.
66
+ */
67
+ clientId?: string;
68
+ /**
69
+ * @deprecated
70
+ * Deprecated (pre-T25 tenant-owned provider client secret reference). Dropped on read; never written.
71
+ */
72
+ clientSecretRef?: string;
73
+ /**
74
+ * @deprecated
75
+ * Deprecated pre-T25 per-environment mapping. On read the development entry's channels become `channels`; never written.
76
+ */
77
+ environments?: {
78
+ development?: SaasEnvironmentMapping;
79
+ preview?: SaasEnvironmentMapping;
80
+ production?: SaasEnvironmentMapping;
81
+ };
82
+ /**
83
+ * @deprecated
84
+ * Deprecated pre-T25 named installations. On read the `nonprod` entry (or the development mapping's) becomes `installation`; never written.
46
85
  */
47
86
  installations?: {
48
- [k: string]: SaasInstallation;
87
+ [k: string]: LegacySaasInstallation;
49
88
  };
50
- createdAt?: string;
51
- updatedAt?: string;
52
89
  }
53
- export interface SaasEnvironmentMapping {
54
- installation: InstallationName;
55
- channels: {
56
- /**
57
- * Provider channel ID; only public, non-archived channels are valid.
58
- */
59
- [k: string]: string;
60
- };
90
+ /**
91
+ * The one bot installation of the platform-owned provider app in this tenant's workspace.
92
+ */
93
+ export interface SaasInstallation {
94
+ tokenRecordId: string;
95
+ workspaceId: string;
96
+ /**
97
+ * Empty only for a legacy record normalised without one.
98
+ */
99
+ botUserId: string;
100
+ status: SaasInstallationStatus;
101
+ installedAt: string;
61
102
  }
62
103
  export interface DevelopmentPreapproval {
63
104
  /**
@@ -69,9 +110,21 @@ export interface DevelopmentPreapproval {
69
110
  */
70
111
  hours: number;
71
112
  }
72
- export interface SaasInstallation {
73
- tokenRecordId: string;
74
- workspaceId: string;
75
- botUserId: string;
76
- status: 'ACTIVE' | 'RECONNECT_REQUIRED' | 'REVOKED';
113
+ /**
114
+ * @deprecated
115
+ */
116
+ export interface SaasEnvironmentMapping {
117
+ installation: InstallationName;
118
+ channels: {
119
+ [k: string]: string;
120
+ };
121
+ }
122
+ /**
123
+ * @deprecated
124
+ */
125
+ export interface LegacySaasInstallation {
126
+ tokenRecordId?: string;
127
+ workspaceId?: string;
128
+ botUserId?: string;
129
+ status?: 'ACTIVE' | 'RECONNECT_REQUIRED' | 'REVOKED';
77
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourier-labs/harbour-contracts",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "Single source of truth for every data shape that crosses a boundary between Harbour components.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -52,7 +52,8 @@
52
52
  "maxItems": 128,
53
53
  "items": { "$ref": "#/definitions/AiPolicyLimit" }
54
54
  },
55
- "retention": { "$ref": "#/definitions/AiPolicyRetention" }
55
+ "retention": { "$ref": "#/definitions/AiPolicyRetention" },
56
+ "assignments": { "$ref": "#/definitions/AiPolicyAssignments" }
56
57
  },
57
58
  "definitions": {
58
59
  "AiPolicyIdentifier": {
@@ -366,6 +367,17 @@
366
367
  "items": { "$ref": "#/definitions/AiPolicyIdentifier" }
367
368
  }
368
369
  }
370
+ },
371
+ "AiPolicyAssignments": {
372
+ "type": "object",
373
+ "additionalProperties": false,
374
+ "required": ["routes", "policies", "budgets"],
375
+ "description": "Application-scope only: the tenant resources this application/environment snapshot pins BY NAME (D-8/D-9). A scope without assignments keeps the default rule (the whole merge-narrowed set is the assignment). Every name must exist in the same snapshot's own family; empty route and budget selections are legal (tenant-scoped limits bind regardless).",
376
+ "properties": {
377
+ "routes": { "type": "array", "maxItems": 256, "uniqueItems": true, "items": { "$ref": "#/definitions/AiPolicyIdentifier" } },
378
+ "policies": { "type": "array", "minItems": 1, "maxItems": 256, "uniqueItems": true, "items": { "$ref": "#/definitions/AiPolicyIdentifier" } },
379
+ "budgets": { "type": "array", "maxItems": 128, "uniqueItems": true, "items": { "$ref": "#/definitions/AiPolicyIdentifier" } }
380
+ }
369
381
  }
370
382
  }
371
383
  }
@@ -104,7 +104,7 @@
104
104
  "properties": {
105
105
  "mappingId": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$" },
106
106
  "version": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" },
107
- "protocol": { "type": "string", "const": "openai-chat" },
107
+ "protocol": { "type": "string", "enum": ["openai-chat", "openai-images"] },
108
108
  "providerId": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[a-z][a-z0-9_.-]{0,63}$" },
109
109
  "apiKeyEnvironmentName": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Z][A-Z0-9_]{0,127}$" },
110
110
  "baseUrlEnvironmentName": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Z][A-Z0-9_]{0,127}$" }
@@ -45,7 +45,13 @@
45
45
  "provider": { "type": "string", "enum": ["openai", "anthropic", "gemini", "openrouter", "deepseek", "elevenlabs", "deepgram", "higgsfield"] },
46
46
  "providerModel": { "type": "string", "minLength": 1, "maxLength": 256 },
47
47
  "outcome": { "type": "string", "enum": ["succeeded", "blocked", "failed", "cancelled"] },
48
- "usage": { "$ref": "#/definitions/AiTraceUsage" },
48
+ "usage": {
49
+ "description": "Trace-level provider-reported usage, or null when no attempt reported any (the reservation stays committed rather than reconciled against invented zero usage).",
50
+ "oneOf": [
51
+ { "$ref": "#/definitions/AiTraceUsage" },
52
+ { "type": "null" }
53
+ ]
54
+ },
49
55
  "priceRevision": { "$ref": "#/definitions/AiTraceIdentifier" },
50
56
  "costMicroUsd": { "type": "integer", "minimum": 0 },
51
57
  "latencyMs": { "type": "integer", "minimum": 0 },
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/app-integrations-declaration.schema.json",
4
4
  "title": "AppIntegrationsDeclaration",
5
- "description": "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 (three operations) and each operation carries a fixed identity mode: slack.channel.history=user, slack.message.post=app, warehouse.view.read=app. A `saas` connection may declare only the Slack operations and a `database` connection only warehouse.view.read. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind enums referenced by the other integration schemas.",
5
+ "description": "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.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["schema", "connections"],
@@ -19,13 +19,13 @@
19
19
  "definitions": {
20
20
  "IntegrationAlias": {
21
21
  "type": "string",
22
- "pattern": "^[a-z][a-z0-9_-]{0,62}$",
22
+ "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$",
23
23
  "description": "Tenant-wide logical alias of a connection (governance sourceId) or of a resource under it."
24
24
  },
25
25
  "IntegrationConnectionKind": { "type": "string", "enum": ["saas", "database"] },
26
26
  "IntegrationOperation": {
27
27
  "type": "string",
28
- "enum": ["slack.channel.history", "slack.message.post", "warehouse.view.read"],
28
+ "enum": ["slack.channel.history", "slack.message.post", "gmail.thread.list", "gmail.message.read", "warehouse.view.read"],
29
29
  "description": "Closed operation set. Extending it is a contracts change."
30
30
  },
31
31
  "IntegrationIdentityMode": {
@@ -38,6 +38,11 @@
38
38
  "enum": ["development", "preview", "production"],
39
39
  "description": "development is the CLI/local kit; preview and production are hosted."
40
40
  },
41
+ "ColumnName": {
42
+ "type": "string",
43
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,62}$",
44
+ "description": "A plain column identifier of a warehouse view."
45
+ },
41
46
  "ResourceNames": {
42
47
  "type": "array",
43
48
  "minItems": 1,
@@ -53,7 +58,7 @@
53
58
  "type": "array",
54
59
  "minItems": 1,
55
60
  "maxItems": 32,
56
- "items": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,127}$" }
61
+ "items": { "$ref": "#/definitions/ColumnName" }
57
62
  }
58
63
  }
59
64
  },
@@ -103,6 +108,8 @@
103
108
  "properties": {
104
109
  "slack.channel.history": { "$ref": "#/definitions/UserModeChannelOperation" },
105
110
  "slack.message.post": { "$ref": "#/definitions/AppModeChannelOperation" },
111
+ "gmail.thread.list": { "$ref": "#/definitions/UserModeChannelOperation" },
112
+ "gmail.message.read": { "$ref": "#/definitions/UserModeChannelOperation" },
106
113
  "warehouse.view.read": { "$ref": "#/definitions/AppModeViewOperation" }
107
114
  }
108
115
  }
@@ -112,7 +119,7 @@
112
119
  "else": {
113
120
  "properties": {
114
121
  "operations": {
115
- "not": { "anyOf": [{ "required": ["slack.channel.history"] }, { "required": ["slack.message.post"] }] }
122
+ "not": { "anyOf": [{ "required": ["slack.channel.history"] }, { "required": ["slack.message.post"] }, { "required": ["gmail.thread.list"] }, { "required": ["gmail.message.read"] }] }
116
123
  }
117
124
  }
118
125
  }
@@ -129,6 +136,13 @@
129
136
  "slack.message.post": { "identity": "app", "resources": ["team-updates"] }
130
137
  }
131
138
  },
139
+ "gmail": {
140
+ "kind": "saas",
141
+ "operations": {
142
+ "gmail.thread.list": { "identity": "user", "resources": ["inbox"] },
143
+ "gmail.message.read": { "identity": "user", "resources": ["inbox"] }
144
+ }
145
+ },
132
146
  "sales-warehouse": {
133
147
  "kind": "database",
134
148
  "operations": {
@@ -2,48 +2,78 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/data-grant-v2.schema.json",
4
4
  "title": "DataGrantV2",
5
- "description": "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. Expiry is enforced from UTC time on every call and SDK calls never extend it. 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, SaaS grants add installation; both hold references only.",
5
+ "description": "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.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["schema", "appId", "sourceId", "kind", "environment", "identityMode", "status", "operations", "resources", "version", "provisioning"],
8
+ "required": ["schema", "tenantId", "appId", "environment", "sourceId", "mode", "kind", "status", "operations", "resources", "version", "provisioning", "createdAt", "updatedAt"],
9
9
  "properties": {
10
10
  "schema": { "type": "string", "const": "harbour.data-grant/2.0" },
11
+ "tenantId": { "type": "string", "minLength": 1, "maxLength": 255 },
11
12
  "appId": { "type": "string", "minLength": 1, "maxLength": 255 },
13
+ "environment": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationEnvironment" },
12
14
  "sourceId": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
15
+ "mode": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationIdentityMode" },
13
16
  "kind": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationConnectionKind" },
14
- "environment": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationEnvironment" },
15
- "identityMode": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationIdentityMode" },
16
17
  "status": { "$ref": "#/definitions/DataGrantV2Status" },
17
18
  "operations": {
18
19
  "type": "array",
19
- "minItems": 1,
20
- "maxItems": 3,
20
+ "maxItems": 5,
21
21
  "uniqueItems": true,
22
22
  "items": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationOperation" },
23
- "description": "Approved operations; each must match this grant's identityMode."
23
+ "description": "Approved operations; each must match this grant's mode. Empty until the first approval."
24
24
  },
25
25
  "resources": {
26
26
  "type": "array",
27
- "minItems": 1,
28
27
  "maxItems": 64,
29
28
  "uniqueItems": true,
30
- "items": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
31
- "description": "Approved logical resource names (channel aliases or view names). Never inherited by wildcard."
29
+ "items": { "$ref": "#/definitions/IntegrationResource" },
30
+ "description": "Approved logical resources. Never inherited by wildcard. Empty until the first approval."
32
31
  },
33
32
  "expiresAt": { "type": "string", "format": "date-time", "description": "Absent only for explicitly approved ongoing production access." },
33
+ "expiredAt": { "type": "string", "format": "date-time", "description": "Set by the recovery sweep once expiry has passed; enforcement itself is the UTC check on every call." },
34
34
  "approvedBy": { "type": "string", "minLength": 1, "maxLength": 255 },
35
+ "approvedAt": { "type": "string", "format": "date-time" },
35
36
  "version": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency version; approvals compare it with the reviewed value." },
36
37
  "pendingRequestId": { "type": "string", "minLength": 1, "maxLength": 128, "description": "The one open access request attached to this grant, if any." },
37
38
  "provisioning": { "$ref": "#/definitions/DataGrantV2Provisioning" },
38
39
  "credentialRef": {
39
40
  "type": "string",
40
41
  "pattern": "^secretref://[A-Za-z0-9][A-Za-z0-9/_.:-]*$",
41
- "description": "Database grants: secretref://tenant/{t}/app/{a}/env/{env}/source/{s}."
42
+ "description": "Database grants: secretref://tenant/{t}/app/{a}/env/{env}/source/{s}. Set only by a successful publish and removed by cleanup."
42
43
  },
43
44
  "mintedPrincipal": { "type": "string", "minLength": 1, "maxLength": 255, "description": "Database grants: the deterministic per tenant/app/environment/source principal." },
44
- "installation": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,62}$", "description": "SaaS grants: the connection installation this environment maps to." }
45
+ "createdAt": { "type": "string", "format": "date-time" },
46
+ "updatedAt": { "type": "string", "format": "date-time" },
47
+ "identityMode": {
48
+ "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationIdentityMode",
49
+ "deprecated": true,
50
+ "description": "Deprecated 0.45.0 spelling of `mode`; the control plane never writes it."
51
+ },
52
+ "installation": {
53
+ "type": "string",
54
+ "pattern": "^[a-z][a-z0-9_-]{0,62}$",
55
+ "deprecated": true,
56
+ "description": "Deprecated: a SaaS grant runs on the connection's single installation; the control plane never writes it."
57
+ }
45
58
  },
46
59
  "definitions": {
60
+ "IntegrationResource": {
61
+ "type": "object",
62
+ "additionalProperties": false,
63
+ "required": ["name"],
64
+ "description": "One approved logical resource: a channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with the approved columns.",
65
+ "properties": {
66
+ "name": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
67
+ "columns": {
68
+ "type": "array",
69
+ "minItems": 1,
70
+ "maxItems": 32,
71
+ "uniqueItems": true,
72
+ "items": { "$ref": "app-integrations-declaration.schema.json#/definitions/ColumnName" },
73
+ "description": "warehouse.view.read only: the approved columns of the view."
74
+ }
75
+ }
76
+ },
47
77
  "DataGrantV2Status": {
48
78
  "type": "string",
49
79
  "enum": ["REQUESTED", "GRANTED", "DENIED", "REVOKED"],
@@ -54,6 +84,10 @@
54
84
  "enum": ["NONE", "PENDING", "FAILED", "DONE"],
55
85
  "description": "Provider-side work for the current approved scope (credential mint, cleanup, installation check)."
56
86
  },
87
+ "DataGrantV2CleanupStatus": {
88
+ "type": "string",
89
+ "enum": ["PENDING", "FAILED", "DONE"]
90
+ },
57
91
  "DataGrantV2Provisioning": {
58
92
  "type": "object",
59
93
  "additionalProperties": false,
@@ -61,7 +95,28 @@
61
95
  "properties": {
62
96
  "status": { "$ref": "#/definitions/DataGrantV2ProvisioningStatus" },
63
97
  "attempts": { "type": "integer", "minimum": 0, "maximum": 3, "description": "Failed attempts; three requires an admin retry." },
64
- "lastError": { "type": "string", "maxLength": 500, "description": "Sanitized reason; never a provider error body." }
98
+ "lastError": { "type": "string", "maxLength": 500, "description": "Sanitized reason; never a provider error body." },
99
+ "lease": {
100
+ "type": "object",
101
+ "additionalProperties": false,
102
+ "required": ["until", "owner"],
103
+ "description": "Conditional worker lease serialising database mutations (120 s).",
104
+ "properties": {
105
+ "until": { "type": "string", "format": "date-time" },
106
+ "owner": { "type": "string", "minLength": 1, "maxLength": 128 }
107
+ }
108
+ },
109
+ "cleanup": {
110
+ "type": "object",
111
+ "additionalProperties": false,
112
+ "required": ["status", "attempts"],
113
+ "description": "Provider-side cleanup after revocation or expiry; a FAILED cleanup waits for an admin retry-cleanup.",
114
+ "properties": {
115
+ "status": { "$ref": "#/definitions/DataGrantV2CleanupStatus" },
116
+ "attempts": { "type": "integer", "minimum": 0, "maximum": 3 },
117
+ "lastError": { "type": "string", "maxLength": 500 }
118
+ }
119
+ }
65
120
  }
66
121
  }
67
122
  },
@@ -75,34 +130,58 @@
75
130
  "examples": [
76
131
  {
77
132
  "schema": "harbour.data-grant/2.0",
133
+ "tenantId": "fourier",
78
134
  "appId": "app-1f2e3d4c",
135
+ "environment": "development",
79
136
  "sourceId": "company-slack",
137
+ "mode": "user",
80
138
  "kind": "saas",
81
- "environment": "development",
82
- "identityMode": "user",
83
139
  "status": "GRANTED",
84
140
  "operations": ["slack.channel.history"],
85
- "resources": ["team-updates"],
141
+ "resources": [{ "name": "team-updates" }],
86
142
  "expiresAt": "2026-09-10T08:00:00Z",
87
143
  "approvedBy": "policy:development-preapproval",
144
+ "approvedAt": "2026-09-09T08:00:00Z",
88
145
  "version": 2,
89
146
  "provisioning": { "status": "DONE", "attempts": 0 },
90
- "installation": "nonprod"
147
+ "createdAt": "2026-09-09T08:00:00Z",
148
+ "updatedAt": "2026-09-09T08:00:00Z"
91
149
  },
92
150
  {
93
151
  "schema": "harbour.data-grant/2.0",
152
+ "tenantId": "fourier",
94
153
  "appId": "app-1f2e3d4c",
154
+ "environment": "preview",
95
155
  "sourceId": "sales-warehouse",
156
+ "mode": "app",
96
157
  "kind": "database",
97
- "environment": "preview",
98
- "identityMode": "app",
99
158
  "status": "REQUESTED",
100
- "operations": ["warehouse.view.read"],
101
- "resources": ["weekly_sales"],
102
- "expiresAt": "2026-09-16T08:00:00Z",
159
+ "operations": [],
160
+ "resources": [],
103
161
  "version": 1,
104
162
  "pendingRequestId": "req_9a8b7c6d",
105
- "provisioning": { "status": "PENDING", "attempts": 1, "lastError": "credential verification timed out" }
163
+ "provisioning": { "status": "PENDING", "attempts": 1, "lastError": "credential verification timed out", "lease": { "until": "2026-09-09T08:02:00Z", "owner": "worker-7" } },
164
+ "createdAt": "2026-09-09T08:00:00Z",
165
+ "updatedAt": "2026-09-09T08:00:00Z"
166
+ },
167
+ {
168
+ "schema": "harbour.data-grant/2.0",
169
+ "tenantId": "fourier",
170
+ "appId": "app-1f2e3d4c",
171
+ "environment": "production",
172
+ "sourceId": "sales-warehouse",
173
+ "mode": "app",
174
+ "kind": "database",
175
+ "status": "REVOKED",
176
+ "operations": ["warehouse.view.read"],
177
+ "resources": [{ "name": "weekly_sales", "columns": ["week", "total"] }],
178
+ "approvedBy": "it-admin@example.com",
179
+ "approvedAt": "2026-09-09T08:00:00Z",
180
+ "version": 5,
181
+ "provisioning": { "status": "NONE", "attempts": 0, "cleanup": { "status": "PENDING", "attempts": 0 } },
182
+ "mintedPrincipal": "harbour_fourier_app1f2e3d4c_production_sales_warehouse",
183
+ "createdAt": "2026-09-09T08:00:00Z",
184
+ "updatedAt": "2026-09-11T08:00:00Z"
106
185
  }
107
186
  ]
108
187
  }
@@ -52,9 +52,17 @@
52
52
  "APP_ACCESS_REQUIRED",
53
53
  "GRANT_DENIED",
54
54
  "TOKEN_REFRESH_PENDING",
55
- "REQUEST_PENDING"
55
+ "REQUEST_PENDING",
56
+ "INPUT_INVALID",
57
+ "IDEMPOTENCY_KEY_REQUIRED",
58
+ "SEND_IN_PROGRESS",
59
+ "CONNECTION_NOT_FOUND",
60
+ "APP_NOT_FOUND",
61
+ "GRANT_NOT_FOUND",
62
+ "AUTH_REQUIRED",
63
+ "UNAVAILABLE"
56
64
  ],
57
- "description": "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."
65
+ "description": "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."
58
66
  }
59
67
  },
60
68
  "examples": [
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/integration-execute-request.schema.json",
4
4
  "title": "IntegrationExecuteRequest",
5
- "description": "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; warehouse.view.read {columns 1..32, limit? 1..1000, default 100}. Transport limits (not expressible here): request <= 16 KiB, total deadline 10 s.",
5
+ "description": "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}; warehouse.view.read {columns 1..32, limit? 1..1000, default 100}. Transport limits (not expressible here): request <= 16 KiB, total deadline 10 s.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["connection", "operation", "resource", "input"],
@@ -16,11 +16,12 @@
16
16
  "properties": {
17
17
  "limit": { "type": "integer", "minimum": 1, "maximum": 1000 },
18
18
  "text": { "type": "string", "minLength": 1, "maxLength": 4000 },
19
+ "messageId": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$", "description": "gmail.message.read: the Gmail message id a gmail.thread.list result named." },
19
20
  "columns": {
20
21
  "type": "array",
21
22
  "minItems": 1,
22
23
  "maxItems": 32,
23
- "items": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,127}$" }
24
+ "items": { "$ref": "app-integrations-declaration.schema.json#/definitions/ColumnName" }
24
25
  }
25
26
  }
26
27
  },
@@ -36,7 +37,7 @@
36
37
  "then": {
37
38
  "not": { "required": ["idempotencyKey"] },
38
39
  "properties": {
39
- "input": { "properties": { "limit": { "maximum": 15 } }, "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }] } }
40
+ "input": { "properties": { "limit": { "maximum": 15 } }, "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }, { "required": ["messageId"] }] } }
40
41
  }
41
42
  }
42
43
  },
@@ -45,7 +46,7 @@
45
46
  "then": {
46
47
  "required": ["idempotencyKey"],
47
48
  "properties": {
48
- "input": { "required": ["text"], "not": { "anyOf": [{ "required": ["limit"] }, { "required": ["columns"] }] } }
49
+ "input": { "required": ["text"], "not": { "anyOf": [{ "required": ["limit"] }, { "required": ["columns"] }, { "required": ["messageId"] }] } }
49
50
  }
50
51
  }
51
52
  },
@@ -54,7 +55,25 @@
54
55
  "then": {
55
56
  "not": { "required": ["idempotencyKey"] },
56
57
  "properties": {
57
- "input": { "required": ["columns"], "not": { "required": ["text"] } }
58
+ "input": { "required": ["columns"], "not": { "anyOf": [{ "required": ["text"] }, { "required": ["messageId"] }] } }
59
+ }
60
+ }
61
+ },
62
+ {
63
+ "if": { "properties": { "operation": { "const": "gmail.thread.list" } } },
64
+ "then": {
65
+ "not": { "required": ["idempotencyKey"] },
66
+ "properties": {
67
+ "input": { "properties": { "limit": { "maximum": 15 } }, "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }, { "required": ["messageId"] }] } }
68
+ }
69
+ }
70
+ },
71
+ {
72
+ "if": { "properties": { "operation": { "const": "gmail.message.read" } } },
73
+ "then": {
74
+ "not": { "required": ["idempotencyKey"] },
75
+ "properties": {
76
+ "input": { "required": ["messageId"], "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }, { "required": ["limit"] }] } }
58
77
  }
59
78
  }
60
79
  }
@@ -68,6 +87,8 @@
68
87
  "input": { "text": "Weekly update posted." },
69
88
  "idempotencyKey": "6f1c2c1e-3a1c-4c3e-9f0e-2b6f7d8a9c10"
70
89
  },
71
- { "connection": "sales-warehouse", "operation": "warehouse.view.read", "resource": "weekly_sales", "input": { "columns": ["week", "total"], "limit": 100 } }
90
+ { "connection": "sales-warehouse", "operation": "warehouse.view.read", "resource": "weekly_sales", "input": { "columns": ["week", "total"], "limit": 100 } },
91
+ { "connection": "gmail", "operation": "gmail.thread.list", "resource": "inbox", "input": { "limit": 15 } },
92
+ { "connection": "gmail", "operation": "gmail.message.read", "resource": "inbox", "input": { "messageId": "18f2c3a4b5d6e7f8" } }
72
93
  ]
73
94
  }