@revenexx/studio-integrations 3.0.3 → 3.2.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +3 -2
  4. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +3 -2
  5. package/dist/runtime/components/Integrations/NodeInspector.vue +1 -1
  6. package/dist/runtime/components/Integrations/RunDetailPanel.vue +3 -2
  7. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +1 -1
  8. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +55 -33
  9. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +14 -2
  10. package/dist/runtime/composables/useIntegrationsAudit.d.ts +7 -2
  11. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -3
  12. package/dist/runtime/composables/useIntegrationsCredentials.js +9 -5
  13. package/dist/runtime/composables/useIntegrationsRuns.js +8 -4
  14. package/dist/runtime/composables/useIntegrationsSchedules.js +6 -3
  15. package/dist/runtime/composables/useIntegrationsSecrets.d.ts +6 -5
  16. package/dist/runtime/composables/useIntegrationsSecrets.js +3 -2
  17. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -1
  18. package/dist/runtime/composables/useIntegrationsTemplates.js +20 -15
  19. package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -4
  20. package/dist/runtime/composables/useNodeRetirement.js +5 -5
  21. package/dist/runtime/i18n/messages/de.json +20 -3
  22. package/dist/runtime/i18n/messages/en.json +20 -3
  23. package/dist/runtime/pages/integrations/audit.vue +43 -9
  24. package/dist/runtime/pages/integrations/credentials.vue +3 -2
  25. package/dist/runtime/pages/integrations/runs.vue +3 -1
  26. package/dist/runtime/pages/integrations/secrets.vue +3 -1
  27. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -23
  28. package/dist/runtime/types/integrations.d.ts +25 -7
  29. package/dist/runtime/utils/apiEnvelope.d.ts +15 -5
  30. package/dist/runtime/utils/apiEnvelope.js +11 -8
  31. package/dist/runtime/utils/credentialTest.d.ts +17 -0
  32. package/dist/runtime/utils/credentialTest.js +14 -0
  33. package/dist/runtime/utils/runEnvelope.d.ts +22 -24
  34. package/dist/runtime/utils/runEnvelope.js +2 -4
  35. package/dist/runtime/utils/stepError.d.ts +13 -0
  36. package/dist/runtime/utils/stepError.js +24 -0
  37. package/package.json +1 -1
  38. package/dist/runtime/data/integrationWorkflowSamples.d.ts +0 -18
  39. package/dist/runtime/data/integrationWorkflowSamples.js +0 -318
@@ -0,0 +1,17 @@
1
+ import type { CredentialTestResult } from '../composables/useIntegrationsCredentials.js';
2
+ /** What a connection test came to, as the three surfaces that run one draw it. */
3
+ export interface TestOutcomeView {
4
+ kind: 'ok' | 'failed' | 'untested';
5
+ /** The tone of the box: green, red, or neutral for a test that did not happen. */
6
+ toneClass: string;
7
+ text: string;
8
+ }
9
+ /**
10
+ * One reading of a test result for every surface (specs/credential-test.md).
11
+ *
12
+ * Three answers, not two: a test the platform could not run — its test broker had no
13
+ * room, or the reader may not try an edit — said nothing about the credential, and
14
+ * drawing it red told the operator the connection was broken (IN-22). The words are
15
+ * the platform's where it sent any, the studio's fixed sentence otherwise.
16
+ */
17
+ export declare function testOutcomeView(result: CredentialTestResult, t: (key: string, params?: Record<string, unknown>) => string): TestOutcomeView;
@@ -0,0 +1,14 @@
1
+ export function testOutcomeView(result, t) {
2
+ if (result.ok) {
3
+ return { kind: "ok", toneClass: "bg-emerald-500/10 text-emerald-700", text: result.message ?? t("integration.credentials.shared.test.ok") };
4
+ }
5
+ if (result.untested) {
6
+ const reason = result.error ?? result.message;
7
+ return {
8
+ kind: "untested",
9
+ toneClass: "bg-slate-500/10 text-slate-700",
10
+ text: reason ? t("integration.credentials.shared.test.notTestedBecause", { reason }) : t("integration.credentials.shared.test.notTested")
11
+ };
12
+ }
13
+ return { kind: "failed", toneClass: "bg-rose-500/10 text-rose-700", text: result.error ?? result.message ?? t("integration.credentials.shared.test.failed") };
14
+ }
@@ -1,28 +1,5 @@
1
1
  /** A run as the API sends it, before anything has been read out of it. */
