@fourier-labs/harbour-contracts 0.47.0 → 0.51.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 +35 -3
- package/contract-release.json +25 -22
- package/dist/gen-ts/app-integrations-declaration.d.ts +1 -0
- package/dist/gen-ts/contract-release.d.ts +6 -6
- package/dist/gen-ts/contract-release.js +6 -6
- package/dist/gen-ts/data-grant-v2.d.ts +1 -1
- package/dist/gen-ts/index.d.ts +1 -0
- package/dist/gen-ts/index.js +1 -0
- package/dist/gen-ts/integration-execute-input-rules.d.ts +187 -0
- package/dist/gen-ts/integration-execute-input-rules.js +114 -0
- package/dist/gen-ts/integration-execute-request.d.ts +46 -4
- package/dist/gen-ts/integration-execute-result.d.ts +1 -1
- package/dist/gen-ts/saas-connection.d.ts +3 -3
- package/dist/gen-ts/transform-report.d.ts +2 -0
- package/dist/gen-ts/well-defined-app-manifest.d.ts +31 -0
- package/gen-ts/app-integrations-declaration.ts +1 -0
- package/gen-ts/contract-release.ts +6 -6
- package/gen-ts/data-grant-v2.ts +6 -1
- package/gen-ts/index.ts +1 -0
- package/gen-ts/integration-execute-input-rules.ts +119 -0
- package/gen-ts/integration-execute-request.ts +51 -4
- package/gen-ts/integration-execute-result.ts +1 -1
- package/gen-ts/saas-connection.ts +8 -3
- package/gen-ts/transform-report.ts +2 -0
- package/gen-ts/well-defined-app-manifest.ts +31 -0
- package/package.json +1 -1
- package/schema/app-integrations-declaration.schema.json +6 -4
- package/schema/integration-execute-request.schema.json +35 -10
- package/schema/integration-execute-result.schema.json +3 -1
- package/schema/saas-connection.schema.json +3 -3
- package/schema/transform-report.schema.json +2 -1
- package/schema/well-defined-app-manifest.schema.json +61 -0
|
@@ -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}; 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 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}. 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,20 +7,37 @@ export type IntegrationExecuteRequest = {
|
|
|
7
7
|
*/
|
|
8
8
|
connection: string;
|
|
9
9
|
/**
|
|
10
|
-
* Closed operation set. Extending it is a contracts change.
|
|
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.
|
|
11
11
|
*/
|
|
12
|
-
operation: 'slack.channel.history' | 'slack.message.post' | 'gmail.thread.list' | 'gmail.message.read' | 'warehouse.view.read';
|
|
12
|
+
operation: 'slack.channel.history' | 'slack.message.post' | 'gmail.thread.list' | 'gmail.message.read' | 'gmail.message.send' | 'warehouse.view.read';
|
|
13
13
|
/**
|
|
14
14
|
* Tenant-wide logical alias of a connection (governance sourceId) or of a resource under it.
|
|
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.
|
|
22
28
|
*/
|
|
23
29
|
messageId?: string;
|
|
30
|
+
to?: EmailAddressList;
|
|
31
|
+
cc?: EmailAddressList1;
|
|
32
|
+
bcc?: EmailAddressList2;
|
|
33
|
+
/**
|
|
34
|
+
* gmail.message.send: one-line subject (no line breaks).
|
|
35
|
+
*/
|
|
36
|
+
subject?: string;
|
|
37
|
+
/**
|
|
38
|
+
* gmail.message.send: a Gmail message id (from gmail.thread.list / gmail.message.read) the email replies to; the send joins that thread.
|
|
39
|
+
*/
|
|
40
|
+
replyToMessageId?: string;
|
|
24
41
|
/**
|
|
25
42
|
* @minItems 1
|
|
26
43
|
* @maxItems 32
|
|
@@ -28,7 +45,32 @@ export type IntegrationExecuteRequest = {
|
|
|
28
45
|
columns?: string[];
|
|
29
46
|
};
|
|
30
47
|
/**
|
|
31
|
-
* UUID of one explicit Send action; required for slack.message.post only. The same key with the same payload and subject returns the recorded outcome for seven days.
|
|
48
|
+
* 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.
|
|
32
49
|
*/
|
|
33
50
|
idempotencyKey?: string;
|
|
34
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* One bare email address (no display name).
|
|
54
|
+
*/
|
|
55
|
+
export type EmailAddress = string;
|
|
56
|
+
/**
|
|
57
|
+
* gmail.message.send: primary recipients.
|
|
58
|
+
*
|
|
59
|
+
* @minItems 1
|
|
60
|
+
* @maxItems 50
|
|
61
|
+
*/
|
|
62
|
+
export type EmailAddressList = EmailAddress[];
|
|
63
|
+
/**
|
|
64
|
+
* gmail.message.send: carbon-copy recipients.
|
|
65
|
+
*
|
|
66
|
+
* @minItems 1
|
|
67
|
+
* @maxItems 50
|
|
68
|
+
*/
|
|
69
|
+
export type EmailAddressList1 = EmailAddress[];
|
|
70
|
+
/**
|
|
71
|
+
* gmail.message.send: blind-copy recipients.
|
|
72
|
+
*
|
|
73
|
+
* @minItems 1
|
|
74
|
+
* @maxItems 50
|
|
75
|
+
*/
|
|
76
|
+
export type EmailAddressList2 = EmailAddress[];
|
|
@@ -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
|
|
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 only, 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.
|
|
3
3
|
*/
|
|
4
4
|
export type IntegrationExecuteResult = {
|
|
5
5
|
/**
|
|
@@ -39,12 +39,12 @@ export interface SaasConnection {
|
|
|
39
39
|
[k: string]: string;
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
|
-
* 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.
|
|
42
|
+
* 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, gmail.message.send). Never warehouse.view.read.
|
|
43
43
|
*
|
|
44
44
|
* @minItems 1
|
|
45
|
-
* @maxItems
|
|
45
|
+
* @maxItems 5
|
|
46
46
|
*/
|
|
47
|
-
operations: ('slack.channel.history' | 'slack.message.post' | 'gmail.thread.list' | 'gmail.message.read' | 'warehouse.view.read')[];
|
|
47
|
+
operations: ('slack.channel.history' | 'slack.message.post' | 'gmail.thread.list' | 'gmail.message.read' | 'gmail.message.send' | 'warehouse.view.read')[];
|
|
48
48
|
developmentPreapproval: DevelopmentPreapproval;
|
|
49
49
|
/**
|
|
50
50
|
* Lower-cased builder emails excluded from development preapproval.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export type TransformReport = {
|
|
5
5
|
/**
|
|
6
|
+
* Optional self-report of the playbooks the agent says it applied. A claim about its own behaviour is not evidence, so nothing validates it; a deployable report may omit it, and one that still carries it stays valid.
|
|
7
|
+
*
|
|
6
8
|
* @minItems 1
|
|
7
9
|
*/
|
|
8
10
|
applied_playbooks?: string[];
|
|
@@ -15,6 +15,7 @@ export type Workload = {
|
|
|
15
15
|
*/
|
|
16
16
|
export type Command = string[];
|
|
17
17
|
export type ResourceProfile = 'standard' | 'memory-1g' | 'memory-2g';
|
|
18
|
+
export type Capability = 'data' | 'files' | 'actions' | 'realtime' | 'telemetry';
|
|
18
19
|
/**
|
|
19
20
|
* Versioned provisioning input emitted by Harbour Transform. It describes runtime wiring and workload lifecycle only; PostgreSQL grants and RLS remain the database authorization authority.
|
|
20
21
|
*/
|
|
@@ -33,6 +34,17 @@ export interface WellDefinedAppManifest {
|
|
|
33
34
|
* @maxItems 128
|
|
34
35
|
*/
|
|
35
36
|
bindings: WorkloadBinding[];
|
|
37
|
+
/**
|
|
38
|
+
* Optional language runtime the application container is built on, spelled exactly as transform-report.application.runtime does (nodejs22, python3.11). Optional so a manifest written before the producer emits it still validates.
|
|
39
|
+
*/
|
|
40
|
+
runtime?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Optional declaration of the Harbour browser capabilities the application uses. Same closed set as the app-capabilities document; this is the declaration, not a source scan.
|
|
43
|
+
*
|
|
44
|
+
* @maxItems 5
|
|
45
|
+
*/
|
|
46
|
+
capabilities?: Capability[];
|
|
47
|
+
routeAuth?: RouteAuth;
|
|
36
48
|
}
|
|
37
49
|
export interface QueueConsumer {
|
|
38
50
|
logicalName: string;
|
|
@@ -64,3 +76,22 @@ export interface WorkloadBinding {
|
|
|
64
76
|
*/
|
|
65
77
|
reference?: string;
|
|
66
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Optional method/path authentication policy. Omitted means every route requires Harbour SSO, the same default the policy states explicitly.
|
|
81
|
+
*/
|
|
82
|
+
export interface RouteAuth {
|
|
83
|
+
default: 'sso_required';
|
|
84
|
+
/**
|
|
85
|
+
* @maxItems 64
|
|
86
|
+
*/
|
|
87
|
+
public: PublicRoute[];
|
|
88
|
+
}
|
|
89
|
+
export interface PublicRoute {
|
|
90
|
+
path: string;
|
|
91
|
+
/**
|
|
92
|
+
* @minItems 1
|
|
93
|
+
* @maxItems 7
|
|
94
|
+
*/
|
|
95
|
+
methods: ('GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS')[];
|
|
96
|
+
mode: 'application_authorized';
|
|
97
|
+
}
|
|
@@ -32,6 +32,7 @@ export interface DeclaredConnection {
|
|
|
32
32
|
'slack.message.post'?: AppModeChannelOperation;
|
|
33
33
|
'gmail.thread.list'?: UserModeChannelOperation;
|
|
34
34
|
'gmail.message.read'?: UserModeChannelOperation;
|
|
35
|
+
'gmail.message.send'?: UserModeChannelOperation;
|
|
35
36
|
'warehouse.view.read'?: AppModeViewOperation;
|
|
36
37
|
};
|
|
37
38
|
}
|
|
@@ -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.
|
|
5
|
-
export const ContractPublishVersion = "0.
|
|
6
|
-
export const ContractPublishTag = "v0.
|
|
7
|
-
export const ContractSchemasDigest = "sha256:
|
|
4
|
+
export const ContractRepositoryVersion = "0.51.0.0" as const;
|
|
5
|
+
export const ContractPublishVersion = "0.51.0" as const;
|
|
6
|
+
export const ContractPublishTag = "v0.51.0" as const;
|
|
7
|
+
export const ContractSchemasDigest = "sha256:018bf3078087a18b07990772c94669f658159bc84b0c8accedc7dd5daa38bee5" as const;
|
|
8
8
|
export const ContractAiFixturesDigest = "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6" as const;
|
|
9
|
-
export const ContractGeneratedGoDigest = "sha256:
|
|
10
|
-
export const ContractGeneratedTypeScriptDigest = "sha256:
|
|
9
|
+
export const ContractGeneratedGoDigest = "sha256:98bb6999738be926c36fd8d0735a17defdad590ced57e94bc431bb9bba53c9ad" as const;
|
|
10
|
+
export const ContractGeneratedTypeScriptDigest = "sha256:1d83b04e4899777f2963177e39e7ac99852cae80ffb8c03b6a294f5fb19d3600" as const;
|
package/gen-ts/data-grant-v2.ts
CHANGED
|
@@ -38,7 +38,12 @@ export interface DataGrantV2 {
|
|
|
38
38
|
* @maxItems 5
|
|
39
39
|
*/
|
|
40
40
|
operations: (
|
|
41
|
-
'slack.channel.history'
|
|
41
|
+
| 'slack.channel.history'
|
|
42
|
+
| 'slack.message.post'
|
|
43
|
+
| 'gmail.thread.list'
|
|
44
|
+
| 'gmail.message.read'
|
|
45
|
+
| 'gmail.message.send'
|
|
46
|
+
| 'warehouse.view.read'
|
|
42
47
|
)[];
|
|
43
48
|
/**
|
|
44
49
|
* Approved logical resources. Never inherited by wildcard. Empty until the first approval.
|
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}; 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 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}. 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,21 +10,43 @@ export type IntegrationExecuteRequest = {
|
|
|
10
10
|
*/
|
|
11
11
|
connection: string;
|
|
12
12
|
/**
|
|
13
|
-
* Closed operation set. Extending it is a contracts change.
|
|
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.
|
|
14
14
|
*/
|
|
15
15
|
operation:
|
|
16
|
-
'slack.channel.history'
|
|
16
|
+
| 'slack.channel.history'
|
|
17
|
+
| 'slack.message.post'
|
|
18
|
+
| 'gmail.thread.list'
|
|
19
|
+
| 'gmail.message.read'
|
|
20
|
+
| 'gmail.message.send'
|
|
21
|
+
| 'warehouse.view.read';
|
|
17
22
|
/**
|
|
18
23
|
* Tenant-wide logical alias of a connection (governance sourceId) or of a resource under it.
|
|
19
24
|
*/
|
|
20
25
|
resource: string;
|
|
21
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
|
+
*/
|
|
22
30
|
limit?: number;
|
|
31
|
+
/**
|
|
32
|
+
* slack.message.post / gmail.message.send: the plain-text body, 1..4000 characters.
|
|
33
|
+
*/
|
|
23
34
|
text?: string;
|
|
24
35
|
/**
|
|
25
36
|
* gmail.message.read: the Gmail message id a gmail.thread.list result named.
|
|
26
37
|
*/
|
|
27
38
|
messageId?: string;
|
|
39
|
+
to?: EmailAddressList;
|
|
40
|
+
cc?: EmailAddressList1;
|
|
41
|
+
bcc?: EmailAddressList2;
|
|
42
|
+
/**
|
|
43
|
+
* gmail.message.send: one-line subject (no line breaks).
|
|
44
|
+
*/
|
|
45
|
+
subject?: string;
|
|
46
|
+
/**
|
|
47
|
+
* gmail.message.send: a Gmail message id (from gmail.thread.list / gmail.message.read) the email replies to; the send joins that thread.
|
|
48
|
+
*/
|
|
49
|
+
replyToMessageId?: string;
|
|
28
50
|
/**
|
|
29
51
|
* @minItems 1
|
|
30
52
|
* @maxItems 32
|
|
@@ -32,7 +54,32 @@ export type IntegrationExecuteRequest = {
|
|
|
32
54
|
columns?: string[];
|
|
33
55
|
};
|
|
34
56
|
/**
|
|
35
|
-
* UUID of one explicit Send action; required for slack.message.post only. The same key with the same payload and subject returns the recorded outcome for seven days.
|
|
57
|
+
* 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.
|
|
36
58
|
*/
|
|
37
59
|
idempotencyKey?: string;
|
|
38
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* One bare email address (no display name).
|
|
63
|
+
*/
|
|
64
|
+
export type EmailAddress = string;
|
|
65
|
+
/**
|
|
66
|
+
* gmail.message.send: primary recipients.
|
|
67
|
+
*
|
|
68
|
+
* @minItems 1
|
|
69
|
+
* @maxItems 50
|
|
70
|
+
*/
|
|
71
|
+
export type EmailAddressList = EmailAddress[];
|
|
72
|
+
/**
|
|
73
|
+
* gmail.message.send: carbon-copy recipients.
|
|
74
|
+
*
|
|
75
|
+
* @minItems 1
|
|
76
|
+
* @maxItems 50
|
|
77
|
+
*/
|
|
78
|
+
export type EmailAddressList1 = EmailAddress[];
|
|
79
|
+
/**
|
|
80
|
+
* gmail.message.send: blind-copy recipients.
|
|
81
|
+
*
|
|
82
|
+
* @minItems 1
|
|
83
|
+
* @maxItems 50
|
|
84
|
+
*/
|
|
85
|
+
export type EmailAddressList2 = EmailAddress[];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the
|
|
5
|
+
* 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 only, 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.
|
|
6
6
|
*/
|
|
7
7
|
export type IntegrationExecuteResult = {
|
|
8
8
|
/**
|
|
@@ -43,13 +43,18 @@ export interface SaasConnection {
|
|
|
43
43
|
[k: string]: string;
|
|
44
44
|
};
|
|
45
45
|
/**
|
|
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.
|
|
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, gmail.message.send). Never warehouse.view.read.
|
|
47
47
|
*
|
|
48
48
|
* @minItems 1
|
|
49
|
-
* @maxItems
|
|
49
|
+
* @maxItems 5
|
|
50
50
|
*/
|
|
51
51
|
operations: (
|
|
52
|
-
'slack.channel.history'
|
|
52
|
+
| 'slack.channel.history'
|
|
53
|
+
| 'slack.message.post'
|
|
54
|
+
| 'gmail.thread.list'
|
|
55
|
+
| 'gmail.message.read'
|
|
56
|
+
| 'gmail.message.send'
|
|
57
|
+
| 'warehouse.view.read'
|
|
53
58
|
)[];
|
|
54
59
|
developmentPreapproval: DevelopmentPreapproval;
|
|
55
60
|
/**
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export type TransformReport = {
|
|
8
8
|
/**
|
|
9
|
+
* Optional self-report of the playbooks the agent says it applied. A claim about its own behaviour is not evidence, so nothing validates it; a deployable report may omit it, and one that still carries it stays valid.
|
|
10
|
+
*
|
|
9
11
|
* @minItems 1
|
|
10
12
|
*/
|
|
11
13
|
applied_playbooks?: string[];
|
|
@@ -18,6 +18,7 @@ export type Workload = {
|
|
|
18
18
|
*/
|
|
19
19
|
export type Command = string[];
|
|
20
20
|
export type ResourceProfile = 'standard' | 'memory-1g' | 'memory-2g';
|
|
21
|
+
export type Capability = 'data' | 'files' | 'actions' | 'realtime' | 'telemetry';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Versioned provisioning input emitted by Harbour Transform. It describes runtime wiring and workload lifecycle only; PostgreSQL grants and RLS remain the database authorization authority.
|
|
@@ -37,6 +38,17 @@ export interface WellDefinedAppManifest {
|
|
|
37
38
|
* @maxItems 128
|
|
38
39
|
*/
|
|
39
40
|
bindings: WorkloadBinding[];
|
|
41
|
+
/**
|
|
42
|
+
* Optional language runtime the application container is built on, spelled exactly as transform-report.application.runtime does (nodejs22, python3.11). Optional so a manifest written before the producer emits it still validates.
|
|
43
|
+
*/
|
|
44
|
+
runtime?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Optional declaration of the Harbour browser capabilities the application uses. Same closed set as the app-capabilities document; this is the declaration, not a source scan.
|
|
47
|
+
*
|
|
48
|
+
* @maxItems 5
|
|
49
|
+
*/
|
|
50
|
+
capabilities?: Capability[];
|
|
51
|
+
routeAuth?: RouteAuth;
|
|
40
52
|
}
|
|
41
53
|
export interface QueueConsumer {
|
|
42
54
|
logicalName: string;
|
|
@@ -68,3 +80,22 @@ export interface WorkloadBinding {
|
|
|
68
80
|
*/
|
|
69
81
|
reference?: string;
|
|
70
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Optional method/path authentication policy. Omitted means every route requires Harbour SSO, the same default the policy states explicitly.
|
|
85
|
+
*/
|
|
86
|
+
export interface RouteAuth {
|
|
87
|
+
default: 'sso_required';
|
|
88
|
+
/**
|
|
89
|
+
* @maxItems 64
|
|
90
|
+
*/
|
|
91
|
+
public: PublicRoute[];
|
|
92
|
+
}
|
|
93
|
+
export interface PublicRoute {
|
|
94
|
+
path: string;
|
|
95
|
+
/**
|
|
96
|
+
* @minItems 1
|
|
97
|
+
* @maxItems 7
|
|
98
|
+
*/
|
|
99
|
+
methods: ('GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS')[];
|
|
100
|
+
mode: 'application_authorized';
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"IntegrationConnectionKind": { "type": "string", "enum": ["saas", "database"] },
|
|
26
26
|
"IntegrationOperation": {
|
|
27
27
|
"type": "string",
|
|
28
|
-
"enum": ["slack.channel.history", "slack.message.post", "gmail.thread.list", "gmail.message.read", "warehouse.view.read"],
|
|
29
|
-
"description": "Closed operation set. Extending it is a contracts change."
|
|
28
|
+
"enum": ["slack.channel.history", "slack.message.post", "gmail.thread.list", "gmail.message.read", "gmail.message.send", "warehouse.view.read"],
|
|
29
|
+
"description": "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."
|
|
30
30
|
},
|
|
31
31
|
"IntegrationIdentityMode": {
|
|
32
32
|
"type": "string",
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
"slack.message.post": { "$ref": "#/definitions/AppModeChannelOperation" },
|
|
111
111
|
"gmail.thread.list": { "$ref": "#/definitions/UserModeChannelOperation" },
|
|
112
112
|
"gmail.message.read": { "$ref": "#/definitions/UserModeChannelOperation" },
|
|
113
|
+
"gmail.message.send": { "$ref": "#/definitions/UserModeChannelOperation" },
|
|
113
114
|
"warehouse.view.read": { "$ref": "#/definitions/AppModeViewOperation" }
|
|
114
115
|
}
|
|
115
116
|
}
|
|
@@ -119,7 +120,7 @@
|
|
|
119
120
|
"else": {
|
|
120
121
|
"properties": {
|
|
121
122
|
"operations": {
|
|
122
|
-
"not": { "anyOf": [{ "required": ["slack.channel.history"] }, { "required": ["slack.message.post"] }, { "required": ["gmail.thread.list"] }, { "required": ["gmail.message.read"] }] }
|
|
123
|
+
"not": { "anyOf": [{ "required": ["slack.channel.history"] }, { "required": ["slack.message.post"] }, { "required": ["gmail.thread.list"] }, { "required": ["gmail.message.read"] }, { "required": ["gmail.message.send"] }] }
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
}
|
|
@@ -140,7 +141,8 @@
|
|
|
140
141
|
"kind": "saas",
|
|
141
142
|
"operations": {
|
|
142
143
|
"gmail.thread.list": { "identity": "user", "resources": ["inbox"] },
|
|
143
|
-
"gmail.message.read": { "identity": "user", "resources": ["inbox"] }
|
|
144
|
+
"gmail.message.read": { "identity": "user", "resources": ["inbox"] },
|
|
145
|
+
"gmail.message.send": { "identity": "user", "resources": ["inbox"] }
|
|
144
146
|
}
|
|
145
147
|
},
|
|
146
148
|
"sales-warehouse": {
|