@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/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 type { SignalService } from "@checkstack/signal-common";
10
- import { eq, desc, and, gte, count } from "drizzle-orm";
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
- * Creates the integration router using contract-based implementation.
112
- *
113
- * Auth and access rules are automatically enforced via autoAuthMiddleware
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
- db,
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: p.supportedEvents,
405
- configSchema:
406
- providerRegistry.getProviderConfigSchema(p.qualifiedId) ?? {},
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: extractErrorMessage(error),
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 redacted version
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: config, // Will be redacted in real usage
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: (updates.config ?? {}) as Record<string, unknown>,
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: extractErrorMessage(error),
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
+ }