@opengeni/db 0.28.9 → 0.31.1
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/dist/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +91 -0
- package/dist/browser-identities.d.ts +128 -0
- package/dist/browser-sessions.d.ts +272 -0
- package/dist/browser-state-artifacts.d.ts +17 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +192 -0
- package/dist/chunk-F5FT7BEZ.js +3535 -0
- package/dist/chunk-F5FT7BEZ.js.map +1 -0
- package/dist/chunk-ISI2TIUG.js +639 -0
- package/dist/chunk-ISI2TIUG.js.map +1 -0
- package/dist/chunk-MWDVXQOL.js +11639 -0
- package/dist/chunk-MWDVXQOL.js.map +1 -0
- package/dist/chunk-NBE6CHRI.js +955 -0
- package/dist/chunk-NBE6CHRI.js.map +1 -0
- package/dist/{chunk-VHBFHMPC.js → chunk-YHZXQ7HP.js} +754 -11
- package/dist/chunk-YHZXQ7HP.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +52 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +698 -111
- package/dist/index.js +27573 -10322
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +51 -4
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-features.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +3 -0
- package/dist/interaction-schema.d.ts +5372 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +3 -3
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +18 -6
- package/dist/schema.d.ts +18342 -9099
- package/dist/schema.js +165 -1
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +4 -4
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +1 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/transcription-recordings.d.ts +2 -2
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +216 -0
- package/dist/video-generation.js +66 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/package.json +18 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +1896 -0
- package/src/browser-identities.ts +1168 -0
- package/src/browser-sessions.ts +2005 -0
- package/src/browser-state-artifacts.ts +59 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1663 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +427 -23
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/generated-images.ts +740 -0
- package/src/index.ts +5917 -1182
- package/src/insights.ts +160 -13
- package/src/integration-bindings.ts +455 -0
- package/src/integration-features.ts +859 -0
- package/src/interaction-revisions.ts +40 -0
- package/src/interaction-schema.ts +1303 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +929 -0
- package/src/persistence-errors.ts +4 -7
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +302 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +508 -5
- package/src/schema.ts +5117 -3119
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +8 -5
- package/src/session-queue-commands.ts +187 -14
- package/src/session-realtime-context.ts +2 -2
- package/src/session-realtime-ledger.ts +3 -3
- package/src/session-realtime.ts +7 -7
- package/src/session-tool-call-settlement.ts +9 -0
- package/src/slack-user-link-access.ts +702 -0
- package/src/video-generation.ts +1452 -0
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/dist/chunk-P6CUHXQZ.js +0 -7248
- package/dist/chunk-P6CUHXQZ.js.map +0 -1
- package/dist/chunk-VHBFHMPC.js.map +0 -1
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { stableJson } from "@opengeni/contracts";
|
|
4
|
+
import { and, asc, eq, ne, sql } from "drizzle-orm";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
setSubjectRlsContext,
|
|
8
|
+
withRlsContext,
|
|
9
|
+
withWorkspaceSubjectRls,
|
|
10
|
+
type Database,
|
|
11
|
+
} from "./database";
|
|
12
|
+
import {
|
|
13
|
+
IntegrationFeatureBindingOwnershipConflictError,
|
|
14
|
+
IntegrationFeatureBindingVersionConflictError,
|
|
15
|
+
listIntegrationFeatureBindingOwners,
|
|
16
|
+
removeIntegrationFeatureBindingOwner,
|
|
17
|
+
upsertIntegrationFeatureBinding,
|
|
18
|
+
} from "./integration-bindings";
|
|
19
|
+
import * as schema from "./schema";
|
|
20
|
+
|
|
21
|
+
export type IntegrationFeatureKind =
|
|
22
|
+
| "knowledge_source"
|
|
23
|
+
| "inbound_trigger"
|
|
24
|
+
| "delivery_destination"
|
|
25
|
+
| "identity_link";
|
|
26
|
+
|
|
27
|
+
export type IntegrationFeatureBindingSummary = {
|
|
28
|
+
id: string;
|
|
29
|
+
featureKey: string;
|
|
30
|
+
kind: IntegrationFeatureKind;
|
|
31
|
+
bindingKey: string;
|
|
32
|
+
displayName: string;
|
|
33
|
+
connectionId: string | null;
|
|
34
|
+
status: "active" | "paused" | "needs_attention" | "disabled";
|
|
35
|
+
config: Record<string, unknown>;
|
|
36
|
+
version: number;
|
|
37
|
+
hasCursor: boolean;
|
|
38
|
+
lastSuccessAt: string | null;
|
|
39
|
+
lastErrorCode: string | null;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type IntegrationFeatureDefinitionSummary = {
|
|
45
|
+
featureKey: string;
|
|
46
|
+
kind: IntegrationFeatureKind;
|
|
47
|
+
configSchema: Record<string, unknown>;
|
|
48
|
+
capabilities: Record<string, unknown>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type IntegrationInstanceFeatures = {
|
|
52
|
+
capabilityId: string;
|
|
53
|
+
instanceKey: string;
|
|
54
|
+
providerDomain: string;
|
|
55
|
+
connectionId: string | null;
|
|
56
|
+
features: {
|
|
57
|
+
definition: IntegrationFeatureDefinitionSummary;
|
|
58
|
+
binding: IntegrationFeatureBindingSummary | null;
|
|
59
|
+
}[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type IntegrationFeatureMutationResult = {
|
|
63
|
+
capabilityId: string;
|
|
64
|
+
instanceKey: string;
|
|
65
|
+
featureKey: string;
|
|
66
|
+
status: "configured" | "paused" | "active";
|
|
67
|
+
binding: IntegrationFeatureBindingSummary;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type IntegrationFeatureRemovalResult = {
|
|
71
|
+
capabilityId: string;
|
|
72
|
+
instanceKey: string;
|
|
73
|
+
featureKey: string;
|
|
74
|
+
status: "not_configured" | "removed" | "retained_by_other_owners";
|
|
75
|
+
binding: IntegrationFeatureBindingSummary | null;
|
|
76
|
+
remainingOwners: {
|
|
77
|
+
kind: "direct" | "plugin" | "pack" | "migration";
|
|
78
|
+
id: string;
|
|
79
|
+
removable: boolean;
|
|
80
|
+
}[];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export class IntegrationFeatureNotFoundError extends Error {
|
|
84
|
+
readonly name = "IntegrationFeatureNotFoundError";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class IntegrationFeatureConnectionError extends Error {
|
|
88
|
+
readonly name = "IntegrationFeatureConnectionError";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class IntegrationFeatureConfigError extends Error {
|
|
92
|
+
readonly name = "IntegrationFeatureConfigError";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class IntegrationFeatureOperationIdempotencyError extends Error {
|
|
96
|
+
readonly name = "IntegrationFeatureOperationIdempotencyError";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type IntegrationInstanceContext = {
|
|
100
|
+
accountId: string;
|
|
101
|
+
integrationFacetInstallationId: string;
|
|
102
|
+
integrationFacetId: string;
|
|
103
|
+
providerDomain: string;
|
|
104
|
+
connectionId: string | null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export async function listIntegrationInstanceFeatures(
|
|
108
|
+
db: Database,
|
|
109
|
+
workspaceId: string,
|
|
110
|
+
subjectId: string,
|
|
111
|
+
capabilityId: string,
|
|
112
|
+
instanceKey: string,
|
|
113
|
+
): Promise<IntegrationInstanceFeatures> {
|
|
114
|
+
return await withWorkspaceSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
115
|
+
const context = await loadIntegrationInstanceContext(
|
|
116
|
+
scopedDb,
|
|
117
|
+
workspaceId,
|
|
118
|
+
subjectId,
|
|
119
|
+
capabilityId,
|
|
120
|
+
instanceKey,
|
|
121
|
+
);
|
|
122
|
+
if (!context) throw new IntegrationFeatureNotFoundError("Integration instance not found");
|
|
123
|
+
const rows = await scopedDb
|
|
124
|
+
.select({
|
|
125
|
+
featureKey: schema.integrationFeatureFacets.featureKey,
|
|
126
|
+
kind: schema.integrationFeatureFacets.kind,
|
|
127
|
+
configSchema: schema.integrationFeatureFacets.configSchema,
|
|
128
|
+
capabilities: schema.integrationFeatureFacets.capabilities,
|
|
129
|
+
bindingId: schema.integrationFeatureBindings.id,
|
|
130
|
+
bindingKey: schema.integrationFeatureBindings.bindingKey,
|
|
131
|
+
displayName: schema.integrationFeatureBindings.displayName,
|
|
132
|
+
connectionId: schema.integrationFeatureBindings.connectionId,
|
|
133
|
+
status: schema.integrationFeatureBindings.status,
|
|
134
|
+
config: schema.integrationFeatureBindings.config,
|
|
135
|
+
cursor: schema.integrationFeatureBindings.cursor,
|
|
136
|
+
version: schema.integrationFeatureBindings.version,
|
|
137
|
+
lastSuccessAt: schema.integrationFeatureBindings.lastSuccessAt,
|
|
138
|
+
lastErrorCode: schema.integrationFeatureBindings.lastErrorCode,
|
|
139
|
+
createdAt: schema.integrationFeatureBindings.createdAt,
|
|
140
|
+
updatedAt: schema.integrationFeatureBindings.updatedAt,
|
|
141
|
+
})
|
|
142
|
+
.from(schema.integrationFeatureFacets)
|
|
143
|
+
.leftJoin(
|
|
144
|
+
schema.integrationFeatureBindings,
|
|
145
|
+
and(
|
|
146
|
+
eq(schema.integrationFeatureBindings.featureFacetId, schema.integrationFeatureFacets.id),
|
|
147
|
+
eq(
|
|
148
|
+
schema.integrationFeatureBindings.integrationFacetInstallationId,
|
|
149
|
+
context.integrationFacetInstallationId,
|
|
150
|
+
),
|
|
151
|
+
eq(schema.integrationFeatureBindings.bindingKey, instanceKey),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
.where(
|
|
155
|
+
and(
|
|
156
|
+
eq(schema.integrationFeatureFacets.integrationFacetId, context.integrationFacetId),
|
|
157
|
+
ne(schema.integrationFeatureFacets.kind, "tools"),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
.orderBy(
|
|
161
|
+
asc(schema.integrationFeatureFacets.kind),
|
|
162
|
+
asc(schema.integrationFeatureFacets.featureKey),
|
|
163
|
+
);
|
|
164
|
+
return {
|
|
165
|
+
capabilityId,
|
|
166
|
+
instanceKey,
|
|
167
|
+
providerDomain: context.providerDomain,
|
|
168
|
+
connectionId: context.connectionId,
|
|
169
|
+
features: rows.map((row) => ({
|
|
170
|
+
definition: {
|
|
171
|
+
featureKey: row.featureKey,
|
|
172
|
+
kind: featureKind(row.kind),
|
|
173
|
+
configSchema: row.configSchema,
|
|
174
|
+
capabilities: row.capabilities,
|
|
175
|
+
},
|
|
176
|
+
binding: row.bindingId
|
|
177
|
+
? bindingSummary({
|
|
178
|
+
id: row.bindingId,
|
|
179
|
+
featureKey: row.featureKey,
|
|
180
|
+
kind: row.kind,
|
|
181
|
+
bindingKey: row.bindingKey!,
|
|
182
|
+
displayName: row.displayName!,
|
|
183
|
+
connectionId: row.connectionId ?? null,
|
|
184
|
+
status: row.status!,
|
|
185
|
+
config: row.config!,
|
|
186
|
+
cursor: row.cursor!,
|
|
187
|
+
version: row.version!,
|
|
188
|
+
lastSuccessAt: row.lastSuccessAt ?? null,
|
|
189
|
+
lastErrorCode: row.lastErrorCode ?? null,
|
|
190
|
+
createdAt: row.createdAt!,
|
|
191
|
+
updatedAt: row.updatedAt!,
|
|
192
|
+
})
|
|
193
|
+
: null,
|
|
194
|
+
})),
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function configureIntegrationFeature(
|
|
200
|
+
db: Database,
|
|
201
|
+
input: {
|
|
202
|
+
accountId: string;
|
|
203
|
+
workspaceId: string;
|
|
204
|
+
subjectId: string;
|
|
205
|
+
capabilityId: string;
|
|
206
|
+
instanceKey: string;
|
|
207
|
+
featureKey: string;
|
|
208
|
+
displayName: string;
|
|
209
|
+
config: Record<string, unknown>;
|
|
210
|
+
expectedVersion?: number;
|
|
211
|
+
idempotencyKey: string;
|
|
212
|
+
},
|
|
213
|
+
): Promise<IntegrationFeatureMutationResult> {
|
|
214
|
+
const requestDigest = sha256(
|
|
215
|
+
stableJson({
|
|
216
|
+
action: "configure",
|
|
217
|
+
capabilityId: input.capabilityId,
|
|
218
|
+
instanceKey: input.instanceKey,
|
|
219
|
+
featureKey: input.featureKey,
|
|
220
|
+
displayName: input.displayName,
|
|
221
|
+
config: input.config,
|
|
222
|
+
expectedVersion: input.expectedVersion ?? null,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
return await withFeatureOperation(db, input, requestDigest, "configure", async (tx) => {
|
|
226
|
+
const context = await requireMutationContext(tx, input);
|
|
227
|
+
const definition = await requireFeatureDefinition(tx, context, input.featureKey);
|
|
228
|
+
assertFeatureConfig(input.config, definition.configSchema);
|
|
229
|
+
await requireFeatureConnection(tx, input, context, definition.capabilities);
|
|
230
|
+
const result = await upsertIntegrationFeatureBinding(tx, {
|
|
231
|
+
accountId: input.accountId,
|
|
232
|
+
workspaceId: input.workspaceId,
|
|
233
|
+
integrationFacetInstallationId: context.integrationFacetInstallationId,
|
|
234
|
+
featureFacetId: definition.id,
|
|
235
|
+
bindingKey: input.instanceKey,
|
|
236
|
+
displayName: input.displayName,
|
|
237
|
+
runtimeKey: null,
|
|
238
|
+
connectionId: context.connectionId,
|
|
239
|
+
config: input.config,
|
|
240
|
+
createdBySubjectId: input.subjectId,
|
|
241
|
+
owner: featureOwner(input.capabilityId, input.instanceKey, input.featureKey),
|
|
242
|
+
...(input.expectedVersion !== undefined ? { expectedVersion: input.expectedVersion } : {}),
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
capabilityId: input.capabilityId,
|
|
246
|
+
instanceKey: input.instanceKey,
|
|
247
|
+
featureKey: input.featureKey,
|
|
248
|
+
status: "configured",
|
|
249
|
+
binding: bindingSummary({
|
|
250
|
+
...result.row,
|
|
251
|
+
featureKey: definition.featureKey,
|
|
252
|
+
kind: definition.kind,
|
|
253
|
+
}),
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function setIntegrationFeatureLifecycle(
|
|
259
|
+
db: Database,
|
|
260
|
+
input: {
|
|
261
|
+
accountId: string;
|
|
262
|
+
workspaceId: string;
|
|
263
|
+
subjectId: string;
|
|
264
|
+
capabilityId: string;
|
|
265
|
+
instanceKey: string;
|
|
266
|
+
featureKey: string;
|
|
267
|
+
action: "pause" | "resume";
|
|
268
|
+
expectedVersion: number;
|
|
269
|
+
idempotencyKey: string;
|
|
270
|
+
},
|
|
271
|
+
): Promise<IntegrationFeatureMutationResult> {
|
|
272
|
+
const requestDigest = sha256(
|
|
273
|
+
stableJson({
|
|
274
|
+
...input,
|
|
275
|
+
accountId: undefined,
|
|
276
|
+
workspaceId: undefined,
|
|
277
|
+
subjectId: undefined,
|
|
278
|
+
idempotencyKey: undefined,
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
return await withFeatureOperation(db, input, requestDigest, "update", async (tx) => {
|
|
282
|
+
const context = await requireMutationContext(tx, input);
|
|
283
|
+
const definition = await requireFeatureDefinition(tx, context, input.featureKey);
|
|
284
|
+
const binding = await loadFeatureBinding(tx, context, input.instanceKey, definition.id, true);
|
|
285
|
+
if (!binding || binding.status === "disabled") {
|
|
286
|
+
throw new IntegrationFeatureNotFoundError("Integration feature is not configured");
|
|
287
|
+
}
|
|
288
|
+
if (binding.version !== input.expectedVersion) {
|
|
289
|
+
throw new IntegrationFeatureBindingVersionConflictError(
|
|
290
|
+
input.instanceKey,
|
|
291
|
+
input.expectedVersion,
|
|
292
|
+
binding.version,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
await assertDirectFeatureOwnership(tx, binding.id, input);
|
|
296
|
+
const targetStatus = input.action === "pause" ? "paused" : "active";
|
|
297
|
+
let row = binding;
|
|
298
|
+
if (binding.status !== targetStatus) {
|
|
299
|
+
const [updated] = await tx
|
|
300
|
+
.update(schema.integrationFeatureBindings)
|
|
301
|
+
.set({
|
|
302
|
+
status: targetStatus,
|
|
303
|
+
version: binding.version + 1,
|
|
304
|
+
...(input.action === "resume" ? { lastErrorCode: null } : {}),
|
|
305
|
+
updatedAt: new Date(),
|
|
306
|
+
})
|
|
307
|
+
.where(eq(schema.integrationFeatureBindings.id, binding.id))
|
|
308
|
+
.returning();
|
|
309
|
+
if (!updated) throw new Error("Failed to update Integration feature lifecycle");
|
|
310
|
+
row = updated;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
capabilityId: input.capabilityId,
|
|
314
|
+
instanceKey: input.instanceKey,
|
|
315
|
+
featureKey: input.featureKey,
|
|
316
|
+
status: input.action === "pause" ? "paused" : "active",
|
|
317
|
+
binding: bindingSummary({
|
|
318
|
+
...row,
|
|
319
|
+
featureKey: definition.featureKey,
|
|
320
|
+
kind: definition.kind,
|
|
321
|
+
}),
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function removeIntegrationFeature(
|
|
327
|
+
db: Database,
|
|
328
|
+
input: {
|
|
329
|
+
accountId: string;
|
|
330
|
+
workspaceId: string;
|
|
331
|
+
subjectId: string;
|
|
332
|
+
capabilityId: string;
|
|
333
|
+
instanceKey: string;
|
|
334
|
+
featureKey: string;
|
|
335
|
+
expectedVersion: number;
|
|
336
|
+
idempotencyKey: string;
|
|
337
|
+
},
|
|
338
|
+
): Promise<IntegrationFeatureRemovalResult> {
|
|
339
|
+
const requestDigest = sha256(
|
|
340
|
+
stableJson({
|
|
341
|
+
action: "remove",
|
|
342
|
+
capabilityId: input.capabilityId,
|
|
343
|
+
instanceKey: input.instanceKey,
|
|
344
|
+
featureKey: input.featureKey,
|
|
345
|
+
expectedVersion: input.expectedVersion,
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
return await withFeatureOperation(db, input, requestDigest, "disconnect", async (tx) => {
|
|
349
|
+
const context = await requireMutationContext(tx, input);
|
|
350
|
+
const definition = await requireFeatureDefinition(tx, context, input.featureKey);
|
|
351
|
+
const binding = await loadFeatureBinding(tx, context, input.instanceKey, definition.id, true);
|
|
352
|
+
const owner = featureOwner(input.capabilityId, input.instanceKey, input.featureKey);
|
|
353
|
+
if (!binding) {
|
|
354
|
+
return {
|
|
355
|
+
capabilityId: input.capabilityId,
|
|
356
|
+
instanceKey: input.instanceKey,
|
|
357
|
+
featureKey: input.featureKey,
|
|
358
|
+
status: "not_configured",
|
|
359
|
+
binding: null,
|
|
360
|
+
remainingOwners: [],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (binding.version !== input.expectedVersion) {
|
|
364
|
+
throw new IntegrationFeatureBindingVersionConflictError(
|
|
365
|
+
input.instanceKey,
|
|
366
|
+
input.expectedVersion,
|
|
367
|
+
binding.version,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const owners = await listIntegrationFeatureBindingOwners(tx, binding.id);
|
|
371
|
+
if (!owners.some((candidate) => candidate.kind === owner.kind && candidate.id === owner.id)) {
|
|
372
|
+
return {
|
|
373
|
+
capabilityId: input.capabilityId,
|
|
374
|
+
instanceKey: input.instanceKey,
|
|
375
|
+
featureKey: input.featureKey,
|
|
376
|
+
status: "not_configured",
|
|
377
|
+
binding: bindingSummary({
|
|
378
|
+
...binding,
|
|
379
|
+
featureKey: definition.featureKey,
|
|
380
|
+
kind: definition.kind,
|
|
381
|
+
}),
|
|
382
|
+
remainingOwners: owners,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const removed = await removeIntegrationFeatureBindingOwner(tx, {
|
|
386
|
+
workspaceId: input.workspaceId,
|
|
387
|
+
bindingId: binding.id,
|
|
388
|
+
owner,
|
|
389
|
+
expectedVersion: input.expectedVersion,
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
capabilityId: input.capabilityId,
|
|
393
|
+
instanceKey: input.instanceKey,
|
|
394
|
+
featureKey: input.featureKey,
|
|
395
|
+
status: removed.remainingOwners.length > 0 ? "retained_by_other_owners" : "removed",
|
|
396
|
+
binding: removed.binding
|
|
397
|
+
? bindingSummary({
|
|
398
|
+
...removed.binding,
|
|
399
|
+
featureKey: definition.featureKey,
|
|
400
|
+
kind: definition.kind,
|
|
401
|
+
})
|
|
402
|
+
: null,
|
|
403
|
+
remainingOwners: removed.remainingOwners,
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function withFeatureOperation<T extends Record<string, unknown>>(
|
|
409
|
+
db: Database,
|
|
410
|
+
input: {
|
|
411
|
+
accountId: string;
|
|
412
|
+
workspaceId: string;
|
|
413
|
+
subjectId: string;
|
|
414
|
+
capabilityId: string;
|
|
415
|
+
instanceKey: string;
|
|
416
|
+
featureKey: string;
|
|
417
|
+
idempotencyKey: string;
|
|
418
|
+
},
|
|
419
|
+
requestDigest: string,
|
|
420
|
+
kind: string,
|
|
421
|
+
execute: (tx: Database) => Promise<T>,
|
|
422
|
+
): Promise<T> {
|
|
423
|
+
return await withRlsContext(
|
|
424
|
+
db,
|
|
425
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
426
|
+
async (scopedDb) => {
|
|
427
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
428
|
+
return await scopedDb.transaction(async (txRaw) => {
|
|
429
|
+
const tx = txRaw as unknown as Database;
|
|
430
|
+
await tx.execute(
|
|
431
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`capability-operation:${input.workspaceId}:${input.idempotencyKey}`}, 0))`,
|
|
432
|
+
);
|
|
433
|
+
const [existing] = await tx
|
|
434
|
+
.select()
|
|
435
|
+
.from(schema.capabilityOperations)
|
|
436
|
+
.where(
|
|
437
|
+
and(
|
|
438
|
+
eq(schema.capabilityOperations.workspaceId, input.workspaceId),
|
|
439
|
+
eq(schema.capabilityOperations.idempotencyKey, input.idempotencyKey),
|
|
440
|
+
),
|
|
441
|
+
)
|
|
442
|
+
.for("update")
|
|
443
|
+
.limit(1);
|
|
444
|
+
if (existing) {
|
|
445
|
+
if (existing.requestDigest !== requestDigest) {
|
|
446
|
+
throw new IntegrationFeatureOperationIdempotencyError(
|
|
447
|
+
"Integration feature idempotency key was reused",
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (existing.status === "completed" && existing.result) return existing.result as T;
|
|
451
|
+
throw new IntegrationFeatureOperationIdempotencyError(
|
|
452
|
+
"Integration feature operation is already in progress",
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
const result = await execute(tx);
|
|
456
|
+
await tx.insert(schema.capabilityOperations).values({
|
|
457
|
+
accountId: input.accountId,
|
|
458
|
+
workspaceId: input.workspaceId,
|
|
459
|
+
idempotencyKey: input.idempotencyKey,
|
|
460
|
+
requestDigest,
|
|
461
|
+
kind,
|
|
462
|
+
targetKind: "facet_binding",
|
|
463
|
+
targetId: `facet:${sha256(
|
|
464
|
+
`${input.capabilityId}\0${input.instanceKey}\0${input.featureKey}`,
|
|
465
|
+
)}`,
|
|
466
|
+
status: "completed",
|
|
467
|
+
phase: "completed",
|
|
468
|
+
result,
|
|
469
|
+
createdBySubjectId: input.subjectId,
|
|
470
|
+
completedAt: new Date(),
|
|
471
|
+
});
|
|
472
|
+
return result;
|
|
473
|
+
});
|
|
474
|
+
},
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function requireMutationContext(
|
|
479
|
+
db: Database,
|
|
480
|
+
input: {
|
|
481
|
+
workspaceId: string;
|
|
482
|
+
subjectId: string;
|
|
483
|
+
capabilityId: string;
|
|
484
|
+
instanceKey: string;
|
|
485
|
+
},
|
|
486
|
+
): Promise<IntegrationInstanceContext> {
|
|
487
|
+
const context = await loadIntegrationInstanceContext(
|
|
488
|
+
db,
|
|
489
|
+
input.workspaceId,
|
|
490
|
+
input.subjectId,
|
|
491
|
+
input.capabilityId,
|
|
492
|
+
input.instanceKey,
|
|
493
|
+
true,
|
|
494
|
+
);
|
|
495
|
+
if (!context) throw new IntegrationFeatureNotFoundError("Integration instance not found");
|
|
496
|
+
return context;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function loadIntegrationInstanceContext(
|
|
500
|
+
db: Database,
|
|
501
|
+
workspaceId: string,
|
|
502
|
+
subjectId: string,
|
|
503
|
+
capabilityId: string,
|
|
504
|
+
instanceKey: string,
|
|
505
|
+
lock = false,
|
|
506
|
+
): Promise<IntegrationInstanceContext | null> {
|
|
507
|
+
let query = db
|
|
508
|
+
.select({
|
|
509
|
+
accountId: schema.integrationFeatureBindings.accountId,
|
|
510
|
+
integrationFacetInstallationId:
|
|
511
|
+
schema.integrationFeatureBindings.integrationFacetInstallationId,
|
|
512
|
+
integrationFacetId: schema.integrationFeatureFacets.integrationFacetId,
|
|
513
|
+
providerDomain: schema.capabilityIntegrationFacets.providerDomain,
|
|
514
|
+
connectionId: schema.integrationFeatureBindings.connectionId,
|
|
515
|
+
})
|
|
516
|
+
.from(schema.integrationFeatureBindings)
|
|
517
|
+
.innerJoin(
|
|
518
|
+
schema.integrationFeatureFacets,
|
|
519
|
+
eq(schema.integrationFeatureFacets.id, schema.integrationFeatureBindings.featureFacetId),
|
|
520
|
+
)
|
|
521
|
+
.innerJoin(
|
|
522
|
+
schema.capabilityIntegrationFacets,
|
|
523
|
+
eq(
|
|
524
|
+
schema.capabilityIntegrationFacets.facetId,
|
|
525
|
+
schema.integrationFeatureFacets.integrationFacetId,
|
|
526
|
+
),
|
|
527
|
+
)
|
|
528
|
+
.innerJoin(
|
|
529
|
+
schema.capabilityFacetInstallations,
|
|
530
|
+
eq(
|
|
531
|
+
schema.capabilityFacetInstallations.id,
|
|
532
|
+
schema.integrationFeatureBindings.integrationFacetInstallationId,
|
|
533
|
+
),
|
|
534
|
+
)
|
|
535
|
+
.innerJoin(
|
|
536
|
+
schema.capabilityPluginInstallations,
|
|
537
|
+
eq(
|
|
538
|
+
schema.capabilityPluginInstallations.id,
|
|
539
|
+
schema.capabilityFacetInstallations.pluginInstallationId,
|
|
540
|
+
),
|
|
541
|
+
)
|
|
542
|
+
.innerJoin(
|
|
543
|
+
schema.capabilityPluginVersions,
|
|
544
|
+
eq(schema.capabilityPluginVersions.id, schema.capabilityPluginInstallations.pluginVersionId),
|
|
545
|
+
)
|
|
546
|
+
.where(
|
|
547
|
+
and(
|
|
548
|
+
eq(schema.integrationFeatureBindings.workspaceId, workspaceId),
|
|
549
|
+
eq(schema.integrationFeatureBindings.bindingKey, instanceKey),
|
|
550
|
+
eq(schema.integrationFeatureBindings.status, "active"),
|
|
551
|
+
eq(schema.integrationFeatureFacets.kind, "tools"),
|
|
552
|
+
eq(schema.capabilityPluginInstallations.status, "active"),
|
|
553
|
+
sql`${schema.capabilityPluginVersions.manifest} ->> 'capabilityId' = ${capabilityId}`,
|
|
554
|
+
sql`(
|
|
555
|
+
${schema.integrationFeatureBindings.connectionId} is null
|
|
556
|
+
or exists (
|
|
557
|
+
select 1 from ${schema.connections} connection
|
|
558
|
+
where connection.id = ${schema.integrationFeatureBindings.connectionId}
|
|
559
|
+
and (connection.subject_id is null or connection.subject_id = ${subjectId})
|
|
560
|
+
)
|
|
561
|
+
)`,
|
|
562
|
+
),
|
|
563
|
+
)
|
|
564
|
+
.limit(2);
|
|
565
|
+
if (lock) query = query.for("update") as typeof query;
|
|
566
|
+
const rows = await query;
|
|
567
|
+
if (rows.length === 0) return null;
|
|
568
|
+
if (rows.length !== 1) throw new Error(`API Integration ${capabilityId} instance is ambiguous`);
|
|
569
|
+
return rows[0]!;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function requireFeatureDefinition(
|
|
573
|
+
db: Database,
|
|
574
|
+
context: IntegrationInstanceContext,
|
|
575
|
+
featureKey: string,
|
|
576
|
+
): Promise<typeof schema.integrationFeatureFacets.$inferSelect> {
|
|
577
|
+
const [definition] = await db
|
|
578
|
+
.select()
|
|
579
|
+
.from(schema.integrationFeatureFacets)
|
|
580
|
+
.where(
|
|
581
|
+
and(
|
|
582
|
+
eq(schema.integrationFeatureFacets.integrationFacetId, context.integrationFacetId),
|
|
583
|
+
eq(schema.integrationFeatureFacets.featureKey, featureKey),
|
|
584
|
+
ne(schema.integrationFeatureFacets.kind, "tools"),
|
|
585
|
+
),
|
|
586
|
+
)
|
|
587
|
+
.limit(1);
|
|
588
|
+
if (!definition) throw new IntegrationFeatureNotFoundError("Integration feature not found");
|
|
589
|
+
featureKind(definition.kind);
|
|
590
|
+
return definition;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function requireFeatureConnection(
|
|
594
|
+
db: Database,
|
|
595
|
+
input: { workspaceId: string; subjectId: string },
|
|
596
|
+
context: IntegrationInstanceContext,
|
|
597
|
+
capabilities: Record<string, unknown>,
|
|
598
|
+
): Promise<void> {
|
|
599
|
+
if (!context.connectionId) {
|
|
600
|
+
if (capabilities.connectionRequired === true) {
|
|
601
|
+
throw new IntegrationFeatureConnectionError(
|
|
602
|
+
"Integration feature requires the instance Connection",
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const [connection] = await db
|
|
608
|
+
.select({
|
|
609
|
+
providerDomain: schema.connections.providerDomain,
|
|
610
|
+
subjectId: schema.connections.subjectId,
|
|
611
|
+
status: schema.connections.status,
|
|
612
|
+
})
|
|
613
|
+
.from(schema.connections)
|
|
614
|
+
.where(
|
|
615
|
+
and(
|
|
616
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
617
|
+
eq(schema.connections.id, context.connectionId),
|
|
618
|
+
),
|
|
619
|
+
)
|
|
620
|
+
.limit(1);
|
|
621
|
+
if (
|
|
622
|
+
!connection ||
|
|
623
|
+
connection.status !== "active" ||
|
|
624
|
+
(connection.subjectId !== null && connection.subjectId !== input.subjectId) ||
|
|
625
|
+
connection.providerDomain.toLowerCase() !== context.providerDomain.toLowerCase()
|
|
626
|
+
) {
|
|
627
|
+
throw new IntegrationFeatureConnectionError(
|
|
628
|
+
"Integration feature Connection is unavailable or does not match the provider",
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function loadFeatureBinding(
|
|
634
|
+
db: Database,
|
|
635
|
+
context: IntegrationInstanceContext,
|
|
636
|
+
instanceKey: string,
|
|
637
|
+
featureFacetId: string,
|
|
638
|
+
lock = false,
|
|
639
|
+
): Promise<typeof schema.integrationFeatureBindings.$inferSelect | null> {
|
|
640
|
+
let query = db
|
|
641
|
+
.select()
|
|
642
|
+
.from(schema.integrationFeatureBindings)
|
|
643
|
+
.where(
|
|
644
|
+
and(
|
|
645
|
+
eq(
|
|
646
|
+
schema.integrationFeatureBindings.integrationFacetInstallationId,
|
|
647
|
+
context.integrationFacetInstallationId,
|
|
648
|
+
),
|
|
649
|
+
eq(schema.integrationFeatureBindings.featureFacetId, featureFacetId),
|
|
650
|
+
eq(schema.integrationFeatureBindings.bindingKey, instanceKey),
|
|
651
|
+
),
|
|
652
|
+
)
|
|
653
|
+
.limit(1);
|
|
654
|
+
if (lock) query = query.for("update") as typeof query;
|
|
655
|
+
const [binding] = await query;
|
|
656
|
+
return binding ?? null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function assertDirectFeatureOwnership(
|
|
660
|
+
db: Database,
|
|
661
|
+
bindingId: string,
|
|
662
|
+
input: { capabilityId: string; instanceKey: string; featureKey: string },
|
|
663
|
+
): Promise<void> {
|
|
664
|
+
const expected = featureOwner(input.capabilityId, input.instanceKey, input.featureKey);
|
|
665
|
+
const owners = await listIntegrationFeatureBindingOwners(db, bindingId);
|
|
666
|
+
if (!owners.some((owner) => owner.kind === expected.kind && owner.id === expected.id)) {
|
|
667
|
+
throw new IntegrationFeatureBindingOwnershipConflictError(
|
|
668
|
+
"Integration feature is not directly owned",
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (owners.some((owner) => owner.kind !== expected.kind || owner.id !== expected.id)) {
|
|
672
|
+
throw new IntegrationFeatureBindingOwnershipConflictError(
|
|
673
|
+
"Integration feature is shared by another owner and cannot change lifecycle in place",
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function featureOwner(capabilityId: string, instanceKey: string, featureKey: string) {
|
|
679
|
+
return {
|
|
680
|
+
kind: "direct" as const,
|
|
681
|
+
id: `feature:${sha256(`${capabilityId}\0${instanceKey}\0${featureKey}`)}`,
|
|
682
|
+
removable: true,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
type BindingSummaryRow = Pick<
|
|
687
|
+
typeof schema.integrationFeatureBindings.$inferSelect,
|
|
688
|
+
| "id"
|
|
689
|
+
| "bindingKey"
|
|
690
|
+
| "displayName"
|
|
691
|
+
| "connectionId"
|
|
692
|
+
| "status"
|
|
693
|
+
| "config"
|
|
694
|
+
| "cursor"
|
|
695
|
+
| "version"
|
|
696
|
+
| "lastSuccessAt"
|
|
697
|
+
| "lastErrorCode"
|
|
698
|
+
| "createdAt"
|
|
699
|
+
| "updatedAt"
|
|
700
|
+
> & {
|
|
701
|
+
featureKey: string;
|
|
702
|
+
kind: string;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
function bindingSummary(row: BindingSummaryRow): IntegrationFeatureBindingSummary {
|
|
706
|
+
return {
|
|
707
|
+
id: row.id,
|
|
708
|
+
featureKey: row.featureKey,
|
|
709
|
+
kind: featureKind(row.kind),
|
|
710
|
+
bindingKey: row.bindingKey,
|
|
711
|
+
displayName: row.displayName,
|
|
712
|
+
connectionId: row.connectionId,
|
|
713
|
+
status: bindingStatus(row.status),
|
|
714
|
+
config: row.config,
|
|
715
|
+
version: row.version,
|
|
716
|
+
hasCursor: Object.keys(row.cursor).length > 0,
|
|
717
|
+
lastSuccessAt: row.lastSuccessAt?.toISOString() ?? null,
|
|
718
|
+
lastErrorCode: row.lastErrorCode,
|
|
719
|
+
createdAt: row.createdAt.toISOString(),
|
|
720
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function featureKind(value: string): IntegrationFeatureKind {
|
|
725
|
+
if (
|
|
726
|
+
value !== "knowledge_source" &&
|
|
727
|
+
value !== "inbound_trigger" &&
|
|
728
|
+
value !== "delivery_destination" &&
|
|
729
|
+
value !== "identity_link"
|
|
730
|
+
) {
|
|
731
|
+
throw new Error(`Unknown Integration feature kind: ${value}`);
|
|
732
|
+
}
|
|
733
|
+
return value;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function bindingStatus(value: string): "active" | "paused" | "needs_attention" | "disabled" {
|
|
737
|
+
if (
|
|
738
|
+
value !== "active" &&
|
|
739
|
+
value !== "paused" &&
|
|
740
|
+
value !== "needs_attention" &&
|
|
741
|
+
value !== "disabled"
|
|
742
|
+
) {
|
|
743
|
+
throw new Error(`Unknown Integration feature status: ${value}`);
|
|
744
|
+
}
|
|
745
|
+
return value;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function assertFeatureConfig(
|
|
749
|
+
config: Record<string, unknown>,
|
|
750
|
+
schemaValue: Record<string, unknown>,
|
|
751
|
+
): void {
|
|
752
|
+
const serialized = stableJson(config);
|
|
753
|
+
if (Buffer.byteLength(serialized, "utf8") > 131_072) {
|
|
754
|
+
throw new IntegrationFeatureConfigError("Integration feature config exceeds 131072 bytes");
|
|
755
|
+
}
|
|
756
|
+
validateSchemaValue(config, schemaValue, "config", 0);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function validateSchemaValue(
|
|
760
|
+
value: unknown,
|
|
761
|
+
schemaValue: Record<string, unknown>,
|
|
762
|
+
path: string,
|
|
763
|
+
depth: number,
|
|
764
|
+
): void {
|
|
765
|
+
if (depth > 8) throw new IntegrationFeatureConfigError(`${path} exceeds supported depth`);
|
|
766
|
+
const type = schemaValue.type;
|
|
767
|
+
if (
|
|
768
|
+
Array.isArray(schemaValue.enum) &&
|
|
769
|
+
!schemaValue.enum.some((item) => stableJson(item) === stableJson(value))
|
|
770
|
+
) {
|
|
771
|
+
throw new IntegrationFeatureConfigError(`${path} is not an allowed value`);
|
|
772
|
+
}
|
|
773
|
+
if (type === "object") {
|
|
774
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
775
|
+
throw new IntegrationFeatureConfigError(`${path} must be an object`);
|
|
776
|
+
}
|
|
777
|
+
const object = value as Record<string, unknown>;
|
|
778
|
+
const properties = objectValue(schemaValue.properties);
|
|
779
|
+
const required = Array.isArray(schemaValue.required)
|
|
780
|
+
? schemaValue.required.filter((entry): entry is string => typeof entry === "string")
|
|
781
|
+
: [];
|
|
782
|
+
for (const key of required) {
|
|
783
|
+
if (!(key in object)) throw new IntegrationFeatureConfigError(`${path}.${key} is required`);
|
|
784
|
+
}
|
|
785
|
+
if (schemaValue.additionalProperties === false) {
|
|
786
|
+
const unknown = Object.keys(object).find((key) => !(key in properties));
|
|
787
|
+
if (unknown) throw new IntegrationFeatureConfigError(`${path}.${unknown} is not supported`);
|
|
788
|
+
}
|
|
789
|
+
for (const [key, child] of Object.entries(object)) {
|
|
790
|
+
const childSchema = objectValue(properties[key]);
|
|
791
|
+
if (Object.keys(childSchema).length > 0) {
|
|
792
|
+
validateSchemaValue(child, childSchema, `${path}.${key}`, depth + 1);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (type === "array") {
|
|
798
|
+
if (!Array.isArray(value)) {
|
|
799
|
+
throw new IntegrationFeatureConfigError(`${path} must be an array`);
|
|
800
|
+
}
|
|
801
|
+
if (typeof schemaValue.minItems === "number" && value.length < schemaValue.minItems) {
|
|
802
|
+
throw new IntegrationFeatureConfigError(`${path} has too few items`);
|
|
803
|
+
}
|
|
804
|
+
if (typeof schemaValue.maxItems === "number" && value.length > schemaValue.maxItems) {
|
|
805
|
+
throw new IntegrationFeatureConfigError(`${path} has too many items`);
|
|
806
|
+
}
|
|
807
|
+
const itemSchema = objectValue(schemaValue.items);
|
|
808
|
+
if (Object.keys(itemSchema).length > 0) {
|
|
809
|
+
value.forEach((item, index) =>
|
|
810
|
+
validateSchemaValue(item, itemSchema, `${path}[${index}]`, depth + 1),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (type === "string") {
|
|
816
|
+
if (typeof value !== "string")
|
|
817
|
+
throw new IntegrationFeatureConfigError(`${path} must be a string`);
|
|
818
|
+
if (typeof schemaValue.minLength === "number" && value.length < schemaValue.minLength) {
|
|
819
|
+
throw new IntegrationFeatureConfigError(`${path} is too short`);
|
|
820
|
+
}
|
|
821
|
+
if (typeof schemaValue.maxLength === "number" && value.length > schemaValue.maxLength) {
|
|
822
|
+
throw new IntegrationFeatureConfigError(`${path} is too long`);
|
|
823
|
+
}
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (type === "boolean") {
|
|
827
|
+
if (typeof value !== "boolean")
|
|
828
|
+
throw new IntegrationFeatureConfigError(`${path} must be a boolean`);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (type === "integer" || type === "number") {
|
|
832
|
+
if (
|
|
833
|
+
typeof value !== "number" ||
|
|
834
|
+
!Number.isFinite(value) ||
|
|
835
|
+
(type === "integer" && !Number.isInteger(value))
|
|
836
|
+
) {
|
|
837
|
+
throw new IntegrationFeatureConfigError(
|
|
838
|
+
`${path} must be ${type === "integer" ? "an integer" : "a number"}`,
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
if (typeof schemaValue.minimum === "number" && value < schemaValue.minimum) {
|
|
842
|
+
throw new IntegrationFeatureConfigError(`${path} is below the minimum`);
|
|
843
|
+
}
|
|
844
|
+
if (typeof schemaValue.maximum === "number" && value > schemaValue.maximum) {
|
|
845
|
+
throw new IntegrationFeatureConfigError(`${path} exceeds the maximum`);
|
|
846
|
+
}
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function objectValue(value: unknown): Record<string, unknown> {
|
|
852
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
853
|
+
? (value as Record<string, unknown>)
|
|
854
|
+
: {};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function sha256(value: string): string {
|
|
858
|
+
return createHash("sha256").update(value).digest("hex");
|
|
859
|
+
}
|