@revenexx/studio-integrations 3.0.3 → 3.1.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 (36) hide show
  1. package/CHANGELOG.md +12 -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/TemplateFlowPreview.vue +1 -1
  7. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +28 -10
  8. package/dist/runtime/composables/useIntegrationsAudit.d.ts +7 -2
  9. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -3
  10. package/dist/runtime/composables/useIntegrationsCredentials.js +9 -5
  11. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  12. package/dist/runtime/composables/useIntegrationsRuns.js +8 -4
  13. package/dist/runtime/composables/useIntegrationsSchedules.js +6 -3
  14. package/dist/runtime/composables/useIntegrationsSecrets.d.ts +6 -5
  15. package/dist/runtime/composables/useIntegrationsSecrets.js +3 -2
  16. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
  17. package/dist/runtime/composables/useIntegrationsTemplates.js +5 -6
  18. package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -4
  19. package/dist/runtime/composables/useNodeRetirement.js +5 -5
  20. package/dist/runtime/i18n/messages/de.json +16 -3
  21. package/dist/runtime/i18n/messages/en.json +16 -3
  22. package/dist/runtime/pages/integrations/audit.vue +43 -9
  23. package/dist/runtime/pages/integrations/credentials.vue +3 -2
  24. package/dist/runtime/pages/integrations/runs.vue +3 -1
  25. package/dist/runtime/pages/integrations/secrets.vue +3 -1
  26. package/dist/runtime/pages/integrations/templates/[slug].vue +1 -1
  27. package/dist/runtime/types/integrations.d.ts +9 -6
  28. package/dist/runtime/utils/apiEnvelope.d.ts +15 -5
  29. package/dist/runtime/utils/apiEnvelope.js +11 -8
  30. package/dist/runtime/utils/credentialTest.d.ts +17 -0
  31. package/dist/runtime/utils/credentialTest.js +14 -0
  32. package/dist/runtime/utils/runEnvelope.d.ts +22 -24
  33. package/dist/runtime/utils/runEnvelope.js +2 -4
  34. package/package.json +1 -1
  35. package/dist/runtime/data/integrationWorkflowSamples.d.ts +0 -18
  36. package/dist/runtime/data/integrationWorkflowSamples.js +0 -318
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @revenexx/studio-integrations
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 23bb093: The audit log offers every kind of change and of thing a tenant's log can hold — a cancelled or resumed run, a retired or restored node version, deleted stored state — and names a trigger by its kind and workflow and a run by its workflow, since the platform records no name for either. A connection test the platform could not run (its test broker was busy, or the reader may not try changed values) now reads "Not tested" instead of a failed connection. A secret key may be anything the platform stores — lower case and hyphens included, up to 191 characters; a runs search is cut to the platform's 200 characters instead of being refused; an install name stops at 255. Installing a template wires a step with two credential settings setting by setting,, a template's industry outside the six the studio used to know is kept and found by search, and a template started by a webhook says which method it answers rather than a path it never carries. An answer the schedules page cannot read is reported as a failed load, not as a tenant with no schedules.
8
+
9
+ For hosts: single workflows, runs and credentials are now read only in the bare shape the Integrations service sends; a `data`-wrapped answer is refused. `instantiate()` also accepts `{ nodeId: { settingKey: credentialId } }` beside a bare id. Three exported types widen to what the service sends — `WorkflowTemplate.industries` is `string[]`, `CredentialType.icon` and `description` may be `null`, and a credential field option's `label` may be localised.
10
+
11
+ ### Patch Changes
12
+
13
+ - ef3f3eb: A dead letter's `reason` is typed as the string the platform always sends (empty when nothing was recorded) and its `trigger_id` as the number it is. Rotating a webhook's endpoint secret reads the platform's answer in the one shape it has ever sent. Nothing else a host sees changes: the rest of this release brings the browser tests' mock in line with what the Integrations service actually answers.
14
+
3
15
  ## 3.0.3
4
16
 
5
17
  ### Patch Changes
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
3
  "configKey": "studioIntegrations",
4
- "version": "3.0.3",
4
+ "version": "3.1.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
@@ -23,6 +23,7 @@ import { resolveLocalized } from "../../utils/localize";
23
23
  import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
24
24
  import { errorReason } from "../../utils/apiError";
25
25
  import { keepOpenBeside } from "../../utils/dialog";
