@workos-inc/widgets 1.20.0-next.1791237372282 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 1.20.0-next.1791237372282
3
+ ## 1.20.0
4
4
 
5
5
  ### Minor Changes
6
6
 
7
7
  - Prepare the sectioned `<Pipes>` and `<PipesAdmin>` experiences (`enableSharedConnections`) for multiple connections per provider. Nothing changes until multiple connections are enabled for your environment; until then each provider keeps its single connection and looks as it did before. Once enabled, a provider with several connections renders as a group with one row per connection, oldest first, each with its own status and actions. "Add connection" and "Connect another account" add one (asking for an optional name) when the API reports it available, and connections can be renamed. Repairs target the exact connection, and adds are retry-safe.
8
8
 
9
+ - Add the optional PipesAdmin `userId` prop for attributing shared API-key and client-credentials writes with application credential tokens. User sessions send their authenticated user automatically. Block missing actors and mismatched session actors with configuration guidance instead of actor-entry UI. Application-token hosts must supply an acting WorkOS user before upgrading; publish only after the API accepts attribution.
10
+
9
11
  - Surface data integration connection failures in the Pipes widget
10
12
 
11
13
  When an OAuth handoff popup fails or closes without completing, the widget now
package/README.md CHANGED
@@ -3,3 +3,47 @@
3
3
  A suite of React components providing functionality for enterprise app workflows.
4
4
 
