@knocklabs/react-core 0.13.8 → 0.13.9
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 +6 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/modules/core/hooks/useAuthPostMessageListener.js +1 -1
- package/dist/cjs/modules/core/hooks/useAuthPostMessageListener.js.map +1 -1
- package/dist/cjs/modules/slack/hooks/useSlackAuth.js +1 -1
- package/dist/cjs/modules/slack/hooks/useSlackAuth.js.map +1 -1
- package/dist/esm/index.mjs +26 -25
- package/dist/esm/modules/core/hooks/useAuthPostMessageListener.mjs +36 -12
- package/dist/esm/modules/core/hooks/useAuthPostMessageListener.mjs.map +1 -1
- package/dist/esm/modules/slack/hooks/useSlackAuth.mjs +38 -31
- package/dist/esm/modules/slack/hooks/useSlackAuth.mjs.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modules/core/hooks/useAuthPostMessageListener.d.ts +9 -1
- package/dist/types/modules/core/hooks/useAuthPostMessageListener.d.ts.map +1 -1
- package/dist/types/modules/slack/hooks/index.d.ts +1 -1
- package/dist/types/modules/slack/hooks/index.d.ts.map +1 -1
- package/dist/types/modules/slack/hooks/useSlackAuth.d.ts +1 -0
- package/dist/types/modules/slack/hooks/useSlackAuth.d.ts.map +1 -1
- package/dist/types/modules/slack/index.d.ts +1 -1
- package/dist/types/modules/slack/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/modules/core/hooks/useAuthPostMessageListener.ts +70 -9
- package/src/modules/slack/hooks/index.ts +4 -1
- package/src/modules/slack/hooks/useSlackAuth.ts +15 -0
- package/src/modules/slack/index.ts +1 -0
package/CHANGELOG.md
CHANGED
package/dist/cjs/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("./modules/core/context/KnockProvider.js"),
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("./modules/core/context/KnockProvider.js"),a=require("./modules/core/hooks/useAuthenticatedKnockClient.js"),d=require("./modules/core/hooks/useStableOptions.js"),l=require("./modules/core/hooks/useAuthPostMessageListener.js"),k=require("./modules/core/hooks/useAuthPolling.js"),K=require("./modules/core/constants.js"),e=require("./modules/core/utils.js"),t=require("./modules/feed/context/KnockFeedProvider.js"),S=require("./modules/feed/hooks/useNotifications.js"),C=require("./modules/feed/hooks/useFeedSettings.js"),n=require("./modules/feed/hooks/useNotificationStore.js"),o=require("./modules/guide/context/KnockGuideProvider.js"),P=require("./modules/guide/hooks/useGuide.js"),m=require("./modules/guide/hooks/useGuides.js"),q=require("./modules/guide/hooks/useGuideContext.js"),r=require("./modules/ms-teams/context/KnockMsTeamsProvider.js"),v=require("./modules/ms-teams/hooks/useMsTeamsConnectionStatus.js"),T=require("./modules/ms-teams/hooks/useMsTeamsAuth.js"),g=require("./modules/ms-teams/hooks/useMsTeamsTeams.js"),h=require("./modules/ms-teams/hooks/useMsTeamsChannels.js"),f=require("./modules/ms-teams/hooks/useConnectedMsTeamsChannels.js"),u=require("./modules/slack/context/KnockSlackProvider.js"),M=require("./modules/slack/hooks/useSlackConnectionStatus.js"),A=require("./modules/slack/hooks/useSlackChannels.js"),G=require("./modules/slack/hooks/useConnectedSlackChannels.js"),i=require("./modules/slack/hooks/useSlackAuth.js"),c=require("./modules/i18n/context/KnockI18nProvider.js"),y=require("./modules/i18n/hooks/useTranslations.js"),F=require("./modules/i18n/languages/index.js"),b=require("./modules/preferences/hooks/usePreferences.js"),N=require("@tanstack/react-store");exports.KnockProvider=s.KnockProvider;exports.useKnockClient=s.useKnockClient;exports.useAuthenticatedKnockClient=a;exports.useStableOptions=d;exports.useAuthPostMessageListener=l.useAuthPostMessageListener;exports.useAuthPolling=k.useAuthPolling;exports.FilterStatus=K.FilterStatus;exports.feedProviderKey=e.feedProviderKey;exports.formatBadgeCount=e.formatBadgeCount;exports.formatTimestamp=e.formatTimestamp;exports.getBadgeAriaLabel=e.getBadgeAriaLabel;exports.msTeamsProviderKey=e.msTeamsProviderKey;exports.renderNodeOrFallback=e.renderNodeOrFallback;exports.slackProviderKey=e.slackProviderKey;exports.toSentenceCase=e.toSentenceCase;exports.KnockFeedProvider=t.KnockFeedProvider;exports.useKnockFeed=t.useKnockFeed;exports.useNotifications=S;exports.useFeedSettings=C;exports.useCreateNotificationStore=n.useCreateNotificationStore;exports.useNotificationStore=n.default;exports.KnockGuideContext=o.KnockGuideContext;exports.KnockGuideProvider=o.KnockGuideProvider;exports.useGuide=P.useGuide;exports.useGuides=m.useGuides;exports.useGuideContext=q.useGuideContext;exports.KnockMsTeamsProvider=r.KnockMsTeamsProvider;exports.useKnockMsTeamsClient=r.useKnockMsTeamsClient;exports.useMsTeamsConnectionStatus=v;exports.useMsTeamsAuth=T;exports.useMsTeamsTeams=g;exports.useMsTeamsChannels=h;exports.useConnectedMsTeamsChannels=f;exports.KnockSlackProvider=u.KnockSlackProvider;exports.useKnockSlackClient=u.useKnockSlackClient;exports.useSlackConnectionStatus=M;exports.useSlackChannels=A;exports.useConnectedSlackChannels=G;exports.getSlackNonceStorageKey=i.getSlackNonceStorageKey;exports.useSlackAuth=i.default;exports.I18nContext=c.I18nContext;exports.KnockI18nProvider=c.KnockI18nProvider;exports.useTranslations=y.useTranslations;exports.locales=F.locales;exports.usePreferences=b.usePreferences;Object.defineProperty(exports,"useStore",{enumerable:!0,get:()=>N.useStore});
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});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 o=e.nonce;return typeof o=="string"?o:void 0}}function a(e){const{knockHost:o,popupWindowRef:r,setConnectionStatus:t,onAuthenticationComplete:s,nonceStorageKey:n}=e;d.useEffect(()=>{const i=()=>{r.current&&!r.current.closed&&r.current.close(),r.current=null},f=u=>{if(u.origin!==o)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){t("error"),s==null||s("authFailed"),i();return}}t("connected"),s==null||s(c),i()}else c==="authFailed"&&(n&&sessionStorage.removeItem(n),t("error"),s==null||s(c),r.current=null)};return window.addEventListener("message",f,!1),()=>window.removeEventListener("message",f)},[o,s,t,r,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\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, updates 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 * });\n * ```\n */\nexport function useAuthPostMessageListener(\n options: UseAuthPostMessageListenerOptions,\n): void {\n const {\n knockHost,\n popupWindowRef,\n setConnectionStatus,\n onAuthenticationComplete,\n } = options;\n\n useEffect(() => {\n const receiveMessage = (event: MessageEvent) => {\n // Validate message origin for security\n if (event.origin !== knockHost) {\n return;\n }\n\n
|
|
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,CAE1CA,GAAAA,EAAMC,SAAWZ,EACnB,OAGIa,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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const b=require("../context/KnockSlackProvider.js"),l=require("react");require("../../i18n/context/KnockI18nProvider.js");require("swr/infinite");const y=require("../../core/context/KnockProvider.js"),A=require("@knocklabs/client");require("fast-deep-equal");require("date-fns");require("swr");const C="https://slack.com/oauth/v2/authorize",s=["chat:write","chat:write.public","channels:read","groups:read"];function k(e,t){return`knock:slack-auth-nonce:${e}:${t}`}function g(e){return e?Array.isArray(e)?{scopes:s,additionalScopes:e}:{scopes:e.scopes??s,additionalScopes:e.additionalScopes??[]}:{scopes:s,additionalScopes:[]}}function K(e,t,d){const c=y.useKnockClient(),{setConnectionStatus:n,knockSlackChannelId:r,tenantId:a,setActionLabel:i}=b.useKnockSlackClient(),{scopes:S,additionalScopes:h}=g(d),u=Array.from(new Set(S.concat(h))),_=l.useCallback(async()=>{i(null),n("disconnecting");try{const o=await c.slack.revokeAccessToken({tenant:a,knockChannelId:r});n(o==="ok"?"disconnected":"error")}catch{n("error")}},[n,c.slack,a,r,i]);return{buildSlackAuthUrl:l.useCallback(()=>{const o=crypto.randomUUID();sessionStorage.setItem(k(r,c.userId),o);const p={state:JSON.stringify({nonce:o,redirect_url:t,access_token_object:{object_id:a,collection:A.TENANT_OBJECT_COLLECTION},channel_id:r,public_key:c.apiKey,user_token:c.userToken,branch_slug:c.branch}),client_id:e,scope:u.join(",")};return`${C}?${new URLSearchParams(p)}`},[t,a,r,c.apiKey,c.userId,c.userToken,c.branch,e,u]),disconnectFromSlack:_}}exports.default=K;exports.getSlackNonceStorageKey=k;
|
|
2
2
|
//# sourceMappingURL=useSlackAuth.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSlackAuth.js","sources":["../../../../../src/modules/slack/hooks/useSlackAuth.ts"],"sourcesContent":["import { useKnockSlackClient } from \"..\";\nimport { TENANT_OBJECT_COLLECTION } from \"@knocklabs/client\";\nimport { useCallback } from \"react\";\n\nimport { useKnockClient } from \"../../core\";\n\nconst SLACK_AUTHORIZE_URL = \"https://slack.com/oauth/v2/authorize\";\nconst DEFAULT_SLACK_SCOPES = [\n \"chat:write\",\n \"chat:write.public\",\n \"channels:read\",\n \"groups:read\",\n];\n\ntype UseSlackAuthOutput = {\n buildSlackAuthUrl: () => string;\n disconnectFromSlack: () => void;\n};\n\ntype UseSlackAuthOptions = {\n // When provided, the default scopes will be overridden with the provided scopes\n scopes?: string[];\n // Additional scopes to add to the default scopes\n additionalScopes?: string[];\n};\n\n// Here we normalize the options to be a single object with scopes and additionalScopes\n// The \"options\" parameter can be an array of scopes, an object with scopes and additionalScopes, or undefined\n// We handle the array case because it was the previous way to pass options so we're being backward compatible\nfunction normalizeOptions(options?: UseSlackAuthOptions | string[]): {\n scopes: string[];\n additionalScopes: string[];\n} {\n if (!options) {\n return { scopes: DEFAULT_SLACK_SCOPES, additionalScopes: [] };\n }\n\n if (Array.isArray(options)) {\n return { scopes: DEFAULT_SLACK_SCOPES, additionalScopes: options };\n }\n\n return {\n scopes: options.scopes ?? DEFAULT_SLACK_SCOPES,\n additionalScopes: options.additionalScopes ?? [],\n };\n}\n\nfunction useSlackAuth(\n slackClientId: string,\n redirectUrl?: string,\n options?: UseSlackAuthOptions | string[],\n): UseSlackAuthOutput {\n const knock = useKnockClient();\n const { setConnectionStatus, knockSlackChannelId, tenantId, setActionLabel } =\n useKnockSlackClient();\n\n const { scopes, additionalScopes } = normalizeOptions(options);\n const combinedScopes = Array.from(new Set(scopes.concat(additionalScopes)));\n\n const disconnectFromSlack = useCallback(async () => {\n setActionLabel(null);\n setConnectionStatus(\"disconnecting\");\n try {\n const revoke = await knock.slack.revokeAccessToken({\n tenant: tenantId,\n knockChannelId: knockSlackChannelId,\n });\n\n if (revoke === \"ok\") {\n setConnectionStatus(\"disconnected\");\n } else {\n setConnectionStatus(\"error\");\n }\n } catch (_error) {\n setConnectionStatus(\"error\");\n }\n }, [\n setConnectionStatus,\n knock.slack,\n tenantId,\n knockSlackChannelId,\n setActionLabel,\n ]);\n\n const buildSlackAuthUrl = useCallback(() => {\n const rawParams = {\n state: JSON.stringify({\n redirect_url: redirectUrl,\n access_token_object: {\n object_id: tenantId,\n collection: TENANT_OBJECT_COLLECTION,\n },\n channel_id: knockSlackChannelId,\n public_key: knock.apiKey,\n user_token: knock.userToken,\n branch_slug: knock.branch,\n }),\n client_id: slackClientId,\n scope: combinedScopes.join(\",\"),\n };\n return `${SLACK_AUTHORIZE_URL}?${new URLSearchParams(rawParams)}`;\n }, [\n redirectUrl,\n tenantId,\n knockSlackChannelId,\n knock.apiKey,\n knock.userToken,\n knock.branch,\n slackClientId,\n combinedScopes,\n ]);\n\n return {\n buildSlackAuthUrl,\n disconnectFromSlack,\n };\n}\n\nexport default useSlackAuth;\n"],"names":["SLACK_AUTHORIZE_URL","DEFAULT_SLACK_SCOPES","normalizeOptions","options","Array","isArray","scopes","additionalScopes","useSlackAuth","slackClientId","redirectUrl","knock","useKnockClient","setConnectionStatus","knockSlackChannelId","tenantId","setActionLabel","useKnockSlackClient","combinedScopes","from","Set","concat","disconnectFromSlack","useCallback","revoke","slack","revokeAccessToken","tenant","knockChannelId","buildSlackAuthUrl","rawParams","state","JSON","stringify","redirect_url","access_token_object","object_id","collection","TENANT_OBJECT_COLLECTION","channel_id","public_key","apiKey","user_token","userToken","branch_slug","branch","client_id","scope","join","URLSearchParams"],"mappings":"
|
|
1
|
+
{"version":3,"file":"useSlackAuth.js","sources":["../../../../../src/modules/slack/hooks/useSlackAuth.ts"],"sourcesContent":["import { useKnockSlackClient } from \"..\";\nimport { TENANT_OBJECT_COLLECTION } from \"@knocklabs/client\";\nimport { useCallback } from \"react\";\n\nimport { useKnockClient } from \"../../core\";\n\nconst SLACK_AUTHORIZE_URL = \"https://slack.com/oauth/v2/authorize\";\nconst DEFAULT_SLACK_SCOPES = [\n \"chat:write\",\n \"chat:write.public\",\n \"channels:read\",\n \"groups:read\",\n];\n\nexport function getSlackNonceStorageKey(\n channelId: string,\n userId: string,\n): string {\n return `knock:slack-auth-nonce:${channelId}:${userId}`;\n}\n\ntype UseSlackAuthOutput = {\n buildSlackAuthUrl: () => string;\n disconnectFromSlack: () => void;\n};\n\ntype UseSlackAuthOptions = {\n // When provided, the default scopes will be overridden with the provided scopes\n scopes?: string[];\n // Additional scopes to add to the default scopes\n additionalScopes?: string[];\n};\n\n// Here we normalize the options to be a single object with scopes and additionalScopes\n// The \"options\" parameter can be an array of scopes, an object with scopes and additionalScopes, or undefined\n// We handle the array case because it was the previous way to pass options so we're being backward compatible\nfunction normalizeOptions(options?: UseSlackAuthOptions | string[]): {\n scopes: string[];\n additionalScopes: string[];\n} {\n if (!options) {\n return { scopes: DEFAULT_SLACK_SCOPES, additionalScopes: [] };\n }\n\n if (Array.isArray(options)) {\n return { scopes: DEFAULT_SLACK_SCOPES, additionalScopes: options };\n }\n\n return {\n scopes: options.scopes ?? DEFAULT_SLACK_SCOPES,\n additionalScopes: options.additionalScopes ?? [],\n };\n}\n\nfunction useSlackAuth(\n slackClientId: string,\n redirectUrl?: string,\n options?: UseSlackAuthOptions | string[],\n): UseSlackAuthOutput {\n const knock = useKnockClient();\n const { setConnectionStatus, knockSlackChannelId, tenantId, setActionLabel } =\n useKnockSlackClient();\n\n const { scopes, additionalScopes } = normalizeOptions(options);\n const combinedScopes = Array.from(new Set(scopes.concat(additionalScopes)));\n\n const disconnectFromSlack = useCallback(async () => {\n setActionLabel(null);\n setConnectionStatus(\"disconnecting\");\n try {\n const revoke = await knock.slack.revokeAccessToken({\n tenant: tenantId,\n knockChannelId: knockSlackChannelId,\n });\n\n if (revoke === \"ok\") {\n setConnectionStatus(\"disconnected\");\n } else {\n setConnectionStatus(\"error\");\n }\n } catch (_error) {\n setConnectionStatus(\"error\");\n }\n }, [\n setConnectionStatus,\n knock.slack,\n tenantId,\n knockSlackChannelId,\n setActionLabel,\n ]);\n\n const buildSlackAuthUrl = useCallback(() => {\n const nonce = crypto.randomUUID();\n sessionStorage.setItem(\n getSlackNonceStorageKey(knockSlackChannelId, knock.userId!),\n nonce,\n );\n\n const rawParams = {\n state: JSON.stringify({\n nonce,\n redirect_url: redirectUrl,\n access_token_object: {\n object_id: tenantId,\n collection: TENANT_OBJECT_COLLECTION,\n },\n channel_id: knockSlackChannelId,\n public_key: knock.apiKey,\n user_token: knock.userToken,\n branch_slug: knock.branch,\n }),\n client_id: slackClientId,\n scope: combinedScopes.join(\",\"),\n };\n return `${SLACK_AUTHORIZE_URL}?${new URLSearchParams(rawParams)}`;\n }, [\n redirectUrl,\n tenantId,\n knockSlackChannelId,\n knock.apiKey,\n knock.userId,\n knock.userToken,\n knock.branch,\n slackClientId,\n combinedScopes,\n ]);\n\n return {\n buildSlackAuthUrl,\n disconnectFromSlack,\n };\n}\n\nexport default useSlackAuth;\n"],"names":["SLACK_AUTHORIZE_URL","DEFAULT_SLACK_SCOPES","getSlackNonceStorageKey","channelId","userId","normalizeOptions","options","Array","isArray","scopes","additionalScopes","useSlackAuth","slackClientId","redirectUrl","knock","useKnockClient","setConnectionStatus","knockSlackChannelId","tenantId","setActionLabel","useKnockSlackClient","combinedScopes","from","Set","concat","disconnectFromSlack","useCallback","revoke","slack","revokeAccessToken","tenant","knockChannelId","buildSlackAuthUrl","nonce","crypto","randomUUID","sessionStorage","setItem","rawParams","state","JSON","stringify","redirect_url","access_token_object","object_id","collection","TENANT_OBJECT_COLLECTION","channel_id","public_key","apiKey","user_token","userToken","branch_slug","branch","client_id","scope","join","URLSearchParams"],"mappings":"kZAMA,MAAMA,EAAsB,uCACtBC,EAAuB,CAC3B,aACA,oBACA,gBACA,aAAa,EAGCC,SAAAA,EACdC,EACAC,EACQ,CACD,MAAA,0BAA0BD,CAAS,IAAIC,CAAM,EACtD,CAiBA,SAASC,EAAiBC,EAGxB,CACA,OAAKA,EAIDC,MAAMC,QAAQF,CAAO,EAChB,CAAEG,OAAQR,EAAsBS,iBAAkBJ,CAAQ,EAG5D,CACLG,OAAQH,EAAQG,QAAUR,EAC1BS,iBAAkBJ,EAAQI,kBAAoB,CAAA,CAChD,EAVS,CAAED,OAAQR,EAAsBS,iBAAkB,CAAA,CAAG,CAWhE,CAEA,SAASC,EACPC,EACAC,EACAP,EACoB,CACpB,MAAMQ,EAAQC,EAAAA,eAAe,EACvB,CAAEC,oBAAAA,EAAqBC,oBAAAA,EAAqBC,SAAAA,EAAUC,eAAAA,GAC1DC,sBAAoB,EAEhB,CAAEX,OAAAA,EAAQC,iBAAAA,CAAAA,EAAqBL,EAAiBC,CAAO,EACvDe,EAAiBd,MAAMe,KAAK,IAAIC,IAAId,EAAOe,OAAOd,CAAgB,CAAC,CAAC,EAEpEe,EAAsBC,EAAAA,YAAY,SAAY,CAClDP,EAAe,IAAI,EACnBH,EAAoB,eAAe,EAC/B,GAAA,CACF,MAAMW,EAAS,MAAMb,EAAMc,MAAMC,kBAAkB,CACjDC,OAAQZ,EACRa,eAAgBd,CAAAA,CACjB,EAGCD,EADEW,IAAW,KACO,eAEA,OAFc,OAIrB,CACfX,EAAoB,OAAO,CAAA,CAC7B,EACC,CACDA,EACAF,EAAMc,MACNV,EACAD,EACAE,CAAc,CACf,EAsCM,MAAA,CACLa,kBArCwBN,EAAAA,YAAY,IAAM,CACpCO,MAAAA,EAAQC,OAAOC,WAAW,EAChCC,eAAeC,QACbnC,EAAwBe,EAAqBH,EAAMV,MAAO,EAC1D6B,CACF,EAEA,MAAMK,EAAY,CAChBC,MAAOC,KAAKC,UAAU,CACpBR,MAAAA,EACAS,aAAc7B,EACd8B,oBAAqB,CACnBC,UAAW1B,EACX2B,WAAYC,EAAAA,wBACd,EACAC,WAAY9B,EACZ+B,WAAYlC,EAAMmC,OAClBC,WAAYpC,EAAMqC,UAClBC,YAAatC,EAAMuC,MAAAA,CACpB,EACDC,UAAW1C,EACX2C,MAAOlC,EAAemC,KAAK,GAAG,CAChC,EACA,MAAO,GAAGxD,CAAmB,IAAI,IAAIyD,gBAAgBnB,CAAS,CAAC,EAAA,EAC9D,CACDzB,EACAK,EACAD,EACAH,EAAMmC,OACNnC,EAAMV,OACNU,EAAMqC,UACNrC,EAAMuC,OACNzC,EACAS,CAAc,CACf,EAICI,oBAAAA,CACF,CACF"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { KnockProvider as t, useKnockClient as r } from "./modules/core/context/KnockProvider.mjs";
|
|
2
2
|
import { default as a } from "./modules/core/hooks/useAuthenticatedKnockClient.mjs";
|
|
3
|
-
import { default as
|
|
3
|
+
import { default as u } from "./modules/core/hooks/useStableOptions.mjs";
|
|
4
4
|
import { useAuthPostMessageListener as m } from "./modules/core/hooks/useAuthPostMessageListener.mjs";
|
|
5
|
-
import { useAuthPolling as
|
|
5
|
+
import { useAuthPolling as d } from "./modules/core/hooks/useAuthPolling.mjs";
|
|
6
6
|
import { FilterStatus as x } from "./modules/core/constants.mjs";
|
|
7
|
-
import { feedProviderKey as
|
|
8
|
-
import { KnockFeedProvider as
|
|
7
|
+
import { feedProviderKey as p, formatBadgeCount as k, formatTimestamp as C, getBadgeAriaLabel as S, msTeamsProviderKey as K, renderNodeOrFallback as P, slackProviderKey as T, toSentenceCase as h } from "./modules/core/utils.mjs";
|
|
8
|
+
import { KnockFeedProvider as g, useKnockFeed as M } from "./modules/feed/context/KnockFeedProvider.mjs";
|
|
9
9
|
import { default as F } from "./modules/feed/hooks/useNotifications.mjs";
|
|
10
10
|
import { default as N } from "./modules/feed/hooks/useFeedSettings.mjs";
|
|
11
|
-
import { useCreateNotificationStore as
|
|
11
|
+
import { useCreateNotificationStore as b, default as B } from "./modules/feed/hooks/useNotificationStore.mjs";
|
|
12
12
|
import { KnockGuideContext as L, KnockGuideProvider as O } from "./modules/guide/context/KnockGuideProvider.mjs";
|
|
13
13
|
import { useGuide as q } from "./modules/guide/hooks/useGuide.mjs";
|
|
14
14
|
import { useGuides as z } from "./modules/guide/hooks/useGuides.mjs";
|
|
@@ -21,45 +21,46 @@ import { default as _ } from "./modules/ms-teams/hooks/useMsTeamsChannels.mjs";
|
|
|
21
21
|
import { default as ee } from "./modules/ms-teams/hooks/useConnectedMsTeamsChannels.mjs";
|
|
22
22
|
import { KnockSlackProvider as te, useKnockSlackClient as re } from "./modules/slack/context/KnockSlackProvider.mjs";
|
|
23
23
|
import { default as ae } from "./modules/slack/hooks/useSlackConnectionStatus.mjs";
|
|
24
|
-
import { default as
|
|
24
|
+
import { default as ue } from "./modules/slack/hooks/useSlackChannels.mjs";
|
|
25
25
|
import { default as me } from "./modules/slack/hooks/useConnectedSlackChannels.mjs";
|
|
26
|
-
import { default as
|
|
27
|
-
import { I18nContext as
|
|
28
|
-
import { useTranslations as
|
|
26
|
+
import { getSlackNonceStorageKey as de, default as ie } from "./modules/slack/hooks/useSlackAuth.mjs";
|
|
27
|
+
import { I18nContext as ce, KnockI18nProvider as pe } from "./modules/i18n/context/KnockI18nProvider.mjs";
|
|
28
|
+
import { useTranslations as Ce } from "./modules/i18n/hooks/useTranslations.mjs";
|
|
29
29
|
import { locales as Ke } from "./modules/i18n/languages/index.mjs";
|
|
30
|
-
import { usePreferences as
|
|
31
|
-
import { useStore as
|
|
30
|
+
import { usePreferences as Te } from "./modules/preferences/hooks/usePreferences.mjs";
|
|
31
|
+
import { useStore as ve } from "@tanstack/react-store";
|
|
32
32
|
export {
|
|
33
33
|
x as FilterStatus,
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
ce as I18nContext,
|
|
35
|
+
g as KnockFeedProvider,
|
|
36
36
|
L as KnockGuideContext,
|
|
37
37
|
O as KnockGuideProvider,
|
|
38
38
|
pe as KnockI18nProvider,
|
|
39
39
|
J as KnockMsTeamsProvider,
|
|
40
40
|
t as KnockProvider,
|
|
41
41
|
te as KnockSlackProvider,
|
|
42
|
-
|
|
42
|
+
p as feedProviderKey,
|
|
43
43
|
k as formatBadgeCount,
|
|
44
44
|
C as formatTimestamp,
|
|
45
|
-
|
|
45
|
+
S as getBadgeAriaLabel,
|
|
46
|
+
de as getSlackNonceStorageKey,
|
|
46
47
|
Ke as locales,
|
|
47
|
-
|
|
48
|
+
K as msTeamsProviderKey,
|
|
48
49
|
P as renderNodeOrFallback,
|
|
49
50
|
T as slackProviderKey,
|
|
50
51
|
h as toSentenceCase,
|
|
51
|
-
|
|
52
|
+
d as useAuthPolling,
|
|
52
53
|
m as useAuthPostMessageListener,
|
|
53
54
|
a as useAuthenticatedKnockClient,
|
|
54
55
|
ee as useConnectedMsTeamsChannels,
|
|
55
56
|
me as useConnectedSlackChannels,
|
|
56
|
-
|
|
57
|
+
b as useCreateNotificationStore,
|
|
57
58
|
N as useFeedSettings,
|
|
58
59
|
q as useGuide,
|
|
59
60
|
E as useGuideContext,
|
|
60
61
|
z as useGuides,
|
|
61
62
|
r as useKnockClient,
|
|
62
|
-
|
|
63
|
+
M as useKnockFeed,
|
|
63
64
|
Q as useKnockMsTeamsClient,
|
|
64
65
|
re as useKnockSlackClient,
|
|
65
66
|
W as useMsTeamsAuth,
|
|
@@ -68,12 +69,12 @@ export {
|
|
|
68
69
|
Y as useMsTeamsTeams,
|
|
69
70
|
B as useNotificationStore,
|
|
70
71
|
F as useNotifications,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
Te as usePreferences,
|
|
73
|
+
ie as useSlackAuth,
|
|
74
|
+
ue as useSlackChannels,
|
|
74
75
|
ae as useSlackConnectionStatus,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
u as useStableOptions,
|
|
77
|
+
ve as useStore,
|
|
78
|
+
Ce as useTranslations
|
|
78
79
|
};
|
|
79
80
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1,19 +1,43 @@
|
|
|
1
|
-
import { useEffect as
|
|
2
|
-
function d(
|
|
1
|
+
import { useEffect as p } from "react";
|
|
2
|
+
function d(e) {
|
|
3
|
+
return typeof e == "object" && e !== null && "type" in e ? e.type : e;
|
|
4
|
+
}
|
|
5
|
+
function y(e) {
|
|
6
|
+
if (typeof e == "object" && e !== null && "nonce" in e) {
|
|
7
|
+
const o = e.nonce;
|
|
8
|
+
return typeof o == "string" ? o : void 0;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function v(e) {
|
|
3
12
|
const {
|
|
4
|
-
knockHost:
|
|
5
|
-
popupWindowRef:
|
|
13
|
+
knockHost: o,
|
|
14
|
+
popupWindowRef: s,
|
|
6
15
|
setConnectionStatus: t,
|
|
7
|
-
onAuthenticationComplete:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
onAuthenticationComplete: r,
|
|
17
|
+
nonceStorageKey: n
|
|
18
|
+
} = e;
|
|
19
|
+
p(() => {
|
|
20
|
+
const i = () => {
|
|
21
|
+
s.current && !s.current.closed && s.current.close(), s.current = null;
|
|
22
|
+
}, f = (u) => {
|
|
23
|
+
if (u.origin !== o)
|
|
24
|
+
return;
|
|
25
|
+
const c = d(u.data);
|
|
26
|
+
if (c === "authComplete") {
|
|
27
|
+
if (n) {
|
|
28
|
+
const g = y(u.data), l = sessionStorage.getItem(n);
|
|
29
|
+
if (sessionStorage.removeItem(n), !g || l && l !== g) {
|
|
30
|
+
t("error"), r == null || r("authFailed"), i();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
t("connected"), r == null || r(c), i();
|
|
35
|
+
} else c === "authFailed" && (n && sessionStorage.removeItem(n), t("error"), r == null || r(c), s.current = null);
|
|
12
36
|
};
|
|
13
|
-
return window.addEventListener("message",
|
|
14
|
-
}, [
|
|
37
|
+
return window.addEventListener("message", f, !1), () => window.removeEventListener("message", f);
|
|
38
|
+
}, [o, r, t, s, n]);
|
|
15
39
|
}
|
|
16
40
|
export {
|
|
17
|
-
|
|
41
|
+
v as useAuthPostMessageListener
|
|
18
42
|
};
|
|
19
43
|
//# sourceMappingURL=useAuthPostMessageListener.mjs.map
|
|
@@ -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\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, updates 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 * });\n * ```\n */\nexport function useAuthPostMessageListener(\n options: UseAuthPostMessageListenerOptions,\n): void {\n const {\n knockHost,\n popupWindowRef,\n setConnectionStatus,\n onAuthenticationComplete,\n } = options;\n\n useEffect(() => {\n const receiveMessage = (event: MessageEvent) => {\n // Validate message origin for security\n if (event.origin !== knockHost) {\n return;\n }\n\n
|
|
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;AAE1CA,UAAAA,EAAMC,WAAWZ;AACnB;AAGIa,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,69 +1,76 @@
|
|
|
1
|
-
import { useKnockSlackClient as
|
|
1
|
+
import { useKnockSlackClient as h } from "../context/KnockSlackProvider.mjs";
|
|
2
2
|
import { useCallback as u } from "react";
|
|
3
3
|
import "../../i18n/context/KnockI18nProvider.mjs";
|
|
4
4
|
import "swr/infinite";
|
|
5
|
-
import { useKnockClient as
|
|
6
|
-
import { TENANT_OBJECT_COLLECTION as
|
|
5
|
+
import { useKnockClient as _ } from "../../core/context/KnockProvider.mjs";
|
|
6
|
+
import { TENANT_OBJECT_COLLECTION as f } from "@knocklabs/client";
|
|
7
7
|
import "fast-deep-equal";
|
|
8
8
|
import "date-fns";
|
|
9
9
|
import "swr";
|
|
10
|
-
const A = "https://slack.com/oauth/v2/authorize",
|
|
11
|
-
function b(e) {
|
|
10
|
+
const A = "https://slack.com/oauth/v2/authorize", s = ["chat:write", "chat:write.public", "channels:read", "groups:read"];
|
|
11
|
+
function b(e, r) {
|
|
12
|
+
return `knock:slack-auth-nonce:${e}:${r}`;
|
|
13
|
+
}
|
|
14
|
+
function y(e) {
|
|
12
15
|
return e ? Array.isArray(e) ? {
|
|
13
|
-
scopes:
|
|
16
|
+
scopes: s,
|
|
14
17
|
additionalScopes: e
|
|
15
18
|
} : {
|
|
16
|
-
scopes: e.scopes ??
|
|
19
|
+
scopes: e.scopes ?? s,
|
|
17
20
|
additionalScopes: e.additionalScopes ?? []
|
|
18
21
|
} : {
|
|
19
|
-
scopes:
|
|
22
|
+
scopes: s,
|
|
20
23
|
additionalScopes: []
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
setConnectionStatus:
|
|
26
|
+
function N(e, r, k) {
|
|
27
|
+
const o = _(), {
|
|
28
|
+
setConnectionStatus: c,
|
|
26
29
|
knockSlackChannelId: n,
|
|
27
|
-
tenantId:
|
|
30
|
+
tenantId: a,
|
|
28
31
|
setActionLabel: i
|
|
29
|
-
} =
|
|
32
|
+
} = h(), {
|
|
30
33
|
scopes: d,
|
|
31
34
|
additionalScopes: p
|
|
32
|
-
} =
|
|
33
|
-
i(null),
|
|
35
|
+
} = y(k), l = Array.from(new Set(d.concat(p))), S = u(async () => {
|
|
36
|
+
i(null), c("disconnecting");
|
|
34
37
|
try {
|
|
35
|
-
const
|
|
36
|
-
tenant:
|
|
38
|
+
const t = await o.slack.revokeAccessToken({
|
|
39
|
+
tenant: a,
|
|
37
40
|
knockChannelId: n
|
|
38
41
|
});
|
|
39
|
-
|
|
42
|
+
c(t === "ok" ? "disconnected" : "error");
|
|
40
43
|
} catch {
|
|
41
|
-
|
|
44
|
+
c("error");
|
|
42
45
|
}
|
|
43
|
-
}, [
|
|
46
|
+
}, [c, o.slack, a, n, i]);
|
|
44
47
|
return {
|
|
45
48
|
buildSlackAuthUrl: u(() => {
|
|
46
|
-
const
|
|
49
|
+
const t = crypto.randomUUID();
|
|
50
|
+
sessionStorage.setItem(b(n, o.userId), t);
|
|
51
|
+
const m = {
|
|
47
52
|
state: JSON.stringify({
|
|
48
|
-
|
|
53
|
+
nonce: t,
|
|
54
|
+
redirect_url: r,
|
|
49
55
|
access_token_object: {
|
|
50
|
-
object_id:
|
|
51
|
-
collection:
|
|
56
|
+
object_id: a,
|
|
57
|
+
collection: f
|
|
52
58
|
},
|
|
53
59
|
channel_id: n,
|
|
54
|
-
public_key:
|
|
55
|
-
user_token:
|
|
56
|
-
branch_slug:
|
|
60
|
+
public_key: o.apiKey,
|
|
61
|
+
user_token: o.userToken,
|
|
62
|
+
branch_slug: o.branch
|
|
57
63
|
}),
|
|
58
64
|
client_id: e,
|
|
59
65
|
scope: l.join(",")
|
|
60
66
|
};
|
|
61
|
-
return `${A}?${new URLSearchParams(
|
|
62
|
-
}, [
|
|
63
|
-
disconnectFromSlack:
|
|
67
|
+
return `${A}?${new URLSearchParams(m)}`;
|
|
68
|
+
}, [r, a, n, o.apiKey, o.userId, o.userToken, o.branch, e, l]),
|
|
69
|
+
disconnectFromSlack: S
|
|
64
70
|
};
|
|
65
71
|
}
|
|
66
72
|
export {
|
|
67
|
-
|
|
73
|
+
N as default,
|
|
74
|
+
b as getSlackNonceStorageKey
|
|
68
75
|
};
|
|
69
76
|
//# sourceMappingURL=useSlackAuth.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSlackAuth.mjs","sources":["../../../../../src/modules/slack/hooks/useSlackAuth.ts"],"sourcesContent":["import { useKnockSlackClient } from \"..\";\nimport { TENANT_OBJECT_COLLECTION } from \"@knocklabs/client\";\nimport { useCallback } from \"react\";\n\nimport { useKnockClient } from \"../../core\";\n\nconst SLACK_AUTHORIZE_URL = \"https://slack.com/oauth/v2/authorize\";\nconst DEFAULT_SLACK_SCOPES = [\n \"chat:write\",\n \"chat:write.public\",\n \"channels:read\",\n \"groups:read\",\n];\n\ntype UseSlackAuthOutput = {\n buildSlackAuthUrl: () => string;\n disconnectFromSlack: () => void;\n};\n\ntype UseSlackAuthOptions = {\n // When provided, the default scopes will be overridden with the provided scopes\n scopes?: string[];\n // Additional scopes to add to the default scopes\n additionalScopes?: string[];\n};\n\n// Here we normalize the options to be a single object with scopes and additionalScopes\n// The \"options\" parameter can be an array of scopes, an object with scopes and additionalScopes, or undefined\n// We handle the array case because it was the previous way to pass options so we're being backward compatible\nfunction normalizeOptions(options?: UseSlackAuthOptions | string[]): {\n scopes: string[];\n additionalScopes: string[];\n} {\n if (!options) {\n return { scopes: DEFAULT_SLACK_SCOPES, additionalScopes: [] };\n }\n\n if (Array.isArray(options)) {\n return { scopes: DEFAULT_SLACK_SCOPES, additionalScopes: options };\n }\n\n return {\n scopes: options.scopes ?? DEFAULT_SLACK_SCOPES,\n additionalScopes: options.additionalScopes ?? [],\n };\n}\n\nfunction useSlackAuth(\n slackClientId: string,\n redirectUrl?: string,\n options?: UseSlackAuthOptions | string[],\n): UseSlackAuthOutput {\n const knock = useKnockClient();\n const { setConnectionStatus, knockSlackChannelId, tenantId, setActionLabel } =\n useKnockSlackClient();\n\n const { scopes, additionalScopes } = normalizeOptions(options);\n const combinedScopes = Array.from(new Set(scopes.concat(additionalScopes)));\n\n const disconnectFromSlack = useCallback(async () => {\n setActionLabel(null);\n setConnectionStatus(\"disconnecting\");\n try {\n const revoke = await knock.slack.revokeAccessToken({\n tenant: tenantId,\n knockChannelId: knockSlackChannelId,\n });\n\n if (revoke === \"ok\") {\n setConnectionStatus(\"disconnected\");\n } else {\n setConnectionStatus(\"error\");\n }\n } catch (_error) {\n setConnectionStatus(\"error\");\n }\n }, [\n setConnectionStatus,\n knock.slack,\n tenantId,\n knockSlackChannelId,\n setActionLabel,\n ]);\n\n const buildSlackAuthUrl = useCallback(() => {\n const rawParams = {\n state: JSON.stringify({\n redirect_url: redirectUrl,\n access_token_object: {\n object_id: tenantId,\n collection: TENANT_OBJECT_COLLECTION,\n },\n channel_id: knockSlackChannelId,\n public_key: knock.apiKey,\n user_token: knock.userToken,\n branch_slug: knock.branch,\n }),\n client_id: slackClientId,\n scope: combinedScopes.join(\",\"),\n };\n return `${SLACK_AUTHORIZE_URL}?${new URLSearchParams(rawParams)}`;\n }, [\n redirectUrl,\n tenantId,\n knockSlackChannelId,\n knock.apiKey,\n knock.userToken,\n knock.branch,\n slackClientId,\n combinedScopes,\n ]);\n\n return {\n buildSlackAuthUrl,\n disconnectFromSlack,\n };\n}\n\nexport default useSlackAuth;\n"],"names":["SLACK_AUTHORIZE_URL","DEFAULT_SLACK_SCOPES","normalizeOptions","options","Array","isArray","scopes","additionalScopes","useSlackAuth","slackClientId","redirectUrl","knock","useKnockClient","setConnectionStatus","knockSlackChannelId","tenantId","setActionLabel","useKnockSlackClient","combinedScopes","from","Set","concat","disconnectFromSlack","useCallback","revoke","slack","revokeAccessToken","tenant","knockChannelId","buildSlackAuthUrl","rawParams","state","JSON","stringify","redirect_url","access_token_object","object_id","collection","TENANT_OBJECT_COLLECTION","channel_id","public_key","apiKey","user_token","userToken","branch_slug","branch","client_id","scope","join","URLSearchParams"],"mappings":";;;;;;;;;AAMA,MAAMA,IAAsB,wCACtBC,IAAuB,CAC3B,cACA,qBACA,iBACA,aAAa;
|
|
1
|
+
{"version":3,"file":"useSlackAuth.mjs","sources":["../../../../../src/modules/slack/hooks/useSlackAuth.ts"],"sourcesContent":["import { useKnockSlackClient } from \"..\";\nimport { TENANT_OBJECT_COLLECTION } from \"@knocklabs/client\";\nimport { useCallback } from \"react\";\n\nimport { useKnockClient } from \"../../core\";\n\nconst SLACK_AUTHORIZE_URL = \"https://slack.com/oauth/v2/authorize\";\nconst DEFAULT_SLACK_SCOPES = [\n \"chat:write\",\n \"chat:write.public\",\n \"channels:read\",\n \"groups:read\",\n];\n\nexport function getSlackNonceStorageKey(\n channelId: string,\n userId: string,\n): string {\n return `knock:slack-auth-nonce:${channelId}:${userId}`;\n}\n\ntype UseSlackAuthOutput = {\n buildSlackAuthUrl: () => string;\n disconnectFromSlack: () => void;\n};\n\ntype UseSlackAuthOptions = {\n // When provided, the default scopes will be overridden with the provided scopes\n scopes?: string[];\n // Additional scopes to add to the default scopes\n additionalScopes?: string[];\n};\n\n// Here we normalize the options to be a single object with scopes and additionalScopes\n// The \"options\" parameter can be an array of scopes, an object with scopes and additionalScopes, or undefined\n// We handle the array case because it was the previous way to pass options so we're being backward compatible\nfunction normalizeOptions(options?: UseSlackAuthOptions | string[]): {\n scopes: string[];\n additionalScopes: string[];\n} {\n if (!options) {\n return { scopes: DEFAULT_SLACK_SCOPES, additionalScopes: [] };\n }\n\n if (Array.isArray(options)) {\n return { scopes: DEFAULT_SLACK_SCOPES, additionalScopes: options };\n }\n\n return {\n scopes: options.scopes ?? DEFAULT_SLACK_SCOPES,\n additionalScopes: options.additionalScopes ?? [],\n };\n}\n\nfunction useSlackAuth(\n slackClientId: string,\n redirectUrl?: string,\n options?: UseSlackAuthOptions | string[],\n): UseSlackAuthOutput {\n const knock = useKnockClient();\n const { setConnectionStatus, knockSlackChannelId, tenantId, setActionLabel } =\n useKnockSlackClient();\n\n const { scopes, additionalScopes } = normalizeOptions(options);\n const combinedScopes = Array.from(new Set(scopes.concat(additionalScopes)));\n\n const disconnectFromSlack = useCallback(async () => {\n setActionLabel(null);\n setConnectionStatus(\"disconnecting\");\n try {\n const revoke = await knock.slack.revokeAccessToken({\n tenant: tenantId,\n knockChannelId: knockSlackChannelId,\n });\n\n if (revoke === \"ok\") {\n setConnectionStatus(\"disconnected\");\n } else {\n setConnectionStatus(\"error\");\n }\n } catch (_error) {\n setConnectionStatus(\"error\");\n }\n }, [\n setConnectionStatus,\n knock.slack,\n tenantId,\n knockSlackChannelId,\n setActionLabel,\n ]);\n\n const buildSlackAuthUrl = useCallback(() => {\n const nonce = crypto.randomUUID();\n sessionStorage.setItem(\n getSlackNonceStorageKey(knockSlackChannelId, knock.userId!),\n nonce,\n );\n\n const rawParams = {\n state: JSON.stringify({\n nonce,\n redirect_url: redirectUrl,\n access_token_object: {\n object_id: tenantId,\n collection: TENANT_OBJECT_COLLECTION,\n },\n channel_id: knockSlackChannelId,\n public_key: knock.apiKey,\n user_token: knock.userToken,\n branch_slug: knock.branch,\n }),\n client_id: slackClientId,\n scope: combinedScopes.join(\",\"),\n };\n return `${SLACK_AUTHORIZE_URL}?${new URLSearchParams(rawParams)}`;\n }, [\n redirectUrl,\n tenantId,\n knockSlackChannelId,\n knock.apiKey,\n knock.userId,\n knock.userToken,\n knock.branch,\n slackClientId,\n combinedScopes,\n ]);\n\n return {\n buildSlackAuthUrl,\n disconnectFromSlack,\n };\n}\n\nexport default useSlackAuth;\n"],"names":["SLACK_AUTHORIZE_URL","DEFAULT_SLACK_SCOPES","getSlackNonceStorageKey","channelId","userId","normalizeOptions","options","Array","isArray","scopes","additionalScopes","useSlackAuth","slackClientId","redirectUrl","knock","useKnockClient","setConnectionStatus","knockSlackChannelId","tenantId","setActionLabel","useKnockSlackClient","combinedScopes","from","Set","concat","disconnectFromSlack","useCallback","revoke","slack","revokeAccessToken","tenant","knockChannelId","buildSlackAuthUrl","nonce","crypto","randomUUID","sessionStorage","setItem","rawParams","state","JSON","stringify","redirect_url","access_token_object","object_id","collection","TENANT_OBJECT_COLLECTION","channel_id","public_key","apiKey","user_token","userToken","branch_slug","branch","client_id","scope","join","URLSearchParams"],"mappings":";;;;;;;;;AAMA,MAAMA,IAAsB,wCACtBC,IAAuB,CAC3B,cACA,qBACA,iBACA,aAAa;AAGCC,SAAAA,EACdC,GACAC,GACQ;AACD,SAAA,0BAA0BD,CAAS,IAAIC,CAAM;AACtD;AAiBA,SAASC,EAAiBC,GAGxB;AACA,SAAKA,IAIDC,MAAMC,QAAQF,CAAO,IAChB;AAAA,IAAEG,QAAQR;AAAAA,IAAsBS,kBAAkBJ;AAAAA,EAAQ,IAG5D;AAAA,IACLG,QAAQH,EAAQG,UAAUR;AAAAA,IAC1BS,kBAAkBJ,EAAQI,oBAAoB,CAAA;AAAA,EAChD,IAVS;AAAA,IAAED,QAAQR;AAAAA,IAAsBS,kBAAkB,CAAA;AAAA,EAAG;AAWhE;AAEA,SAASC,EACPC,GACAC,GACAP,GACoB;AACpB,QAAMQ,IAAQC,EAAe,GACvB;AAAA,IAAEC,qBAAAA;AAAAA,IAAqBC,qBAAAA;AAAAA,IAAqBC,UAAAA;AAAAA,IAAUC,gBAAAA;AAAAA,MAC1DC,EAAoB,GAEhB;AAAA,IAAEX,QAAAA;AAAAA,IAAQC,kBAAAA;AAAAA,EAAAA,IAAqBL,EAAiBC,CAAO,GACvDe,IAAiBd,MAAMe,KAAK,IAAIC,IAAId,EAAOe,OAAOd,CAAgB,CAAC,CAAC,GAEpEe,IAAsBC,EAAY,YAAY;AAClDP,IAAAA,EAAe,IAAI,GACnBH,EAAoB,eAAe;AAC/B,QAAA;AACF,YAAMW,IAAS,MAAMb,EAAMc,MAAMC,kBAAkB;AAAA,QACjDC,QAAQZ;AAAAA,QACRa,gBAAgBd;AAAAA,MAAAA,CACjB;AAED,MACED,EADEW,MAAW,OACO,iBAEA,OAFc;AAAA,YAIrB;AACfX,MAAAA,EAAoB,OAAO;AAAA,IAAA;AAAA,EAC7B,GACC,CACDA,GACAF,EAAMc,OACNV,GACAD,GACAE,CAAc,CACf;AAsCM,SAAA;AAAA,IACLa,mBArCwBN,EAAY,MAAM;AACpCO,YAAAA,IAAQC,OAAOC,WAAW;AAChCC,qBAAeC,QACbnC,EAAwBe,GAAqBH,EAAMV,MAAO,GAC1D6B,CACF;AAEA,YAAMK,IAAY;AAAA,QAChBC,OAAOC,KAAKC,UAAU;AAAA,UACpBR,OAAAA;AAAAA,UACAS,cAAc7B;AAAAA,UACd8B,qBAAqB;AAAA,YACnBC,WAAW1B;AAAAA,YACX2B,YAAYC;AAAAA,UACd;AAAA,UACAC,YAAY9B;AAAAA,UACZ+B,YAAYlC,EAAMmC;AAAAA,UAClBC,YAAYpC,EAAMqC;AAAAA,UAClBC,aAAatC,EAAMuC;AAAAA,QAAAA,CACpB;AAAA,QACDC,WAAW1C;AAAAA,QACX2C,OAAOlC,EAAemC,KAAK,GAAG;AAAA,MAChC;AACA,aAAO,GAAGxD,CAAmB,IAAI,IAAIyD,gBAAgBnB,CAAS,CAAC;AAAA,IAAA,GAC9D,CACDzB,GACAK,GACAD,GACAH,EAAMmC,QACNnC,EAAMV,QACNU,EAAMqC,WACNrC,EAAMuC,QACNzC,GACAS,CAAc,CACf;AAAA,IAICI,qBAAAA;AAAAA,EACF;AACF;"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { FilterStatus, KnockProvider, feedProviderKey, type BadgeCountType, form
|
|
|
2
2
|
export { KnockFeedProvider, type KnockFeedProviderProps, type KnockFeedProviderState, type Selector, useCreateNotificationStore, useFeedSettings, useKnockFeed, useNotificationStore, useNotifications, } from './modules/feed';
|
|
3
3
|
export { KnockGuideProvider, KnockGuideContext, type KnockGuideProviderProps, useGuide, useGuides, useGuideContext, } from './modules/guide';
|
|
4
4
|
export { type MsTeamsChannelQueryOptions, type MsTeamsTeamQueryOptions, KnockMsTeamsProvider, type KnockMsTeamsProviderProps, type KnockMsTeamsProviderState, useConnectedMsTeamsChannels, useKnockMsTeamsClient, useMsTeamsAuth, useMsTeamsChannels, useMsTeamsConnectionStatus, useMsTeamsTeams, } from './modules/ms-teams';
|
|
5
|
-
export { KnockSlackProvider, type ContainerObject, type KnockSlackProviderProps, type KnockSlackProviderState, type SlackChannelQueryOptions, useConnectedSlackChannels, useKnockSlackClient, useSlackAuth, useSlackChannels, useSlackConnectionStatus, } from './modules/slack';
|
|
5
|
+
export { KnockSlackProvider, type ContainerObject, type KnockSlackProviderProps, type KnockSlackProviderState, type SlackChannelQueryOptions, getSlackNonceStorageKey, useConnectedSlackChannels, useKnockSlackClient, useSlackAuth, useSlackChannels, useSlackConnectionStatus, } from './modules/slack';
|
|
6
6
|
export { I18nContext, KnockI18nProvider, locales, type I18nContent, type KnockI18nProviderProps, type Translations, useTranslations, } from './modules/i18n';
|
|
7
7
|
export { type RecipientObject } from './interfaces';
|
|
8
8
|
export { usePreferences } from './modules/preferences';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,eAAe,EACf,KAAK,cAAc,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,2BAA2B,EAC3B,cAAc,EACd,0BAA0B,EAC1B,cAAc,EACd,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,QAAQ,EACb,0BAA0B,EAC1B,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,uBAAuB,EAC5B,QAAQ,EACR,SAAS,EACT,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,uBAAuB,EAC5B,oBAAoB,EACpB,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,2BAA2B,EAC3B,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,0BAA0B,EAC1B,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,yBAAyB,EACzB,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,OAAO,EACP,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAMvD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,eAAe,EACf,KAAK,cAAc,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,2BAA2B,EAC3B,cAAc,EACd,0BAA0B,EAC1B,cAAc,EACd,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,QAAQ,EACb,0BAA0B,EAC1B,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,uBAAuB,EAC5B,QAAQ,EACR,SAAS,EACT,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,uBAAuB,EAC5B,oBAAoB,EACpB,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,2BAA2B,EAC3B,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,0BAA0B,EAC1B,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,uBAAuB,EACvB,yBAAyB,EACzB,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,OAAO,EACP,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAMvD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -4,12 +4,19 @@ export interface UseAuthPostMessageListenerOptions {
|
|
|
4
4
|
popupWindowRef: React.MutableRefObject<Window | null>;
|
|
5
5
|
setConnectionStatus: (status: ConnectionStatus) => void;
|
|
6
6
|
onAuthenticationComplete?: (authenticationResp: string) => void;
|
|
7
|
+
/**
|
|
8
|
+
* The sessionStorage key where the CSRF nonce was stored when the auth URL
|
|
9
|
+
* was built. When provided, the listener will verify the nonce returned in
|
|
10
|
+
* the postMessage payload matches the stored value.
|
|
11
|
+
*/
|
|
12
|
+
nonceStorageKey?: string;
|
|
7
13
|
}
|
|
8
14
|
/**
|
|
9
15
|
* Hook that listens for postMessage events from OAuth popup windows.
|
|
10
16
|
*
|
|
11
17
|
* Handles "authComplete" and "authFailed" messages sent from the OAuth flow popup,
|
|
12
|
-
* validates the message origin,
|
|
18
|
+
* validates the message origin, optionally verifies the CSRF nonce, updates
|
|
19
|
+
* connection status, and closes the popup.
|
|
13
20
|
*
|
|
14
21
|
* @param options - Configuration options for the postMessage listener
|
|
15
22
|
*
|
|
@@ -20,6 +27,7 @@ export interface UseAuthPostMessageListenerOptions {
|
|
|
20
27
|
* popupWindowRef,
|
|
21
28
|
* setConnectionStatus,
|
|
22
29
|
* onAuthenticationComplete,
|
|
30
|
+
* nonceStorageKey: "knock:slack-auth-nonce:channel_123:user_1",
|
|
23
31
|
* });
|
|
24
32
|
* ```
|
|
25
33
|
*/
|
|
@@ -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;
|
|
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,CAmEN"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { default as useSlackConnectionStatus } from './useSlackConnectionStatus';
|
|
2
2
|
export { default as useSlackChannels } from './useSlackChannels';
|
|
3
3
|
export { default as useConnectedSlackChannels } from './useConnectedSlackChannels';
|
|
4
|
-
export { default as useSlackAuth } from './useSlackAuth';
|
|
4
|
+
export { default as useSlackAuth, getSlackNonceStorageKey, } from './useSlackAuth';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/modules/slack/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/modules/slack/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EACL,OAAO,IAAI,YAAY,EACvB,uBAAuB,GACxB,MAAM,gBAAgB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSlackAuth.d.ts","sourceRoot":"","sources":["../../../../../src/modules/slack/hooks/useSlackAuth.ts"],"names":[],"mappings":"AAcA,KAAK,kBAAkB,GAAG;IACxB,iBAAiB,EAAE,MAAM,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC,CAAC;AAEF,KAAK,mBAAmB,GAAG;IAEzB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAElB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B,CAAC;AAuBF,iBAAS,YAAY,CACnB,aAAa,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,mBAAmB,GAAG,MAAM,EAAE,GACvC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"useSlackAuth.d.ts","sourceRoot":"","sources":["../../../../../src/modules/slack/hooks/useSlackAuth.ts"],"names":[],"mappings":"AAcA,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED,KAAK,kBAAkB,GAAG;IACxB,iBAAiB,EAAE,MAAM,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC,CAAC;AAEF,KAAK,mBAAmB,GAAG;IAEzB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAElB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B,CAAC;AAuBF,iBAAS,YAAY,CACnB,aAAa,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,mBAAmB,GAAG,MAAM,EAAE,GACvC,kBAAkB,CAyEpB;AAED,eAAe,YAAY,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { KnockSlackProvider, useKnockSlackClient, type KnockSlackProviderState, type KnockSlackProviderProps, } from './context';
|
|
2
|
-
export { useSlackConnectionStatus, useSlackChannels, useConnectedSlackChannels, useSlackAuth, } from './hooks';
|
|
2
|
+
export { useSlackConnectionStatus, useSlackChannels, useConnectedSlackChannels, useSlackAuth, getSlackNonceStorageKey, } from './hooks';
|
|
3
3
|
export { type ContainerObject, type SlackChannelQueryOptions, } from './interfaces';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/slack/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,GAC7B,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,yBAAyB,EACzB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/slack/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,GAC7B,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,yBAAyB,EACzB,YAAY,EACZ,uBAAuB,GACxB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,wBAAwB,GAC9B,MAAM,cAAc,CAAC"}
|
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.9",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "dist/cjs/index.js",
|
|
8
8
|
"module": "dist/esm/index.mjs",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@types/react": "^19.1.8",
|
|
61
61
|
"@types/react-dom": "^19.1.6",
|
|
62
62
|
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
|
63
|
-
"@typescript-eslint/parser": "^8.
|
|
63
|
+
"@typescript-eslint/parser": "^8.58.0",
|
|
64
64
|
"@vitejs/plugin-react": "^4.5.1",
|
|
65
65
|
"babel-plugin-react-require": "^4.0.3",
|
|
66
66
|
"eslint": "^8.56.0",
|
package/src/index.ts
CHANGED
|
@@ -7,13 +7,44 @@ export interface UseAuthPostMessageListenerOptions {
|
|
|
7
7
|
popupWindowRef: React.MutableRefObject<Window | null>;
|
|
8
8
|
setConnectionStatus: (status: ConnectionStatus) => void;
|
|
9
9
|
onAuthenticationComplete?: (authenticationResp: string) => void;
|
|
10
|
+
/**
|
|
11
|
+
* The sessionStorage key where the CSRF nonce was stored when the auth URL
|
|
12
|
+
* was built. When provided, the listener will verify the nonce returned in
|
|
13
|
+
* the postMessage payload matches the stored value.
|
|
14
|
+
*/
|
|
15
|
+
nonceStorageKey?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extracts the message type from a postMessage event data payload.
|
|
20
|
+
* Supports both the legacy string format ("authComplete") and the new
|
|
21
|
+
* structured format ({ type: "authComplete", nonce: "..." }).
|
|
22
|
+
*/
|
|
23
|
+
function getMessageType(data: unknown): string {
|
|
24
|
+
if (typeof data === "object" && data !== null && "type" in data) {
|
|
25
|
+
return (data as { type: string }).type;
|
|
26
|
+
}
|
|
27
|
+
return data as string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extracts the nonce from a structured postMessage event data payload.
|
|
32
|
+
* Returns undefined for legacy string-format messages.
|
|
33
|
+
*/
|
|
34
|
+
function getMessageNonce(data: unknown): string | undefined {
|
|
35
|
+
if (typeof data === "object" && data !== null && "nonce" in data) {
|
|
36
|
+
const nonce = (data as { nonce: unknown }).nonce;
|
|
37
|
+
return typeof nonce === "string" ? nonce : undefined;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
10
40
|
}
|
|
11
41
|
|
|
12
42
|
/**
|
|
13
43
|
* Hook that listens for postMessage events from OAuth popup windows.
|
|
14
44
|
*
|
|
15
45
|
* Handles "authComplete" and "authFailed" messages sent from the OAuth flow popup,
|
|
16
|
-
* validates the message origin,
|
|
46
|
+
* validates the message origin, optionally verifies the CSRF nonce, updates
|
|
47
|
+
* connection status, and closes the popup.
|
|
17
48
|
*
|
|
18
49
|
* @param options - Configuration options for the postMessage listener
|
|
19
50
|
*
|
|
@@ -24,6 +55,7 @@ export interface UseAuthPostMessageListenerOptions {
|
|
|
24
55
|
* popupWindowRef,
|
|
25
56
|
* setConnectionStatus,
|
|
26
57
|
* onAuthenticationComplete,
|
|
58
|
+
* nonceStorageKey: "knock:slack-auth-nonce:channel_123:user_1",
|
|
27
59
|
* });
|
|
28
60
|
* ```
|
|
29
61
|
*/
|
|
@@ -35,26 +67,54 @@ export function useAuthPostMessageListener(
|
|
|
35
67
|
popupWindowRef,
|
|
36
68
|
setConnectionStatus,
|
|
37
69
|
onAuthenticationComplete,
|
|
70
|
+
nonceStorageKey,
|
|
38
71
|
} = options;
|
|
39
72
|
|
|
40
73
|
useEffect(() => {
|
|
74
|
+
const closePopup = () => {
|
|
75
|
+
if (popupWindowRef.current && !popupWindowRef.current.closed) {
|
|
76
|
+
popupWindowRef.current.close();
|
|
77
|
+
}
|
|
78
|
+
popupWindowRef.current = null;
|
|
79
|
+
};
|
|
80
|
+
|
|
41
81
|
const receiveMessage = (event: MessageEvent) => {
|
|
42
82
|
// Validate message origin for security
|
|
43
83
|
if (event.origin !== knockHost) {
|
|
44
84
|
return;
|
|
45
85
|
}
|
|
46
86
|
|
|
47
|
-
|
|
87
|
+
const messageType = getMessageType(event.data);
|
|
88
|
+
|
|
89
|
+
if (messageType === "authComplete") {
|
|
90
|
+
// Verify CSRF nonce when a nonceStorageKey is configured.
|
|
91
|
+
if (nonceStorageKey) {
|
|
92
|
+
const returnedNonce = getMessageNonce(event.data);
|
|
93
|
+
const storedNonce = sessionStorage.getItem(nonceStorageKey);
|
|
94
|
+
sessionStorage.removeItem(nonceStorageKey);
|
|
95
|
+
|
|
96
|
+
// If nonce already consumed by a prior handler invocation, then bail
|
|
97
|
+
// out from checking again.
|
|
98
|
+
if (
|
|
99
|
+
!returnedNonce ||
|
|
100
|
+
(storedNonce && storedNonce !== returnedNonce)
|
|
101
|
+
) {
|
|
102
|
+
setConnectionStatus("error");
|
|
103
|
+
onAuthenticationComplete?.("authFailed");
|
|
104
|
+
closePopup();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
48
109
|
setConnectionStatus("connected");
|
|
49
|
-
onAuthenticationComplete?.(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
110
|
+
onAuthenticationComplete?.(messageType);
|
|
111
|
+
closePopup();
|
|
112
|
+
} else if (messageType === "authFailed") {
|
|
113
|
+
if (nonceStorageKey) {
|
|
114
|
+
sessionStorage.removeItem(nonceStorageKey);
|
|
53
115
|
}
|
|
54
|
-
popupWindowRef.current = null;
|
|
55
|
-
} else if (event.data === "authFailed") {
|
|
56
116
|
setConnectionStatus("error");
|
|
57
|
-
onAuthenticationComplete?.(
|
|
117
|
+
onAuthenticationComplete?.(messageType);
|
|
58
118
|
popupWindowRef.current = null;
|
|
59
119
|
}
|
|
60
120
|
};
|
|
@@ -66,5 +126,6 @@ export function useAuthPostMessageListener(
|
|
|
66
126
|
onAuthenticationComplete,
|
|
67
127
|
setConnectionStatus,
|
|
68
128
|
popupWindowRef,
|
|
129
|
+
nonceStorageKey,
|
|
69
130
|
]);
|
|
70
131
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { default as useSlackConnectionStatus } from "./useSlackConnectionStatus";
|
|
2
2
|
export { default as useSlackChannels } from "./useSlackChannels";
|
|
3
3
|
export { default as useConnectedSlackChannels } from "./useConnectedSlackChannels";
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
default as useSlackAuth,
|
|
6
|
+
getSlackNonceStorageKey,
|
|
7
|
+
} from "./useSlackAuth";
|
|
@@ -12,6 +12,13 @@ const DEFAULT_SLACK_SCOPES = [
|
|
|
12
12
|
"groups:read",
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
+
export function getSlackNonceStorageKey(
|
|
16
|
+
channelId: string,
|
|
17
|
+
userId: string,
|
|
18
|
+
): string {
|
|
19
|
+
return `knock:slack-auth-nonce:${channelId}:${userId}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
type UseSlackAuthOutput = {
|
|
16
23
|
buildSlackAuthUrl: () => string;
|
|
17
24
|
disconnectFromSlack: () => void;
|
|
@@ -83,8 +90,15 @@ function useSlackAuth(
|
|
|
83
90
|
]);
|
|
84
91
|
|
|
85
92
|
const buildSlackAuthUrl = useCallback(() => {
|
|
93
|
+
const nonce = crypto.randomUUID();
|
|
94
|
+
sessionStorage.setItem(
|
|
95
|
+
getSlackNonceStorageKey(knockSlackChannelId, knock.userId!),
|
|
96
|
+
nonce,
|
|
97
|
+
);
|
|
98
|
+
|
|
86
99
|
const rawParams = {
|
|
87
100
|
state: JSON.stringify({
|
|
101
|
+
nonce,
|
|
88
102
|
redirect_url: redirectUrl,
|
|
89
103
|
access_token_object: {
|
|
90
104
|
object_id: tenantId,
|
|
@@ -104,6 +118,7 @@ function useSlackAuth(
|
|
|
104
118
|
tenantId,
|
|
105
119
|
knockSlackChannelId,
|
|
106
120
|
knock.apiKey,
|
|
121
|
+
knock.userId,
|
|
107
122
|
knock.userToken,
|
|
108
123
|
knock.branch,
|
|
109
124
|
slackClientId,
|