@open-mercato/webhooks 0.10.1-develop.7618.1.ba9531c18e → 0.10.1-develop.7622.1.0584ba2a04
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/webhooks/__integration__/TC-WEBHOOK-013.spec.js +191 -0
- package/dist/modules/webhooks/__integration__/TC-WEBHOOK-013.spec.js.map +7 -0
- package/dist/modules/webhooks/api/helpers.js +31 -4
- package/dist/modules/webhooks/api/helpers.js.map +2 -2
- package/dist/modules/webhooks/api/webhooks/[id]/route.js +30 -6
- package/dist/modules/webhooks/api/webhooks/[id]/route.js.map +2 -2
- package/dist/modules/webhooks/api/webhooks/route.js +21 -4
- package/dist/modules/webhooks/api/webhooks/route.js.map +2 -2
- package/dist/modules/webhooks/lib/custom-headers.js +60 -0
- package/dist/modules/webhooks/lib/custom-headers.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/webhooks/__integration__/TC-WEBHOOK-013.spec.ts +231 -0
- package/src/modules/webhooks/api/helpers.ts +43 -2
- package/src/modules/webhooks/api/webhooks/[id]/__tests__/credential-headers.test.ts +358 -0
- package/src/modules/webhooks/api/webhooks/[id]/__tests__/optimistic-lock.test.ts +2 -0
- package/src/modules/webhooks/api/webhooks/[id]/__tests__/reactivation.test.ts +2 -0
- package/src/modules/webhooks/api/webhooks/[id]/__tests__/subscription-cache-invalidation.test.ts +2 -0
- package/src/modules/webhooks/api/webhooks/[id]/route.ts +31 -6
- package/src/modules/webhooks/api/webhooks/__tests__/credential-headers.test.ts +96 -0
- package/src/modules/webhooks/api/webhooks/route.ts +21 -4
- package/src/modules/webhooks/i18n/de.json +3 -1
- package/src/modules/webhooks/i18n/en.json +3 -1
- package/src/modules/webhooks/i18n/es.json +3 -1
- package/src/modules/webhooks/i18n/ko.json +3 -1
- package/src/modules/webhooks/i18n/pl.json +3 -1
- package/src/modules/webhooks/lib/__tests__/custom-headers.test.ts +144 -0
- package/src/modules/webhooks/lib/custom-headers.ts +84 -0
|
@@ -45,9 +45,11 @@
|
|
|
45
45
|
"webhooks.detail.testResult": "Latest Test Result",
|
|
46
46
|
"webhooks.detail.testSuccess": "Test delivery succeeded.",
|
|
47
47
|
"webhooks.detail.title": "Webhook Detail",
|
|
48
|
+
"webhooks.errors.credentialHeadersRequireReentry": "El encabezado \"{header}\" tiene un valor oculto. Vuelve a introducirlo o elimínalo al cambiar la URL del webhook.",
|
|
48
49
|
"webhooks.errors.forbidden": "You do not have permission to perform this action.",
|
|
49
50
|
"webhooks.errors.notFound": "Webhook not found.",
|
|
50
51
|
"webhooks.errors.orgRequired": "Organization context required.",
|
|
52
|
+
"webhooks.errors.redactedHeaderWithoutValue": "El encabezado \"{header}\" tiene un valor oculto, pero no hay ningún valor guardado que conservar. Introduce el valor del encabezado.",
|
|
51
53
|
"webhooks.errors.tenantRequired": "Tenant context required.",
|
|
52
54
|
"webhooks.form.advancedStrategyTip": "HTTP delivery is active today. Use custom headers for routing hints, and keep secrets out of request headers.",
|
|
53
55
|
"webhooks.form.autoDisableThreshold": "Auto-Disable Threshold",
|
|
@@ -55,7 +57,7 @@
|
|
|
55
57
|
"webhooks.form.createError": "Failed to create webhook.",
|
|
56
58
|
"webhooks.form.createSuccess": "Webhook created successfully.",
|
|
57
59
|
"webhooks.form.customHeaders": "Custom Headers",
|
|
58
|
-
"webhooks.form.customHeadersHint": "
|
|
60
|
+
"webhooks.form.customHeadersHint": "Objeto JSON opcional con encabezados adicionales que se envían con cada solicitud del webhook. Los valores ocultos (••••••••) se conservan al guardar; sustituye uno para cambiarlo.",
|
|
59
61
|
"webhooks.form.customHeadersInvalid": "Custom headers must be a valid JSON object with string values.",
|
|
60
62
|
"webhooks.form.customHeadersTip": "Use headers for static metadata such as environment or source identifiers. Do not put access tokens or secrets here.",
|
|
61
63
|
"webhooks.form.deliveryDefaultsTip": "The defaults are a safe starting point for most consumers. Increase retries or timeout only if the receiver is known to be slow.",
|
|
@@ -45,9 +45,11 @@
|
|
|
45
45
|
"webhooks.detail.testResult": "최근 테스트 결과",
|
|
46
46
|
"webhooks.detail.testSuccess": "테스트 전달에 성공했습니다.",
|
|
47
47
|
"webhooks.detail.title": "웹훅 상세 정보",
|
|
48
|
+
"webhooks.errors.credentialHeadersRequireReentry": "\"{header}\" 헤더에 숨겨진 값이 있습니다. 웹훅 URL을 변경할 때는 값을 다시 입력하거나 헤더를 제거하세요.",
|
|
48
49
|
"webhooks.errors.forbidden": "이 작업을 수행할 권한이 없습니다.",
|
|
49
50
|
"webhooks.errors.notFound": "웹훅을 찾을 수 없습니다.",
|
|
50
51
|
"webhooks.errors.orgRequired": "조직 컨텍스트가 필요합니다.",
|
|
52
|
+
"webhooks.errors.redactedHeaderWithoutValue": "\"{header}\" 헤더에 숨겨진 값이 있지만 유지할 저장된 값이 없습니다. 헤더 값을 입력하세요.",
|
|
51
53
|
"webhooks.errors.tenantRequired": "테넌트 컨텍스트가 필요합니다.",
|
|
52
54
|
"webhooks.form.advancedStrategyTip": "현재 HTTP 전달이 활성화되어 있습니다. 라우팅 힌트에는 커스텀 헤더를 사용하고, 시크릿은 요청 헤더에 넣지 마세요.",
|
|
53
55
|
"webhooks.form.autoDisableThreshold": "자동 비활성화 임계값",
|
|
@@ -55,7 +57,7 @@
|
|
|
55
57
|
"webhooks.form.createError": "웹훅 생성에 실패했습니다.",
|
|
56
58
|
"webhooks.form.createSuccess": "웹훅이 성공적으로 생성되었습니다.",
|
|
57
59
|
"webhooks.form.customHeaders": "커스텀 헤더",
|
|
58
|
-
"webhooks.form.customHeadersHint": "각 웹훅 요청과 함께 전송할 추가 헤더의 선택적 JSON 객체입니다.",
|
|
60
|
+
"webhooks.form.customHeadersHint": "각 웹훅 요청과 함께 전송할 추가 헤더의 선택적 JSON 객체입니다. 숨겨진 값(••••••••)은 저장할 때 유지되며, 변경하려면 새 값으로 바꾸세요.",
|
|
59
61
|
"webhooks.form.customHeadersInvalid": "커스텀 헤더는 문자열 값을 가진 유효한 JSON 객체여야 합니다.",
|
|
60
62
|
"webhooks.form.customHeadersTip": "환경이나 소스 식별자와 같은 정적 메타데이터에 헤더를 사용하세요. 여기에 액세스 토큰이나 시크릿을 넣지 마세요.",
|
|
61
63
|
"webhooks.form.deliveryDefaultsTip": "기본값은 대부분의 소비자에게 안전한 시작점입니다. 수신자가 느린 것으로 알려진 경우에만 재시도 횟수나 시간 제한을 늘리세요.",
|
|
@@ -45,9 +45,11 @@
|
|
|
45
45
|
"webhooks.detail.testResult": "Wynik ostatniego testu",
|
|
46
46
|
"webhooks.detail.testSuccess": "Testowa dostawa powiodła się.",
|
|
47
47
|
"webhooks.detail.title": "Szczegóły webhooka",
|
|
48
|
+
"webhooks.errors.credentialHeadersRequireReentry": "Nagłówek \"{header}\" ma ukrytą wartość. Wpisz ją ponownie lub usuń nagłówek, gdy zmieniasz adres URL webhooka.",
|
|
48
49
|
"webhooks.errors.forbidden": "Nie masz uprawnień do wykonania tej akcji.",
|
|
49
50
|
"webhooks.errors.notFound": "Nie znaleziono webhooka.",
|
|
50
51
|
"webhooks.errors.orgRequired": "Wymagany jest kontekst organizacji.",
|
|
52
|
+
"webhooks.errors.redactedHeaderWithoutValue": "Nagłówek \"{header}\" ma ukrytą wartość, ale brak zapisanej wartości do zachowania. Wpisz wartość nagłówka.",
|
|
51
53
|
"webhooks.errors.tenantRequired": "Wymagany jest kontekst najemcy.",
|
|
52
54
|
"webhooks.form.advancedStrategyTip": "Obecnie działa dostawa przez HTTP. Nagłówków własnych używaj do wskazówek routingu i nie umieszczaj w nich sekretów.",
|
|
53
55
|
"webhooks.form.autoDisableThreshold": "Próg automatycznego wyłączenia",
|
|
@@ -55,7 +57,7 @@
|
|
|
55
57
|
"webhooks.form.createError": "Nie udało się utworzyć webhooka.",
|
|
56
58
|
"webhooks.form.createSuccess": "Utworzono webhook.",
|
|
57
59
|
"webhooks.form.customHeaders": "Nagłówki własne",
|
|
58
|
-
"webhooks.form.customHeadersHint": "Opcjonalny obiekt JSON z dodatkowymi nagłówkami wysyłanymi z każdym żądaniem webhooka.",
|
|
60
|
+
"webhooks.form.customHeadersHint": "Opcjonalny obiekt JSON z dodatkowymi nagłówkami wysyłanymi z każdym żądaniem webhooka. Ukryte wartości (••••••••) są zachowywane przy zapisie; zastąp wartość, aby ją zmienić.",
|
|
59
61
|
"webhooks.form.customHeadersInvalid": "Nagłówki własne muszą być poprawnym obiektem JSON z wartościami tekstowymi.",
|
|
60
62
|
"webhooks.form.customHeadersTip": "Używaj nagłówków do statycznych metadanych, takich jak środowisko czy identyfikator źródła. Nie umieszczaj w nich tokenów dostępu ani sekretów.",
|
|
61
63
|
"webhooks.form.deliveryDefaultsTip": "Wartości domyślne są bezpiecznym punktem wyjścia dla większości odbiorców. Zwiększaj liczbę ponowień lub limit czasu tylko wtedy, gdy wiesz, że odbiorca działa wolno.",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WEBHOOK_REDACTED_HEADER_VALUE as REDACTED,
|
|
3
|
+
isCredentialWebhookHeader,
|
|
4
|
+
planWebhookCustomHeadersUpdate,
|
|
5
|
+
redactWebhookCustomHeaders,
|
|
6
|
+
resolveRedactedWebhookCustomHeaders,
|
|
7
|
+
} from '../custom-headers'
|
|
8
|
+
|
|
9
|
+
describe('isCredentialWebhookHeader', () => {
|
|
10
|
+
it.each([
|
|
11
|
+
'Authorization',
|
|
12
|
+
'Proxy-Authorization',
|
|
13
|
+
'X-API-Key',
|
|
14
|
+
'x-mock-webhook-token',
|
|
15
|
+
'X-Signature',
|
|
16
|
+
'x-sig',
|
|
17
|
+
'x-session-id',
|
|
18
|
+
'Cookie',
|
|
19
|
+
'X-Client-Secret',
|
|
20
|
+
'x-pass',
|
|
21
|
+
'X-Functions-Key',
|
|
22
|
+
'Ocp-Apim-Subscription-Key',
|
|
23
|
+
'X-Hub-Hmac',
|
|
24
|
+
])('treats %s as a credential header', (name) => {
|
|
25
|
+
expect(isCredentialWebhookHeader(name, 'value')).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it.each(['x-source', 'x-environment', 'accept-language', 'x-tenant'])('keeps %s readable', (name) => {
|
|
29
|
+
expect(isCredentialWebhookHeader(name, 'plain')).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it.each([
|
|
33
|
+
'Bearer abc',
|
|
34
|
+
'basic dXNlcjpwYXNz',
|
|
35
|
+
'eyJhbGciOiJIUzI1NiJ9.e30.sig',
|
|
36
|
+
'sk_live_123',
|
|
37
|
+
'ghp_abc',
|
|
38
|
+
'xoxb-123',
|
|
39
|
+
'glpat-abc',
|
|
40
|
+
])('treats a neutral header name with value %s as a credential', (value) => {
|
|
41
|
+
expect(isCredentialWebhookHeader('x-forward', value)).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('redactWebhookCustomHeaders', () => {
|
|
46
|
+
it('masks credential values and keeps names and other values', () => {
|
|
47
|
+
expect(redactWebhookCustomHeaders({ Authorization: 'Bearer t', 'x-source': 'qa' })).toEqual({
|
|
48
|
+
Authorization: REDACTED,
|
|
49
|
+
'x-source': 'qa',
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('returns null for missing headers', () => {
|
|
54
|
+
expect(redactWebhookCustomHeaders(null)).toBeNull()
|
|
55
|
+
expect(redactWebhookCustomHeaders(undefined)).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('resolveRedactedWebhookCustomHeaders', () => {
|
|
60
|
+
const stored = { Authorization: 'Bearer t', 'x-source': 'qa' }
|
|
61
|
+
|
|
62
|
+
it('restores marked values from storage under the incoming key', () => {
|
|
63
|
+
expect(resolveRedactedWebhookCustomHeaders({ authorization: REDACTED, 'x-source': 'erp' }, stored)).toEqual({
|
|
64
|
+
ok: true,
|
|
65
|
+
headers: { authorization: 'Bearer t', 'x-source': 'erp' },
|
|
66
|
+
keptStoredValues: ['authorization'],
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('rejects a marker without a stored value', () => {
|
|
71
|
+
expect(resolveRedactedWebhookCustomHeaders({ 'x-token': REDACTED }, stored)).toEqual({ ok: false, header: 'x-token' })
|
|
72
|
+
expect(resolveRedactedWebhookCustomHeaders({ 'x-token': REDACTED }, null)).toEqual({ ok: false, header: 'x-token' })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('prefers an exact key over a case-insensitive match', () => {
|
|
76
|
+
const result = resolveRedactedWebhookCustomHeaders(
|
|
77
|
+
{ authorization: REDACTED },
|
|
78
|
+
{ Authorization: 'Bearer upper', authorization: 'Bearer lower' },
|
|
79
|
+
)
|
|
80
|
+
expect(result).toEqual({ ok: true, headers: { authorization: 'Bearer lower' }, keptStoredValues: ['authorization'] })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('never resolves a marker to a stored marker', () => {
|
|
84
|
+
expect(resolveRedactedWebhookCustomHeaders({ Authorization: REDACTED }, { Authorization: REDACTED })).toEqual({
|
|
85
|
+
ok: false,
|
|
86
|
+
header: 'Authorization',
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('planWebhookCustomHeadersUpdate', () => {
|
|
92
|
+
const current = { url: 'https://example.com/hook', customHeaders: { Authorization: 'Bearer t', 'x-source': 'qa' } }
|
|
93
|
+
|
|
94
|
+
it('leaves headers untouched when they are not part of the update', () => {
|
|
95
|
+
expect(planWebhookCustomHeadersUpdate({ current, next: { url: current.url }, canRevealCredentials: false })).toEqual({
|
|
96
|
+
ok: true,
|
|
97
|
+
customHeaders: undefined,
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('refuses to carry stored credentials to a new URL without webhooks.secrets', () => {
|
|
102
|
+
expect(planWebhookCustomHeadersUpdate({ current, next: { url: 'https://evil.example.net/' }, canRevealCredentials: false })).toEqual({
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: 'credentialHeadersRequireReentry',
|
|
105
|
+
header: 'Authorization',
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('allows a new URL when the stored headers carry no credential', () => {
|
|
110
|
+
expect(
|
|
111
|
+
planWebhookCustomHeadersUpdate({
|
|
112
|
+
current: { url: current.url, customHeaders: { 'x-source': 'qa' } },
|
|
113
|
+
next: { url: 'https://other.example.net/' },
|
|
114
|
+
canRevealCredentials: false,
|
|
115
|
+
}),
|
|
116
|
+
).toEqual({ ok: true, customHeaders: undefined })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('treats a path change on the same host as a URL change', () => {
|
|
120
|
+
expect(
|
|
121
|
+
planWebhookCustomHeadersUpdate({
|
|
122
|
+
current,
|
|
123
|
+
next: { url: 'https://example.com/other-bin', customHeaders: { Authorization: REDACTED } },
|
|
124
|
+
canRevealCredentials: false,
|
|
125
|
+
}),
|
|
126
|
+
).toMatchObject({ ok: false, reason: 'credentialHeadersRequireReentry' })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('does not count a non-credential header sent back as the marker', () => {
|
|
130
|
+
expect(
|
|
131
|
+
planWebhookCustomHeadersUpdate({
|
|
132
|
+
current,
|
|
133
|
+
next: { url: 'https://other.example.net/', customHeaders: { 'x-source': REDACTED } },
|
|
134
|
+
canRevealCredentials: false,
|
|
135
|
+
}),
|
|
136
|
+
).toEqual({ ok: true, customHeaders: { 'x-source': 'qa' } })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('allows clearing headers on a new URL', () => {
|
|
140
|
+
expect(
|
|
141
|
+
planWebhookCustomHeadersUpdate({ current, next: { url: 'https://other.example.net/', customHeaders: null }, canRevealCredentials: false }),
|
|
142
|
+
).toEqual({ ok: true, customHeaders: null })
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -14,3 +14,87 @@ export function sanitizeWebhookCustomHeaders(
|
|
|
14
14
|
Object.entries(customHeaders).filter(([name]) => !isReservedWebhookCustomHeader(name)),
|
|
15
15
|
)
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
export const WEBHOOK_REDACTED_HEADER_VALUE = '••••••••'
|
|
19
|
+
|
|
20
|
+
const CREDENTIAL_HEADER_NAME_PATTERN = /(auth|token|secret|pass|pwd|key|credential|sig|hmac|session|cookie|jwt|bearer|private)/i
|
|
21
|
+
const CREDENTIAL_HEADER_VALUE_PATTERN = /^(?:(?:bearer|basic|digest|token)\s|eyJ|sk_(?:live|test)_|rk_(?:live|test)_|gh[pousr]_|github_pat_|xox[abpr]-|glpat-)/i
|
|
22
|
+
|
|
23
|
+
export function isCredentialWebhookHeader(name: string, value?: string | null): boolean {
|
|
24
|
+
if (CREDENTIAL_HEADER_NAME_PATTERN.test(name.trim())) return true
|
|
25
|
+
return typeof value === 'string' && CREDENTIAL_HEADER_VALUE_PATTERN.test(value.trim())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function redactWebhookCustomHeaders(
|
|
29
|
+
customHeaders: Record<string, string> | null | undefined,
|
|
30
|
+
): Record<string, string> | null {
|
|
31
|
+
if (!customHeaders) return null
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
Object.entries(customHeaders).map(([name, value]) => [
|
|
34
|
+
name,
|
|
35
|
+
isCredentialWebhookHeader(name, value) ? WEBHOOK_REDACTED_HEADER_VALUE : value,
|
|
36
|
+
]),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ResolvedWebhookCustomHeaders =
|
|
41
|
+
| { ok: true; headers: Record<string, string>; keptStoredValues: string[] }
|
|
42
|
+
| { ok: false; header: string }
|
|
43
|
+
|
|
44
|
+
function findStoredHeaderValue(name: string, stored: Record<string, string>): string | null {
|
|
45
|
+
if (Object.prototype.hasOwnProperty.call(stored, name)) return stored[name]
|
|
46
|
+
const normalized = name.trim().toLowerCase()
|
|
47
|
+
const matches = Object.keys(stored).filter((storedName) => storedName.trim().toLowerCase() === normalized)
|
|
48
|
+
return matches.length === 1 ? stored[matches[0]] : null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveRedactedWebhookCustomHeaders(
|
|
52
|
+
next: Record<string, string>,
|
|
53
|
+
stored: Record<string, string> | null | undefined,
|
|
54
|
+
): ResolvedWebhookCustomHeaders {
|
|
55
|
+
const headers: Record<string, string> = {}
|
|
56
|
+
const keptStoredValues: string[] = []
|
|
57
|
+
for (const [name, value] of Object.entries(next)) {
|
|
58
|
+
if (value !== WEBHOOK_REDACTED_HEADER_VALUE) {
|
|
59
|
+
headers[name] = value
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
const storedValue = stored ? findStoredHeaderValue(name, stored) : null
|
|
63
|
+
if (storedValue === null || storedValue === WEBHOOK_REDACTED_HEADER_VALUE) return { ok: false, header: name }
|
|
64
|
+
headers[name] = storedValue
|
|
65
|
+
if (isCredentialWebhookHeader(name, storedValue)) keptStoredValues.push(name)
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, headers, keptStoredValues }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type WebhookCustomHeadersUpdate =
|
|
71
|
+
| { ok: true; customHeaders: Record<string, string> | null | undefined }
|
|
72
|
+
| { ok: false; reason: 'redactedHeaderWithoutValue' | 'credentialHeadersRequireReentry'; header: string }
|
|
73
|
+
|
|
74
|
+
export function planWebhookCustomHeadersUpdate(input: {
|
|
75
|
+
current: { url: string; customHeaders?: Record<string, string> | null }
|
|
76
|
+
next: { url?: string; customHeaders?: Record<string, string> | null }
|
|
77
|
+
canRevealCredentials: boolean
|
|
78
|
+
}): WebhookCustomHeadersUpdate {
|
|
79
|
+
const { current, next, canRevealCredentials } = input
|
|
80
|
+
let customHeaders: Record<string, string> | null | undefined = next.customHeaders
|
|
81
|
+
let keptCredentialHeaders: string[] = []
|
|
82
|
+
|
|
83
|
+
if (next.customHeaders === undefined) {
|
|
84
|
+
keptCredentialHeaders = Object.entries(current.customHeaders ?? {})
|
|
85
|
+
.filter(([name, value]) => isCredentialWebhookHeader(name, value))
|
|
86
|
+
.map(([name]) => name)
|
|
87
|
+
} else if (next.customHeaders !== null) {
|
|
88
|
+
const resolved = resolveRedactedWebhookCustomHeaders(next.customHeaders, current.customHeaders)
|
|
89
|
+
if (!resolved.ok) return { ok: false, reason: 'redactedHeaderWithoutValue', header: resolved.header }
|
|
90
|
+
customHeaders = resolved.headers
|
|
91
|
+
keptCredentialHeaders = resolved.keptStoredValues
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const urlChanged = next.url !== undefined && next.url !== current.url
|
|
95
|
+
if (!canRevealCredentials && urlChanged && keptCredentialHeaders.length > 0) {
|
|
96
|
+
return { ok: false, reason: 'credentialHeadersRequireReentry', header: keptCredentialHeaders[0] }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { ok: true, customHeaders }
|
|
100
|
+
}
|