@checkstack/integration-backend 0.0.2
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/CHANGELOG.md +85 -0
- package/drizzle/0000_glossy_red_hulk.sql +28 -0
- package/drizzle/0001_rich_fixer.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +191 -0
- package/drizzle/meta/0001_snapshot.json +190 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +8 -0
- package/package.json +36 -0
- package/src/connection-store.test.ts +468 -0
- package/src/connection-store.ts +463 -0
- package/src/delivery-coordinator.ts +390 -0
- package/src/event-registry.test.ts +396 -0
- package/src/event-registry.ts +99 -0
- package/src/hook-subscriber.ts +104 -0
- package/src/index.ts +306 -0
- package/src/provider-registry.test.ts +314 -0
- package/src/provider-registry.ts +107 -0
- package/src/provider-types.ts +257 -0
- package/src/router.ts +858 -0
- package/src/schema.ts +78 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic connection store for integration providers.
|
|
3
|
+
* Each connection is stored individually using ConfigService with the provider's
|
|
4
|
+
* actual connectionSchema, which enables proper secret encryption and redaction.
|
|
5
|
+
*
|
|
6
|
+
* Key pattern: integration_connection_{providerId}_{connectionId}
|
|
7
|
+
* Index pattern: integration_connection_index_{providerId} (tracks connection IDs)
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import type {
|
|
11
|
+
ConfigService,
|
|
12
|
+
Logger,
|
|
13
|
+
Migration,
|
|
14
|
+
} from "@checkstack/backend-api";
|
|
15
|
+
import type { IntegrationProviderRegistry } from "./provider-registry";
|
|
16
|
+
import type {
|
|
17
|
+
ProviderConnection,
|
|
18
|
+
ProviderConnectionRedacted,
|
|
19
|
+
} from "@checkstack/integration-common";
|
|
20
|
+
|
|
21
|
+
// Schema for connection metadata (stored separately from config)
|
|
22
|
+
const ConnectionMetadataSchema = z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
providerId: z.string(),
|
|
25
|
+
name: z.string(),
|
|
26
|
+
createdAt: z.coerce.date(),
|
|
27
|
+
updatedAt: z.coerce.date(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Schema for provider's connection index (list of connection IDs)
|
|
31
|
+
const ConnectionIndexSchema = z.object({
|
|
32
|
+
connectionIds: z.array(z.string()),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const CONNECTION_STORAGE_VERSION = 1;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Configuration key for a single connection's config.
|
|
39
|
+
*/
|
|
40
|
+
function getConnectionConfigKey(
|
|
41
|
+
providerId: string,
|
|
42
|
+
connectionId: string
|
|
43
|
+
): string {
|
|
44
|
+
const sanitizedProvider = providerId.replaceAll(".", "_");
|
|
45
|
+
return `integration_connection_${sanitizedProvider}_${connectionId}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Configuration key for a single connection's metadata.
|
|
50
|
+
*/
|
|
51
|
+
function getConnectionMetadataKey(
|
|
52
|
+
providerId: string,
|
|
53
|
+
connectionId: string
|
|
54
|
+
): string {
|
|
55
|
+
const sanitizedProvider = providerId.replaceAll(".", "_");
|
|
56
|
+
return `integration_connection_meta_${sanitizedProvider}_${connectionId}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Configuration key for provider's connection index.
|
|
61
|
+
*/
|
|
62
|
+
function getConnectionIndexKey(providerId: string): string {
|
|
63
|
+
const sanitizedProvider = providerId.replaceAll(".", "_");
|
|
64
|
+
return `integration_connection_index_${sanitizedProvider}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ConnectionStore {
|
|
68
|
+
/** List all connections for a provider (secrets redacted) */
|
|
69
|
+
listConnections(providerId: string): Promise<ProviderConnectionRedacted[]>;
|
|
70
|
+
|
|
71
|
+
/** Get a single connection (secrets redacted) */
|
|
72
|
+
getConnection(
|
|
73
|
+
connectionId: string
|
|
74
|
+
): Promise<ProviderConnectionRedacted | undefined>;
|
|
75
|
+
|
|
76
|
+
/** Get a connection with full credentials (internal use only) */
|
|
77
|
+
getConnectionWithCredentials(
|
|
78
|
+
connectionId: string
|
|
79
|
+
): Promise<ProviderConnection | undefined>;
|
|
80
|
+
|
|
81
|
+
/** Create a new connection */
|
|
82
|
+
createConnection(params: {
|
|
83
|
+
providerId: string;
|
|
84
|
+
name: string;
|
|
85
|
+
config: Record<string, unknown>;
|
|
86
|
+
}): Promise<ProviderConnection>;
|
|
87
|
+
|
|
88
|
+
/** Update an existing connection */
|
|
89
|
+
updateConnection(params: {
|
|
90
|
+
connectionId: string;
|
|
91
|
+
updates: {
|
|
92
|
+
name?: string;
|
|
93
|
+
config?: Record<string, unknown>;
|
|
94
|
+
};
|
|
95
|
+
}): Promise<ProviderConnection>;
|
|
96
|
+
|
|
97
|
+
/** Delete a connection */
|
|
98
|
+
deleteConnection(connectionId: string): Promise<boolean>;
|
|
99
|
+
|
|
100
|
+
/** Find which provider owns a connection */
|
|
101
|
+
findConnectionProvider(connectionId: string): Promise<string | undefined>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface ConnectionStoreDeps {
|
|
105
|
+
configService: ConfigService;
|
|
106
|
+
providerRegistry: IntegrationProviderRegistry;
|
|
107
|
+
logger: Logger;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createConnectionStore(
|
|
111
|
+
deps: ConnectionStoreDeps
|
|
112
|
+
): ConnectionStore {
|
|
113
|
+
const { configService, providerRegistry, logger } = deps;
|
|
114
|
+
|
|
115
|
+
// Cache of connectionId -> providerId for efficient lookups
|
|
116
|
+
const connectionProviderCache = new Map<string, string>();
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the list of connection IDs for a provider.
|
|
120
|
+
*/
|
|
121
|
+
async function getConnectionIndex(providerId: string): Promise<string[]> {
|
|
122
|
+
const key = getConnectionIndexKey(providerId);
|
|
123
|
+
const data = await configService.get(
|
|
124
|
+
key,
|
|
125
|
+
ConnectionIndexSchema,
|
|
126
|
+
CONNECTION_STORAGE_VERSION
|
|
127
|
+
);
|
|
128
|
+
return data?.connectionIds ?? [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Save the list of connection IDs for a provider.
|
|
133
|
+
*/
|
|
134
|
+
async function setConnectionIndex(
|
|
135
|
+
providerId: string,
|
|
136
|
+
connectionIds: string[]
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
const key = getConnectionIndexKey(providerId);
|
|
139
|
+
await configService.set(
|
|
140
|
+
key,
|
|
141
|
+
ConnectionIndexSchema,
|
|
142
|
+
CONNECTION_STORAGE_VERSION,
|
|
143
|
+
{ connectionIds }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get connection metadata.
|
|
149
|
+
*/
|
|
150
|
+
async function getConnectionMetadata(
|
|
151
|
+
providerId: string,
|
|
152
|
+
connectionId: string
|
|
153
|
+
): Promise<z.infer<typeof ConnectionMetadataSchema> | undefined> {
|
|
154
|
+
const key = getConnectionMetadataKey(providerId, connectionId);
|
|
155
|
+
return configService.get(
|
|
156
|
+
key,
|
|
157
|
+
ConnectionMetadataSchema,
|
|
158
|
+
CONNECTION_STORAGE_VERSION
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Save connection metadata.
|
|
164
|
+
*/
|
|
165
|
+
async function setConnectionMetadata(
|
|
166
|
+
providerId: string,
|
|
167
|
+
connectionId: string,
|
|
168
|
+
metadata: z.infer<typeof ConnectionMetadataSchema>
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const key = getConnectionMetadataKey(providerId, connectionId);
|
|
171
|
+
await configService.set(
|
|
172
|
+
key,
|
|
173
|
+
ConnectionMetadataSchema,
|
|
174
|
+
CONNECTION_STORAGE_VERSION,
|
|
175
|
+
metadata
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get connection config (with full credentials).
|
|
181
|
+
*/
|
|
182
|
+
async function getConnectionConfigRaw(
|
|
183
|
+
providerId: string,
|
|
184
|
+
connectionId: string
|
|
185
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
186
|
+
const provider = providerRegistry.getProvider(providerId);
|
|
187
|
+
if (!provider?.connectionSchema) return undefined;
|
|
188
|
+
|
|
189
|
+
const key = getConnectionConfigKey(providerId, connectionId);
|
|
190
|
+
const config = await configService.get(
|
|
191
|
+
key,
|
|
192
|
+
provider.connectionSchema.schema,
|
|
193
|
+
provider.connectionSchema.version,
|
|
194
|
+
provider.connectionSchema.migrations as
|
|
195
|
+
| Migration<unknown, unknown>[]
|
|
196
|
+
| undefined
|
|
197
|
+
);
|
|
198
|
+
return config as Record<string, unknown> | undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get connection config (secrets redacted).
|
|
203
|
+
*/
|
|
204
|
+
async function getConnectionConfigRedacted(
|
|
205
|
+
providerId: string,
|
|
206
|
+
connectionId: string
|
|
207
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
208
|
+
const provider = providerRegistry.getProvider(providerId);
|
|
209
|
+
if (!provider?.connectionSchema) return undefined;
|
|
210
|
+
|
|
211
|
+
const key = getConnectionConfigKey(providerId, connectionId);
|
|
212
|
+
const config = await configService.getRedacted(
|
|
213
|
+
key,
|
|
214
|
+
provider.connectionSchema.schema,
|
|
215
|
+
provider.connectionSchema.version,
|
|
216
|
+
provider.connectionSchema.migrations as
|
|
217
|
+
| Migration<unknown, unknown>[]
|
|
218
|
+
| undefined
|
|
219
|
+
);
|
|
220
|
+
return config as Record<string, unknown> | undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Save connection config.
|
|
225
|
+
*/
|
|
226
|
+
async function setConnectionConfig(
|
|
227
|
+
providerId: string,
|
|
228
|
+
connectionId: string,
|
|
229
|
+
config: Record<string, unknown>
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
const provider = providerRegistry.getProvider(providerId);
|
|
232
|
+
if (!provider?.connectionSchema) {
|
|
233
|
+
throw new Error(`Provider ${providerId} has no connectionSchema`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const key = getConnectionConfigKey(providerId, connectionId);
|
|
237
|
+
await configService.set(
|
|
238
|
+
key,
|
|
239
|
+
provider.connectionSchema.schema,
|
|
240
|
+
provider.connectionSchema.version,
|
|
241
|
+
config
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Delete connection config and metadata.
|
|
247
|
+
*/
|
|
248
|
+
async function deleteConnectionData(
|
|
249
|
+
providerId: string,
|
|
250
|
+
connectionId: string
|
|
251
|
+
): Promise<void> {
|
|
252
|
+
const configKey = getConnectionConfigKey(providerId, connectionId);
|
|
253
|
+
await configService.delete(configKey);
|
|
254
|
+
|
|
255
|
+
const metaKey = getConnectionMetadataKey(providerId, connectionId);
|
|
256
|
+
await configService.delete(metaKey);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
async listConnections(providerId) {
|
|
261
|
+
// Validate provider exists and has connectionSchema
|
|
262
|
+
const provider = providerRegistry.getProvider(providerId);
|
|
263
|
+
if (!provider?.connectionSchema) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const connectionIds = await getConnectionIndex(providerId);
|
|
268
|
+
const connections: ProviderConnectionRedacted[] = [];
|
|
269
|
+
|
|
270
|
+
for (const connectionId of connectionIds) {
|
|
271
|
+
const metadata = await getConnectionMetadata(providerId, connectionId);
|
|
272
|
+
const configPreview = await getConnectionConfigRedacted(
|
|
273
|
+
providerId,
|
|
274
|
+
connectionId
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (metadata && configPreview) {
|
|
278
|
+
connectionProviderCache.set(connectionId, providerId);
|
|
279
|
+
connections.push({
|
|
280
|
+
id: metadata.id,
|
|
281
|
+
providerId: metadata.providerId,
|
|
282
|
+
name: metadata.name,
|
|
283
|
+
configPreview,
|
|
284
|
+
createdAt: metadata.createdAt,
|
|
285
|
+
updatedAt: metadata.updatedAt,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return connections;
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
async getConnection(connectionId) {
|
|
294
|
+
let providerId = connectionProviderCache.get(connectionId);
|
|
295
|
+
|
|
296
|
+
if (!providerId) {
|
|
297
|
+
providerId = await this.findConnectionProvider(connectionId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!providerId) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const metadata = await getConnectionMetadata(providerId, connectionId);
|
|
305
|
+
const configPreview = await getConnectionConfigRedacted(
|
|
306
|
+
providerId,
|
|
307
|
+
connectionId
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (!metadata || !configPreview) return;
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
id: metadata.id,
|
|
314
|
+
providerId: metadata.providerId,
|
|
315
|
+
name: metadata.name,
|
|
316
|
+
configPreview,
|
|
317
|
+
createdAt: metadata.createdAt,
|
|
318
|
+
updatedAt: metadata.updatedAt,
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
async getConnectionWithCredentials(connectionId) {
|
|
323
|
+
let providerId = connectionProviderCache.get(connectionId);
|
|
324
|
+
|
|
325
|
+
if (!providerId) {
|
|
326
|
+
providerId = await this.findConnectionProvider(connectionId);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!providerId) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const metadata = await getConnectionMetadata(providerId, connectionId);
|
|
334
|
+
const config = await getConnectionConfigRaw(providerId, connectionId);
|
|
335
|
+
|
|
336
|
+
if (!metadata || !config) return;
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
id: metadata.id,
|
|
340
|
+
providerId: metadata.providerId,
|
|
341
|
+
name: metadata.name,
|
|
342
|
+
config,
|
|
343
|
+
createdAt: metadata.createdAt,
|
|
344
|
+
updatedAt: metadata.updatedAt,
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
async createConnection({ providerId, name, config }) {
|
|
349
|
+
const now = new Date();
|
|
350
|
+
const id = crypto.randomUUID();
|
|
351
|
+
|
|
352
|
+
const metadata = {
|
|
353
|
+
id,
|
|
354
|
+
providerId,
|
|
355
|
+
name,
|
|
356
|
+
createdAt: now,
|
|
357
|
+
updatedAt: now,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Save metadata and config separately
|
|
361
|
+
await setConnectionMetadata(providerId, id, metadata);
|
|
362
|
+
await setConnectionConfig(providerId, id, config);
|
|
363
|
+
|
|
364
|
+
// Add to index
|
|
365
|
+
const connectionIds = await getConnectionIndex(providerId);
|
|
366
|
+
connectionIds.push(id);
|
|
367
|
+
await setConnectionIndex(providerId, connectionIds);
|
|
368
|
+
|
|
369
|
+
connectionProviderCache.set(id, providerId);
|
|
370
|
+
logger.info(
|
|
371
|
+
`Created connection "${name}" (${id}) for provider ${providerId}`
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
return { ...metadata, config };
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
async updateConnection({ connectionId, updates }) {
|
|
378
|
+
const providerId =
|
|
379
|
+
connectionProviderCache.get(connectionId) ??
|
|
380
|
+
(await this.findConnectionProvider(connectionId));
|
|
381
|
+
|
|
382
|
+
if (!providerId) {
|
|
383
|
+
throw new Error(`Connection not found: ${connectionId}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const metadata = await getConnectionMetadata(providerId, connectionId);
|
|
387
|
+
const existingConfig = await getConnectionConfigRaw(
|
|
388
|
+
providerId,
|
|
389
|
+
connectionId
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (!metadata || !existingConfig) {
|
|
393
|
+
throw new Error(`Connection not found: ${connectionId}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const updatedMetadata = {
|
|
397
|
+
...metadata,
|
|
398
|
+
name: updates.name ?? metadata.name,
|
|
399
|
+
updatedAt: new Date(),
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const updatedConfig = updates.config
|
|
403
|
+
? { ...existingConfig, ...updates.config }
|
|
404
|
+
: existingConfig;
|
|
405
|
+
|
|
406
|
+
await setConnectionMetadata(providerId, connectionId, updatedMetadata);
|
|
407
|
+
await setConnectionConfig(providerId, connectionId, updatedConfig);
|
|
408
|
+
|
|
409
|
+
logger.info(
|
|
410
|
+
`Updated connection "${updatedMetadata.name}" (${connectionId})`
|
|
411
|
+
);
|
|
412
|
+
return { ...updatedMetadata, config: updatedConfig };
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
async deleteConnection(connectionId) {
|
|
416
|
+
const providerId =
|
|
417
|
+
connectionProviderCache.get(connectionId) ??
|
|
418
|
+
(await this.findConnectionProvider(connectionId));
|
|
419
|
+
|
|
420
|
+
if (!providerId) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Delete config and metadata
|
|
425
|
+
await deleteConnectionData(providerId, connectionId);
|
|
426
|
+
|
|
427
|
+
// Remove from index
|
|
428
|
+
const connectionIds = await getConnectionIndex(providerId);
|
|
429
|
+
const filtered = connectionIds.filter((id) => id !== connectionId);
|
|
430
|
+
|
|
431
|
+
if (filtered.length === connectionIds.length) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
await setConnectionIndex(providerId, filtered);
|
|
436
|
+
connectionProviderCache.delete(connectionId);
|
|
437
|
+
|
|
438
|
+
logger.info(
|
|
439
|
+
`Deleted connection ${connectionId} from provider ${providerId}`
|
|
440
|
+
);
|
|
441
|
+
return true;
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
async findConnectionProvider(connectionId) {
|
|
445
|
+
// Check cache first
|
|
446
|
+
const cached = connectionProviderCache.get(connectionId);
|
|
447
|
+
if (cached) return cached;
|
|
448
|
+
|
|
449
|
+
// Iterate through providers that have connectionSchema
|
|
450
|
+
for (const provider of providerRegistry.getProviders()) {
|
|
451
|
+
if (!provider.connectionSchema) continue;
|
|
452
|
+
|
|
453
|
+
const connectionIds = await getConnectionIndex(provider.qualifiedId);
|
|
454
|
+
if (connectionIds.includes(connectionId)) {
|
|
455
|
+
connectionProviderCache.set(connectionId, provider.qualifiedId);
|
|
456
|
+
return provider.qualifiedId;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return;
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|