2
2
  export type RunLike = Record<string, unknown>;
3
- /**
4
- * The run out of a single-run response, enveloped or not.
5
- *
6
- * `GET /workflows/{id}/runs/{runId}` answers the run itself, and the studio's own
7
- * mock and some callers wrap it in `{ data: … }`. So the envelope has to be
8
- * optional — but unwrapping on the mere presence of a `data` key is wrong: a run
9
- * that ever carried one would be unwrapped into it, and the caller would be
10
- * handed a field instead of a run. Hence the two guards: a body that already
11
- * looks like a run (`status`, `temporal_run_id`) is the run, whatever else it has
12
- * on it.
13
- *
14
- * The two guards are `RUN_OWN_FIELDS`, exported because `unwrapRecord` reads the
15
- * same endpoint from `useIntegrationsRuns.ts` and has to apply the same rule —
16
- * one list, two readers, rather than the two literals that used to sit here
17
- * (PO-394). What stays different is deliberate: this one answers `null` where
18
- * `unwrapRecord` throws, because a caller following a run has to count an
19
- * unusable answer as a failed read rather than have it raised at it.
20
- *
21
- * This is deliberately generous about the *rest* of the shape. It answers "where
22
- * is the run in this body", not "is this a usable run" — the caller decides that,
23
- * because a caller following a run has to be able to count an unusable answer as
24
- * a failed read rather than sit on it.
25
- */
26
3
  /**
27
4
  * The raw platform statuses a run does not come back from.
28
5
  *
@@ -37,7 +14,28 @@ export declare function isTerminalRun(run: {
37
14
  } | null | undefined): boolean;
38
15
  /**
39
16
  * The fields that mark a body as the run itself rather than an envelope around
40
- * one. A run carrying its own `data` field would otherwise be unwrapped into it.
17
+ * one. A run carrying its own `data` field would otherwise be taken for an envelope.
41
18
  */
42
19
  export declare const RUN_OWN_FIELDS: readonly ["status", "temporal_run_id"];
20
+ /**
21
+ * The run out of a single-run response — the body itself.
22
+ *
23
+ * `GET /workflows/{id}/runs/{runId}` answers the run bare, and no version of it
24
+ * sent an envelope. Until IN-22 this accepted `{ data: run }` as well, because the
25
+ * studio's own mocks wrapped it; that tolerance is what let them stay wrong, so a
26
+ * body shaped like an envelope is now no run at all. "Shaped like an envelope"
27
+ * needs care: a run may carry a `data` field of its own, so a body that already
28
+ * looks like a run (`status`, `temporal_run_id`) is the run, whatever else it has
29
+ * on it.
30
+ *
31
+ * Those two guards are `RUN_OWN_FIELDS`, exported because `bareRecord` reads the
32
+ * same endpoint from `useIntegrationsRuns.ts` and has to apply the same rule —
33
+ * one list, two readers, rather than the two literals that used to sit here
34
+ * (PO-394). What stays different is deliberate: this one answers `null` where
35
+ * `bareRecord` throws, because a caller following a run has to count an
36
+ * unusable answer as a failed read rather than have it raised at it.
37
+ *
38
+ * This is deliberately generous about the *rest* of the shape. It answers "is
39
+ * there a run in this body", not "is this a usable run" — the caller decides that.
40
+ */
43
41
  export declare function unwrapRun(res: unknown): RunLike | null;
@@ -4,10 +4,8 @@ export function isTerminalRun(run) {
4
4
  }
5
5
  export const RUN_OWN_FIELDS = ["status", "temporal_run_id"];
6
6
  export function unwrapRun(res) {
7
- if (!res || typeof res !== "object") return null;
7
+ if (!res || typeof res !== "object" || Array.isArray(res)) return null;
8
8
  const o = res;
9
- if ("data" in o && o.data && typeof o.data === "object" && !RUN_OWN_FIELDS.some((f) => f in o)) {
10
- return o.data;
11
- }
9
+ if ("data" in o && !RUN_OWN_FIELDS.some((f) => f in o)) return null;
12
10
  return o;
13
11
  }