5
5
  [Read the documentation](https://workos.com/docs/widgets)
6
+
7
+ ## Shared Pipes credentials
8
+
9
+ `PipesAdmin` defaults to provider settings only. Opt in to shared connections
10
+ with `enableSharedConnections`. User-backed widget sessions and AuthKit user
11
+ access tokens attribute shared API-key and client-credentials writes to the
12
+ signed-in user without any new host prop.
13
+
14
+ Hosts using an application credential token must pass the acting person's
15
+ WorkOS user ID programmatically when connecting, rotating, or repairing shared
16
+ credentials:
17
+
18
+ ```tsx
19
+ <PipesAdmin
20
+ authToken={applicationCredentialToken}
21
+ enableSharedConnections
22
+ userId={authenticatedWorkOsUser.id}
23
+ />
24
+ ```
25
+
26
+ Source `authenticatedWorkOsUser.id` from the host's authenticated user context
27
+ or an existing mapping to that WorkOS user (`user_...`), not an arbitrary
28
+ application-local ID or the credential token's subject. `userId` is an optional
29
+ React prop, never a rendered field or member picker. A missing actor blocks
30
+ these shared credential writes with configuration guidance, while reads,
31
+ provider configuration, and OAuth flows remain available. User-session hosts
32
+ should omit the prop; a different ID is rejected. Standalone widget sessions
33
+ cannot supply an actor through this prop.
34
+
35
+ An authorized application caller asserts whom it acts on behalf of. Passing
36
+ `userId` does not authenticate that person or grant write access: the API still
37
+ validates permissions, identity, and active membership of the token-scoped
38
+ organization. If there is no acting person, do not fabricate one; machine-managed
39
+ credentials require separate policy support.
40
+
41
+ ### Upgrade and rollout
42
+
43
+ Before upgrading application-token hosts, supply `userId` from their authenticated
44
+ user context for shared credential actions. User-session hosts require no changes.
45
+ Publish this widget change only after the deployed Widgets GraphQL API accepts
46
+ `userId` on both organization credential inputs (PVIT-1079). Subsequent server-side
47
+ required-actor enforcement (PVIT-1081) also requires operator confirmation of the
48
+ affected hosts' upgrade and communication plan; publication alone does not prove
49
+ adoption.
@@ -1,9 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/api/claims.ts
3
+ function getAuthenticatedUserId(claims) {
4
+ return (claims.sub_profile === void 0 || claims.sub_profile === "user") && (claims.sub_type === "authkit" || claims.sub_type === void 0 && !!claims.sid && typeof claims.auth_time === "number") && claims.sub?.startsWith("user_") ? claims.sub : void 0;
5
+ }
3
6
  function getClaims(accessToken) {
4
7
  return JSON.parse(atob(accessToken.split(".")[1]));
5
8
  }
6
9
  //#endregion
10
+ exports.getAuthenticatedUserId = getAuthenticatedUserId;
7
11
  exports.getClaims = getClaims;
8
12
 
9
13
  //# sourceMappingURL=claims.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"claims.cjs","names":[],"sources":["../../../src/api/claims.ts"],"sourcesContent":["interface Claims {\n sid: string;\n permissions?: string[];\n exp?: number;\n}\n\nexport function getClaims(accessToken: string): Claims {\n return JSON.parse(atob(accessToken.split(\".\")[1]));\n}\n"],"mappings":";;AAMA,SAAgB,UAAU,aAA6B;CACrD,OAAO,KAAK,MAAM,KAAK,YAAY,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;AACnD"}
1
+ {"version":3,"file":"claims.cjs","names":[],"sources":["../../../src/api/claims.ts"],"sourcesContent":["interface Claims {\n sid?: string;\n sub?: string;\n sub_type?: string;\n sub_profile?: string;\n auth_time?: number;\n permissions?: string[];\n exp?: number;\n}\n\nexport function getAuthenticatedUserId(claims: Claims): string | undefined {\n const isUserSession =\n (claims.sub_profile === undefined || claims.sub_profile === \"user\") &&\n (claims.sub_type === \"authkit\" ||\n (claims.sub_type === undefined &&\n !!claims.sid &&\n typeof claims.auth_time === \"number\"));\n\n return isUserSession && claims.sub?.startsWith(\"user_\")\n ? claims.sub\n : undefined;\n}\n\nexport function getClaims(accessToken: string): Claims {\n return JSON.parse(atob(accessToken.split(\".\")[1]));\n}\n"],"mappings":";;AAUA,SAAgB,uBAAuB,QAAoC;CAQzE,QANG,OAAO,gBAAgB,UAAa,OAAO,gBAAgB,YAC3D,OAAO,aAAa,aAClB,OAAO,aAAa,UACnB,CAAC,CAAC,OAAO,OACT,OAAO,OAAO,cAAc,aAEV,OAAO,KAAK,WAAW,OAAO,IAClD,OAAO,MACP;AACN;AAEA,SAAgB,UAAU,aAA6B;CACrD,OAAO,KAAK,MAAM,KAAK,YAAY,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;AACnD"}
@@ -1,9 +1,14 @@
1
1
  //#region src/api/claims.d.ts
2
2
  interface Claims {
3
- sid: string;
3
+ sid?: string;
4
+ sub?: string;
5
+ sub_type?: string;
6
+ sub_profile?: string;
7
+ auth_time?: number;
4
8
  permissions?: string[];
5
9
  exp?: number;
6
10
  }
11
+ export declare function getAuthenticatedUserId(claims: Claims): string | undefined;
7
12
  export declare function getClaims(accessToken: string): Claims;
8
13
  //#endregion
9
14
  //# sourceMappingURL=claims.d.cts.map
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_lib_i18n_use_translation = require("./i18n/use-translation.cjs");
4
+ const require_api_claims = require("../api/claims.cjs");
5
+ const require_api_api_provider = require("../api/api-provider.cjs");
6
+ let react = require("react");
7
+ //#region src/lib/pipes-credential-attribution.tsx
8
+ const PipesCredentialUserIdContext = (0, react.createContext)(void 0);
9
+ function usePipesCredentialAttribution() {
10
+ const hostUserId = (0, react.useContext)(PipesCredentialUserIdContext)?.trim();
11
+ const { authToken } = require_api_api_provider.useApi();
12
+ const translate = require_lib_i18n_use_translation.useTranslation();
13
+ const claims = authToken ? require_api_claims.getClaims(authToken) : void 0;
14
+ const sessionUserId = claims && require_api_claims.getAuthenticatedUserId(claims);
15
+ const userId = sessionUserId ?? (claims?.sub_type === "client_api_credential" && hostUserId?.startsWith("user_") ? hostUserId : void 0);
16
+ return {
17
+ userId,
18
+ error: sessionUserId && hostUserId && hostUserId !== sessionUserId ? translate({
19
+ defaultMessage: "Shared credentials must be attributed to the signed-in user. The application must omit PipesAdmin's userId prop or pass the signed-in user's WorkOS user ID.",
20
+ description: "Configuration error when the host-supplied PipesAdmin user ID differs from the authenticated user",
21
+ id: "pipesCredentialUserMismatch"
22
+ }) : !userId ? translate({
23
+ defaultMessage: "Shared credentials require an acting WorkOS user. The application must use a user-backed session or supply the acting person's WorkOS user ID through PipesAdmin's userId prop when using an application credential token.",
24
+ description: "Configuration guidance for shared credential writes without an acting user; no user ID entry is requested",
25
+ id: "pipesCredentialUserRequired"
26
+ }) : void 0
27
+ };
28
+ }
29
+ //#endregion
30
+ exports.PipesCredentialUserIdContext = PipesCredentialUserIdContext;
31
+ exports.usePipesCredentialAttribution = usePipesCredentialAttribution;
32
+
33
+ //# sourceMappingURL=pipes-credential-attribution.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipes-credential-attribution.cjs","names":["createContext","useContext","useApi","useTranslation","getClaims","getAuthenticatedUserId"],"sources":["../../../src/lib/pipes-credential-attribution.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport { useApi } from \"../api/api-provider.js\";\nimport { getAuthenticatedUserId, getClaims } from \"../api/claims.js\";\nimport { useTranslation } from \"./i18n/use-translation.js\";\n\nexport const PipesCredentialUserIdContext = createContext<string | undefined>(\n undefined,\n);\n\nexport function usePipesCredentialAttribution() {\n const hostUserId = useContext(PipesCredentialUserIdContext)?.trim();\n const { authToken } = useApi();\n const translate = useTranslation();\n const claims = authToken ? getClaims(authToken) : undefined;\n const sessionUserId = claims && getAuthenticatedUserId(claims);\n const userId =\n sessionUserId ??\n (claims?.sub_type === \"client_api_credential\" &&\n hostUserId?.startsWith(\"user_\")\n ? hostUserId\n : undefined);\n\n const error =\n sessionUserId && hostUserId && hostUserId !== sessionUserId\n ? translate({\n defaultMessage:\n \"Shared credentials must be attributed to the signed-in user. The application must omit PipesAdmin's userId prop or pass the signed-in user's WorkOS user ID.\",\n description:\n \"Configuration error when the host-supplied PipesAdmin user ID differs from the authenticated user\",\n id: \"pipesCredentialUserMismatch\",\n })\n : !userId\n ? translate({\n defaultMessage:\n \"Shared credentials require an acting WorkOS user. The application must use a user-backed session or supply the acting person's WorkOS user ID through PipesAdmin's userId prop when using an application credential token.\",\n description:\n \"Configuration guidance for shared credential writes without an acting user; no user ID entry is requested\",\n id: \"pipesCredentialUserRequired\",\n })\n : undefined;\n\n return { userId, error };\n}\n"],"mappings":";;;;;;;AAOA,MAAa,gCAAA,GAA+BA,MAAAA,cAAAA,CAC1C,MACF;AAEA,SAAgB,gCAAgC;CAC9C,MAAM,cAAA,GAAaC,MAAAA,WAAAA,CAAW,4BAA4B,CAAC,EAAE,KAAK;CAClE,MAAM,EAAE,cAAcC,yBAAAA,OAAO;CAC7B,MAAM,YAAYC,iCAAAA,eAAe;CACjC,MAAM,SAAS,YAAYC,mBAAAA,UAAU,SAAS,IAAI;CAClD,MAAM,gBAAgB,UAAUC,mBAAAA,uBAAuB,MAAM;CAC7D,MAAM,SACJ,kBACC,QAAQ,aAAa,2BACtB,YAAY,WAAW,OAAO,IAC1B,aACA;CAqBN,OAAO;EAAE;EAAQ,OAlBf,iBAAiB,cAAc,eAAe,gBAC1C,UAAU;GACR,gBACE;GACF,aACE;GACF,IAAI;EACN,CAAC,IACD,CAAC,SACC,UAAU;GACR,gBACE;GACF,aACE;GACF,IAAI;EACN,CAAC,IACD;CAEe;AACzB"}
@@ -0,0 +1,8 @@
1
+ //#region src/lib/pipes-credential-attribution.d.ts
2
+ export declare const PipesCredentialUserIdContext: import("react").Context<string | undefined>;
3
+ export declare function usePipesCredentialAttribution(): {
4
+ userId: string | undefined;
5
+ error: string | undefined;
6
+ };
7
+ //#endregion
8
+ //# sourceMappingURL=pipes-credential-attribution.d.cts.map
@@ -19,6 +19,7 @@ const require_lib_data_integration_icon = require("./data-integration-icon.cjs")
19
19
  const require_lib_api_pipes = require("./api/pipes.cjs");
20
20
  const require_lib_pipes_connections = require("./pipes-connections.cjs");
21
21
  const require_lib_redacted_secret = require("./redacted-secret.cjs");
22
+ const require_lib_pipes_credential_attribution = require("./pipes-credential-attribution.cjs");
22
23
  let react = require("react");
23
24
  let _radix_ui_themes = require("@radix-ui/themes");
24
25
  let react_jsx_runtime = require("react/jsx-runtime");
@@ -748,6 +749,8 @@ const API_KEY_REDACTED_PREFIX = "••••";
748
749
  */
749
750
  function ApiKeyDialog({ integration, installation = integration.installation, owner = "user", target = require_lib_pipes_connections.getConnectionTarget(installation), open, onOpenChange, onSuccess, collectName = false }) {
750
751
  const invalidateConnections = require_lib_api_pipes.useInvalidateConnections(owner);
752
+ const attribution = require_lib_pipes_credential_attribution.usePipesCredentialAttribution();
753
+ const attributionError = owner === "organization" ? attribution.error : void 0;
751
754
  const [name, setName] = (0, react.useState)("");
752
755
  const isRotation = installation?.auth_method === "api_key" && installation.state === "connected";
753
756
  const last4 = installation?.api_key_last_4;
@@ -779,8 +782,13 @@ function ApiKeyDialog({ integration, installation = integration.installation, ow
779
782
  ...require_lib_pipes_connections.toCreationTargetInput(target),
780
783
  ...collectName ? require_lib_pipes_connections.toAccountDisplayNameInput(name) : {}
781
784
  };
782
- if (owner === "organization") organizationUpsert.mutate(input);
783
- else userUpsert.mutate(input);
785
+ if (owner === "organization") {
786
+ if (attributionError || !attribution.userId) return;
787
+ organizationUpsert.mutate({
788
+ ...input,
789
+ userId: attribution.userId
790
+ });
791
+ } else userUpsert.mutate(input);
784
792
  };
785
793
  (0, react.useEffect)(() => {
786
794
  reset();
@@ -816,11 +824,11 @@ function ApiKeyDialog({ integration, installation = integration.installation, ow
816
824
  mt: "1",
817
825
  children: integration.description
818
826
  }),
819
- error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_themes.Callout.Root, {
827
+ (attributionError || error) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_themes.Callout.Root, {
820
828
  my: "3",
821
829
  color: "red",
822
830
  role: "alert",
823
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_themes.Callout.Text, { children: errorMessage ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_i18n_translation.Translation, {
831
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_themes.Callout.Text, { children: attributionError ?? errorMessage ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_i18n_translation.Translation, {
824
832
  defaultMessage: "An error occurred. Please check the value and try again.",
825
833
  id: "bYLbLL",
826
834
  description: "Generic error message for API key submission failure"
@@ -882,7 +890,7 @@ function ApiKeyDialog({ integration, installation = integration.installation, ow
882
890
  })
883
891
  }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_elements.Button, {
884
892
  type: "button",
885
- disabled: isPending || !secret.trim(),
893
+ disabled: !!attributionError || isPending || !secret.trim(),
886
894
  loading: isPending,
887
895
  onClick: () => upsertApiKey(secret.trim()),
888
896
  children: isRotation ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_i18n_translation.Translation, {
@@ -933,6 +941,8 @@ function ApiKeyConnectButton({ integration }) {
933
941
  */
934
942
  function ClientCredentialsDialog({ integration, installation = integration.installation, owner = "user", target = require_lib_pipes_connections.getConnectionTarget(installation), fields, open, onOpenChange, onSuccess, collectName = false }) {
935
943
  const invalidateConnections = require_lib_api_pipes.useInvalidateConnections(owner);
944
+ const attribution = require_lib_pipes_credential_attribution.usePipesCredentialAttribution();
945
+ const attributionError = owner === "organization" ? attribution.error : void 0;
936
946
  const [name, setName] = (0, react.useState)("");
937
947
  const isUpdate = installation?.auth_method === "client_credentials";
938
948
  const storedConfig = (0, react.useMemo)(() => isUpdate ? installation?.config ?? {} : {}, [isUpdate, installation?.config]);
@@ -975,6 +985,7 @@ function ClientCredentialsDialog({ integration, installation = integration.insta
975
985
  const { error, isPending } = owner === "organization" ? organizationUpsert : userUpsert;
976
986
  const errorMessage = error instanceof require_api_errors.ApiError && error.message ? error.message : null;
977
987
  const handleSubmit = () => {
988
+ if (attributionError) return;
978
989
  if (!configForm.validate()) return;
979
990
  const config = configForm.isDirty ? configForm.buildConfigPayload(storedConfig) : void 0;
980
991
  const input = {
@@ -985,8 +996,13 @@ function ClientCredentialsDialog({ integration, installation = integration.insta
985
996
  ...require_lib_pipes_connections.toCreationTargetInput(target),
986
997
  ...collectName ? require_lib_pipes_connections.toAccountDisplayNameInput(name) : {}
987
998
  };
988
- if (owner === "organization") organizationUpsert.mutate(input);
989
- else userUpsert.mutate(input);
999
+ if (owner === "organization") {
1000
+ if (!attribution.userId) return;
1001
+ organizationUpsert.mutate({
1002
+ ...input,
1003
+ userId: attribution.userId
1004
+ });
1005
+ } else userUpsert.mutate(input);
990
1006
  };
991
1007
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_themes_components_dialog.Root, {
992
1008
  open,
@@ -1009,11 +1025,11 @@ function ClientCredentialsDialog({ integration, installation = integration.insta
1009
1025
  values: { integrationName: integration.name }
1010
1026
  })
1011
1027
  }),
1012
- error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_themes.Callout.Root, {
1028
+ (attributionError || error) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_themes.Callout.Root, {
1013
1029
  my: "3",
1014
1030
  color: "red",
1015
1031
  role: "alert",
1016
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_themes.Callout.Text, { children: errorMessage ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_i18n_translation.Translation, {
1032
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_themes.Callout.Text, { children: attributionError ?? errorMessage ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_i18n_translation.Translation, {
1017
1033
  defaultMessage: "An error occurred. Please check the value and try again.",
1018
1034
  id: "bYLbLL",
1019
1035
  description: "Generic error message for API key submission failure"
@@ -1109,7 +1125,7 @@ function ClientCredentialsDialog({ integration, installation = integration.insta
1109
1125
  })
1110
1126
  }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_elements.Button, {
1111
1127
  type: "button",
1112
- disabled: isPending || !clientId.trim() || secretMissing || configForm.hasMissingRequired,
1128
+ disabled: !!attributionError || isPending || !clientId.trim() || secretMissing || configForm.hasMissingRequired,
1113
1129
  loading: isPending,
1114
1130
  onClick: handleSubmit,
1115
1131
  children: isUpdate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_i18n_translation.Translation, {