@checkstack/integration-backend 0.1.30 → 0.3.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/CHANGELOG.md +188 -0
- package/package.json +11 -9
- package/src/connection-credentials-migration.test.ts +171 -0
- package/src/connection-credentials-migration.ts +182 -0
- package/src/connection-credentials.test.ts +180 -0
- package/src/connection-credentials.ts +165 -0
- package/src/connection-store.ts +119 -6
- package/src/index.ts +55 -181
- package/src/provider-registry.test.ts +56 -286
- package/src/provider-registry.ts +5 -20
- package/src/provider-types.ts +23 -149
- package/src/router.ts +87 -608
- package/src/schema.ts +13 -59
- package/src/test-connection-masking.test.ts +98 -0
- package/tsconfig.json +6 -0
- package/src/delivery-coordinator.ts +0 -391
- package/src/event-registry.test.ts +0 -396
- package/src/event-registry.ts +0 -99
- package/src/hook-subscriber.ts +0 -105
package/src/router.ts
CHANGED
|
@@ -1,409 +1,58 @@
|
|
|
1
1
|
import { implement, ORPCError } from "@orpc/server";
|
|
2
|
-
import type { SafeDatabase } from "@checkstack/backend-api";
|
|
3
2
|
import {
|
|
4
3
|
autoAuthMiddleware,
|
|
5
4
|
correlationMiddleware,
|
|
6
|
-
type RpcContext,
|
|
7
5
|
type Logger,
|
|
6
|
+
type RpcContext,
|
|
7
|
+
type SafeDatabase,
|
|
8
8
|
} from "@checkstack/backend-api";
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
9
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
10
|
+
import { maskSecrets } from "@checkstack/secrets-common";
|
|
11
|
+
import { integrationContract } from "@checkstack/integration-common";
|
|
11
12
|
|
|
12
|
-
import type { IntegrationEventRegistry } from "./event-registry";
|
|
13
13
|
import type { IntegrationProviderRegistry } from "./provider-registry";
|
|
14
|
-
import type { DeliveryCoordinator } from "./delivery-coordinator";
|
|
15
14
|
import type { ConnectionStore } from "./connection-store";
|
|
16
15
|
import * as schema from "./schema";
|
|
17
|
-
import {
|
|
18
|
-
integrationContract,
|
|
19
|
-
INTEGRATION_SUBSCRIPTION_CHANGED,
|
|
20
|
-
} from "@checkstack/integration-common";
|
|
21
|
-
import { extractErrorMessage } from "@checkstack/common";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Recursively extracts flattened property paths from a JSON Schema.
|
|
25
|
-
* Used to provide template hints for payload properties.
|
|
26
|
-
*/
|
|
27
|
-
interface JsonSchemaProperty {
|
|
28
|
-
path: string;
|
|
29
|
-
type: string;
|
|
30
|
-
description?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function extractJsonSchemaProperties(
|
|
34
|
-
schema: Record<string, unknown>,
|
|
35
|
-
basePath: string = ""
|
|
36
|
-
): JsonSchemaProperty[] {
|
|
37
|
-
const properties: JsonSchemaProperty[] = [];
|
|
38
|
-
|
|
39
|
-
const schemaType = schema["type"] as string | string[] | undefined;
|
|
40
|
-
const schemaProperties = schema["properties"] as
|
|
41
|
-
| Record<string, Record<string, unknown>>
|
|
42
|
-
| undefined;
|
|
43
|
-
const schemaItems = schema["items"] as Record<string, unknown> | undefined;
|
|
44
|
-
const schemaDescription = schema["description"] as string | undefined;
|
|
45
|
-
|
|
46
|
-
// Handle object with properties
|
|
47
|
-
if (schemaProperties) {
|
|
48
|
-
for (const [key, propSchema] of Object.entries(schemaProperties)) {
|
|
49
|
-
const propPath = basePath ? `${basePath}.${key}` : key;
|
|
50
|
-
const propType = (propSchema["type"] as string) || "unknown";
|
|
51
|
-
const propDescription = propSchema["description"] as string | undefined;
|
|
52
|
-
|
|
53
|
-
// Add this property
|
|
54
|
-
properties.push({
|
|
55
|
-
path: propPath,
|
|
56
|
-
type: Array.isArray(propType) ? propType.join(" | ") : propType,
|
|
57
|
-
description: propDescription,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Recurse into nested objects
|
|
61
|
-
if (propType === "object" || propSchema["properties"]) {
|
|
62
|
-
properties.push(...extractJsonSchemaProperties(propSchema, propPath));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Recurse into arrays (add [n] notation)
|
|
66
|
-
if (propType === "array" && propSchema["items"]) {
|
|
67
|
-
const itemsSchema = propSchema["items"] as Record<string, unknown>;
|
|
68
|
-
properties.push(
|
|
69
|
-
...extractJsonSchemaProperties(itemsSchema, `${propPath}[n]`)
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Handle array at root level
|
|
76
|
-
if (schemaType === "array" && schemaItems) {
|
|
77
|
-
properties.push(
|
|
78
|
-
...extractJsonSchemaProperties(schemaItems, `${basePath}[n]`)
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// If this is a primitive with a path, add it
|
|
83
|
-
if (
|
|
84
|
-
basePath &&
|
|
85
|
-
schemaType &&
|
|
86
|
-
schemaType !== "object" &&
|
|
87
|
-
schemaType !== "array" &&
|
|
88
|
-
!schemaProperties
|
|
89
|
-
) {
|
|
90
|
-
properties.push({
|
|
91
|
-
path: basePath,
|
|
92
|
-
type: Array.isArray(schemaType) ? schemaType.join(" | ") : schemaType,
|
|
93
|
-
description: schemaDescription,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return properties;
|
|
98
|
-
}
|
|
99
16
|
|
|
100
17
|
interface RouterDeps {
|
|
101
18
|
db: SafeDatabase<typeof schema>;
|
|
102
|
-
eventRegistry: IntegrationEventRegistry;
|
|
103
19
|
providerRegistry: IntegrationProviderRegistry;
|
|
104
|
-
deliveryCoordinator: DeliveryCoordinator;
|
|
105
20
|
connectionStore: ConnectionStore;
|
|
106
|
-
signalService: SignalService;
|
|
107
21
|
logger: Logger;
|
|
108
22
|
}
|
|
109
23
|
|
|
110
24
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* based on the contract's meta.userType and meta.access.
|
|
25
|
+
* Integration router — connection management only. The legacy
|
|
26
|
+
* subscription / event-listing / delivery-log endpoints were removed
|
|
27
|
+
* when the platform moved to the Automation Platform model.
|
|
115
28
|
*/
|
|
116
29
|
export function createIntegrationRouter(deps: RouterDeps) {
|
|
117
|
-
const {
|
|
118
|
-
|
|
119
|
-
eventRegistry,
|
|
120
|
-
providerRegistry,
|
|
121
|
-
deliveryCoordinator,
|
|
122
|
-
connectionStore,
|
|
123
|
-
signalService,
|
|
124
|
-
logger,
|
|
125
|
-
} = deps;
|
|
126
|
-
|
|
127
|
-
// Create contract implementer with context type AND auto auth middleware
|
|
30
|
+
const { db, providerRegistry, connectionStore, logger } = deps;
|
|
31
|
+
|
|
128
32
|
const os = implement(integrationContract)
|
|
129
33
|
.$context<RpcContext>()
|
|
130
34
|
.use(correlationMiddleware)
|
|
131
35
|
.use(autoAuthMiddleware);
|
|
132
36
|
|
|
133
37
|
return os.router({
|
|
134
|
-
//
|
|
135
|
-
// SUBSCRIPTION MANAGEMENT
|
|
136
|
-
// =========================================================================
|
|
137
|
-
|
|
138
|
-
listSubscriptions: os.listSubscriptions.handler(async ({ input }) => {
|
|
139
|
-
const { limit, offset, providerId, eventType, enabled } = input;
|
|
140
|
-
|
|
141
|
-
// Build where conditions
|
|
142
|
-
const conditions = [];
|
|
143
|
-
if (providerId) {
|
|
144
|
-
conditions.push(eq(schema.webhookSubscriptions.providerId, providerId));
|
|
145
|
-
}
|
|
146
|
-
if (enabled !== undefined) {
|
|
147
|
-
conditions.push(eq(schema.webhookSubscriptions.enabled, enabled));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const whereClause =
|
|
151
|
-
conditions.length > 0 ? and(...conditions) : undefined;
|
|
152
|
-
|
|
153
|
-
// Get total count
|
|
154
|
-
const [{ value: total }] = await db
|
|
155
|
-
.select({ value: count() })
|
|
156
|
-
.from(schema.webhookSubscriptions)
|
|
157
|
-
.where(whereClause);
|
|
158
|
-
|
|
159
|
-
// Get paginated results
|
|
160
|
-
let query = db
|
|
161
|
-
.select()
|
|
162
|
-
.from(schema.webhookSubscriptions)
|
|
163
|
-
.orderBy(desc(schema.webhookSubscriptions.createdAt))
|
|
164
|
-
.limit(limit)
|
|
165
|
-
.offset(offset);
|
|
166
|
-
|
|
167
|
-
if (whereClause) {
|
|
168
|
-
query = query.where(whereClause) as typeof query;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const subscriptions = await query;
|
|
172
|
-
|
|
173
|
-
// Filter by event type if specified
|
|
174
|
-
const filtered = eventType
|
|
175
|
-
? subscriptions.filter((s) => s.eventId === eventType)
|
|
176
|
-
: subscriptions;
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
items: filtered.map((s) => ({
|
|
180
|
-
...s,
|
|
181
|
-
description: s.description ?? undefined,
|
|
182
|
-
systemFilter: s.systemFilter ?? undefined,
|
|
183
|
-
createdAt: s.createdAt,
|
|
184
|
-
updatedAt: s.updatedAt,
|
|
185
|
-
})),
|
|
186
|
-
total: Number(total),
|
|
187
|
-
limit,
|
|
188
|
-
offset,
|
|
189
|
-
};
|
|
190
|
-
}),
|
|
191
|
-
|
|
192
|
-
getSubscription: os.getSubscription.handler(async ({ input }) => {
|
|
193
|
-
const [subscription] = await db
|
|
194
|
-
.select()
|
|
195
|
-
.from(schema.webhookSubscriptions)
|
|
196
|
-
.where(eq(schema.webhookSubscriptions.id, input.id));
|
|
197
|
-
|
|
198
|
-
if (!subscription) {
|
|
199
|
-
throw new ORPCError("NOT_FOUND", {
|
|
200
|
-
message: "Subscription not found",
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
...subscription,
|
|
206
|
-
description: subscription.description ?? undefined,
|
|
207
|
-
systemFilter: subscription.systemFilter ?? undefined,
|
|
208
|
-
createdAt: subscription.createdAt,
|
|
209
|
-
updatedAt: subscription.updatedAt,
|
|
210
|
-
};
|
|
211
|
-
}),
|
|
212
|
-
|
|
213
|
-
createSubscription: os.createSubscription.handler(async ({ input }) => {
|
|
214
|
-
const {
|
|
215
|
-
name,
|
|
216
|
-
description,
|
|
217
|
-
providerId,
|
|
218
|
-
providerConfig,
|
|
219
|
-
eventId,
|
|
220
|
-
systemFilter,
|
|
221
|
-
} = input;
|
|
222
|
-
|
|
223
|
-
// Validate provider exists
|
|
224
|
-
const provider = providerRegistry.getProvider(providerId);
|
|
225
|
-
if (!provider) {
|
|
226
|
-
throw new ORPCError("BAD_REQUEST", {
|
|
227
|
-
message: `Provider not found: ${providerId}`,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Validate event exists
|
|
232
|
-
if (!eventRegistry.hasEvent(eventId)) {
|
|
233
|
-
throw new ORPCError("BAD_REQUEST", {
|
|
234
|
-
message: `Event type not found: ${eventId}`,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Validate providerConfig against the provider's schema
|
|
239
|
-
const configParseResult =
|
|
240
|
-
provider.config.schema.safeParse(providerConfig);
|
|
241
|
-
if (!configParseResult.success) {
|
|
242
|
-
throw new ORPCError("BAD_REQUEST", {
|
|
243
|
-
message: `Invalid provider configuration: ${configParseResult.error.message}`,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const id = crypto.randomUUID();
|
|
248
|
-
const now = new Date();
|
|
249
|
-
|
|
250
|
-
await db.insert(schema.webhookSubscriptions).values({
|
|
251
|
-
id,
|
|
252
|
-
name,
|
|
253
|
-
description,
|
|
254
|
-
providerId,
|
|
255
|
-
providerConfig,
|
|
256
|
-
eventId,
|
|
257
|
-
systemFilter,
|
|
258
|
-
enabled: true,
|
|
259
|
-
createdAt: now,
|
|
260
|
-
updatedAt: now,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Emit signal
|
|
264
|
-
await signalService.broadcast(INTEGRATION_SUBSCRIPTION_CHANGED, {
|
|
265
|
-
action: "created",
|
|
266
|
-
subscriptionId: id,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
logger.info(`Created webhook subscription: ${name} (${id})`);
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
id,
|
|
273
|
-
name,
|
|
274
|
-
description,
|
|
275
|
-
providerId,
|
|
276
|
-
providerConfig,
|
|
277
|
-
eventId,
|
|
278
|
-
systemFilter,
|
|
279
|
-
enabled: true,
|
|
280
|
-
createdAt: now,
|
|
281
|
-
updatedAt: now,
|
|
282
|
-
};
|
|
283
|
-
}),
|
|
284
|
-
|
|
285
|
-
updateSubscription: os.updateSubscription.handler(async ({ input }) => {
|
|
286
|
-
const { id, updates } = input;
|
|
287
|
-
|
|
288
|
-
// Check subscription exists
|
|
289
|
-
const [existing] = await db
|
|
290
|
-
.select()
|
|
291
|
-
.from(schema.webhookSubscriptions)
|
|
292
|
-
.where(eq(schema.webhookSubscriptions.id, id));
|
|
293
|
-
|
|
294
|
-
if (!existing) {
|
|
295
|
-
throw new ORPCError("NOT_FOUND", {
|
|
296
|
-
message: "Subscription not found",
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Validate event if updated
|
|
301
|
-
if (updates.eventId && !eventRegistry.hasEvent(updates.eventId)) {
|
|
302
|
-
throw new ORPCError("BAD_REQUEST", {
|
|
303
|
-
message: `Event type not found: ${updates.eventId}`,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Validate providerConfig if updated
|
|
308
|
-
if (updates.providerConfig) {
|
|
309
|
-
const provider = providerRegistry.getProvider(existing.providerId);
|
|
310
|
-
if (provider) {
|
|
311
|
-
const configParseResult = provider.config.schema.safeParse(
|
|
312
|
-
updates.providerConfig
|
|
313
|
-
);
|
|
314
|
-
if (!configParseResult.success) {
|
|
315
|
-
throw new ORPCError("BAD_REQUEST", {
|
|
316
|
-
message: `Invalid provider configuration: ${configParseResult.error.message}`,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const now = new Date();
|
|
323
|
-
|
|
324
|
-
await db
|
|
325
|
-
.update(schema.webhookSubscriptions)
|
|
326
|
-
.set({
|
|
327
|
-
...updates,
|
|
328
|
-
updatedAt: now,
|
|
329
|
-
})
|
|
330
|
-
.where(eq(schema.webhookSubscriptions.id, id));
|
|
331
|
-
|
|
332
|
-
// Emit signal
|
|
333
|
-
await signalService.broadcast(INTEGRATION_SUBSCRIPTION_CHANGED, {
|
|
334
|
-
action: "updated",
|
|
335
|
-
subscriptionId: id,
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// Re-fetch updated subscription
|
|
339
|
-
const [updated] = await db
|
|
340
|
-
.select()
|
|
341
|
-
.from(schema.webhookSubscriptions)
|
|
342
|
-
.where(eq(schema.webhookSubscriptions.id, id));
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
...updated,
|
|
346
|
-
description: updated.description ?? undefined,
|
|
347
|
-
systemFilter: updated.systemFilter ?? undefined,
|
|
348
|
-
createdAt: updated.createdAt,
|
|
349
|
-
updatedAt: updated.updatedAt,
|
|
350
|
-
};
|
|
351
|
-
}),
|
|
352
|
-
|
|
353
|
-
deleteSubscription: os.deleteSubscription.handler(async ({ input }) => {
|
|
354
|
-
const { id } = input;
|
|
355
|
-
|
|
356
|
-
await db
|
|
357
|
-
.delete(schema.webhookSubscriptions)
|
|
358
|
-
.where(eq(schema.webhookSubscriptions.id, id));
|
|
359
|
-
|
|
360
|
-
// Emit signal
|
|
361
|
-
await signalService.broadcast(INTEGRATION_SUBSCRIPTION_CHANGED, {
|
|
362
|
-
action: "deleted",
|
|
363
|
-
subscriptionId: id,
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
logger.info(`Deleted webhook subscription: ${id}`);
|
|
367
|
-
|
|
368
|
-
return { success: true };
|
|
369
|
-
}),
|
|
370
|
-
|
|
371
|
-
toggleSubscription: os.toggleSubscription.handler(async ({ input }) => {
|
|
372
|
-
const { id, enabled } = input;
|
|
373
|
-
|
|
374
|
-
await db
|
|
375
|
-
.update(schema.webhookSubscriptions)
|
|
376
|
-
.set({
|
|
377
|
-
enabled,
|
|
378
|
-
updatedAt: new Date(),
|
|
379
|
-
})
|
|
380
|
-
.where(eq(schema.webhookSubscriptions.id, id));
|
|
381
|
-
|
|
382
|
-
// Emit signal
|
|
383
|
-
await signalService.broadcast(INTEGRATION_SUBSCRIPTION_CHANGED, {
|
|
384
|
-
action: "updated",
|
|
385
|
-
subscriptionId: id,
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
return { success: true };
|
|
389
|
-
}),
|
|
390
|
-
|
|
391
|
-
// =========================================================================
|
|
392
|
-
// PROVIDER DISCOVERY
|
|
393
|
-
// =========================================================================
|
|
38
|
+
// ─── Providers ───────────────────────────────────────────────────────
|
|
394
39
|
|
|
395
40
|
listProviders: os.listProviders.handler(async () => {
|
|
396
41
|
const providers = providerRegistry.getProviders();
|
|
397
|
-
|
|
398
42
|
return providers.map((p) => ({
|
|
399
43
|
qualifiedId: p.qualifiedId,
|
|
400
44
|
displayName: p.displayName,
|
|
401
45
|
description: p.description,
|
|
402
46
|
icon: p.icon,
|
|
403
47
|
ownerPluginId: p.ownerPluginId,
|
|
404
|
-
supportedEvents
|
|
405
|
-
|
|
406
|
-
|
|
48
|
+
// Legacy `supportedEvents` is no longer modelled on providers
|
|
49
|
+
// (the trigger registry owns event metadata now). Return empty
|
|
50
|
+
// so the wire schema stays stable.
|
|
51
|
+
supportedEvents: [],
|
|
52
|
+
// Legacy `configSchema` was the per-subscription config; that
|
|
53
|
+
// lives on action definitions now. Returning an empty object
|
|
54
|
+
// preserves the wire shape until the schema is bumped.
|
|
55
|
+
configSchema: {},
|
|
407
56
|
hasConnectionSchema: !!p.connectionSchema,
|
|
408
57
|
connectionSchema: p.connectionSchema
|
|
409
58
|
? providerRegistry.getProviderConnectionSchema(p.qualifiedId)
|
|
@@ -432,23 +81,27 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
432
81
|
const result = await provider.testConnection(config);
|
|
433
82
|
return result;
|
|
434
83
|
} catch (error) {
|
|
84
|
+
// Mask any credential the submitted config carries out of the
|
|
85
|
+
// provider error before returning (same guard as the saved-
|
|
86
|
+
// connection testConnection path below).
|
|
87
|
+
const values: string[] = [];
|
|
88
|
+
collectStringLeaves(config, values);
|
|
435
89
|
return {
|
|
436
90
|
success: false,
|
|
437
|
-
message:
|
|
91
|
+
message: maskSecrets({
|
|
92
|
+
text: extractErrorMessage(error),
|
|
93
|
+
values,
|
|
94
|
+
}),
|
|
438
95
|
};
|
|
439
96
|
}
|
|
440
|
-
}
|
|
97
|
+
},
|
|
441
98
|
),
|
|
442
99
|
|
|
443
|
-
//
|
|
444
|
-
// CONNECTION MANAGEMENT
|
|
445
|
-
// Generic CRUD for site-wide provider connections
|
|
446
|
-
// =========================================================================
|
|
100
|
+
// ─── Connections ─────────────────────────────────────────────────────
|
|
447
101
|
|
|
448
102
|
listConnections: os.listConnections.handler(async ({ input }) => {
|
|
449
103
|
const { providerId } = input;
|
|
450
104
|
|
|
451
|
-
// Verify provider exists and has connectionSchema
|
|
452
105
|
const provider = providerRegistry.getProvider(providerId);
|
|
453
106
|
if (!provider) {
|
|
454
107
|
throw new ORPCError("NOT_FOUND", {
|
|
@@ -481,7 +134,6 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
481
134
|
createConnection: os.createConnection.handler(async ({ input }) => {
|
|
482
135
|
const { providerId, name, config } = input;
|
|
483
136
|
|
|
484
|
-
// Verify provider exists and has connectionSchema
|
|
485
137
|
const provider = providerRegistry.getProvider(providerId);
|
|
486
138
|
if (!provider) {
|
|
487
139
|
throw new ORPCError("NOT_FOUND", {
|
|
@@ -495,7 +147,6 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
495
147
|
});
|
|
496
148
|
}
|
|
497
149
|
|
|
498
|
-
// Validate config against provider's connectionSchema
|
|
499
150
|
const parseResult = provider.connectionSchema.schema.safeParse(config);
|
|
500
151
|
if (!parseResult.success) {
|
|
501
152
|
throw new ORPCError("BAD_REQUEST", {
|
|
@@ -503,7 +154,6 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
503
154
|
});
|
|
504
155
|
}
|
|
505
156
|
|
|
506
|
-
// parseResult.data is typed correctly after guard
|
|
507
157
|
const validatedConfig = parseResult.data as unknown as Record<
|
|
508
158
|
string,
|
|
509
159
|
unknown
|
|
@@ -517,12 +167,15 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
517
167
|
|
|
518
168
|
logger.info(`Created connection "${name}" for provider ${providerId}`);
|
|
519
169
|
|
|
520
|
-
// Return
|
|
170
|
+
// Return the REDACTED preview (secret fields stripped) rather than
|
|
171
|
+
// echoing the raw submitted config back — credentials must never
|
|
172
|
+
// cross back to the browser, even on create.
|
|
173
|
+
const redacted = await connectionStore.getConnection(connection.id);
|
|
521
174
|
return {
|
|
522
175
|
id: connection.id,
|
|
523
176
|
providerId: connection.providerId,
|
|
524
177
|
name: connection.name,
|
|
525
|
-
configPreview:
|
|
178
|
+
configPreview: redacted?.configPreview ?? {},
|
|
526
179
|
createdAt: connection.createdAt,
|
|
527
180
|
updatedAt: connection.updatedAt,
|
|
528
181
|
};
|
|
@@ -537,18 +190,20 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
537
190
|
updates,
|
|
538
191
|
});
|
|
539
192
|
|
|
193
|
+
// Return the REDACTED preview rather than echoing the submitted
|
|
194
|
+
// config — credentials must never cross back to the browser.
|
|
195
|
+
const redacted = await connectionStore.getConnection(connection.id);
|
|
540
196
|
return {
|
|
541
197
|
id: connection.id,
|
|
542
198
|
providerId: connection.providerId,
|
|
543
199
|
name: connection.name,
|
|
544
|
-
configPreview:
|
|
200
|
+
configPreview: redacted?.configPreview ?? {},
|
|
545
201
|
createdAt: connection.createdAt,
|
|
546
202
|
updatedAt: connection.updatedAt,
|
|
547
203
|
};
|
|
548
204
|
} catch (error) {
|
|
549
205
|
throw new ORPCError("NOT_FOUND", {
|
|
550
|
-
message:
|
|
551
|
-
extractErrorMessage(error, "Connection not found"),
|
|
206
|
+
message: extractErrorMessage(error, "Connection not found"),
|
|
552
207
|
});
|
|
553
208
|
}
|
|
554
209
|
}),
|
|
@@ -570,7 +225,7 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
570
225
|
const { connectionId } = input;
|
|
571
226
|
|
|
572
227
|
const connection = await connectionStore.getConnectionWithCredentials(
|
|
573
|
-
connectionId
|
|
228
|
+
connectionId,
|
|
574
229
|
);
|
|
575
230
|
if (!connection) {
|
|
576
231
|
return { success: false, message: "Connection not found" };
|
|
@@ -592,18 +247,44 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
592
247
|
const result = await provider.testConnection(connection.config);
|
|
593
248
|
return result;
|
|
594
249
|
} catch (error) {
|
|
250
|
+
// The resolved connection config carries live credentials. A
|
|
251
|
+
// provider error may echo a token (e.g. "401 with Bearer <token>").
|
|
252
|
+
// There is no run-scoped secret registry on this path, so build a
|
|
253
|
+
// per-call mask set from the resolved config's string leaves and
|
|
254
|
+
// run the error through it before returning to the browser.
|
|
255
|
+
const values: string[] = [];
|
|
256
|
+
collectStringLeaves(connection.config, values);
|
|
595
257
|
return {
|
|
596
258
|
success: false,
|
|
597
|
-
message:
|
|
259
|
+
message: maskSecrets({
|
|
260
|
+
text: extractErrorMessage(error),
|
|
261
|
+
values,
|
|
262
|
+
}),
|
|
598
263
|
};
|
|
599
264
|
}
|
|
600
265
|
}),
|
|
601
266
|
|
|
267
|
+
// ─── One-time migration support ──────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
listLegacySubscriptions: os.listLegacySubscriptions.handler(async () => {
|
|
270
|
+
const rows = await db.select().from(schema.webhookSubscriptions);
|
|
271
|
+
return rows.map((row) => ({
|
|
272
|
+
id: row.id,
|
|
273
|
+
name: row.name,
|
|
274
|
+
description: row.description ?? undefined,
|
|
275
|
+
providerId: row.providerId,
|
|
276
|
+
providerConfig: row.providerConfig,
|
|
277
|
+
eventId: row.eventId,
|
|
278
|
+
systemFilter: row.systemFilter ?? undefined,
|
|
279
|
+
enabled: row.enabled,
|
|
280
|
+
}));
|
|
281
|
+
}),
|
|
282
|
+
|
|
602
283
|
getConnectionOptions: os.getConnectionOptions.handler(async ({ input }) => {
|
|
603
284
|
const { providerId, connectionId, resolverName, context } = input;
|
|
604
285
|
|
|
605
286
|
logger.debug(
|
|
606
|
-
`getConnectionOptions called: providerId=${providerId}, connectionId=${connectionId}, resolverName=${resolverName}
|
|
287
|
+
`getConnectionOptions called: providerId=${providerId}, connectionId=${connectionId}, resolverName=${resolverName}`,
|
|
607
288
|
);
|
|
608
289
|
|
|
609
290
|
const provider = providerRegistry.getProvider(providerId);
|
|
@@ -629,235 +310,33 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
629
310
|
connectionStore.getConnectionWithCredentials.bind(connectionStore),
|
|
630
311
|
});
|
|
631
312
|
logger.debug(
|
|
632
|
-
`getConnectionOptions returned ${options.length} options for ${resolverName}
|
|
313
|
+
`getConnectionOptions returned ${options.length} options for ${resolverName}`,
|
|
633
314
|
);
|
|
634
315
|
return options;
|
|
635
316
|
} catch (error) {
|
|
636
317
|
logger.error(`Failed to get connection options: ${error}`);
|
|
637
318
|
throw new ORPCError("INTERNAL_SERVER_ERROR", {
|
|
638
|
-
message:
|
|
639
|
-
extractErrorMessage(error, "Failed to fetch options"),
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
}),
|
|
643
|
-
|
|
644
|
-
// =========================================================================
|
|
645
|
-
// EVENT DISCOVERY
|
|
646
|
-
// =========================================================================
|
|
647
|
-
|
|
648
|
-
listEventTypes: os.listEventTypes.handler(async () => {
|
|
649
|
-
const events = eventRegistry.getEvents();
|
|
650
|
-
|
|
651
|
-
return events.map((e) => ({
|
|
652
|
-
eventId: e.eventId,
|
|
653
|
-
displayName: e.displayName,
|
|
654
|
-
description: e.description,
|
|
655
|
-
category: e.category,
|
|
656
|
-
ownerPluginId: e.ownerPluginId,
|
|
657
|
-
payloadSchema: e.payloadJsonSchema,
|
|
658
|
-
}));
|
|
659
|
-
}),
|
|
660
|
-
|
|
661
|
-
getEventsByCategory: os.getEventsByCategory.handler(async () => {
|
|
662
|
-
const byCategory = eventRegistry.getEventsByCategory();
|
|
663
|
-
|
|
664
|
-
return [...byCategory.entries()].map(([category, events]) => ({
|
|
665
|
-
category,
|
|
666
|
-
events: events.map((e) => ({
|
|
667
|
-
eventId: e.eventId,
|
|
668
|
-
displayName: e.displayName,
|
|
669
|
-
description: e.description,
|
|
670
|
-
category: e.category,
|
|
671
|
-
ownerPluginId: e.ownerPluginId,
|
|
672
|
-
payloadSchema: e.payloadJsonSchema,
|
|
673
|
-
})),
|
|
674
|
-
}));
|
|
675
|
-
}),
|
|
676
|
-
|
|
677
|
-
getEventPayloadSchema: os.getEventPayloadSchema.handler(
|
|
678
|
-
async ({ input }) => {
|
|
679
|
-
const { eventId } = input;
|
|
680
|
-
|
|
681
|
-
const event = eventRegistry.getEvent(eventId);
|
|
682
|
-
if (!event) {
|
|
683
|
-
throw new ORPCError("NOT_FOUND", {
|
|
684
|
-
message: `Event not found: ${eventId}`,
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Extract flattened properties from JSON Schema
|
|
689
|
-
const availableProperties = extractJsonSchemaProperties(
|
|
690
|
-
event.payloadJsonSchema,
|
|
691
|
-
"payload"
|
|
692
|
-
);
|
|
693
|
-
|
|
694
|
-
return {
|
|
695
|
-
eventId: event.eventId,
|
|
696
|
-
payloadSchema: event.payloadJsonSchema,
|
|
697
|
-
availableProperties,
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
),
|
|
701
|
-
|
|
702
|
-
// =========================================================================
|
|
703
|
-
// DELIVERY LOGS
|
|
704
|
-
// =========================================================================
|
|
705
|
-
|
|
706
|
-
getDeliveryLogs: os.getDeliveryLogs.handler(async ({ input }) => {
|
|
707
|
-
const { subscriptionId, eventType, status, limit, offset } = input;
|
|
708
|
-
|
|
709
|
-
// Build where conditions
|
|
710
|
-
const conditions = [];
|
|
711
|
-
if (subscriptionId) {
|
|
712
|
-
conditions.push(eq(schema.deliveryLogs.subscriptionId, subscriptionId));
|
|
713
|
-
}
|
|
714
|
-
if (eventType) {
|
|
715
|
-
conditions.push(eq(schema.deliveryLogs.eventType, eventType));
|
|
716
|
-
}
|
|
717
|
-
if (status) {
|
|
718
|
-
conditions.push(eq(schema.deliveryLogs.status, status));
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const whereClause =
|
|
722
|
-
conditions.length > 0 ? and(...conditions) : undefined;
|
|
723
|
-
|
|
724
|
-
// Get total count
|
|
725
|
-
const [{ value: total }] = await db
|
|
726
|
-
.select({ value: count() })
|
|
727
|
-
.from(schema.deliveryLogs)
|
|
728
|
-
.where(whereClause);
|
|
729
|
-
|
|
730
|
-
// Get paginated results with subscription name
|
|
731
|
-
const logs = await db
|
|
732
|
-
.select({
|
|
733
|
-
log: schema.deliveryLogs,
|
|
734
|
-
subscriptionName: schema.webhookSubscriptions.name,
|
|
735
|
-
})
|
|
736
|
-
.from(schema.deliveryLogs)
|
|
737
|
-
.leftJoin(
|
|
738
|
-
schema.webhookSubscriptions,
|
|
739
|
-
eq(schema.deliveryLogs.subscriptionId, schema.webhookSubscriptions.id)
|
|
740
|
-
)
|
|
741
|
-
.where(whereClause)
|
|
742
|
-
.orderBy(desc(schema.deliveryLogs.createdAt))
|
|
743
|
-
.limit(limit)
|
|
744
|
-
.offset(offset);
|
|
745
|
-
|
|
746
|
-
return {
|
|
747
|
-
items: logs.map(({ log, subscriptionName }) => ({
|
|
748
|
-
...log,
|
|
749
|
-
subscriptionName: subscriptionName ?? undefined,
|
|
750
|
-
createdAt: log.createdAt,
|
|
751
|
-
lastAttemptAt: log.lastAttemptAt ?? undefined,
|
|
752
|
-
nextRetryAt: log.nextRetryAt ?? undefined,
|
|
753
|
-
externalId: log.externalId ?? undefined,
|
|
754
|
-
errorMessage: log.errorMessage ?? undefined,
|
|
755
|
-
})),
|
|
756
|
-
total: Number(total),
|
|
757
|
-
limit,
|
|
758
|
-
offset,
|
|
759
|
-
};
|
|
760
|
-
}),
|
|
761
|
-
|
|
762
|
-
getDeliveryLog: os.getDeliveryLog.handler(async ({ input }) => {
|
|
763
|
-
const [result] = await db
|
|
764
|
-
.select({
|
|
765
|
-
log: schema.deliveryLogs,
|
|
766
|
-
subscriptionName: schema.webhookSubscriptions.name,
|
|
767
|
-
})
|
|
768
|
-
.from(schema.deliveryLogs)
|
|
769
|
-
.leftJoin(
|
|
770
|
-
schema.webhookSubscriptions,
|
|
771
|
-
eq(schema.deliveryLogs.subscriptionId, schema.webhookSubscriptions.id)
|
|
772
|
-
)
|
|
773
|
-
.where(eq(schema.deliveryLogs.id, input.id));
|
|
774
|
-
|
|
775
|
-
if (!result) {
|
|
776
|
-
throw new ORPCError("NOT_FOUND", {
|
|
777
|
-
message: "Delivery log not found",
|
|
319
|
+
message: extractErrorMessage(error, "Failed to fetch options"),
|
|
778
320
|
});
|
|
779
321
|
}
|
|
780
|
-
|
|
781
|
-
return {
|
|
782
|
-
...result.log,
|
|
783
|
-
subscriptionName: result.subscriptionName ?? undefined,
|
|
784
|
-
createdAt: result.log.createdAt,
|
|
785
|
-
lastAttemptAt: result.log.lastAttemptAt ?? undefined,
|
|
786
|
-
nextRetryAt: result.log.nextRetryAt ?? undefined,
|
|
787
|
-
externalId: result.log.externalId ?? undefined,
|
|
788
|
-
errorMessage: result.log.errorMessage ?? undefined,
|
|
789
|
-
};
|
|
790
|
-
}),
|
|
791
|
-
|
|
792
|
-
retryDelivery: os.retryDelivery.handler(async ({ input }) => {
|
|
793
|
-
return deliveryCoordinator.retryDelivery(input.logId);
|
|
794
|
-
}),
|
|
795
|
-
|
|
796
|
-
getDeliveryStats: os.getDeliveryStats.handler(async ({ input }) => {
|
|
797
|
-
const { hours } = input;
|
|
798
|
-
const since = new Date(Date.now() - hours * 60 * 60 * 1000);
|
|
799
|
-
|
|
800
|
-
// Get counts by status
|
|
801
|
-
const statusCounts = await db
|
|
802
|
-
.select({
|
|
803
|
-
status: schema.deliveryLogs.status,
|
|
804
|
-
count: count(),
|
|
805
|
-
})
|
|
806
|
-
.from(schema.deliveryLogs)
|
|
807
|
-
.where(gte(schema.deliveryLogs.createdAt, since))
|
|
808
|
-
.groupBy(schema.deliveryLogs.status);
|
|
809
|
-
|
|
810
|
-
// Get counts by event type
|
|
811
|
-
const eventCounts = await db
|
|
812
|
-
.select({
|
|
813
|
-
eventType: schema.deliveryLogs.eventType,
|
|
814
|
-
count: count(),
|
|
815
|
-
})
|
|
816
|
-
.from(schema.deliveryLogs)
|
|
817
|
-
.where(gte(schema.deliveryLogs.createdAt, since))
|
|
818
|
-
.groupBy(schema.deliveryLogs.eventType);
|
|
819
|
-
|
|
820
|
-
// Get counts by provider (via subscription)
|
|
821
|
-
const providerCounts = await db
|
|
822
|
-
.select({
|
|
823
|
-
providerId: schema.webhookSubscriptions.providerId,
|
|
824
|
-
count: count(),
|
|
825
|
-
})
|
|
826
|
-
.from(schema.deliveryLogs)
|
|
827
|
-
.innerJoin(
|
|
828
|
-
schema.webhookSubscriptions,
|
|
829
|
-
eq(schema.deliveryLogs.subscriptionId, schema.webhookSubscriptions.id)
|
|
830
|
-
)
|
|
831
|
-
.where(gte(schema.deliveryLogs.createdAt, since))
|
|
832
|
-
.groupBy(schema.webhookSubscriptions.providerId);
|
|
833
|
-
|
|
834
|
-
// Build response
|
|
835
|
-
const statusMap = new Map(
|
|
836
|
-
statusCounts.map((s) => [s.status, Number(s.count)])
|
|
837
|
-
);
|
|
838
|
-
const total =
|
|
839
|
-
(statusMap.get("success") ?? 0) +
|
|
840
|
-
(statusMap.get("failed") ?? 0) +
|
|
841
|
-
(statusMap.get("retrying") ?? 0) +
|
|
842
|
-
(statusMap.get("pending") ?? 0);
|
|
843
|
-
|
|
844
|
-
return {
|
|
845
|
-
total,
|
|
846
|
-
successful: statusMap.get("success") ?? 0,
|
|
847
|
-
failed: statusMap.get("failed") ?? 0,
|
|
848
|
-
retrying: statusMap.get("retrying") ?? 0,
|
|
849
|
-
pending: statusMap.get("pending") ?? 0,
|
|
850
|
-
byEvent: eventCounts.map((e) => ({
|
|
851
|
-
eventType: e.eventType,
|
|
852
|
-
count: Number(e.count),
|
|
853
|
-
})),
|
|
854
|
-
byProvider: providerCounts.map((p) => ({
|
|
855
|
-
providerId: p.providerId,
|
|
856
|
-
count: Number(p.count),
|
|
857
|
-
})),
|
|
858
|
-
};
|
|
859
322
|
}),
|
|
860
323
|
});
|
|
861
324
|
}
|
|
862
325
|
|
|
863
326
|
export type IntegrationRouter = ReturnType<typeof createIntegrationRouter>;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Collect every string leaf in a JSON-like value into `out`. Used to build
|
|
330
|
+
* a per-call secret mask set from a resolved/submitted connection config so
|
|
331
|
+
* a provider error echoing a credential can be redacted before it crosses
|
|
332
|
+
* back to the browser. Mirrors the dispatch engine's run-secret capture.
|
|
333
|
+
*/
|
|
334
|
+
function collectStringLeaves(value: unknown, out: string[]): void {
|
|
335
|
+
if (typeof value === "string") {
|
|
336
|
+
out.push(value);
|
|
337
|
+
} else if (Array.isArray(value)) {
|
|
338
|
+
for (const v of value) collectStringLeaves(v, out);
|
|
339
|
+
} else if (value !== null && typeof value === "object") {
|
|
340
|
+
for (const v of Object.values(value)) collectStringLeaves(v, out);
|
|
341
|
+
}
|
|
342
|
+
}
|