@open-mercato/webhooks 0.10.1-develop.7618.1.ba9531c18e → 0.10.1-develop.7619.1.97f9a4bda8

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 (28) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-013.spec.js +191 -0
  3. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-013.spec.js.map +7 -0
  4. package/dist/modules/webhooks/api/helpers.js +31 -4
  5. package/dist/modules/webhooks/api/helpers.js.map +2 -2
  6. package/dist/modules/webhooks/api/webhooks/[id]/route.js +30 -6
  7. package/dist/modules/webhooks/api/webhooks/[id]/route.js.map +2 -2
  8. package/dist/modules/webhooks/api/webhooks/route.js +21 -4
  9. package/dist/modules/webhooks/api/webhooks/route.js.map +2 -2
  10. package/dist/modules/webhooks/lib/custom-headers.js +60 -0
  11. package/dist/modules/webhooks/lib/custom-headers.js.map +2 -2
  12. package/package.json +7 -7
  13. package/src/modules/webhooks/__integration__/TC-WEBHOOK-013.spec.ts +231 -0
  14. package/src/modules/webhooks/api/helpers.ts +43 -2
  15. package/src/modules/webhooks/api/webhooks/[id]/__tests__/credential-headers.test.ts +358 -0
  16. package/src/modules/webhooks/api/webhooks/[id]/__tests__/optimistic-lock.test.ts +2 -0
  17. package/src/modules/webhooks/api/webhooks/[id]/__tests__/reactivation.test.ts +2 -0
  18. package/src/modules/webhooks/api/webhooks/[id]/__tests__/subscription-cache-invalidation.test.ts +2 -0
  19. package/src/modules/webhooks/api/webhooks/[id]/route.ts +31 -6
  20. package/src/modules/webhooks/api/webhooks/__tests__/credential-headers.test.ts +96 -0
  21. package/src/modules/webhooks/api/webhooks/route.ts +21 -4
  22. package/src/modules/webhooks/i18n/de.json +3 -1
  23. package/src/modules/webhooks/i18n/en.json +3 -1
  24. package/src/modules/webhooks/i18n/es.json +3 -1
  25. package/src/modules/webhooks/i18n/ko.json +3 -1
  26. package/src/modules/webhooks/i18n/pl.json +3 -1
  27. package/src/modules/webhooks/lib/__tests__/custom-headers.test.ts +144 -0
  28. package/src/modules/webhooks/lib/custom-headers.ts +84 -0
@@ -1,2 +1,2 @@
1
- [build:webhooks] found 77 entry points
1
+ [build:webhooks] found 78 entry points
2
2
  [build:webhooks] built successfully
