@knocklabs/react-core 0.13.13 → 0.13.14
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/CHANGELOG.md +9 -0
- package/dist/cjs/modules/core/hooks/useAuthPostMessageListener.js +1 -1
- package/dist/cjs/modules/core/hooks/useAuthPostMessageListener.js.map +1 -1
- package/dist/esm/modules/core/hooks/useAuthPostMessageListener.mjs +9 -9
- package/dist/esm/modules/core/hooks/useAuthPostMessageListener.mjs.map +1 -1
- package/dist/types/modules/core/hooks/useAuthPostMessageListener.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/modules/core/hooks/useAuthPostMessageListener.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e6ac8cc: chore(deps): bump swr from 2.4.0 to 2.4.1
|
|
8
|
+
- abac967: Fix auth button cross-contamination when SlackKit and TeamsKit are rendered simultaneously. The shared `useAuthPostMessageListener` hook now checks whether its own popup is open before processing `authComplete` messages, preventing one integration's OAuth completion from incorrectly updating the other's connection state.
|
|
9
|
+
- Updated dependencies [454f947]
|
|
10
|
+
- @knocklabs/client@0.21.13
|
|
11
|
+
|
|
3
12
|
## 0.13.13
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("react");function y(e){return typeof e=="object"&&e!==null&&"type"in e?e.type:e}function p(e){if(typeof e=="object"&&e!==null&&"nonce"in e){const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("react");function y(e){return typeof e=="object"&&e!==null&&"type"in e?e.type:e}function p(e){if(typeof e=="object"&&e!==null&&"nonce"in e){const t=e.nonce;return typeof t=="string"?t:void 0}}function a(e){const{knockHost:t,popupWindowRef:s,setConnectionStatus:o,onAuthenticationComplete:r,nonceStorageKey:n}=e;d.useEffect(()=>{const i=()=>{s.current&&!s.current.closed&&s.current.close(),s.current=null},f=u=>{if(u.origin!==t||!s.current)return;const c=y(u.data);if(c==="authComplete"){if(n){const g=p(u.data),l=sessionStorage.getItem(n);if(sessionStorage.removeItem(n),!g||l&&l!==g){o("error"),r==null||r("authFailed"),i();return}}o("connected"),r==null||r(c),i()}else c==="authFailed"&&(n&&sessionStorage.removeItem(n),o("error"),r==null||r(c),s.current=null)};return window.addEventListener("message",f,!1),()=>window.removeEventListener("message",f)},[t,r,o,s,n])}exports.useAuthPostMessageListener=a;
|
|
2
2
|
//# sourceMappingURL=useAuthPostMessageListener.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuthPostMessageListener.js","sources":["../../../../../src/modules/core/hooks/useAuthPostMessageListener.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport { ConnectionStatus } from \"../types\";\n\nexport interface UseAuthPostMessageListenerOptions {\n knockHost: string;\n popupWindowRef: React.MutableRefObject<Window | null>;\n setConnectionStatus: (status: ConnectionStatus) => void;\n onAuthenticationComplete?: (authenticationResp: string) => void;\n /**\n * The sessionStorage key where the CSRF nonce was stored when the auth URL\n * was built. When provided, the listener will verify the nonce returned in\n * the postMessage payload matches the stored value.\n */\n nonceStorageKey?: string;\n}\n\n/**\n * Extracts the message type from a postMessage event data payload.\n * Supports both the legacy string format (\"authComplete\") and the new\n * structured format ({ type: \"authComplete\", nonce: \"...\" }).\n */\nfunction getMessageType(data: unknown): string {\n if (typeof data === \"object\" && data !== null && \"type\" in data) {\n return (data as { type: string }).type;\n }\n return data as string;\n}\n\n/**\n * Extracts the nonce from a structured postMessage event data payload.\n * Returns undefined for legacy string-format messages.\n */\nfunction getMessageNonce(data: unknown): string | undefined {\n if (typeof data === \"object\" && data !== null && \"nonce\" in data) {\n const nonce = (data as { nonce: unknown }).nonce;\n return typeof nonce === \"string\" ? nonce : undefined;\n }\n return undefined;\n}\n\n/**\n * Hook that listens for postMessage events from OAuth popup windows.\n *\n * Handles \"authComplete\" and \"authFailed\" messages sent from the OAuth flow popup,\n * validates the message origin, optionally verifies the CSRF nonce, updates\n * connection status, and closes the popup.\n *\n * @param options - Configuration options for the postMessage listener\n *\n * @example\n * ```tsx\n * useAuthPostMessageListener({\n * knockHost: knock.host,\n * popupWindowRef,\n * setConnectionStatus,\n * onAuthenticationComplete,\n * nonceStorageKey: \"knock:slack-auth-nonce:channel_123:user_1\",\n * });\n * ```\n */\nexport function useAuthPostMessageListener(\n options: UseAuthPostMessageListenerOptions,\n): void {\n const {\n knockHost,\n popupWindowRef,\n setConnectionStatus,\n onAuthenticationComplete,\n nonceStorageKey,\n } = options;\n\n useEffect(() => {\n const closePopup = () => {\n if (popupWindowRef.current && !popupWindowRef.current.closed) {\n popupWindowRef.current.close();\n }\n popupWindowRef.current = null;\n };\n\n const receiveMessage = (event: MessageEvent) => {\n // Validate message origin for security\n if (event.origin !== knockHost) {\n return;\n }\n\n const messageType = getMessageType(event.data);\n\n if (messageType === \"authComplete\") {\n // Verify CSRF nonce when a nonceStorageKey is configured.\n if (nonceStorageKey) {\n const returnedNonce = getMessageNonce(event.data);\n const storedNonce = sessionStorage.getItem(nonceStorageKey);\n sessionStorage.removeItem(nonceStorageKey);\n\n // If nonce already consumed by a prior handler invocation, then bail\n // out from checking again.\n if (\n !returnedNonce ||\n (storedNonce && storedNonce !== returnedNonce)\n ) {\n setConnectionStatus(\"error\");\n onAuthenticationComplete?.(\"authFailed\");\n closePopup();\n return;\n }\n }\n\n setConnectionStatus(\"connected\");\n onAuthenticationComplete?.(messageType);\n closePopup();\n } else if (messageType === \"authFailed\") {\n if (nonceStorageKey) {\n sessionStorage.removeItem(nonceStorageKey);\n }\n setConnectionStatus(\"error\");\n onAuthenticationComplete?.(messageType);\n popupWindowRef.current = null;\n }\n };\n\n window.addEventListener(\"message\", receiveMessage, false);\n return () => window.removeEventListener(\"message\", receiveMessage);\n }, [\n knockHost,\n onAuthenticationComplete,\n setConnectionStatus,\n popupWindowRef,\n nonceStorageKey,\n ]);\n}\n"],"names":["getMessageType","data","type","getMessageNonce","nonce","undefined","useAuthPostMessageListener","options","knockHost","popupWindowRef","setConnectionStatus","onAuthenticationComplete","nonceStorageKey","useEffect","closePopup","current","closed","close","receiveMessage","event","origin","messageType","returnedNonce","storedNonce","sessionStorage","getItem","removeItem","addEventListener","window","removeEventListener"],"mappings":"yGAsBA,SAASA,EAAeC,EAAuB,CAC7C,OAAI,OAAOA,GAAS,UAAYA,IAAS,MAAQ,SAAUA,EACjDA,EAA0BC,KAE7BD,CACT,CAMA,SAASE,EAAgBF,EAAmC,CAC1D,GAAI,OAAOA,GAAS,UAAYA,IAAS,MAAQ,UAAWA,EAAM,CAChE,MAAMG,EAASH,EAA4BG,MACpC,OAAA,OAAOA,GAAU,SAAWA,EAAQC,MAAAA,CAG/C,CAsBO,SAASC,EACdC,EACM,CACA,KAAA,CACJC,UAAAA,EACAC,eAAAA,EACAC,oBAAAA,EACAC,yBAAAA,EACAC,gBAAAA,CAAAA,EACEL,EAEJM,EAAAA,UAAU,IAAM,CACd,MAAMC,EAAaA,IAAM,CACnBL,EAAeM,SAAW,CAACN,EAAeM,QAAQC,QACpDP,EAAeM,QAAQE,MAAM,EAE/BR,EAAeM,QAAU,IAC3B,EAEMG,EAAkBC,GAAwB,
|
|
1
|
+
{"version":3,"file":"useAuthPostMessageListener.js","sources":["../../../../../src/modules/core/hooks/useAuthPostMessageListener.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport { ConnectionStatus } from \"../types\";\n\nexport interface UseAuthPostMessageListenerOptions {\n knockHost: string;\n popupWindowRef: React.MutableRefObject<Window | null>;\n setConnectionStatus: (status: ConnectionStatus) => void;\n onAuthenticationComplete?: (authenticationResp: string) => void;\n /**\n * The sessionStorage key where the CSRF nonce was stored when the auth URL\n * was built. When provided, the listener will verify the nonce returned in\n * the postMessage payload matches the stored value.\n */\n nonceStorageKey?: string;\n}\n\n/**\n * Extracts the message type from a postMessage event data payload.\n * Supports both the legacy string format (\"authComplete\") and the new\n * structured format ({ type: \"authComplete\", nonce: \"...\" }).\n */\nfunction getMessageType(data: unknown): string {\n if (typeof data === \"object\" && data !== null && \"type\" in data) {\n return (data as { type: string }).type;\n }\n return data as string;\n}\n\n/**\n * Extracts the nonce from a structured postMessage event data payload.\n * Returns undefined for legacy string-format messages.\n */\nfunction getMessageNonce(data: unknown): string | undefined {\n if (typeof data === \"object\" && data !== null && \"nonce\" in data) {\n const nonce = (data as { nonce: unknown }).nonce;\n return typeof nonce === \"string\" ? nonce : undefined;\n }\n return undefined;\n}\n\n/**\n * Hook that listens for postMessage events from OAuth popup windows.\n *\n * Handles \"authComplete\" and \"authFailed\" messages sent from the OAuth flow popup,\n * validates the message origin, optionally verifies the CSRF nonce, updates\n * connection status, and closes the popup.\n *\n * @param options - Configuration options for the postMessage listener\n *\n * @example\n * ```tsx\n * useAuthPostMessageListener({\n * knockHost: knock.host,\n * popupWindowRef,\n * setConnectionStatus,\n * onAuthenticationComplete,\n * nonceStorageKey: \"knock:slack-auth-nonce:channel_123:user_1\",\n * });\n * ```\n */\nexport function useAuthPostMessageListener(\n options: UseAuthPostMessageListenerOptions,\n): void {\n const {\n knockHost,\n popupWindowRef,\n setConnectionStatus,\n onAuthenticationComplete,\n nonceStorageKey,\n } = options;\n\n useEffect(() => {\n const closePopup = () => {\n if (popupWindowRef.current && !popupWindowRef.current.closed) {\n popupWindowRef.current.close();\n }\n popupWindowRef.current = null;\n };\n\n const receiveMessage = (event: MessageEvent) => {\n // Validate message origin for security\n if (event.origin !== knockHost) {\n return;\n }\n\n // Ignore messages when this integration hasn't opened a popup\n if (!popupWindowRef.current) {\n return;\n }\n\n const messageType = getMessageType(event.data);\n\n if (messageType === \"authComplete\") {\n // Verify CSRF nonce when a nonceStorageKey is configured.\n if (nonceStorageKey) {\n const returnedNonce = getMessageNonce(event.data);\n const storedNonce = sessionStorage.getItem(nonceStorageKey);\n sessionStorage.removeItem(nonceStorageKey);\n\n // If nonce already consumed by a prior handler invocation, then bail\n // out from checking again.\n if (\n !returnedNonce ||\n (storedNonce && storedNonce !== returnedNonce)\n ) {\n setConnectionStatus(\"error\");\n onAuthenticationComplete?.(\"authFailed\");\n closePopup();\n return;\n }\n }\n\n setConnectionStatus(\"connected\");\n onAuthenticationComplete?.(messageType);\n closePopup();\n } else if (messageType === \"authFailed\") {\n if (nonceStorageKey) {\n sessionStorage.removeItem(nonceStorageKey);\n }\n setConnectionStatus(\"error\");\n onAuthenticationComplete?.(messageType);\n popupWindowRef.current = null;\n }\n };\n\n window.addEventListener(\"message\", receiveMessage, false);\n return () => window.removeEventListener(\"message\", receiveMessage);\n }, [\n knockHost,\n onAuthenticationComplete,\n setConnectionStatus,\n popupWindowRef,\n nonceStorageKey,\n ]);\n}\n"],"names":["getMessageType","data","type","getMessageNonce","nonce","undefined","useAuthPostMessageListener","options","knockHost","popupWindowRef","setConnectionStatus","onAuthenticationComplete","nonceStorageKey","useEffect","closePopup","current","closed","close","receiveMessage","event","origin","messageType","returnedNonce","storedNonce","sessionStorage","getItem","removeItem","addEventListener","window","removeEventListener"],"mappings":"yGAsBA,SAASA,EAAeC,EAAuB,CAC7C,OAAI,OAAOA,GAAS,UAAYA,IAAS,MAAQ,SAAUA,EACjDA,EAA0BC,KAE7BD,CACT,CAMA,SAASE,EAAgBF,EAAmC,CAC1D,GAAI,OAAOA,GAAS,UAAYA,IAAS,MAAQ,UAAWA,EAAM,CAChE,MAAMG,EAASH,EAA4BG,MACpC,OAAA,OAAOA,GAAU,SAAWA,EAAQC,MAAAA,CAG/C,CAsBO,SAASC,EACdC,EACM,CACA,KAAA,CACJC,UAAAA,EACAC,eAAAA,EACAC,oBAAAA,EACAC,yBAAAA,EACAC,gBAAAA,CAAAA,EACEL,EAEJM,EAAAA,UAAU,IAAM,CACd,MAAMC,EAAaA,IAAM,CACnBL,EAAeM,SAAW,CAACN,EAAeM,QAAQC,QACpDP,EAAeM,QAAQE,MAAM,EAE/BR,EAAeM,QAAU,IAC3B,EAEMG,EAAkBC,GAAwB,CAO1C,GALAA,EAAMC,SAAWZ,GAKjB,CAACC,EAAeM,QAClB,OAGIM,MAAAA,EAAcrB,EAAemB,EAAMlB,IAAI,EAE7C,GAAIoB,IAAgB,eAAgB,CAElC,GAAIT,EAAiB,CACbU,MAAAA,EAAgBnB,EAAgBgB,EAAMlB,IAAI,EAC1CsB,EAAcC,eAAeC,QAAQb,CAAe,EAK1D,GAJAY,eAAeE,WAAWd,CAAe,EAKvC,CAACU,GACAC,GAAeA,IAAgBD,EAChC,CACAZ,EAAoB,OAAO,EAC3BC,GAAAA,MAAAA,EAA2B,cAChBG,EAAA,EACX,MAAA,CACF,CAGFJ,EAAoB,WAAW,EAC/BC,GAAAA,MAAAA,EAA2BU,GAChBP,EAAA,CAAA,MACFO,IAAgB,eACrBT,GACFY,eAAeE,WAAWd,CAAe,EAE3CF,EAAoB,OAAO,EAC3BC,GAAAA,MAAAA,EAA2BU,GAC3BZ,EAAeM,QAAU,KAE7B,EAEOY,cAAAA,iBAAiB,UAAWT,EAAgB,EAAK,EACjD,IAAMU,OAAOC,oBAAoB,UAAWX,CAAc,CAAA,EAChE,CACDV,EACAG,EACAD,EACAD,EACAG,CAAe,CAChB,CACH"}
|
|
@@ -11,31 +11,31 @@ function y(e) {
|
|
|
11
11
|
function v(e) {
|
|
12
12
|
const {
|
|
13
13
|
knockHost: o,
|
|
14
|
-
popupWindowRef:
|
|
14
|
+
popupWindowRef: n,
|
|
15
15
|
setConnectionStatus: t,
|
|
16
16
|
onAuthenticationComplete: r,
|
|
17
|
-
nonceStorageKey:
|
|
17
|
+
nonceStorageKey: s
|
|
18
18
|
} = e;
|
|
19
19
|
p(() => {
|
|
20
20
|
const i = () => {
|
|
21
|
-
|
|
21
|
+
n.current && !n.current.closed && n.current.close(), n.current = null;
|
|
22
22
|
}, f = (u) => {
|
|
23
|
-
if (u.origin !== o)
|
|
23
|
+
if (u.origin !== o || !n.current)
|
|
24
24
|
return;
|
|
25
25
|
const c = d(u.data);
|
|
26
26
|
if (c === "authComplete") {
|
|
27
|
-
if (
|
|
28
|
-
const g = y(u.data), l = sessionStorage.getItem(
|
|
29
|
-
if (sessionStorage.removeItem(
|
|
27
|
+
if (s) {
|
|
28
|
+
const g = y(u.data), l = sessionStorage.getItem(s);
|
|
29
|
+
if (sessionStorage.removeItem(s), !g || l && l !== g) {
|
|
30
30
|
t("error"), r == null || r("authFailed"), i();
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
t("connected"), r == null || r(c), i();
|
|
35
|
-
} else c === "authFailed" && (
|
|
35
|
+
} else c === "authFailed" && (s && sessionStorage.removeItem(s), t("error"), r == null || r(c), n.current = null);
|
|
36
36
|
};
|
|
37
37
|
return window.addEventListener("message", f, !1), () => window.removeEventListener("message", f);
|
|
38
|
-
}, [o, r, t,
|
|
38
|
+
}, [o, r, t, n, s]);
|
|
39
39
|
}
|
|
40
40
|
export {
|
|
41
41
|
v as useAuthPostMessageListener
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuthPostMessageListener.mjs","sources":["../../../../../src/modules/core/hooks/useAuthPostMessageListener.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport { ConnectionStatus } from \"../types\";\n\nexport interface UseAuthPostMessageListenerOptions {\n knockHost: string;\n popupWindowRef: React.MutableRefObject<Window | null>;\n setConnectionStatus: (status: ConnectionStatus) => void;\n onAuthenticationComplete?: (authenticationResp: string) => void;\n /**\n * The sessionStorage key where the CSRF nonce was stored when the auth URL\n * was built. When provided, the listener will verify the nonce returned in\n * the postMessage payload matches the stored value.\n */\n nonceStorageKey?: string;\n}\n\n/**\n * Extracts the message type from a postMessage event data payload.\n * Supports both the legacy string format (\"authComplete\") and the new\n * structured format ({ type: \"authComplete\", nonce: \"...\" }).\n */\nfunction getMessageType(data: unknown): string {\n if (typeof data === \"object\" && data !== null && \"type\" in data) {\n return (data as { type: string }).type;\n }\n return data as string;\n}\n\n/**\n * Extracts the nonce from a structured postMessage event data payload.\n * Returns undefined for legacy string-format messages.\n */\nfunction getMessageNonce(data: unknown): string | undefined {\n if (typeof data === \"object\" && data !== null && \"nonce\" in data) {\n const nonce = (data as { nonce: unknown }).nonce;\n return typeof nonce === \"string\" ? nonce : undefined;\n }\n return undefined;\n}\n\n/**\n * Hook that listens for postMessage events from OAuth popup windows.\n *\n * Handles \"authComplete\" and \"authFailed\" messages sent from the OAuth flow popup,\n * validates the message origin, optionally verifies the CSRF nonce, updates\n * connection status, and closes the popup.\n *\n * @param options - Configuration options for the postMessage listener\n *\n * @example\n * ```tsx\n * useAuthPostMessageListener({\n * knockHost: knock.host,\n * popupWindowRef,\n * setConnectionStatus,\n * onAuthenticationComplete,\n * nonceStorageKey: \"knock:slack-auth-nonce:channel_123:user_1\",\n * });\n * ```\n */\nexport function useAuthPostMessageListener(\n options: UseAuthPostMessageListenerOptions,\n): void {\n const {\n knockHost,\n popupWindowRef,\n setConnectionStatus,\n onAuthenticationComplete,\n nonceStorageKey,\n } = options;\n\n useEffect(() => {\n const closePopup = () => {\n if (popupWindowRef.current && !popupWindowRef.current.closed) {\n popupWindowRef.current.close();\n }\n popupWindowRef.current = null;\n };\n\n const receiveMessage = (event: MessageEvent) => {\n // Validate message origin for security\n if (event.origin !== knockHost) {\n return;\n }\n\n const messageType = getMessageType(event.data);\n\n if (messageType === \"authComplete\") {\n // Verify CSRF nonce when a nonceStorageKey is configured.\n if (nonceStorageKey) {\n const returnedNonce = getMessageNonce(event.data);\n const storedNonce = sessionStorage.getItem(nonceStorageKey);\n sessionStorage.removeItem(nonceStorageKey);\n\n // If nonce already consumed by a prior handler invocation, then bail\n // out from checking again.\n if (\n !returnedNonce ||\n (storedNonce && storedNonce !== returnedNonce)\n ) {\n setConnectionStatus(\"error\");\n onAuthenticationComplete?.(\"authFailed\");\n closePopup();\n return;\n }\n }\n\n setConnectionStatus(\"connected\");\n onAuthenticationComplete?.(messageType);\n closePopup();\n } else if (messageType === \"authFailed\") {\n if (nonceStorageKey) {\n sessionStorage.removeItem(nonceStorageKey);\n }\n setConnectionStatus(\"error\");\n onAuthenticationComplete?.(messageType);\n popupWindowRef.current = null;\n }\n };\n\n window.addEventListener(\"message\", receiveMessage, false);\n return () => window.removeEventListener(\"message\", receiveMessage);\n }, [\n knockHost,\n onAuthenticationComplete,\n setConnectionStatus,\n popupWindowRef,\n nonceStorageKey,\n ]);\n}\n"],"names":["getMessageType","data","type","getMessageNonce","nonce","undefined","useAuthPostMessageListener","options","knockHost","popupWindowRef","setConnectionStatus","onAuthenticationComplete","nonceStorageKey","useEffect","closePopup","current","closed","close","receiveMessage","event","origin","messageType","returnedNonce","storedNonce","sessionStorage","getItem","removeItem","addEventListener","window","removeEventListener"],"mappings":";AAsBA,SAASA,EAAeC,GAAuB;AAC7C,SAAI,OAAOA,KAAS,YAAYA,MAAS,QAAQ,UAAUA,IACjDA,EAA0BC,OAE7BD;AACT;AAMA,SAASE,EAAgBF,GAAmC;AAC1D,MAAI,OAAOA,KAAS,YAAYA,MAAS,QAAQ,WAAWA,GAAM;AAChE,UAAMG,IAASH,EAA4BG;AACpC,WAAA,OAAOA,KAAU,WAAWA,IAAQC;AAAAA,EAAAA;AAG/C;AAsBO,SAASC,EACdC,GACM;AACA,QAAA;AAAA,IACJC,WAAAA;AAAAA,IACAC,gBAAAA;AAAAA,IACAC,qBAAAA;AAAAA,IACAC,0BAAAA;AAAAA,IACAC,iBAAAA;AAAAA,EAAAA,IACEL;AAEJM,EAAAA,EAAU,MAAM;AACd,UAAMC,IAAaA,MAAM;AACvB,MAAIL,EAAeM,WAAW,CAACN,EAAeM,QAAQC,UACpDP,EAAeM,QAAQE,MAAM,GAE/BR,EAAeM,UAAU;AAAA,IAC3B,GAEMG,IAAiBA,CAACC,MAAwB;
|
|
1
|
+
{"version":3,"file":"useAuthPostMessageListener.mjs","sources":["../../../../../src/modules/core/hooks/useAuthPostMessageListener.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport { ConnectionStatus } from \"../types\";\n\nexport interface UseAuthPostMessageListenerOptions {\n knockHost: string;\n popupWindowRef: React.MutableRefObject<Window | null>;\n setConnectionStatus: (status: ConnectionStatus) => void;\n onAuthenticationComplete?: (authenticationResp: string) => void;\n /**\n * The sessionStorage key where the CSRF nonce was stored when the auth URL\n * was built. When provided, the listener will verify the nonce returned in\n * the postMessage payload matches the stored value.\n */\n nonceStorageKey?: string;\n}\n\n/**\n * Extracts the message type from a postMessage event data payload.\n * Supports both the legacy string format (\"authComplete\") and the new\n * structured format ({ type: \"authComplete\", nonce: \"...\" }).\n */\nfunction getMessageType(data: unknown): string {\n if (typeof data === \"object\" && data !== null && \"type\" in data) {\n return (data as { type: string }).type;\n }\n return data as string;\n}\n\n/**\n * Extracts the nonce from a structured postMessage event data payload.\n * Returns undefined for legacy string-format messages.\n */\nfunction getMessageNonce(data: unknown): string | undefined {\n if (typeof data === \"object\" && data !== null && \"nonce\" in data) {\n const nonce = (data as { nonce: unknown }).nonce;\n return typeof nonce === \"string\" ? nonce : undefined;\n }\n return undefined;\n}\n\n/**\n * Hook that listens for postMessage events from OAuth popup windows.\n *\n * Handles \"authComplete\" and \"authFailed\" messages sent from the OAuth flow popup,\n * validates the message origin, optionally verifies the CSRF nonce, updates\n * connection status, and closes the popup.\n *\n * @param options - Configuration options for the postMessage listener\n *\n * @example\n * ```tsx\n * useAuthPostMessageListener({\n * knockHost: knock.host,\n * popupWindowRef,\n * setConnectionStatus,\n * onAuthenticationComplete,\n * nonceStorageKey: \"knock:slack-auth-nonce:channel_123:user_1\",\n * });\n * ```\n */\nexport function useAuthPostMessageListener(\n options: UseAuthPostMessageListenerOptions,\n): void {\n const {\n knockHost,\n popupWindowRef,\n setConnectionStatus,\n onAuthenticationComplete,\n nonceStorageKey,\n } = options;\n\n useEffect(() => {\n const closePopup = () => {\n if (popupWindowRef.current && !popupWindowRef.current.closed) {\n popupWindowRef.current.close();\n }\n popupWindowRef.current = null;\n };\n\n const receiveMessage = (event: MessageEvent) => {\n // Validate message origin for security\n if (event.origin !== knockHost) {\n return;\n }\n\n // Ignore messages when this integration hasn't opened a popup\n if (!popupWindowRef.current) {\n return;\n }\n\n const messageType = getMessageType(event.data);\n\n if (messageType === \"authComplete\") {\n // Verify CSRF nonce when a nonceStorageKey is configured.\n if (nonceStorageKey) {\n const returnedNonce = getMessageNonce(event.data);\n const storedNonce = sessionStorage.getItem(nonceStorageKey);\n sessionStorage.removeItem(nonceStorageKey);\n\n // If nonce already consumed by a prior handler invocation, then bail\n // out from checking again.\n if (\n !returnedNonce ||\n (storedNonce && storedNonce !== returnedNonce)\n ) {\n setConnectionStatus(\"error\");\n onAuthenticationComplete?.(\"authFailed\");\n closePopup();\n return;\n }\n }\n\n setConnectionStatus(\"connected\");\n onAuthenticationComplete?.(messageType);\n closePopup();\n } else if (messageType === \"authFailed\") {\n if (nonceStorageKey) {\n sessionStorage.removeItem(nonceStorageKey);\n }\n setConnectionStatus(\"error\");\n onAuthenticationComplete?.(messageType);\n popupWindowRef.current = null;\n }\n };\n\n window.addEventListener(\"message\", receiveMessage, false);\n return () => window.removeEventListener(\"message\", receiveMessage);\n }, [\n knockHost,\n onAuthenticationComplete,\n setConnectionStatus,\n popupWindowRef,\n nonceStorageKey,\n ]);\n}\n"],"names":["getMessageType","data","type","getMessageNonce","nonce","undefined","useAuthPostMessageListener","options","knockHost","popupWindowRef","setConnectionStatus","onAuthenticationComplete","nonceStorageKey","useEffect","closePopup","current","closed","close","receiveMessage","event","origin","messageType","returnedNonce","storedNonce","sessionStorage","getItem","removeItem","addEventListener","window","removeEventListener"],"mappings":";AAsBA,SAASA,EAAeC,GAAuB;AAC7C,SAAI,OAAOA,KAAS,YAAYA,MAAS,QAAQ,UAAUA,IACjDA,EAA0BC,OAE7BD;AACT;AAMA,SAASE,EAAgBF,GAAmC;AAC1D,MAAI,OAAOA,KAAS,YAAYA,MAAS,QAAQ,WAAWA,GAAM;AAChE,UAAMG,IAASH,EAA4BG;AACpC,WAAA,OAAOA,KAAU,WAAWA,IAAQC;AAAAA,EAAAA;AAG/C;AAsBO,SAASC,EACdC,GACM;AACA,QAAA;AAAA,IACJC,WAAAA;AAAAA,IACAC,gBAAAA;AAAAA,IACAC,qBAAAA;AAAAA,IACAC,0BAAAA;AAAAA,IACAC,iBAAAA;AAAAA,EAAAA,IACEL;AAEJM,EAAAA,EAAU,MAAM;AACd,UAAMC,IAAaA,MAAM;AACvB,MAAIL,EAAeM,WAAW,CAACN,EAAeM,QAAQC,UACpDP,EAAeM,QAAQE,MAAM,GAE/BR,EAAeM,UAAU;AAAA,IAC3B,GAEMG,IAAiBA,CAACC,MAAwB;AAO1C,UALAA,EAAMC,WAAWZ,KAKjB,CAACC,EAAeM;AAClB;AAGIM,YAAAA,IAAcrB,EAAemB,EAAMlB,IAAI;AAE7C,UAAIoB,MAAgB,gBAAgB;AAElC,YAAIT,GAAiB;AACbU,gBAAAA,IAAgBnB,EAAgBgB,EAAMlB,IAAI,GAC1CsB,IAAcC,eAAeC,QAAQb,CAAe;AAK1D,cAJAY,eAAeE,WAAWd,CAAe,GAKvC,CAACU,KACAC,KAAeA,MAAgBD,GAChC;AACAZ,YAAAA,EAAoB,OAAO,GAC3BC,KAAAA,QAAAA,EAA2B,eAChBG,EAAA;AACX;AAAA,UAAA;AAAA,QACF;AAGFJ,QAAAA,EAAoB,WAAW,GAC/BC,KAAAA,QAAAA,EAA2BU,IAChBP,EAAA;AAAA,MAAA,MACb,CAAWO,MAAgB,iBACrBT,KACFY,eAAeE,WAAWd,CAAe,GAE3CF,EAAoB,OAAO,GAC3BC,KAAAA,QAAAA,EAA2BU,IAC3BZ,EAAeM,UAAU;AAAA,IAE7B;AAEOY,kBAAAA,iBAAiB,WAAWT,GAAgB,EAAK,GACjD,MAAMU,OAAOC,oBAAoB,WAAWX,CAAc;AAAA,EAAA,GAChE,CACDV,GACAG,GACAD,GACAD,GACAG,CAAe,CAChB;AACH;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuthPostMessageListener.d.ts","sourceRoot":"","sources":["../../../../../src/modules/core/hooks/useAuthPostMessageListener.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE5C,MAAM,WAAW,iCAAiC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,mBAAmB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACxD,wBAAwB,CAAC,EAAE,CAAC,kBAAkB,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AA0BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,IAAI,
|
|
1
|
+
{"version":3,"file":"useAuthPostMessageListener.d.ts","sourceRoot":"","sources":["../../../../../src/modules/core/hooks/useAuthPostMessageListener.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE5C,MAAM,WAAW,iCAAiC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,mBAAmB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACxD,wBAAwB,CAAC,EAAE,CAAC,kBAAkB,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AA0BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,IAAI,CAwEN"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@knocklabs/react-core",
|
|
3
3
|
"description": "A set of React components to build notification experiences powered by Knock",
|
|
4
4
|
"author": "@knocklabs",
|
|
5
|
-
"version": "0.13.
|
|
5
|
+
"version": "0.13.14",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "dist/cjs/index.js",
|
|
8
8
|
"module": "dist/esm/index.mjs",
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@knocklabs/client": "^0.21.
|
|
50
|
+
"@knocklabs/client": "^0.21.13",
|
|
51
51
|
"@tanstack/react-store": "^0.7.3",
|
|
52
52
|
"date-fns": "^4.0.0",
|
|
53
53
|
"fast-deep-equal": "^3.1.3",
|
|
54
|
-
"swr": "^2.4.
|
|
54
|
+
"swr": "^2.4.1"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@codecov/vite-plugin": "^
|
|
57
|
+
"@codecov/vite-plugin": "^2.0.1",
|
|
58
58
|
"@testing-library/dom": "^10.4.1",
|
|
59
59
|
"@testing-library/react": "^16.3.2",
|
|
60
60
|
"@types/react": "^19.2.14",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"eslint": "^8.56.0",
|
|
67
67
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
68
68
|
"eslint-plugin-react-refresh": "^0.5.2",
|
|
69
|
-
"jsdom": "^
|
|
69
|
+
"jsdom": "^29.1.0",
|
|
70
70
|
"react": "^19.2.5",
|
|
71
71
|
"react-dom": "^19.2.5",
|
|
72
72
|
"rimraf": "^6.0.1",
|
|
@@ -84,6 +84,11 @@ export function useAuthPostMessageListener(
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Ignore messages when this integration hasn't opened a popup
|
|
88
|
+
if (!popupWindowRef.current) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
const messageType = getMessageType(event.data);
|
|
88
93
|
|
|
89
94
|
if (messageType === "authComplete") {
|