@@ -0,0 +1,13 @@
1
+ import type { RunStepError, RunStepFrame } from '../types/integrations.js';
2
+ /**
3
+ * A failed step's error as an operator reads it (IN-12).
4
+ *
5
+ * The platform sends it as an object — the message, the failure's type, and where
6
+ * the node package's own code was when it failed (`frames`, PO-447). The studio used
7
+ * to read it as text, and a template prints an object as JSON. A row from before the
8
+ * object, or a mock that still answers a string, is read as the message itself, so
9
+ * no shape ever reaches the screen as a dump.
10
+ */
11
+ export declare function stepError(error: unknown): RunStepError | null;
12
+ /** One frame as a reader names it: `src/nodes/Foo.ts:12:5` and the function, where known. */
13
+ export declare function frameLabel(frame: RunStepFrame): string;
@@ -0,0 +1,24 @@
1
+ export function stepError(error) {
2
+ if (typeof error === "string") return error.trim() === "" ? null : { message: error };
3
+ if (error === null || typeof error !== "object") return null;
4
+ const raw = error;
5
+ const message = typeof raw.message === "string" && raw.message.trim() !== "" ? raw.message : null;
6
+ if (message === null) return null;
7
+ return {
8
+ message,
9
+ ...typeof raw.type === "string" && raw.type.trim() !== "" ? { type: raw.type } : {},
10
+ ...Array.isArray(raw.frames) ? { frames: raw.frames.filter(isFrame).map(withName) } : {}
11
+ };
12
+ }
13
+ export function frameLabel(frame) {
14
+ const at = `${frame.file}:${frame.line}:${frame.column}`;
15
+ return frame.name ? `${frame.name} (${at})` : at;
16
+ }
17
+ function isFrame(value) {
18
+ if (value === null || typeof value !== "object") return false;
19
+ const f = value;
20
+ return typeof f.file === "string" && typeof f.line === "number" && typeof f.column === "number";
21
+ }
22
+ function withName(frame) {
23
+ return typeof frame.name === "string" && frame.name.trim() !== "" ? frame : { file: frame.file, line: frame.line, column: frame.column };
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
- "version": "3.0.3",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
6
6
  "repository": {
@@ -1,18 +0,0 @@
1
- import type { Connection, ConnectionTemplate, Workflow } from '../types/integrations.js';
2
- /**
3
- * Seed data for the workflow list. Built strictly with the manifests the
4
- * backend currently has fixtures for (`rest-get`, `control-if`,
5
- * `control-switch`) and the trigger output port the reference workflow
6
- * blobs use (`default` — see
7
- * `playground/integrations/tests/Fixtures/WorkflowBlobs/*.json`).
8
- *
9
- * If you change a manifest's `slug` / `version`, change it here too.
10
- */
11
- export declare const SAMPLE_WORKFLOWS: Workflow[];
12
- export declare const SAMPLE_SECRETS: {
13
- key: string;
14
- created_at: string;
15
- updated_at: string;
16
- }[];
17
- export declare const CONNECTION_TEMPLATES: ConnectionTemplate[];
18
- export declare const SAMPLE_CONNECTIONS: Connection[];
@@ -1,318 +0,0 @@
1
- export const SAMPLE_WORKFLOWS = [
2
- // -----------------------------------------------------------------------
3
- // 1) Manual REST call — mirrors manual-rest-call.json
4
- // -----------------------------------------------------------------------
5
- {
6
- id: 1,
7
- name: "Manual REST GET",
8
- description: "User-triggered call to a REST endpoint configured via workflow variables.",
9
- blob_definition_version: "v0-draft",
10
- active: true,
11
- revision: 4,
12
- created_at: "2026-04-12T08:00:00Z",
13
- updated_at: "2026-05-08T02:14:11Z",
14
- blob: {
15
- nodeManifestVersion: "v0-draft",
16
- name: { en: "Manual REST GET", de: "Manueller REST-Abruf" },
17
- description: {
18
- en: "User-triggered call to a REST endpoint configured via workflow variables.",
19
- de: "Benutzerausgel\xF6ster Aufruf eines REST-Endpunkts, konfiguriert \xFCber Workflow-Variablen."
20
- },
21
- variables: {
22
- endpoint: {
23
- type: "string",
24
- default: "https://api.example.com/health",
25
- description: { en: "Target URL", de: "Ziel-URL" }
26
- },
27
- timeoutSeconds: { type: "number", default: 30 }
28
- },
29
- triggers: [
30
- {
31
- id: "manual-1",
32
- type: "manual",
33
- name: { en: "Run now", de: "Jetzt ausf\xFChren" },
34
- config: {}
35
- }
36
- ],
37
- nodes: [
38
- {
39
- id: "fetch",
40
- nodeSlug: "rest-get",
41
- nodeVersion: "1.0.0",
42
- config: {
43
- url: "${{ vars.endpoint }}",
44
- timeout: "${{ vars.timeoutSeconds }}"
45
- },
46
- position: { x: 220, y: 100 },
47
- retry: { maxAttempts: 3, backoffSeconds: 2, strategy: "exponential" }
48
- }
49
- ],
50
- edges: [
51
- { id: "e1", from: { nodeId: "manual-1", output: "default" }, to: { nodeId: "fetch" } }
52
- ]
53
- }
54
- },
55
- // -----------------------------------------------------------------------
56
- // 2) Webhook receiver with error routing — mirrors webhook-with-error-routing.json
57
- // -----------------------------------------------------------------------
58
- {
59
- id: 2,
60
- name: "Webhook order receiver",
61
- description: "Receives Stripe order webhooks, validates the payload, and forwards to the fulfillment API. Errors flow into a notification node via the reserved error output.",
62
- blob_definition_version: "v0-draft",
63
- active: true,
64
- revision: 3,
65
- created_at: "2026-04-22T11:30:00Z",
66
- updated_at: "2026-05-09T15:02:33Z",
67
- blob: {
68
- nodeManifestVersion: "v0-draft",
69
- name: "Webhook order receiver",
70
- description: "Receives Stripe order webhooks, validates the payload, and forwards to the fulfillment API. Errors flow into a notification node via the reserved error output.",
71
- triggers: [
72
- {
73
- id: "stripe-hook",
74
- type: "webhook",
75
- name: "Stripe order created",
76
- config: {
77
- path: "/webhooks/stripe/orders",
78
- method: "POST",
79
- secretRef: "STRIPE_WEBHOOK_SECRET"
80
- }
81
- }
82
- ],
83
- nodes: [
84
- {
85
- id: "validate",
86
- nodeSlug: "control-if",
87
- nodeVersion: "1.0.0",
88
- config: { condition: "${{ trigger.payload.type }}" },
89
- position: { x: 260, y: 80 }
90
- },
91
- {
92
- id: "forward",
93
- nodeSlug: "rest-get",
94
- nodeVersion: "1.0.0",
95
- config: {
96
- url: "https://fulfillment.internal/orders",
97
- "bearer-token": "FULFILLMENT_API_TOKEN"
98
- },
99
- position: { x: 520, y: 0 },
100
- retry: { maxAttempts: 4, backoffSeconds: 5, strategy: "linear" }
101
- },
102
- {
103
- id: "notify-failure",
104
- nodeSlug: "rest-get",
105
- nodeVersion: "1.0.0",
106
- config: { url: "https://alerts.internal/notify" },
107
- position: { x: 760, y: 200 }
108
- }
109
- ],
110
- edges: [
111
- { id: "e1", from: { nodeId: "stripe-hook", output: "default" }, to: { nodeId: "validate" } },
112
- { id: "e2", from: { nodeId: "validate", output: "true" }, to: { nodeId: "forward" } },
113
- { id: "e3", from: { nodeId: "forward", output: "error" }, to: { nodeId: "notify-failure" } }
114
- ]
115
- }
116
- },
117
- // -----------------------------------------------------------------------
118
- // 3) Scheduled ETL with branching — mirrors scheduled-with-branching.json
119
- // -----------------------------------------------------------------------
120
- {
121
- id: 3,
122
- name: "Daily ETL with branching",
123
- description: "Pulls orders from upstream, branches per status with control-switch.",
124
- blob_definition_version: "v0-draft",
125
- active: true,
126
- revision: 7,
127
- created_at: "2026-04-12T08:00:00Z",
128
- updated_at: "2026-05-08T02:14:11Z",
129
- blob: {
130
- nodeManifestVersion: "v0-draft",
131
- name: "Daily ETL with branching",
132
- triggers: [
133
- {
134
- id: "nightly",
135
- type: "schedule",
136
- config: { cron: "0 3 * * *", timezone: "Europe/Berlin" }
137
- }
138
- ],
139
- variables: {
140
- source: { type: "string", default: "primary" }
141
- },
142
- nodes: [
143
- {
144
- id: "fetch",
145
- nodeSlug: "rest-get",
146
- nodeVersion: "1.0.0",
147
- config: { url: "https://api.example.com/orders?source=${{ vars.source }}" },
148
- position: { x: 220, y: 80 },
149
- retry: { maxAttempts: 5, backoffSeconds: 1, strategy: "exponential" }
150
- },
151
- {
152
- id: "route",
153
- nodeSlug: "control-switch",
154
- nodeVersion: "1.0.0",
155
- config: {
156
- expression: "${{ nodes.fetch.outputs.success }}",
157
- cases: ["paid", "pending", "failed"]
158
- },
159
- position: { x: 480, y: 80 }
160
- },
161
- {
162
- id: "ship",
163
- nodeSlug: "rest-get",
164
- nodeVersion: "1.0.0",
165
- config: { url: "https://api.example.com/ship" },
166
- position: { x: 760, y: 0 }
167
- },
168
- {
169
- id: "alert",
170
- nodeSlug: "rest-get",
171
- nodeVersion: "1.0.0",
172
- config: { url: "https://api.example.com/alert" },
173
- position: { x: 760, y: 200 }
174
- }
175
- ],
176
- edges: [
177
- { id: "e1", from: { nodeId: "nightly", output: "default" }, to: { nodeId: "fetch" } },
178
- { id: "e2", from: { nodeId: "fetch", output: "success" }, to: { nodeId: "route" } },
179
- { id: "e3", from: { nodeId: "route", output: "paid" }, to: { nodeId: "ship" } },
180
- { id: "e4", from: { nodeId: "fetch", output: "error" }, to: { nodeId: "alert" } }
181
- ]
182
- }
183
- },
184
- // -----------------------------------------------------------------------
185
- // 4) Empty draft — exists to demonstrate the "Draft" status group
186
- // -----------------------------------------------------------------------
187
- {
188
- id: 4,
189
- name: "Empty draft",
190
- description: "No nodes yet. Drag from the palette to start.",
191
- blob_definition_version: "v0-draft",
192
- active: false,
193
- revision: 0,
194
- created_at: "2026-05-10T16:00:00Z",
195
- updated_at: "2026-05-10T16:00:00Z",
196
- blob: {
197
- nodeManifestVersion: "v0-draft",
198
- name: "Empty draft",
199
- triggers: [
200
- { id: "trg-1", type: "manual", config: {} }
201
- ],
202
- nodes: [],
203
- edges: []
204
- }
205
- }
206
- ];
207
- export const SAMPLE_SECRETS = [
208
- { key: "STRIPE_WEBHOOK_SECRET", created_at: "2026-04-01T08:00:00Z", updated_at: "2026-04-01T08:00:00Z" },
209
- { key: "FULFILLMENT_API_TOKEN", created_at: "2026-04-15T11:30:00Z", updated_at: "2026-04-15T11:30:00Z" },
210
- { key: "ERP_API_KEY", created_at: "2026-03-12T09:14:00Z", updated_at: "2026-05-01T14:00:00Z" },
211
- { key: "SLACK_WEBHOOK_URL", created_at: "2026-02-04T16:00:00Z", updated_at: "2026-02-04T16:00:00Z" },
212
- { key: "HUBSPOT_ACCESS_TOKEN", created_at: "2026-04-22T12:00:00Z", updated_at: "2026-04-22T12:00:00Z" }
213
- ];
214
- export const CONNECTION_TEMPLATES = [
215
- {
216
- vendor: "shopify",
217
- name: "Shopify",
218
- category: "shop",
219
- icon: "ShoppingBag",
220
- description: "Ingest orders, customers and products from a Shopify store.",
221
- requiredSecrets: [
222
- { key: "SHOPIFY_ACCESS_TOKEN", label: "Admin API token", sensitive: true },
223
- { key: "SHOPIFY_WEBHOOK_SECRET", label: "Webhook signing secret", sensitive: true }
224
- ]
225
- },
226
- {
227
- vendor: "sap-business-one",
228
- name: "SAP Business One",
229
- category: "erp",
230
- icon: "Server",
231
- description: "Sync products, prices and orders with SAP Business One.",
232
- requiredSecrets: [
233
- { key: "SAP_B1_USER", label: "Service account", sensitive: false },
234
- { key: "SAP_B1_PASSWORD", label: "Service password", sensitive: true }
235
- ]
236
- },
237
- {
238
- vendor: "hubspot",
239
- name: "HubSpot",
240
- category: "crm",
241
- icon: "Contact",
242
- description: "Two-way sync for contacts and deals.",
243
- requiredSecrets: [{ key: "HUBSPOT_ACCESS_TOKEN", label: "Private app token", sensitive: true }]
244
- },
245
- {
246
- vendor: "akeneo",
247
- name: "Akeneo PIM",
248
- category: "pim",
249
- icon: "Layers",
250
- description: "Pull product data and enrichment from Akeneo.",
251
- requiredSecrets: [
252
- { key: "AKENEO_CLIENT_ID", label: "Client ID", sensitive: false },
253
- { key: "AKENEO_CLIENT_SECRET", label: "Client secret", sensitive: true }
254
- ]
255
- },
256
- {
257
- vendor: "slack",
258
- name: "Slack",
259
- category: "other",
260
- icon: "MessageSquare",
261
- description: "Post notifications and alerts to Slack channels.",
262
- requiredSecrets: [{ key: "SLACK_WEBHOOK_URL", label: "Incoming webhook URL", sensitive: true }]
263
- },
264
- {
265
- vendor: "stripe",
266
- name: "Stripe",
267
- category: "other",
268
- icon: "CreditCard",
269
- description: "Receive payment events from Stripe.",
270
- requiredSecrets: [{ key: "STRIPE_WEBHOOK_SECRET", label: "Webhook signing secret", sensitive: true }]
271
- }
272
- ];
273
- export const SAMPLE_CONNECTIONS = [
274
- {
275
- id: "conn-stripe-1",
276
- vendor: "stripe",
277
- name: "Stripe \xB7 live",
278
- status: "connected",
279
- lastVerifiedAt: "2026-05-10T07:14:11Z",
280
- secretRefs: ["STRIPE_WEBHOOK_SECRET"],
281
- config: {},
282
- createdAt: "2026-04-15T11:30:00Z",
283
- updatedAt: "2026-05-09T15:02:33Z"
284
- },
285
- {
286
- id: "conn-sap-1",
287
- vendor: "sap-business-one",
288
- name: "SAP Business One \xB7 production",
289
- status: "misconfigured",
290
- lastVerifiedAt: "2026-05-04T10:00:00Z",
291
- secretRefs: [],
292
- config: { host: "sap.acme.test" },
293
- createdAt: "2026-04-22T11:00:00Z",
294
- updatedAt: "2026-05-04T10:00:00Z"
295
- },
296
- {
297
- id: "conn-hubspot-1",
298
- vendor: "hubspot",
299
- name: "HubSpot \xB7 Sales",
300
- status: "connected",
301
- lastVerifiedAt: "2026-05-10T08:00:00Z",
302
- secretRefs: ["HUBSPOT_ACCESS_TOKEN"],
303
- config: { portalId: "23456789" },
304
- createdAt: "2026-04-22T12:00:00Z",
305
- updatedAt: "2026-04-22T12:00:00Z"
306
- },
307
- {
308
- id: "conn-slack-1",
309
- vendor: "slack",
310
- name: "Slack \xB7 #ops",
311
- status: "connected",
312
- lastVerifiedAt: "2026-05-10T09:00:00Z",
313
- secretRefs: ["SLACK_WEBHOOK_URL"],
314
- config: { channel: "#ops" },
315
- createdAt: "2026-02-04T16:00:00Z",
316
- updatedAt: "2026-02-04T16:00:00Z"
317
- }
318
- ];