@@ -0,0 +1,191 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
3
+ import { getTokenScope } from "@open-mercato/core/helpers/integration/generalFixtures";
4
+ import {
5
+ cleanupWebhooksUser,
6
+ createWebhookFixture,
7
+ deleteWebhookIfExists,
8
+ provisionWebhooksUser
9
+ } from "./helpers/fixtures.js";
10
+ const REDACTED = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
11
+ const DESTINATION_TOKEN = "Bearer tc-webhook-013-destination-token";
12
+ async function readDetail(request, token, id) {
13
+ const response = await apiRequest(request, "GET", `/api/webhooks/${id}`, { token });
14
+ expect(response.status()).toBe(200);
15
+ return await response.json();
16
+ }
17
+ async function loginWithCredentials(page, email, password) {
18
+ const baseUrl = process.env.BASE_URL || "http://localhost:3000";
19
+ await page.context().addCookies([
20
+ { name: "om_demo_notice_ack", value: "ack", url: baseUrl, sameSite: "Lax" },
21
+ { name: "om_cookie_notice_ack", value: "ack", url: baseUrl, sameSite: "Lax" },
22
+ { name: "om_feedback_suppress", value: "1", url: baseUrl, sameSite: "Lax" }
23
+ ]);
24
+ await page.goto("/login");
25
+ await page.waitForSelector('form[data-auth-ready="1"]', { state: "visible", timeout: 3e4 });
26
+ await page.getByLabel("Email").fill(email);
27
+ const passwordInput = page.getByLabel("Password", { exact: true });
28
+ await passwordInput.fill(password);
29
+ await passwordInput.press("Enter");
30
+ await expect(page).toHaveURL(/\/backend(?:\/.*)?$/, { timeout: 3e4 });
31
+ }
32
+ test.describe("TC-WEBHOOK-013: credential custom headers are hidden without webhooks.secrets", () => {
33
+ test("redacts on read, keeps stored values on write, and refuses unsafe writes", async ({ request }) => {
34
+ test.setTimeout(6e4);
35
+ const adminToken = await getAuthToken(request);
36
+ const scope = getTokenScope(adminToken);
37
+ let webhookId = null;
38
+ let viewer = null;
39
+ let manager = null;
40
+ try {
41
+ viewer = await provisionWebhooksUser(request, adminToken, {
42
+ tenantId: scope.tenantId,
43
+ organizationId: scope.organizationId,
44
+ features: ["webhooks.view"],
45
+ slug: "cred-viewer"
46
+ });
47
+ manager = await provisionWebhooksUser(request, adminToken, {
48
+ tenantId: scope.tenantId,
49
+ organizationId: scope.organizationId,
50
+ features: ["webhooks.view", "webhooks.manage"],
51
+ slug: "cred-manager"
52
+ });
53
+ const created = await createWebhookFixture(request, adminToken, {
54
+ name: `Webhook credential headers ${Date.now()}`,
55
+ url: "https://example.com/tc-webhook-013",
56
+ subscribedEvents: ["catalog.product.created"],
57
+ customHeaders: { Authorization: DESTINATION_TOKEN, "x-api-key": "tc-013-api-key", "x-source": "qa" }
58
+ });
59
+ webhookId = created.id;
60
+ const viewerDetail = await readDetail(request, viewer.token, created.id);
61
+ expect(viewerDetail.customHeaders).toEqual({ Authorization: REDACTED, "x-api-key": REDACTED, "x-source": "qa" });
62
+ const aliasResponse = await apiRequest(request, "GET", `/api/webhooks/webhooks/${created.id}`, { token: viewer.token });
63
+ expect(aliasResponse.status()).toBe(200);
64
+ expect((await aliasResponse.json()).customHeaders?.Authorization).toBe(REDACTED);
65
+ const managerDetail = await readDetail(request, manager.token, created.id);
66
+ expect(managerDetail.customHeaders?.Authorization).toBe(REDACTED);
67
+ const adminDetail = await readDetail(request, adminToken, created.id);
68
+ expect(adminDetail.customHeaders).toEqual({ Authorization: DESTINATION_TOKEN, "x-api-key": "tc-013-api-key", "x-source": "qa" });
69
+ const renamed = `Webhook credential headers renamed ${Date.now()}`;
70
+ const echoUpdate = await apiRequest(request, "PUT", `/api/webhooks/${created.id}`, {
71
+ token: manager.token,
72
+ data: { name: renamed, customHeaders: { ...managerDetail.customHeaders, "x-source": "erp" } }
73
+ });
74
+ expect(echoUpdate.status()).toBe(200);
75
+ expect((await echoUpdate.json()).customHeaders?.Authorization).toBe(REDACTED);
76
+ const afterEcho = await readDetail(request, adminToken, created.id);
77
+ expect(afterEcho.name).toBe(renamed);
78
+ expect(afterEcho.customHeaders).toEqual({ Authorization: DESTINATION_TOKEN, "x-api-key": "tc-013-api-key", "x-source": "erp" });
79
+ const orphanMarker = await apiRequest(request, "PUT", `/api/webhooks/${created.id}`, {
80
+ token: manager.token,
81
+ data: { customHeaders: { "x-new-token": REDACTED } }
82
+ });
83
+ expect(orphanMarker.status()).toBe(400);
84
+ expect((await orphanMarker.json()).error).toContain("x-new-token");
85
+ const moveWithHidden = await apiRequest(request, "PUT", `/api/webhooks/${created.id}`, {
86
+ token: manager.token,
87
+ data: { url: "https://other.example.net/tc-webhook-013" }
88
+ });
89
+ expect(moveWithHidden.status()).toBe(400);
90
+ const moveEchoingMarker = await apiRequest(request, "PUT", `/api/webhooks/webhooks/${created.id}`, {
91
+ token: manager.token,
92
+ data: { url: "https://other.example.net/tc-webhook-013", customHeaders: managerDetail.customHeaders }
93
+ });
94
+ expect(moveEchoingMarker.status()).toBe(400);
95
+ const samePathMove = await apiRequest(request, "PUT", `/api/webhooks/${created.id}`, {
96
+ token: manager.token,
97
+ data: { url: "https://example.com/tc-webhook-013-other-bin", customHeaders: managerDetail.customHeaders }
98
+ });
99
+ expect(samePathMove.status()).toBe(400);
100
+ const unchanged = await readDetail(request, adminToken, created.id);
101
+ expect(unchanged.url).toBe("https://example.com/tc-webhook-013");
102
+ expect(unchanged.customHeaders).toEqual(afterEcho.customHeaders);
103
+ const moveReentered = await apiRequest(request, "PUT", `/api/webhooks/${created.id}`, {
104
+ token: manager.token,
105
+ data: {
106
+ url: "https://other.example.net/tc-webhook-013",
107
+ customHeaders: { Authorization: "Bearer re-entered", "x-source": "erp" }
108
+ }
109
+ });
110
+ expect(moveReentered.status()).toBe(200);
111
+ const adminMove = await apiRequest(request, "PUT", `/api/webhooks/${created.id}`, {
112
+ token: adminToken,
113
+ data: { url: "https://example.com/tc-webhook-013", customHeaders: { Authorization: REDACTED, "x-source": "erp" } }
114
+ });
115
+ expect(adminMove.status()).toBe(200);
116
+ expect((await adminMove.json()).customHeaders).toEqual({ Authorization: "Bearer re-entered", "x-source": "erp" });
117
+ } finally {
118
+ await deleteWebhookIfExists(request, adminToken, webhookId);
119
+ await cleanupWebhooksUser(request, adminToken, viewer);
120
+ await cleanupWebhooksUser(request, adminToken, manager);
121
+ }
122
+ });
123
+ test("shows hidden values in the UI and keeps them when a manager saves the edit form", async ({ browser, request }) => {
124
+ test.setTimeout(12e4);
125
+ const adminToken = await getAuthToken(request);
126
+ const scope = getTokenScope(adminToken);
127
+ let webhookId = null;
128
+ let viewer = null;
129
+ let manager = null;
130
+ try {
131
+ viewer = await provisionWebhooksUser(request, adminToken, {
132
+ tenantId: scope.tenantId,
133
+ organizationId: scope.organizationId,
134
+ features: ["webhooks.view"],
135
+ slug: "cred-ui-viewer"
136
+ });
137
+ manager = await provisionWebhooksUser(request, adminToken, {
138
+ tenantId: scope.tenantId,
139
+ organizationId: scope.organizationId,
140
+ features: ["webhooks.view", "webhooks.manage"],
141
+ slug: "cred-ui-manager"
142
+ });
143
+ const created = await createWebhookFixture(request, adminToken, {
144
+ name: `Webhook credential UI ${Date.now()}`,
145
+ url: "https://example.com/tc-webhook-013-ui",
146
+ subscribedEvents: ["catalog.product.created"],
147
+ customHeaders: { Authorization: DESTINATION_TOKEN, "x-source": "qa" }
148
+ });
149
+ webhookId = created.id;
150
+ const viewerContext = await browser.newContext();
151
+ try {
152
+ const viewerPage = await viewerContext.newPage();
153
+ await loginWithCredentials(viewerPage, viewer.email, viewer.password);
154
+ await viewerPage.goto(`/backend/webhooks/${created.id}`);
155
+ const headersCard = viewerPage.locator("pre").filter({ hasText: '"x-source": "qa"' });
156
+ await expect(headersCard).toContainText(`"Authorization": "${REDACTED}"`);
157
+ await expect(viewerPage.getByText(DESTINATION_TOKEN)).toHaveCount(0);
158
+ } finally {
159
+ await viewerContext.close();
160
+ }
161
+ const renamed = `Webhook credential UI renamed ${Date.now()}`;
162
+ const managerContext = await browser.newContext();
163
+ try {
164
+ const managerPage = await managerContext.newPage();
165
+ await loginWithCredentials(managerPage, manager.email, manager.password);
166
+ await managerPage.goto(`/backend/webhooks/${created.id}`);
167
+ await expect(managerPage.locator("pre").filter({ hasText: '"x-source": "qa"' })).toContainText(REDACTED);
168
+ await managerPage.getByRole("button", { name: "Actions" }).hover();
169
+ await managerPage.getByRole("menuitem", { name: "Edit" }).click();
170
+ await expect(managerPage.getByText(`Hidden values (${REDACTED}) are kept when you save`)).toBeVisible();
171
+ await expect(managerPage.getByText(DESTINATION_TOKEN)).toHaveCount(0);
172
+ await expect(managerPage.getByPlaceholder('{"key": "value"}')).toHaveValue(new RegExp(`"Authorization": "${REDACTED}"`));
173
+ const nameInput = managerPage.getByPlaceholder("My Webhook");
174
+ await nameInput.fill(renamed);
175
+ await expect(nameInput).toHaveValue(renamed);
176
+ await managerPage.getByRole("button", { name: "Save" }).first().click();
177
+ await expect(managerPage.locator("pre").filter({ hasText: '"x-source": "qa"' })).toBeVisible({ timeout: 15e3 });
178
+ } finally {
179
+ await managerContext.close();
180
+ }
181
+ const adminDetail = await readDetail(request, adminToken, created.id);
182
+ expect(adminDetail.name).toBe(renamed);
183
+ expect(adminDetail.customHeaders).toEqual({ Authorization: DESTINATION_TOKEN, "x-source": "qa" });
184
+ } finally {
185
+ await deleteWebhookIfExists(request, adminToken, webhookId);
186
+ await cleanupWebhooksUser(request, adminToken, viewer);
187
+ await cleanupWebhooksUser(request, adminToken, manager);
188
+ }
189
+ });
190
+ });
191
+ //# sourceMappingURL=TC-WEBHOOK-013.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/webhooks/__integration__/TC-WEBHOOK-013.spec.ts"],
4
+ "sourcesContent": ["import { expect, test, type Page } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { getTokenScope } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n cleanupWebhooksUser,\n createWebhookFixture,\n deleteWebhookIfExists,\n provisionWebhooksUser,\n type ProvisionedWebhooksUser,\n} from './helpers/fixtures';\n\n/**\n * TC-WEBHOOK-013: credential-like custom header values are hidden from callers without webhooks.secrets\n * Source: https://github.com/open-mercato/open-mercato/issues/7237\n *\n * A viewer (webhooks.view) and a manager without webhooks.secrets receive `\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022` for\n * credential-like custom headers, while ordinary headers stay readable and an admin\n * (webhooks.*) still receives the stored values. Echoing the marker back keeps the stored\n * value, a marker without a stored value is refused, and a manager without webhooks.secrets\n * cannot keep hidden values while changing the webhook URL.\n */\n\nconst REDACTED = '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022';\nconst DESTINATION_TOKEN = 'Bearer tc-webhook-013-destination-token';\n\ntype WebhookDetail = { name: string; url: string; customHeaders: Record<string, string> | null };\n\nasync function readDetail(request: Parameters<typeof apiRequest>[0], token: string, id: string): Promise<WebhookDetail> {\n const response = await apiRequest(request, 'GET', `/api/webhooks/${id}`, { token });\n expect(response.status()).toBe(200);\n return (await response.json()) as WebhookDetail;\n}\n\nasync function loginWithCredentials(page: Page, email: string, password: string): Promise<void> {\n const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n await page.context().addCookies([\n { name: 'om_demo_notice_ack', value: 'ack', url: baseUrl, sameSite: 'Lax' },\n { name: 'om_cookie_notice_ack', value: 'ack', url: baseUrl, sameSite: 'Lax' },\n { name: 'om_feedback_suppress', value: '1', url: baseUrl, sameSite: 'Lax' },\n ]);\n await page.goto('/login');\n await page.waitForSelector('form[data-auth-ready=\"1\"]', { state: 'visible', timeout: 30_000 });\n await page.getByLabel('Email').fill(email);\n const passwordInput = page.getByLabel('Password', { exact: true });\n await passwordInput.fill(password);\n await passwordInput.press('Enter');\n await expect(page).toHaveURL(/\\/backend(?:\\/.*)?$/, { timeout: 30_000 });\n}\n\ntest.describe('TC-WEBHOOK-013: credential custom headers are hidden without webhooks.secrets', () => {\n test('redacts on read, keeps stored values on write, and refuses unsafe writes', async ({ request }) => {\n test.setTimeout(60_000);\n const adminToken = await getAuthToken(request);\n const scope = getTokenScope(adminToken);\n\n let webhookId: string | null = null;\n let viewer: ProvisionedWebhooksUser | null = null;\n let manager: ProvisionedWebhooksUser | null = null;\n\n try {\n viewer = await provisionWebhooksUser(request, adminToken, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: ['webhooks.view'],\n slug: 'cred-viewer',\n });\n manager = await provisionWebhooksUser(request, adminToken, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: ['webhooks.view', 'webhooks.manage'],\n slug: 'cred-manager',\n });\n\n const created = await createWebhookFixture(request, adminToken, {\n name: `Webhook credential headers ${Date.now()}`,\n url: 'https://example.com/tc-webhook-013',\n subscribedEvents: ['catalog.product.created'],\n customHeaders: { Authorization: DESTINATION_TOKEN, 'x-api-key': 'tc-013-api-key', 'x-source': 'qa' },\n });\n webhookId = created.id;\n\n const viewerDetail = await readDetail(request, viewer.token, created.id);\n expect(viewerDetail.customHeaders).toEqual({ Authorization: REDACTED, 'x-api-key': REDACTED, 'x-source': 'qa' });\n\n const aliasResponse = await apiRequest(request, 'GET', `/api/webhooks/webhooks/${created.id}`, { token: viewer.token });\n expect(aliasResponse.status()).toBe(200);\n expect(((await aliasResponse.json()) as WebhookDetail).customHeaders?.Authorization).toBe(REDACTED);\n\n const managerDetail = await readDetail(request, manager.token, created.id);\n expect(managerDetail.customHeaders?.Authorization).toBe(REDACTED);\n\n const adminDetail = await readDetail(request, adminToken, created.id);\n expect(adminDetail.customHeaders).toEqual({ Authorization: DESTINATION_TOKEN, 'x-api-key': 'tc-013-api-key', 'x-source': 'qa' });\n\n const renamed = `Webhook credential headers renamed ${Date.now()}`;\n const echoUpdate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token: manager.token,\n data: { name: renamed, customHeaders: { ...managerDetail.customHeaders, 'x-source': 'erp' } },\n });\n expect(echoUpdate.status()).toBe(200);\n expect(((await echoUpdate.json()) as WebhookDetail).customHeaders?.Authorization).toBe(REDACTED);\n\n const afterEcho = await readDetail(request, adminToken, created.id);\n expect(afterEcho.name).toBe(renamed);\n expect(afterEcho.customHeaders).toEqual({ Authorization: DESTINATION_TOKEN, 'x-api-key': 'tc-013-api-key', 'x-source': 'erp' });\n\n const orphanMarker = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token: manager.token,\n data: { customHeaders: { 'x-new-token': REDACTED } },\n });\n expect(orphanMarker.status()).toBe(400);\n expect(((await orphanMarker.json()) as { error: string }).error).toContain('x-new-token');\n\n const moveWithHidden = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token: manager.token,\n data: { url: 'https://other.example.net/tc-webhook-013' },\n });\n expect(moveWithHidden.status()).toBe(400);\n\n const moveEchoingMarker = await apiRequest(request, 'PUT', `/api/webhooks/webhooks/${created.id}`, {\n token: manager.token,\n data: { url: 'https://other.example.net/tc-webhook-013', customHeaders: managerDetail.customHeaders },\n });\n expect(moveEchoingMarker.status()).toBe(400);\n\n const samePathMove = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token: manager.token,\n data: { url: 'https://example.com/tc-webhook-013-other-bin', customHeaders: managerDetail.customHeaders },\n });\n expect(samePathMove.status()).toBe(400);\n\n const unchanged = await readDetail(request, adminToken, created.id);\n expect(unchanged.url).toBe('https://example.com/tc-webhook-013');\n expect(unchanged.customHeaders).toEqual(afterEcho.customHeaders);\n\n const moveReentered = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token: manager.token,\n data: {\n url: 'https://other.example.net/tc-webhook-013',\n customHeaders: { Authorization: 'Bearer re-entered', 'x-source': 'erp' },\n },\n });\n expect(moveReentered.status()).toBe(200);\n\n const adminMove = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token: adminToken,\n data: { url: 'https://example.com/tc-webhook-013', customHeaders: { Authorization: REDACTED, 'x-source': 'erp' } },\n });\n expect(adminMove.status()).toBe(200);\n expect(((await adminMove.json()) as WebhookDetail).customHeaders).toEqual({ Authorization: 'Bearer re-entered', 'x-source': 'erp' });\n } finally {\n await deleteWebhookIfExists(request, adminToken, webhookId);\n await cleanupWebhooksUser(request, adminToken, viewer);\n await cleanupWebhooksUser(request, adminToken, manager);\n }\n });\n\n test('shows hidden values in the UI and keeps them when a manager saves the edit form', async ({ browser, request }) => {\n test.setTimeout(120_000);\n const adminToken = await getAuthToken(request);\n const scope = getTokenScope(adminToken);\n\n let webhookId: string | null = null;\n let viewer: ProvisionedWebhooksUser | null = null;\n let manager: ProvisionedWebhooksUser | null = null;\n\n try {\n viewer = await provisionWebhooksUser(request, adminToken, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: ['webhooks.view'],\n slug: 'cred-ui-viewer',\n });\n manager = await provisionWebhooksUser(request, adminToken, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: ['webhooks.view', 'webhooks.manage'],\n slug: 'cred-ui-manager',\n });\n\n const created = await createWebhookFixture(request, adminToken, {\n name: `Webhook credential UI ${Date.now()}`,\n url: 'https://example.com/tc-webhook-013-ui',\n subscribedEvents: ['catalog.product.created'],\n customHeaders: { Authorization: DESTINATION_TOKEN, 'x-source': 'qa' },\n });\n webhookId = created.id;\n\n const viewerContext = await browser.newContext();\n try {\n const viewerPage = await viewerContext.newPage();\n await loginWithCredentials(viewerPage, viewer.email, viewer.password);\n await viewerPage.goto(`/backend/webhooks/${created.id}`);\n const headersCard = viewerPage.locator('pre').filter({ hasText: '\"x-source\": \"qa\"' });\n await expect(headersCard).toContainText(`\"Authorization\": \"${REDACTED}\"`);\n await expect(viewerPage.getByText(DESTINATION_TOKEN)).toHaveCount(0);\n } finally {\n await viewerContext.close();\n }\n\n const renamed = `Webhook credential UI renamed ${Date.now()}`;\n const managerContext = await browser.newContext();\n try {\n const managerPage = await managerContext.newPage();\n await loginWithCredentials(managerPage, manager.email, manager.password);\n await managerPage.goto(`/backend/webhooks/${created.id}`);\n await expect(managerPage.locator('pre').filter({ hasText: '\"x-source\": \"qa\"' })).toContainText(REDACTED);\n await managerPage.getByRole('button', { name: 'Actions' }).hover();\n await managerPage.getByRole('menuitem', { name: 'Edit' }).click();\n await expect(managerPage.getByText(`Hidden values (${REDACTED}) are kept when you save`)).toBeVisible();\n await expect(managerPage.getByText(DESTINATION_TOKEN)).toHaveCount(0);\n await expect(managerPage.getByPlaceholder('{\"key\": \"value\"}')).toHaveValue(new RegExp(`\"Authorization\": \"${REDACTED}\"`));\n const nameInput = managerPage.getByPlaceholder('My Webhook');\n await nameInput.fill(renamed);\n await expect(nameInput).toHaveValue(renamed);\n await managerPage.getByRole('button', { name: 'Save' }).first().click();\n await expect(managerPage.locator('pre').filter({ hasText: '\"x-source\": \"qa\"' })).toBeVisible({ timeout: 15_000 });\n } finally {\n await managerContext.close();\n }\n\n const adminDetail = await readDetail(request, adminToken, created.id);\n expect(adminDetail.name).toBe(renamed);\n expect(adminDetail.customHeaders).toEqual({ Authorization: DESTINATION_TOKEN, 'x-source': 'qa' });\n } finally {\n await deleteWebhookIfExists(request, adminToken, webhookId);\n await cleanupWebhooksUser(request, adminToken, viewer);\n await cleanupWebhooksUser(request, adminToken, manager);\n }\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -5,6 +5,7 @@ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
5
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
6
6
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
7
7
  import { WebhookDeliveryEntity, WebhookEntity } from "../data/entities.js";
8
+ import { redactWebhookCustomHeaders } from "../lib/custom-headers.js";
8
9
  function json(payload, init = { status: 200 }) {
9
10
  return new Response(JSON.stringify(payload), {
10
11
  ...init,
@@ -30,9 +31,18 @@ async function resolveWebhookRequestScope(request) {
30
31
  em: container.resolve("em"),
31
32
  tenantId,
32
33
  organizationId,
33
- allowedOrganizationIds
34
+ allowedOrganizationIds,
35
+ auth
34
36
  };
35
37
  }
38
+ async function canRevealWebhookCredentialHeaders(container, auth, webhook) {
39
+ if (!auth?.sub) return false;
40
+ const rbacService = container.resolve("rbacService");
41
+ return rbacService.userHasAllFeatures(auth.sub, ["webhooks.secrets"], {
42
+ tenantId: webhook.tenantId,
43
+ organizationId: webhook.organizationId
44
+ });
45
+ }
36
46
  async function findScopedWebhook(em, scope, webhookId) {
37
47
  const webhook = await findOneWithDecryption(
38
48
  em,
@@ -61,6 +71,21 @@ async function findScopedDelivery(em, scope, deliveryId) {
61
71
  }
62
72
  return delivery;
63
73
  }
74
+ async function translateWebhookCustomHeadersRejection(rejection) {
75
+ const { translate } = await resolveTranslations();
76
+ if (rejection.reason === "credentialHeadersRequireReentry") {
77
+ return translate(
78
+ "webhooks.errors.credentialHeadersRequireReentry",
79
+ 'Header "{header}" has a hidden value. Enter it again or remove it when you change the webhook URL.',
80
+ { header: rejection.header }
81
+ );
82
+ }
83
+ return translate(
84
+ "webhooks.errors.redactedHeaderWithoutValue",
85
+ 'Header "{header}" has a hidden value but no saved value to keep. Enter the header value.',
86
+ { header: rejection.header }
87
+ );
88
+ }
64
89
  function serializeWebhookListItem(item) {
65
90
  return {
66
91
  id: item.id,
@@ -79,10 +104,10 @@ function serializeWebhookListItem(item) {
79
104
  updatedAt: item.updatedAt.toISOString()
80
105
  };
81
106
  }
82
- function serializeWebhookDetail(item) {
107
+ function serializeWebhookDetail(item, options = {}) {
83
108
  return {
84
109
  ...serializeWebhookListItem(item),
85
- customHeaders: item.customHeaders ?? null,
110
+ customHeaders: options.revealCredentialHeaders ? item.customHeaders ?? null : redactWebhookCustomHeaders(item.customHeaders),
86
111
  strategyConfig: item.strategyConfig ?? null,
87
112
  timeoutMs: item.timeoutMs,
88
113
  rateLimitPerMinute: item.rateLimitPerMinute,
@@ -127,6 +152,7 @@ function maskSecret(secret) {
127
152
  return `${secret.slice(0, 6)}\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022`;
128
153
  }
129
154
  export {
155
+ canRevealWebhookCredentialHeaders,
130
156
  findScopedDelivery,
131
157
  findScopedWebhook,
132
158
  json,
@@ -134,6 +160,7 @@ export {
134
160
  serializeDeliveryDetail,
135
161
  serializeDeliveryListItem,
136
162
  serializeWebhookDetail,
137
- serializeWebhookListItem
163
+ serializeWebhookListItem,
164
+ translateWebhookCustomHeadersRejection
138
165
  };
139
166
  //# sourceMappingURL=helpers.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/api/helpers.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\n\nexport type WebhookRequestScope = {\n container: Awaited<ReturnType<typeof createRequestContainer>>\n em: EntityManager\n tenantId: string\n organizationId: string | null\n allowedOrganizationIds: string[] | null\n}\n\nexport function json(payload: unknown, init: ResponseInit = { status: 200 }): Response {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nexport async function resolveWebhookRequestScope(request: Request): Promise<WebhookRequestScope | Response> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(request)\n const { translate } = await resolveTranslations()\n\n if (!auth) {\n return json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = scope?.tenantId ?? auth.tenantId ?? null\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth)\n const allowedOrganizationIds = Array.isArray(scope?.allowedIds) ? scope.allowedIds : null\n\n if (!tenantId) {\n return json({ error: translate('webhooks.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n }\n\n return {\n container,\n em: container.resolve('em') as EntityManager,\n tenantId,\n organizationId,\n allowedOrganizationIds,\n }\n}\n\nexport async function findScopedWebhook(\n em: EntityManager,\n scope: Pick<WebhookRequestScope, 'tenantId' | 'organizationId' | 'allowedOrganizationIds'>,\n webhookId: string,\n): Promise<WebhookEntity | null> {\n const webhook = await findOneWithDecryption(\n em,\n WebhookEntity,\n { id: webhookId, tenantId: scope.tenantId, deletedAt: null },\n {},\n { tenantId: scope.tenantId, organizationId: scope.organizationId ?? '' },\n )\n\n if (!webhook) return null\n if (scope.allowedOrganizationIds && scope.allowedOrganizationIds.length > 0 && !scope.allowedOrganizationIds.includes(webhook.organizationId)) {\n return null\n }\n if (!scope.allowedOrganizationIds && scope.organizationId && webhook.organizationId !== scope.organizationId) {\n return null\n }\n\n return webhook\n}\n\nexport async function findScopedDelivery(\n em: EntityManager,\n scope: Pick<WebhookRequestScope, 'tenantId' | 'organizationId' | 'allowedOrganizationIds'>,\n deliveryId: string,\n): Promise<WebhookDeliveryEntity | null> {\n const delivery = await em.findOne(WebhookDeliveryEntity, { id: deliveryId, tenantId: scope.tenantId })\n if (!delivery) return null\n\n if (scope.allowedOrganizationIds && scope.allowedOrganizationIds.length > 0 && !scope.allowedOrganizationIds.includes(delivery.organizationId)) {\n return null\n }\n if (!scope.allowedOrganizationIds && scope.organizationId && delivery.organizationId !== scope.organizationId) {\n return null\n }\n\n return delivery\n}\n\nexport function serializeWebhookListItem(item: WebhookEntity) {\n return {\n id: item.id,\n name: item.name,\n description: item.description ?? null,\n url: item.url,\n subscribedEvents: item.subscribedEvents,\n httpMethod: item.httpMethod,\n isActive: item.isActive,\n deliveryStrategy: item.deliveryStrategy,\n maxRetries: item.maxRetries,\n consecutiveFailures: item.consecutiveFailures,\n lastSuccessAt: item.lastSuccessAt?.toISOString() ?? null,\n lastFailureAt: item.lastFailureAt?.toISOString() ?? null,\n createdAt: item.createdAt.toISOString(),\n updatedAt: item.updatedAt.toISOString(),\n }\n}\n\nexport function serializeWebhookDetail(item: WebhookEntity) {\n return {\n ...serializeWebhookListItem(item),\n customHeaders: item.customHeaders ?? null,\n strategyConfig: item.strategyConfig ?? null,\n timeoutMs: item.timeoutMs,\n rateLimitPerMinute: item.rateLimitPerMinute,\n autoDisableThreshold: item.autoDisableThreshold,\n integrationId: item.integrationId ?? null,\n maskedSecret: maskSecret(item.secret),\n previousSecretSetAt: item.previousSecretSetAt?.toISOString() ?? null,\n }\n}\n\nexport function serializeDeliveryListItem(\n item: WebhookDeliveryEntity,\n options: { webhookName?: string | null } = {},\n) {\n return {\n id: item.id,\n webhookId: item.webhookId,\n webhookName: options.webhookName ?? null,\n eventType: item.eventType,\n messageId: item.messageId,\n status: item.status,\n responseStatus: item.responseStatus ?? null,\n errorMessage: item.errorMessage ?? null,\n attemptNumber: item.attemptNumber,\n maxAttempts: item.maxAttempts,\n targetUrl: item.targetUrl,\n durationMs: item.durationMs ?? null,\n enqueuedAt: item.enqueuedAt.toISOString(),\n lastAttemptAt: item.lastAttemptAt?.toISOString() ?? null,\n deliveredAt: item.deliveredAt?.toISOString() ?? null,\n createdAt: item.createdAt.toISOString(),\n }\n}\n\nexport function serializeDeliveryDetail(item: WebhookDeliveryEntity) {\n return {\n ...serializeDeliveryListItem(item),\n payload: item.payload,\n responseBody: item.responseBody ?? null,\n responseHeaders: item.responseHeaders ?? null,\n nextRetryAt: item.nextRetryAt?.toISOString() ?? null,\n updatedAt: item.updatedAt.toISOString(),\n }\n}\n\nfunction maskSecret(secret: string): string {\n if (!secret) return '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022'\n return `${secret.slice(0, 6)}\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022`\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { redactWebhookCustomHeaders, type WebhookCustomHeadersUpdate } from '../lib/custom-headers'\n\nexport type WebhookRequestScope = {\n container: Awaited<ReturnType<typeof createRequestContainer>>\n em: EntityManager\n tenantId: string\n organizationId: string | null\n allowedOrganizationIds: string[] | null\n auth?: AuthContext\n}\n\nexport function json(payload: unknown, init: ResponseInit = { status: 200 }): Response {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nexport async function resolveWebhookRequestScope(request: Request): Promise<WebhookRequestScope | Response> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(request)\n const { translate } = await resolveTranslations()\n\n if (!auth) {\n return json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = scope?.tenantId ?? auth.tenantId ?? null\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth)\n const allowedOrganizationIds = Array.isArray(scope?.allowedIds) ? scope.allowedIds : null\n\n if (!tenantId) {\n return json({ error: translate('webhooks.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n }\n\n return {\n container,\n em: container.resolve('em') as EntityManager,\n tenantId,\n organizationId,\n allowedOrganizationIds,\n auth,\n }\n}\n\nexport async function canRevealWebhookCredentialHeaders(\n container: WebhookRequestScope['container'],\n auth: AuthContext | undefined,\n webhook: Pick<WebhookEntity, 'tenantId' | 'organizationId'>,\n): Promise<boolean> {\n if (!auth?.sub) return false\n const rbacService = container.resolve('rbacService') as RbacService\n return rbacService.userHasAllFeatures(auth.sub, ['webhooks.secrets'], {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n}\n\nexport async function findScopedWebhook(\n em: EntityManager,\n scope: Pick<WebhookRequestScope, 'tenantId' | 'organizationId' | 'allowedOrganizationIds'>,\n webhookId: string,\n): Promise<WebhookEntity | null> {\n const webhook = await findOneWithDecryption(\n em,\n WebhookEntity,\n { id: webhookId, tenantId: scope.tenantId, deletedAt: null },\n {},\n { tenantId: scope.tenantId, organizationId: scope.organizationId ?? '' },\n )\n\n if (!webhook) return null\n if (scope.allowedOrganizationIds && scope.allowedOrganizationIds.length > 0 && !scope.allowedOrganizationIds.includes(webhook.organizationId)) {\n return null\n }\n if (!scope.allowedOrganizationIds && scope.organizationId && webhook.organizationId !== scope.organizationId) {\n return null\n }\n\n return webhook\n}\n\nexport async function findScopedDelivery(\n em: EntityManager,\n scope: Pick<WebhookRequestScope, 'tenantId' | 'organizationId' | 'allowedOrganizationIds'>,\n deliveryId: string,\n): Promise<WebhookDeliveryEntity | null> {\n const delivery = await em.findOne(WebhookDeliveryEntity, { id: deliveryId, tenantId: scope.tenantId })\n if (!delivery) return null\n\n if (scope.allowedOrganizationIds && scope.allowedOrganizationIds.length > 0 && !scope.allowedOrganizationIds.includes(delivery.organizationId)) {\n return null\n }\n if (!scope.allowedOrganizationIds && scope.organizationId && delivery.organizationId !== scope.organizationId) {\n return null\n }\n\n return delivery\n}\n\nexport async function translateWebhookCustomHeadersRejection(\n rejection: Extract<WebhookCustomHeadersUpdate, { ok: false }>,\n): Promise<string> {\n const { translate } = await resolveTranslations()\n if (rejection.reason === 'credentialHeadersRequireReentry') {\n return translate(\n 'webhooks.errors.credentialHeadersRequireReentry',\n 'Header \"{header}\" has a hidden value. Enter it again or remove it when you change the webhook URL.',\n { header: rejection.header },\n )\n }\n return translate(\n 'webhooks.errors.redactedHeaderWithoutValue',\n 'Header \"{header}\" has a hidden value but no saved value to keep. Enter the header value.',\n { header: rejection.header },\n )\n}\n\nexport function serializeWebhookListItem(item: WebhookEntity) {\n return {\n id: item.id,\n name: item.name,\n description: item.description ?? null,\n url: item.url,\n subscribedEvents: item.subscribedEvents,\n httpMethod: item.httpMethod,\n isActive: item.isActive,\n deliveryStrategy: item.deliveryStrategy,\n maxRetries: item.maxRetries,\n consecutiveFailures: item.consecutiveFailures,\n lastSuccessAt: item.lastSuccessAt?.toISOString() ?? null,\n lastFailureAt: item.lastFailureAt?.toISOString() ?? null,\n createdAt: item.createdAt.toISOString(),\n updatedAt: item.updatedAt.toISOString(),\n }\n}\n\nexport function serializeWebhookDetail(\n item: WebhookEntity,\n options: { revealCredentialHeaders?: boolean } = {},\n) {\n return {\n ...serializeWebhookListItem(item),\n customHeaders: options.revealCredentialHeaders\n ? item.customHeaders ?? null\n : redactWebhookCustomHeaders(item.customHeaders),\n strategyConfig: item.strategyConfig ?? null,\n timeoutMs: item.timeoutMs,\n rateLimitPerMinute: item.rateLimitPerMinute,\n autoDisableThreshold: item.autoDisableThreshold,\n integrationId: item.integrationId ?? null,\n maskedSecret: maskSecret(item.secret),\n previousSecretSetAt: item.previousSecretSetAt?.toISOString() ?? null,\n }\n}\n\nexport function serializeDeliveryListItem(\n item: WebhookDeliveryEntity,\n options: { webhookName?: string | null } = {},\n) {\n return {\n id: item.id,\n webhookId: item.webhookId,\n webhookName: options.webhookName ?? null,\n eventType: item.eventType,\n messageId: item.messageId,\n status: item.status,\n responseStatus: item.responseStatus ?? null,\n errorMessage: item.errorMessage ?? null,\n attemptNumber: item.attemptNumber,\n maxAttempts: item.maxAttempts,\n targetUrl: item.targetUrl,\n durationMs: item.durationMs ?? null,\n enqueuedAt: item.enqueuedAt.toISOString(),\n lastAttemptAt: item.lastAttemptAt?.toISOString() ?? null,\n deliveredAt: item.deliveredAt?.toISOString() ?? null,\n createdAt: item.createdAt.toISOString(),\n }\n}\n\nexport function serializeDeliveryDetail(item: WebhookDeliveryEntity) {\n return {\n ...serializeDeliveryListItem(item),\n payload: item.payload,\n responseBody: item.responseBody ?? null,\n responseHeaders: item.responseHeaders ?? null,\n nextRetryAt: item.nextRetryAt?.toISOString() ?? null,\n updatedAt: item.updatedAt.toISOString(),\n }\n}\n\nfunction maskSecret(secret: string): string {\n if (!secret) return '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022'\n return `${secret.slice(0, 6)}\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022`\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,12 +1,20 @@
1
1
  import { z } from "zod";
2
2
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
3
3
  import { emitWebhooksEvent } from "../../../events.js";
4
- import { findScopedWebhook, json, resolveWebhookRequestScope, serializeWebhookDetail } from "../../helpers.js";
4
+ import {
5
+ canRevealWebhookCredentialHeaders,
6
+ findScopedWebhook,
7
+ json,
8
+ resolveWebhookRequestScope,
9
+ serializeWebhookDetail,
10
+ translateWebhookCustomHeadersRejection
11
+ } from "../../helpers.js";
5
12
  import { webhookUpdateSchema } from "../../../data/validators.js";
6
13
  import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
7
14
  import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
8
15
  import { invalidateWebhookSubscriptionCacheFor } from "../../../lib/subscription-cache.js";
9
16
  import { applyWebhookActiveState } from "../../../lib/activation.js";
17
+ import { planWebhookCustomHeadersUpdate } from "../../../lib/custom-headers.js";
10
18
  const metadata = {
11
19
  GET: { requireAuth: true, requireFeatures: ["webhooks.view"] },
12
20
  PUT: { requireAuth: true, requireFeatures: ["webhooks.manage"] },
@@ -27,7 +35,9 @@ const webhookDetailResponseSchema = z.object({
27
35
  lastFailureAt: z.string().nullable(),
28
36
  createdAt: z.string(),
29
37
  updatedAt: z.string(),
30
- customHeaders: z.record(z.string(), z.string()).nullable(),
38
+ customHeaders: z.record(z.string(), z.string()).nullable().describe(
39
+ "Credential-like header values are returned as \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 unless the caller has webhooks.secrets. Sending \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 back on update keeps the stored value."
40
+ ),
31
41
  strategyConfig: z.record(z.string(), z.unknown()).nullable(),
32
42
  timeoutMs: z.number(),
33
43
  rateLimitPerMinute: z.number(),
@@ -46,7 +56,8 @@ async function GET(request, context) {
46
56
  if (!webhook) {
47
57
  return json({ error: "Webhook not found" }, { status: 404 });
48
58
  }
49
- return json(serializeWebhookDetail(webhook));
59
+ const revealCredentialHeaders = await canRevealWebhookCredentialHeaders(scope.container, scope.auth, webhook);
60
+ return json(serializeWebhookDetail(webhook, { revealCredentialHeaders }));
50
61
  }
51
62
  async function PUT(request, context) {
52
63
  const scope = await resolveWebhookRequestScope(request);
@@ -73,12 +84,21 @@ async function PUT(request, context) {
73
84
  return json({ error: "Invalid request payload" }, { status: 400 });
74
85
  }
75
86
  const input = parsed.data;
87
+ const revealCredentialHeaders = await canRevealWebhookCredentialHeaders(scope.container, scope.auth, webhook);
88
+ const headersUpdate = planWebhookCustomHeadersUpdate({
89
+ current: webhook,
90
+ next: input,
91
+ canRevealCredentials: revealCredentialHeaders
92
+ });
93
+ if (!headersUpdate.ok) {
94
+ return json({ error: await translateWebhookCustomHeadersRejection(headersUpdate) }, { status: 400 });
95
+ }
76
96
  if (input.name !== void 0) webhook.name = input.name;
77
97
  if (input.description !== void 0) webhook.description = input.description;
78
98
  if (input.url !== void 0) webhook.url = input.url;
79
99
  if (input.subscribedEvents !== void 0) webhook.subscribedEvents = input.subscribedEvents;
80
100
  if (input.httpMethod !== void 0) webhook.httpMethod = input.httpMethod;
81
- if (input.customHeaders !== void 0) webhook.customHeaders = input.customHeaders;
101
+ if (headersUpdate.customHeaders !== void 0) webhook.customHeaders = headersUpdate.customHeaders;
82
102
  if (input.deliveryStrategy !== void 0) webhook.deliveryStrategy = input.deliveryStrategy;
83
103
  if (input.strategyConfig !== void 0) webhook.strategyConfig = input.strategyConfig;
84
104
  if (input.maxRetries !== void 0) webhook.maxRetries = input.maxRetries;
@@ -94,7 +114,7 @@ async function PUT(request, context) {
94
114
  organizationId: webhook.organizationId,
95
115
  tenantId: webhook.tenantId
96
116
  }, { persistent: true });
97
- return json(serializeWebhookDetail(webhook));
117
+ return json(serializeWebhookDetail(webhook, { revealCredentialHeaders }));
98
118
  }
99
119
  async function DELETE(request, context) {
100
120
  const scope = await resolveWebhookRequestScope(request);
@@ -152,7 +172,11 @@ const openApi = {
152
172
  },
153
173
  responses: [{ status: 200, description: "Webhook updated", schema: webhookDetailResponseSchema }],
154
174
  errors: [
155
- { status: 400, description: "Invalid payload", schema: errorSchema },
175
+ {
176
+ status: 400,
177
+ description: "Invalid payload, a hidden header value with no stored value, or a URL change that keeps hidden header values for a caller without webhooks.secrets",
178
+ schema: errorSchema
179
+ },
156
180
  { status: 401, description: "Unauthorized", schema: errorSchema },
157
181
  { status: 404, description: "Webhook not found", schema: errorSchema }
158
182
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/webhooks/api/webhooks/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { emitWebhooksEvent } from '../../../events'\nimport { findScopedWebhook, json, resolveWebhookRequestScope, serializeWebhookDetail } from '../../helpers'\nimport { webhookUpdateSchema } from '../../../data/validators'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateWebhookSubscriptionCacheFor } from '../../../lib/subscription-cache'\nimport { applyWebhookActiveState } from '../../../lib/activation'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['webhooks.view'] },\n PUT: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n}\n\ninterface RouteContext {\n params: Promise<{ id: string }>\n}\n\nconst webhookDetailResponseSchema = z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().nullable(),\n url: z.string(),\n subscribedEvents: z.array(z.string()),\n httpMethod: z.string(),\n isActive: z.boolean(),\n deliveryStrategy: z.string(),\n maxRetries: z.number(),\n consecutiveFailures: z.number(),\n lastSuccessAt: z.string().nullable(),\n lastFailureAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n customHeaders: z.record(z.string(), z.string()).nullable(),\n strategyConfig: z.record(z.string(), z.unknown()).nullable(),\n timeoutMs: z.number(),\n rateLimitPerMinute: z.number(),\n autoDisableThreshold: z.number(),\n integrationId: z.string().nullable(),\n maskedSecret: z.string(),\n previousSecretSetAt: z.string().nullable(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\nconst deleteResponseSchema = z.object({ success: z.literal(true) })\n\nexport async function GET(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const webhook = await findScopedWebhook(scope.em.fork(), scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n return json(serializeWebhookDetail(webhook))\n}\n\nexport async function PUT(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n try {\n await enforceCommandOptimisticLockWithGuards(scope.container, {\n resourceKind: 'webhooks.endpoint',\n resourceId: webhook.id,\n current: webhook.updatedAt ?? null,\n request,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return json(err.body, { status: err.status })\n throw err\n }\n\n const parsed = webhookUpdateSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return json({ error: 'Invalid request payload' }, { status: 400 })\n }\n\n const input = parsed.data\n if (input.name !== undefined) webhook.name = input.name\n if (input.description !== undefined) webhook.description = input.description\n if (input.url !== undefined) webhook.url = input.url\n if (input.subscribedEvents !== undefined) webhook.subscribedEvents = input.subscribedEvents\n if (input.httpMethod !== undefined) webhook.httpMethod = input.httpMethod\n if (input.customHeaders !== undefined) webhook.customHeaders = input.customHeaders\n if (input.deliveryStrategy !== undefined) webhook.deliveryStrategy = input.deliveryStrategy\n if (input.strategyConfig !== undefined) webhook.strategyConfig = input.strategyConfig\n if (input.maxRetries !== undefined) webhook.maxRetries = input.maxRetries\n if (input.timeoutMs !== undefined) webhook.timeoutMs = input.timeoutMs\n if (input.rateLimitPerMinute !== undefined) webhook.rateLimitPerMinute = input.rateLimitPerMinute\n if (input.autoDisableThreshold !== undefined) webhook.autoDisableThreshold = input.autoDisableThreshold\n if (input.integrationId !== undefined) webhook.integrationId = input.integrationId\n if (input.isActive !== undefined) applyWebhookActiveState(webhook, input.isActive)\n\n await em.flush()\n\n await invalidateWebhookSubscriptionCacheFor(scope.container, webhook.tenantId)\n\n await emitWebhooksEvent('webhooks.webhook.updated', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n }, { persistent: true })\n\n return json(serializeWebhookDetail(webhook))\n}\n\nexport async function DELETE(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n const { translate } = await resolveTranslations()\n\n if (!webhook) {\n return json({ error: translate('webhooks.errors.notFound', 'Webhook not found') }, { status: 404 })\n }\n\n try {\n await enforceCommandOptimisticLockWithGuards(scope.container, {\n resourceKind: 'webhooks.endpoint',\n resourceId: webhook.id,\n current: webhook.updatedAt ?? null,\n request,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return json(err.body, { status: err.status })\n throw err\n }\n\n webhook.deletedAt = new Date()\n await em.flush()\n\n await invalidateWebhookSubscriptionCacheFor(scope.container, webhook.tenantId)\n\n await emitWebhooksEvent('webhooks.webhook.deleted', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n }, { persistent: true })\n\n return json({ success: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Get webhook detail',\n description: 'Returns a single webhook configuration for the current tenant scope.',\n methods: {\n GET: {\n summary: 'Get webhook',\n description: 'Returns webhook configuration, masked secret metadata, and delivery settings.',\n pathParams: z.object({ id: z.string().uuid() }),\n responses: [{ status: 200, description: 'Webhook detail', schema: webhookDetailResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n PUT: {\n summary: 'Update webhook',\n description: 'Updates a single webhook configuration.',\n pathParams: z.object({ id: z.string().uuid() }),\n requestBody: {\n contentType: 'application/json',\n schema: webhookUpdateSchema,\n description: 'Webhook fields to update.',\n },\n responses: [{ status: 200, description: 'Webhook updated', schema: webhookDetailResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete webhook',\n description: 'Soft-deletes a webhook endpoint.',\n pathParams: z.object({ id: z.string().uuid() }),\n responses: [{ status: 200, description: 'Webhook deleted', schema: deleteResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { emitWebhooksEvent } from '../../../events'\nimport {\n canRevealWebhookCredentialHeaders,\n findScopedWebhook,\n json,\n resolveWebhookRequestScope,\n serializeWebhookDetail,\n translateWebhookCustomHeadersRejection,\n} from '../../helpers'\nimport { webhookUpdateSchema } from '../../../data/validators'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateWebhookSubscriptionCacheFor } from '../../../lib/subscription-cache'\nimport { applyWebhookActiveState } from '../../../lib/activation'\nimport { planWebhookCustomHeadersUpdate } from '../../../lib/custom-headers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['webhooks.view'] },\n PUT: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['webhooks.manage'] },\n}\n\ninterface RouteContext {\n params: Promise<{ id: string }>\n}\n\nconst webhookDetailResponseSchema = z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().nullable(),\n url: z.string(),\n subscribedEvents: z.array(z.string()),\n httpMethod: z.string(),\n isActive: z.boolean(),\n deliveryStrategy: z.string(),\n maxRetries: z.number(),\n consecutiveFailures: z.number(),\n lastSuccessAt: z.string().nullable(),\n lastFailureAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n customHeaders: z.record(z.string(), z.string()).nullable().describe(\n 'Credential-like header values are returned as \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 unless the caller has webhooks.secrets. Sending \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 back on update keeps the stored value.',\n ),\n strategyConfig: z.record(z.string(), z.unknown()).nullable(),\n timeoutMs: z.number(),\n rateLimitPerMinute: z.number(),\n autoDisableThreshold: z.number(),\n integrationId: z.string().nullable(),\n maskedSecret: z.string(),\n previousSecretSetAt: z.string().nullable(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\nconst deleteResponseSchema = z.object({ success: z.literal(true) })\n\nexport async function GET(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const webhook = await findScopedWebhook(scope.em.fork(), scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n const revealCredentialHeaders = await canRevealWebhookCredentialHeaders(scope.container, scope.auth, webhook)\n return json(serializeWebhookDetail(webhook, { revealCredentialHeaders }))\n}\n\nexport async function PUT(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n\n if (!webhook) {\n return json({ error: 'Webhook not found' }, { status: 404 })\n }\n\n try {\n await enforceCommandOptimisticLockWithGuards(scope.container, {\n resourceKind: 'webhooks.endpoint',\n resourceId: webhook.id,\n current: webhook.updatedAt ?? null,\n request,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return json(err.body, { status: err.status })\n throw err\n }\n\n const parsed = webhookUpdateSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return json({ error: 'Invalid request payload' }, { status: 400 })\n }\n\n const input = parsed.data\n const revealCredentialHeaders = await canRevealWebhookCredentialHeaders(scope.container, scope.auth, webhook)\n const headersUpdate = planWebhookCustomHeadersUpdate({\n current: webhook,\n next: input,\n canRevealCredentials: revealCredentialHeaders,\n })\n if (!headersUpdate.ok) {\n return json({ error: await translateWebhookCustomHeadersRejection(headersUpdate) }, { status: 400 })\n }\n\n if (input.name !== undefined) webhook.name = input.name\n if (input.description !== undefined) webhook.description = input.description\n if (input.url !== undefined) webhook.url = input.url\n if (input.subscribedEvents !== undefined) webhook.subscribedEvents = input.subscribedEvents\n if (input.httpMethod !== undefined) webhook.httpMethod = input.httpMethod\n if (headersUpdate.customHeaders !== undefined) webhook.customHeaders = headersUpdate.customHeaders\n if (input.deliveryStrategy !== undefined) webhook.deliveryStrategy = input.deliveryStrategy\n if (input.strategyConfig !== undefined) webhook.strategyConfig = input.strategyConfig\n if (input.maxRetries !== undefined) webhook.maxRetries = input.maxRetries\n if (input.timeoutMs !== undefined) webhook.timeoutMs = input.timeoutMs\n if (input.rateLimitPerMinute !== undefined) webhook.rateLimitPerMinute = input.rateLimitPerMinute\n if (input.autoDisableThreshold !== undefined) webhook.autoDisableThreshold = input.autoDisableThreshold\n if (input.integrationId !== undefined) webhook.integrationId = input.integrationId\n if (input.isActive !== undefined) applyWebhookActiveState(webhook, input.isActive)\n\n await em.flush()\n\n await invalidateWebhookSubscriptionCacheFor(scope.container, webhook.tenantId)\n\n await emitWebhooksEvent('webhooks.webhook.updated', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n }, { persistent: true })\n\n return json(serializeWebhookDetail(webhook, { revealCredentialHeaders }))\n}\n\nexport async function DELETE(request: Request, context: RouteContext): Promise<Response> {\n const scope = await resolveWebhookRequestScope(request)\n if (scope instanceof Response) return scope\n\n const params = await context.params\n const em = scope.em.fork()\n const webhook = await findScopedWebhook(em, scope, params.id)\n const { translate } = await resolveTranslations()\n\n if (!webhook) {\n return json({ error: translate('webhooks.errors.notFound', 'Webhook not found') }, { status: 404 })\n }\n\n try {\n await enforceCommandOptimisticLockWithGuards(scope.container, {\n resourceKind: 'webhooks.endpoint',\n resourceId: webhook.id,\n current: webhook.updatedAt ?? null,\n request,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return json(err.body, { status: err.status })\n throw err\n }\n\n webhook.deletedAt = new Date()\n await em.flush()\n\n await invalidateWebhookSubscriptionCacheFor(scope.container, webhook.tenantId)\n\n await emitWebhooksEvent('webhooks.webhook.deleted', {\n webhookId: webhook.id,\n organizationId: webhook.organizationId,\n tenantId: webhook.tenantId,\n }, { persistent: true })\n\n return json({ success: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Get webhook detail',\n description: 'Returns a single webhook configuration for the current tenant scope.',\n methods: {\n GET: {\n summary: 'Get webhook',\n description: 'Returns webhook configuration, masked secret metadata, and delivery settings.',\n pathParams: z.object({ id: z.string().uuid() }),\n responses: [{ status: 200, description: 'Webhook detail', schema: webhookDetailResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n PUT: {\n summary: 'Update webhook',\n description: 'Updates a single webhook configuration.',\n pathParams: z.object({ id: z.string().uuid() }),\n requestBody: {\n contentType: 'application/json',\n schema: webhookUpdateSchema,\n description: 'Webhook fields to update.',\n },\n responses: [{ status: 200, description: 'Webhook updated', schema: webhookDetailResponseSchema }],\n errors: [\n {\n status: 400,\n description: 'Invalid payload, a hidden header value with no stored value, or a URL change that keeps hidden header values for a caller without webhooks.secrets',\n schema: errorSchema,\n },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete webhook',\n description: 'Soft-deletes a webhook endpoint.',\n pathParams: z.object({ id: z.string().uuid() }),\n responses: [{ status: 200, description: 'Webhook deleted', schema: deleteResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Webhook not found', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,9 @@ import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern
8
8
  import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
9
9
  import { invalidateWebhookSubscriptionCacheFor } from "../../lib/subscription-cache.js";
10
10
  import { applyWebhookActiveState } from "../../lib/activation.js";
11
+ import { planWebhookCustomHeadersUpdate } from "../../lib/custom-headers.js";
12
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
13
+ import { canRevealWebhookCredentialHeaders, translateWebhookCustomHeadersRejection } from "../helpers.js";
11
14
  function json(payload, init = { status: 200 }) {
12
15
  return new Response(JSON.stringify(payload), {
13
16
  ...init,
@@ -46,7 +49,9 @@ const webhookCreateResponseSchema = z.object({
46
49
  isActive: z.boolean()
47
50
  });
48
51
  const webhookDetailResponseSchema = webhookListItemSchema.extend({
49
- customHeaders: z.record(z.string(), z.string()).nullable(),
52
+ customHeaders: z.record(z.string(), z.string()).nullable().describe(
53
+ "Credential-like header values are returned as \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 unless the caller has webhooks.secrets. Sending \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 back on update keeps the stored value."
54
+ ),
50
55
  strategyConfig: z.record(z.string(), z.unknown()).nullable(),
51
56
  timeoutMs: z.number(),
52
57
  rateLimitPerMinute: z.number(),
@@ -106,13 +111,21 @@ const crud = makeCrudRoute({
106
111
  },
107
112
  update: {
108
113
  schema: webhookUpdateSchema,
109
- applyToEntity: (entity, input) => {
114
+ applyToEntity: async (entity, input, ctx) => {
115
+ const headersUpdate = planWebhookCustomHeadersUpdate({
116
+ current: entity,
117
+ next: input,
118
+ canRevealCredentials: await canRevealWebhookCredentialHeaders(ctx.container, ctx.auth, entity)
119
+ });
120
+ if (!headersUpdate.ok) {
121
+ throw new CrudHttpError(400, { error: await translateWebhookCustomHeadersRejection(headersUpdate) });
122
+ }
110
123
  if (input.name !== void 0) entity.name = input.name;
111
124
  if (input.description !== void 0) entity.description = input.description;
112
125
  if (input.url !== void 0) entity.url = input.url;
113
126
  if (input.subscribedEvents !== void 0) entity.subscribedEvents = input.subscribedEvents;
114
127
  if (input.httpMethod !== void 0) entity.httpMethod = input.httpMethod;
115
- if (input.customHeaders !== void 0) entity.customHeaders = input.customHeaders;
128
+ if (headersUpdate.customHeaders !== void 0) entity.customHeaders = headersUpdate.customHeaders;
116
129
  if (input.deliveryStrategy !== void 0) entity.deliveryStrategy = input.deliveryStrategy;
117
130
  if (input.strategyConfig !== void 0) entity.strategyConfig = input.strategyConfig;
118
131
  if (input.maxRetries !== void 0) entity.maxRetries = input.maxRetries;
@@ -261,7 +274,11 @@ const openApi = {
261
274
  requestBody: { contentType: "application/json", schema: webhookUpdateSchema, description: "Fields to update." },
262
275
  responses: [{ status: 200, description: "Webhook updated", schema: webhookDetailResponseSchema }],
263
276
  errors: [
264
- { status: 400, description: "Invalid payload", schema: errorSchema },
277
+ {
278
+ status: 400,
279
+ description: "Invalid payload, a hidden header value with no stored value, or a URL change that keeps hidden header values for a caller without webhooks.secrets",
280
+ schema: errorSchema
281
+ },
265
282
  { status: 404, description: "Not found", schema: errorSchema }
266
283
  ]
267
284
  },