26
+ import { testOutcomeView } from "../../utils/credentialTest";
26
27
  const props = defineProps({
27
28
  open: { type: Boolean, required: true },
28
29
  credentialType: { type: [String, Array], required: false }
@@ -247,12 +248,12 @@ async function submit() {
247
248
  <div
248
249
  v-if="testResult"
249
250
  class="rounded-md px-3 py-2 text-sm"
250
- :class="testResult.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
251
+ :class="testOutcomeView(testResult, t).toneClass"
251
252
  >
252
253
  <!-- The type's own sentence, either way. This is the surface where it is
253
254
  worth the most: nothing is stored yet, so the operator is deciding
254
255
  whether these access data reach what they meant (specs/credential-test.md). -->
255
- {{ testResult.ok ? testResult.message ?? t("integration.credentials.shared.test.ok") : testResult.error ?? testResult.message ?? t("integration.credentials.shared.test.failed") }}
256
+ {{ testOutcomeView(testResult, t).text }}
256
257
  </div>
257
258
  </div>
258
259
 
@@ -39,6 +39,7 @@ import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
39
39
  import { resolveLocalized } from "../../utils/localize";
40
40
  import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
41
41
  import { keepOpenBeside } from "../../utils/dialog";
42
+ import { testOutcomeView } from "../../utils/credentialTest";
42
43
  const props = defineProps({
43
44
  open: { type: Boolean, required: true },
44
45
  credential: { type: [Object, null], required: true },
@@ -375,9 +376,9 @@ async function connect() {
375
376
  <div
376
377
  v-if="liveTest"
377
378
  class="rounded-md px-3 py-2 text-sm"
378
- :class="liveTest.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
379
+ :class="testOutcomeView(liveTest, t).toneClass"
379
380
  >
380
- {{ liveTest.ok ? liveTest.message ?? t("integration.credentials.shared.test.ok") : liveTest.error ?? liveTest.message ?? t("integration.credentials.shared.test.failed") }}
381
+ {{ testOutcomeView(liveTest, t).text }}
381
382
  </div>
382
383
 
383
384
  </div>
@@ -472,7 +472,7 @@ async function rotateEndpointSecret() {
472
472
  const api2 = useIntegrationsApi();
473
473
  const res = await api2.post(`/workflows/${props.workflowId}/triggers/${wt.id}/rotate-secret`, {});
474
474
  const body = res ?? {};
475
- const secret = body.inbound_secret ?? body.data?.inbound_secret ?? null;
475
+ const secret = body.inbound_secret ?? null;
476
476
  if (secret) {
477
477
  rotatedSecret.value = secret;
478
478
  rotateOpen.value = true;
@@ -30,7 +30,7 @@ const boxes = computed(() => {
30
30
  triggers.forEach((t2, i) => {
31
31
  const pos = blob.triggerPositions?.[t2.id] ?? { x: 40, y: 40 + i * 160 };
32
32
  const template = TRIGGER_TEMPLATES.find((tt) => tt.type === t2.type);
33
- const subtitle = t2.type === "schedule" && t2.config?.cron ? describeCron(String(t2.config.cron)) ?? String(t2.config.cron) : t2.type === "webhook" && t2.config?.path ? String(t2.config.path) : resolveLocalized(template?.description ?? "");
33
+ const subtitle = t2.type === "schedule" && t2.config?.cron ? describeCron(String(t2.config.cron)) ?? String(t2.config.cron) : t2.type === "webhook" && t2.config?.method ? String(t2.config.method).toUpperCase() : resolveLocalized(template?.description ?? "");
34
34
  out.push({
35
35
  key: `t-${t2.id}`,
36
36
  kind: "trigger",
@@ -29,6 +29,7 @@ const installing = ref(false);
29
29
  const error = ref(null);
30
30
  const name = ref("");
31
31
  const bindings = reactive({});
32
+ const slotKey = (nodeId, configKey) => `${nodeId}\0${configKey}`;
32
33
  let run = 0;
33
34
  watch(() => props.slug, async (slug) => {
34
35
  const mine = ++run;
@@ -68,16 +69,30 @@ const secretChecks = computed(() => {
68
69
  present: secretKeys.value.includes(key)
69
70
  }));
70
71
  });
71
- function nodesFor(usedBy) {
72
- return [...new Set(usedBy.map((u) => u.nodeId))];
72
+ function slotsFor(usedBy) {
73
+ const all = credentialReqs.value.flatMap((r) => r.usedBy);
74
+ const seen = /* @__PURE__ */ new Set();
75
+ return usedBy.filter((u) => {
76
+ const key = slotKey(u.nodeId, u.configKey);
77
+ if (seen.has(key)) return false;
78
+ seen.add(key);
79
+ return true;
80
+ }).map((u) => ({
81
+ key: slotKey(u.nodeId, u.configKey),
82
+ nodeId: u.nodeId,
83
+ configKey: u.configKey,
84
+ label: new Set(all.filter((o) => o.nodeId === u.nodeId).map((o) => o.configKey)).size > 1 ? t("integration.templates.detail.install.settingLabel", { id: u.nodeId, setting: u.configKey }) : t("integration.templates.detail.install.nodeLabel", { id: u.nodeId })
85
+ }));
73
86
  }
74
87
  async function confirmInstall() {
75
88
  installing.value = true;
76
89
  error.value = null;
77
90
  try {
78
91
  const credentialBindings = {};
79
- for (const [nodeId, credId] of Object.entries(bindings)) {
80
- if (credId && credId !== SELECT_LATER) credentialBindings[nodeId] = credId;
92
+ for (const [key, credId] of Object.entries(bindings)) {
93
+ if (!credId || credId === SELECT_LATER) continue;
94
+ const [nodeId, configKey] = key.split("\0");
95
+ (credentialBindings[nodeId] ??= {})[configKey] = credId;
81
96
  }
82
97
  const id = await instantiate(props.slug, {
83
98
  name: name.value.trim(),
@@ -98,7 +113,8 @@ async function confirmInstall() {
98
113
  <div class="space-y-4 py-1 max-h-[28rem] overflow-y-auto pr-1">
99
114
  <div class="space-y-1.5">
100
115
  <Label for="tpl-install-name">{{ t("integration.templates.detail.install.nameLabel") }}</Label>
101
- <Input id="tpl-install-name" v-model="name" class="bg-white" :placeholder="t('integration.templates.detail.install.namePlaceholder')" />
116
+ <!-- 255: the platform's limit on a workflow's name (InstantiateTemplateRequest). -->
117
+ <Input id="tpl-install-name" v-model="name" class="bg-white" :maxlength="255" :placeholder="t('integration.templates.detail.install.namePlaceholder')" />
102
118
  </div>
103
119
 
104
120
  <div v-if="loading" class="flex items-center gap-2 py-4 text-sm text-muted-foreground">
@@ -131,15 +147,17 @@ async function confirmInstall() {
131
147
  <NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
132
148
  {{ t("integration.templates.detail.install.orInstallLater") }}
133
149
  </p>
134
- <div v-for="nodeId in nodesFor(req.usedBy)" v-else :key="nodeId" class="space-y-1">
135
- <Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">{{ t("integration.templates.detail.install.nodeLabel", { id: nodeId }) }}</Label>
150
+ <div v-for="(slot, si) in slotsFor(req.usedBy)" v-else :key="slot.key" class="space-y-1">
151
+ <!-- By position, not by joining two free strings: `a-b`+`c` and `a`+`b-c`
152
+ would share an id, and a label would name the wrong select. -->
153
+ <Label :for="`tpl-cred-${req.slug}-${si}`" class="text-xs text-muted-foreground font-mono">{{ slot.label }}</Label>
136
154
  <Select
137
- :model-value="bindings[nodeId] ?? SELECT_LATER"
155
+ :model-value="bindings[slot.key] ?? SELECT_LATER"
138
156
  @update:model-value="(v) => {
139
- bindings[nodeId] = v ?? SELECT_LATER;
157
+ bindings[slot.key] = v ?? SELECT_LATER;
140
158
  }"
141
159
  >
142
- <SelectTrigger :id="`tpl-cred-${nodeId}`" class="bg-white">
160
+ <SelectTrigger :id="`tpl-cred-${req.slug}-${si}`" class="bg-white">
143
161
  <SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
144
162
  </SelectTrigger>
145
163
  <SelectContent>
@@ -1,5 +1,3 @@
1
- export type AuditResourceType = 'credential' | 'secret' | 'workflow' | 'trigger' | 'workflow_run';
2
- export type AuditAction = 'create' | 'update' | 'delete' | 'run.started' | 'run.finished' | 'run.failed';
3
1
  export interface AuditEntry {
4
2
  id: number;
5
3
  org_id?: string;
@@ -8,12 +6,19 @@ export interface AuditEntry {
8
6
  /** As the actor's sign-in stated it when the entry was written; null for the platform, and for entries from before names were recorded (PO-609). */
9
7
  actor_name?: string | null;
10
8
  actor_email?: string | null;
9
+ /** Whether the platform wrote the entry with no person behind it, and by which route it arrived. Sent on every row; not shown yet. */
10
+ via_system?: boolean;
11
+ channel?: string;
11
12
  action: string;
12
13
  resource_type: string;
13
14
  resource_id?: string;
14
15
  metadata?: Record<string, unknown> | null;
15
16
  created_at: string;
16
17
  }
18
+ /**
19
+ * The narrowing the feed itself accepts. The page sends none of it today — it
20
+ * narrows in the browser over what it has loaded (specs/audit-log.md `## Gaps`).
21
+ */
17
22
  export interface AuditFilters {
18
23
  sub?: string;
19
24
  resource_type?: string;
@@ -8,11 +8,13 @@ import { IntegrationsApiError } from '../utils/apiError.js';
8
8
  * GET /credentials[?type=slug] → list
9
9
  * POST /credentials → create { credential_type_slug, name, config }
10
10
  * GET /credentials/{id} → show (masked)
11
- * PATCH /credentials/{id} → update { name?, config? } (blank secret = keep)
11
+ * PATCH /credentials/{id} → update { name?, config? } (blank secret = keep,
12
+ * unless the same edit changes any other value:
13
+ * then 422 at config.<secret>, PO-765)
12
14
  * DELETE /credentials/{id} → remove
13
15
  * POST /credentials/test → inline test { credential_type_slug, config }
14
- * POST /credentials/{id}/test → test saved instance
15
- * POST /credentials/{id}/oauth/authorize-url → { authorize_url, state }
16
+ * POST /credentials/{id}/test → test { config? } → { ok, message, recorded, last_test_at, last_test_ok }
17
+ * POST /credentials/{id}/oauth/authorize-url → { authorize_url }
16
18
  */
17
19
  /**
18
20
  * `message` is what the credential type said about the test, whichever way it went —
@@ -33,6 +35,12 @@ export interface CredentialTestResult {
33
35
  * test of an edit that tried values other than the stored ones (PO-535).
34
36
  */
35
37
  recorded?: boolean;
38
+ /**
39
+ * The test did not happen, so it says nothing about the credential: the platform's
40
+ * test broker had no room for it, or the reader may not try changed values (IN-22).
41
+ * Drawn neither as a pass nor as a failure.
42
+ */
43
+ untested?: true;
36
44
  /**
37
45
  * The refusal the service answered instead of testing, held as it arrived: values it
38
46
  * would not try as given — a stored secret kept for a changed server (PO-765). A form
@@ -2,7 +2,7 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
5
- import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
5
+ import { bareRecord, unwrapList } from "../utils/apiEnvelope.js";
6
6
  import { useLoadGate } from "./useLoadGate.js";
7
7
  export function useIntegrationsCredentials() {
8
8
  const credentials = useState("integrations.credentials", () => []);
@@ -33,15 +33,15 @@ export function useIntegrationsCredentials() {
33
33
  return credentials.value.filter((c) => c.credential_type_slug === slug);
34
34
  }
35
35
  async function get(id) {
36
- return unwrapRecord(await api.get(`/credentials/${encodeURIComponent(id)}`));
36
+ return bareRecord(await api.get(`/credentials/${encodeURIComponent(id)}`));
37
37
  }
38
38
  async function create(input) {
39
- const created = unwrapRecord(await api.post("/credentials", input));
39
+ const created = bareRecord(await api.post("/credentials", input));
40
40
  await load(true);
41
41
  return created;
42
42
  }
43
43
  async function update(id, input) {
44
- const updated = unwrapRecord(await api.patch(`/credentials/${encodeURIComponent(id)}`, input));
44
+ const updated = bareRecord(await api.patch(`/credentials/${encodeURIComponent(id)}`, input));
45
45
  await load(true);
46
46
  return updated;
47
47
  }
@@ -63,15 +63,19 @@ export function useIntegrationsCredentials() {
63
63
  `/credentials/${encodeURIComponent(id)}/test`,
64
64
  config ? { config } : void 0
65
65
  );
66
+ if (!config && res?.ok === false && res.recorded === false) return { ...res, untested: true };
66
67
  return res?.ok === false ? res : { ok: true, message: res?.message, recorded: res?.recorded };
67
68
  } catch (e) {
69
+ if (config && e instanceof IntegrationsApiError && e.status === 403) {
70
+ return { ok: false, untested: true, error: t("integration.credentials.shared.test.notAllowed") };
71
+ }
68
72
  const refusal = e instanceof IntegrationsApiError && e.status === 422 ? e : void 0;
69
73
  return { ok: false, error: errorReason(e, t("integration.credentials.error.testFailed")), refusal };
70
74
  }
71
75
  }
72
76
  async function oauthAuthorizeUrl(id) {
73
77
  const res = await api.post(`/credentials/${encodeURIComponent(id)}/oauth/authorize-url`);
74
- const authorizeUrl = unwrapRecord(res).authorize_url;
78
+ const authorizeUrl = bareRecord(res).authorize_url;
75
79
  if (!authorizeUrl) throw new IntegrationsApiError(0, res, t("integration.credentials.error.noAuthorizeUrl"));
76
80
  return authorizeUrl;
77
81
  }
@@ -44,7 +44,7 @@ export declare function useIntegrationsRuns(): {
44
44
  latestLoading: any;
45
45
  loadLatestPerWorkflow: (force?: boolean) => Promise<WorkflowRun[]>;
46
46
  lastRunFor: (workflowId: number) => WorkflowRun | null;
47
- trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "async" | "sync") => Promise<unknown>;
47
+ trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "sync" | "async") => Promise<unknown>;
48
48
  cancelRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
49
49
  terminateRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
50
50
  retryRun: (workflowId: number | string, runId: number | string) => Promise<unknown>;
@@ -2,7 +2,7 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { errorReason } from "../utils/apiError.js";
5
- import { unwrapRecord } from "../utils/apiEnvelope.js";
5
+ import { bareRecord } from "../utils/apiEnvelope.js";
6
6
  import { RUN_OWN_FIELDS } from "../utils/runEnvelope.js";
7
7
  import { useLoadGate } from "./useLoadGate.js";
8
8
  import {
@@ -58,6 +58,10 @@ export function useIntegrationsRuns() {
58
58
  // The platform reports what started the run. This was hardcoded to
59
59
  // `manual` because the payload carried nothing, which left the trigger
60
60
  // column and its filter with one populated value in production.
61
+ // Null — the trigger was deleted, or the run predates trigger_handle, or the
62
+ // row came from a per-workflow read, which sends no trigger at all — is read
63
+ // as `manual` here although the platform said nothing (IN-9, api-contract
64
+ // runs.feed F6 and runs.execution F2).
61
65
  triggerType: r.trigger_type ?? "manual",
62
66
  startedAt: r.started_at ?? "",
63
67
  finishedAt: r.completed_at ?? null,
@@ -187,11 +191,11 @@ export function useIntegrationsRuns() {
187
191
  async function trigger(workflowId, payload = {}, mode) {
188
192
  const body = { trigger_payload: payload };
189
193
  if (mode) body.mode = mode;
190
- return unwrapRecord(await api.post(`/workflows/${workflowId}/runs`, body));
194
+ return bareRecord(await api.post(`/workflows/${workflowId}/runs`, body), RUN_OWN_FIELDS);
191
195
  }
192
196
  async function runAction(workflowId, runId, action, reason) {
193
197
  const body = reason ? { reason } : {};
194
- return unwrapRecord(await api.post(`/workflows/${workflowId}/runs/${runId}/${action}`, body));
198
+ return bareRecord(await api.post(`/workflows/${workflowId}/runs/${runId}/${action}`, body), RUN_OWN_FIELDS);
195
199
  }
196
200
  const cancelRun = (workflowId, runId, reason) => runAction(workflowId, runId, "cancel", reason);
197
201
  const terminateRun = (workflowId, runId, reason) => runAction(workflowId, runId, "terminate", reason);
@@ -199,7 +203,7 @@ export function useIntegrationsRuns() {
199
203
  const resumeRun = (workflowId, runId, reason) => runAction(workflowId, runId, "resume", reason);
200
204
  const getRun = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}`);
201
205
  async function fetchRun(workflowId, runId, workflowName) {
202
- const apiRun = unwrapRecord(await getRun(workflowId, runId), RUN_OWN_FIELDS);
206
+ const apiRun = bareRecord(await getRun(workflowId, runId), RUN_OWN_FIELDS);
203
207
  return mapRun(apiRun, workflowName);
204
208
  }
205
209
  const getDetails = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/details`);
@@ -2,7 +2,8 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { useLoadGate } from "./useLoadGate.js";
5
- import { errorReason } from "../utils/apiError.js";
5
+ import { errorReason, IntegrationsApiError } from "../utils/apiError.js";
6
+ import { unwrapList } from "../utils/apiEnvelope.js";
6
7
  import { scheduleTimezone } from "../utils/schedule.js";
7
8
  export function useIntegrationsSchedules() {
8
9
  const api = useIntegrationsApi();
@@ -66,8 +67,10 @@ export function useIntegrationsSchedules() {
66
67
  error.value = null;
67
68
  try {
68
69
  const res = await api.get("/schedules");
69
- entries.value = (res?.data ?? []).map(mapEntry);
70
- meta.value = res?.meta ?? { total: 0, live_count: 0, timezones: [] };
70
+ const rows = unwrapList(res);
71
+ if (!res?.meta) throw new IntegrationsApiError(200, res, "");
72
+ entries.value = rows.map(mapEntry);
73
+ meta.value = res.meta;
71
74
  loaded.value = true;
72
75
  } catch (e) {
73
76
  error.value = errorReason(e, t("integration.schedules.error.loadFailed"));
@@ -1,12 +1,13 @@
1
1
  import type { SecretRecord } from '../types/integrations.js';
2
+ export declare const SECRET_KEY_MAX = 191;
2
3
  /**
3
4
  * Per-tenant encrypted key/value store, backed by the Integrations service
4
- * (`/api/v1/secrets`). The backend NEVER returns values — only keys +
5
- * timestamps — so the list is keys-only by design.
5
+ * (`/api/v1/secrets`). The backend NEVER returns a value.
6
6
  *
7
- * GET /secrets → list (keys + timestamps)
8
- * POST /secrets → upsert { key, value }
9
- * DELETE /secrets/{key} → remove
7
+ * GET /secrets → { data: [{ key, created_at, updated_at }] }, key-ascending
8
+ * POST /secrets → upsert { key, value } — 201 created, 200 replaced, bare record
9
+ * PATCH /secrets/{key} → replace the value (404 unknown key) — not used by this UI
10
+ * DELETE /secrets/{key} → 204, or 404 for a key this tenant does not hold
10
11
  */
11
12
  export declare function useIntegrationsSecrets(): {
12
13
  secrets: any;
@@ -4,7 +4,8 @@ import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { errorReason } from "../utils/apiError.js";
5
5
  import { unwrapList } from "../utils/apiEnvelope.js";
6
6
  import { useLoadGate } from "./useLoadGate.js";
7
- const KEY_PATTERN = /^[A-Z0-9_]+$/;
7
+ const KEY_PATTERN = /^[\w-]+$/;
8
+ export const SECRET_KEY_MAX = 191;
8
9
  export function useIntegrationsSecrets() {
9
10
  const secrets = useState("integrations.secrets", () => []);
10
11
  const loading = useState("integrations.secrets.loading", () => false);
@@ -14,7 +15,7 @@ export function useIntegrationsSecrets() {
14
15
  const api = useIntegrationsApi();
15
16
  const t = useIntegrationsT();
16
17
  function isValidKey(key) {
17
- return KEY_PATTERN.test(key);
18
+ return KEY_PATTERN.test(key) && [...key].length <= SECRET_KEY_MAX;
18
19
  }
19
20
  function normalize(res) {
20
21
  const obj = res;
@@ -14,7 +14,7 @@ export declare function useIntegrationsTemplates(): {
14
14
  requirements: (slug: string) => Promise<TemplateRequirements | null>;
15
15
  instantiate: (slug: string, opts?: {
16
16
  name?: string;
17
- credentialBindings?: Record<string, string>;
17
+ credentialBindings?: Record<string, string | Record<string, string>>;
18
18
  }) => Promise<number | null>;
19
19
  install: (slug: string) => Promise<number | null>;
20
20
  };
@@ -1,12 +1,11 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { errorReason } from "../utils/apiError.js";
4
- import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
4
+ import { bareRecord, unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
5
5
  import { resolveLocalized } from "../utils/localize.js";
6
6
  import { useLoadGate } from "./useLoadGate.js";
7
7
  import { createDetailLoader, keyedBatch } from "../utils/detailLoader.js";
8
8
  const BLOB_VERSION = "v0-draft";
9
- const INDUSTRY_VALUES = ["mechanical-engineering", "chemicals", "wholesale", "manufacturing", "retail", "any"];
10
9
  function plainExcerpt(md) {
11
10
  for (const raw of md.split("\n")) {
12
11
  const line = raw.trim();
@@ -21,7 +20,7 @@ function publisherFor(slug, vendors) {
21
20
  return { name: ns, kind };
22
21
  }
23
22
  function mapIndustries(arr) {
24
- return (arr ?? []).filter((i) => INDUSTRY_VALUES.includes(i));
23
+ return (arr ?? []).filter((i) => typeof i === "string" && i.trim() !== "").map((i) => i.trim());
25
24
  }
26
25
  function emptyBlob(name) {
27
26
  return { nodeManifestVersion: BLOB_VERSION, name, nodes: [], edges: [], triggerPositions: {}, triggers: [] };
@@ -58,7 +57,7 @@ function mapListItem(api) {
58
57
  function applyDetail(base, api) {
59
58
  const def = api.definition ?? {};
60
59
  const triggers = (api.triggers ?? []).map((t) => ({
61
- id: t.handle ?? String(t.id ?? ""),
60
+ id: t.handle,
62
61
  type: t.type,
63
62
  name: typeof t.name === "string" ? t.name : void 0,
64
63
  active: t.active,
@@ -184,10 +183,10 @@ export function useIntegrationsTemplates() {
184
183
  const body = {};
185
184
  if (opts.name) body.name = opts.name;
186
185
  const bindings = Object.fromEntries(
187
- Object.entries(opts.credentialBindings ?? {}).filter(([, v]) => v)
186
+ Object.entries(opts.credentialBindings ?? {}).map(([nodeId, b]) => [nodeId, typeof b === "string" ? b : Object.fromEntries(Object.entries(b).filter(([, id]) => id))]).filter(([, b]) => typeof b === "string" ? b !== "" : Object.keys(b).length > 0)
188
187
  );
189
188
  if (Object.keys(bindings).length) body.credential_bindings = bindings;
190
- const created = unwrapRecord(await api.post(`/templates/${slug}/instantiate`, body));
189
+ const created = bareRecord(await api.post(`/templates/${slug}/instantiate`, body));
191
190
  return Number(created.id);
192
191
  }
193
192
  async function install(slug) {
@@ -3,7 +3,7 @@ import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { assignTriggerHandles, pausedTriggers } from "../utils/workflowFile.js";
5
5
  import { errorReason } from "../utils/apiError.js";
6
- import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
6
+ import { bareRecord, unwrapList } from "../utils/apiEnvelope.js";
7
7
  import { useLoadGate } from "./useLoadGate.js";
8
8
  import { eventConfigForPlatform } from "../data/eventTrigger.js";
9
9
  const BLOB_VERSION = "v0-draft";
@@ -140,7 +140,7 @@ export function useIntegrationsWorkflows() {
140
140
  }
141
141
  async function get(id, { strict = false } = {}) {
142
142
  try {
143
- const wf = ingest(unwrapRecord(await api.get(`/workflows/${id}`)));
143
+ const wf = ingest(bareRecord(await api.get(`/workflows/${id}`)));
144
144
  upsertCache(wf);
145
145
  return wf;
146
146
  } catch (e) {
@@ -149,7 +149,7 @@ export function useIntegrationsWorkflows() {
149
149
  }
150
150
  }
151
151
  async function create(input) {
152
- const wf = ingest(unwrapRecord(await api.post("/workflows", toPayload({
152
+ const wf = ingest(bareRecord(await api.post("/workflows", toPayload({
153
153
  name: input.name,
154
154
  description: input.description,
155
155
  blob: input.blob,
@@ -182,7 +182,7 @@ export function useIntegrationsWorkflows() {
182
182
  throw lastError instanceof Error ? lastError : new Error(t("integration.workflows.error.duplicateFailed"));
183
183
  }
184
184
  async function update(id, patch) {
185
- const wf = ingest(unwrapRecord(await api.put(`/workflows/${id}`, toPayload({
185
+ const wf = ingest(bareRecord(await api.put(`/workflows/${id}`, toPayload({
186
186
  name: patch.name,
187
187
  description: patch.description,
188
188
  blob: patch.blob,
@@ -1,5 +1,5 @@
1
1
  import { IntegrationsApiError } from "../utils/apiError.js";
2
- import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
2
+ import { bareRecord, unwrapList } from "../utils/apiEnvelope.js";
3
3
  import { useIntegrationsApi } from "./integrationsApi.js";
4
4
  function versionPath(slug, version) {
5
5
  return `/nodes/${encodeURIComponent(slug)}/${encodeURIComponent(version)}`;
@@ -11,7 +11,7 @@ export function useNodeRetirement() {
11
11
  return all.filter((n) => n.package?.name === packageName);
12
12
  }
13
13
  async function usage(slug, version) {
14
- const answer = unwrapRecord(await api.get(`${versionPath(slug, version)}/usage`), ["usage"]);
14
+ const answer = bareRecord(await api.get(`${versionPath(slug, version)}/usage`));
15
15
  const counts = answer?.usage;
16
16
  if (!counts || typeof counts.workflows !== "number") {
17
17
  throw new IntegrationsApiError(200, answer, "");
@@ -19,13 +19,13 @@ export function useNodeRetirement() {
19
19
  return counts;
20
20
  }
21
21
  async function retire(slug, version, reason) {
22
- return unwrapRecord(await api.post(`${versionPath(slug, version)}/retire`, {
22
+ return bareRecord(await api.post(`${versionPath(slug, version)}/retire`, {
23
23
  confirm: `${slug}@${version}`,
24
24
  ...reason ? { reason } : {}
25
- }), ["usage"]);
25
+ }));
26
26
  }
27
27
  async function restore(slug, version) {
28
- return unwrapRecord(await api.del(`${versionPath(slug, version)}/retire`), ["usage"]);
28
+ return bareRecord(await api.del(`${versionPath(slug, version)}/retire`));
29
29
  }
30
30
  return { versionsOfPackage, usage, retire, restore };
31
31
  }
@@ -3,8 +3,12 @@
3
3
  "integration.audit.action.create": "erstellt",
4
4
  "integration.audit.action.delete": "gelöscht",
5
5
  "integration.audit.action.loadMore": "Weitere laden",
6
+ "integration.audit.action.restore": "wiederhergestellt",
7
+ "integration.audit.action.retire": "ausgemustert",
8
+ "integration.audit.action.runCancelled": "Run abgebrochen",
6
9
  "integration.audit.action.runFailed": "Run fehlgeschlagen",
7
10
  "integration.audit.action.runFinished": "Run beendet",
11
+ "integration.audit.action.runResumed": "Run fortgesetzt",
8
12
  "integration.audit.action.runStarted": "Run gestartet",
9
13
  "integration.audit.action.update": "aktualisiert",
10
14
  "integration.audit.actor.system": "System",
@@ -14,13 +18,18 @@
14
18
  "integration.audit.detail.id": "ID",
15
19
  "integration.audit.detail.tenant": "Mandant",
16
20
  "integration.audit.head.title": "Audit-Log · Integration Studio",
17
- "integration.audit.header.description": "Wer hat was geändert, und wann. Nur anfügend, pro Mandant — Zugangsdaten, Secrets, Workflows, Trigger und Runs.",
21
+ "integration.audit.header.description": "Wer hat was geändert, und wann. Nur anfügend, pro Mandant — Zugangsdaten, Secrets, Workflows, Trigger, Runs, Node-Versionen und gespeicherter Zustand.",
18
22
  "integration.audit.header.title": "Audit-Log",
19
23
  "integration.audit.panel.close.aria": "Details schließen",
20
24
  "integration.audit.panel.dismiss.aria": "Details schließen",
21
25
  "integration.audit.panel.title.aria": "Details zum Audit-Eintrag",
26
+ "integration.audit.resource.run": "Run von Workflow #{workflow}",
27
+ "integration.audit.resource.trigger": "{type}-Trigger von Workflow #{workflow}",
22
28
  "integration.audit.resourceType.credential": "Zugangsdaten",
29
+ "integration.audit.resourceType.nodeVersion": "Node-Version",
23
30
  "integration.audit.resourceType.secret": "Secret",
31
+ "integration.audit.resourceType.stateEntry": "Zustandseintrag",
32
+ "integration.audit.resourceType.stateNamespace": "Zustands-Namespace",
24
33
  "integration.audit.resourceType.trigger": "Trigger",
25
34
  "integration.audit.resourceType.workflow": "Workflow",
26
35
  "integration.audit.resourceType.workflowRun": "Workflow-Run",
@@ -285,6 +294,9 @@
285
294
  "integration.credentials.shared.field.select.placeholder": "Auswählen",
286
295
  "integration.credentials.shared.oauthHint": "Nach dem Speichern öffnet sich ein Fenster, um die Verbindung mit dem Anbieter herzustellen.",
287
296
  "integration.credentials.shared.test.failed": "Verbindung fehlgeschlagen.",
297
+ "integration.credentials.shared.test.notAllowed": "Nur wer diese Zugangsdaten ändern darf, kann Änderungen daran testen.",
298
+ "integration.credentials.shared.test.notTested": "Nicht getestet — die Plattform konnte den Test nicht ausführen. Über die Verbindung ist nichts bekannt.",
299
+ "integration.credentials.shared.test.notTestedBecause": "Nicht getestet — {reason}",
288
300
  "integration.credentials.shared.test.ok": "Verbindung ok.",
289
301
  "integration.credentials.state.connected": "Verbunden",
290
302
  "integration.credentials.state.failed": "Fehlgeschlagen",
@@ -1096,10 +1108,10 @@
1096
1108
  "integration.secrets.dialog.create.title": "Secret anlegen",
1097
1109
  "integration.secrets.dialog.delete.body": "wird endgültig entfernt. Workflows, die darauf verweisen, funktionieren beim nächsten Run nicht mehr.",
1098
1110
  "integration.secrets.dialog.delete.title": "Secret löschen?",
1099
- "integration.secrets.error.invalidKey": "Der Schlüssel muss dem Muster ^[A-Z0-9_]+$ entsprechen.",
1111
+ "integration.secrets.error.invalidKey": "Der Schlüssel muss dem Muster ^[A-Za-z0-9_-]+$ entsprechen und darf höchstens 191 Zeichen lang sein.",
1100
1112
  "integration.secrets.error.loadFailed": "Secrets konnten nicht geladen werden.",
1101
1113
  "integration.secrets.error.valueRequired": "Der Wert ist erforderlich.",
1102
- "integration.secrets.form.error.invalidKey": "Der Schlüssel muss ^[A-Z0-9_]+$ entsprechen (Großbuchstaben, Ziffern, Unterstriche).",
1114
+ "integration.secrets.form.error.invalidKey": "Der Schlüssel muss ^[A-Za-z0-9_-]+$ entsprechen (Buchstaben, Ziffern, Unterstriche, Bindestriche).",
1103
1115
  "integration.secrets.form.error.saveFailed": "Das Secret konnte nicht gespeichert werden.",
1104
1116
  "integration.secrets.form.key.label": "Schlüssel",
1105
1117
  "integration.secrets.form.key.patternLabel": "Muster:",
@@ -1156,6 +1168,7 @@
1156
1168
  "integration.templates.detail.install.secretsDescriptionSuffix": ".",
1157
1169
  "integration.templates.detail.install.secretsHeader": "Secrets",
1158
1170
  "integration.templates.detail.install.selectLater": "Später auswählen",
1171
+ "integration.templates.detail.install.settingLabel": "Node {id} · {setting}",
1159
1172
  "integration.templates.detail.install.state.loading": "Es wird geprüft, was diese Vorlage benötigt…",
1160
1173
  "integration.templates.detail.install.submit": "Installieren",
1161
1174
  "integration.templates.detail.install.submitting": "Wird eingerichtet…",