@camstack/addon-auth 1.2.137 → 1.2.140

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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-C3Gd6eZD.js");
5
+ const require_dist = require("../dist-CiG_dP05.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  node_crypto = require_dist.__toESM(node_crypto);
8
8
  //#region ../../node_modules/jose/dist/webapi/lib/buffer_utils.js
@@ -704,6 +704,188 @@ function createRemoteJWKSet(url, options) {
704
704
  });
705
705
  }
706
706
  //#endregion
707
+ //#region src/oidc/oidc-connection-schema.ts
708
+ var DEFAULT_OIDC_ICON = "shield-check";
709
+ var OIDC_CONNECTION_FORM_SCHEMA = { sections: [{
710
+ id: "identity",
711
+ title: "Identity",
712
+ columns: 2,
713
+ fields: [{
714
+ type: "text",
715
+ key: "displayName",
716
+ label: "Display name",
717
+ description: "Shown on the login button.",
718
+ default: "OpenID Connect"
719
+ }, {
720
+ type: "icon",
721
+ key: "icon",
722
+ label: "Icon",
723
+ description: "Shown on the login button.",
724
+ default: DEFAULT_OIDC_ICON
725
+ }]
726
+ }, {
727
+ id: "idp",
728
+ title: "Identity provider",
729
+ columns: 2,
730
+ fields: [
731
+ {
732
+ type: "text",
733
+ key: "issuerUrl",
734
+ label: "Issuer URL",
735
+ description: "IdP base URL — discovery doc lives at <issuer>/.well-known/openid-configuration.",
736
+ default: ""
737
+ },
738
+ {
739
+ type: "text",
740
+ key: "clientId",
741
+ label: "Client ID",
742
+ default: ""
743
+ },
744
+ {
745
+ type: "password",
746
+ key: "clientSecret",
747
+ label: "Client Secret",
748
+ showToggle: true,
749
+ default: ""
750
+ },
751
+ {
752
+ type: "text",
753
+ key: "redirectUri",
754
+ label: "Redirect URI override",
755
+ description: "Optional — leave empty. Empty: a sign-in returns to the hub address it was started from (LAN, mesh or public, as listed on the Network page). Set: pins that one callback.",
756
+ default: ""
757
+ },
758
+ {
759
+ type: "text",
760
+ key: "scopes",
761
+ label: "Scopes",
762
+ description: "Space-separated.",
763
+ default: "openid profile email"
764
+ },
765
+ {
766
+ type: "select",
767
+ key: "usernameClaim",
768
+ label: "Username claim",
769
+ default: "preferred_username",
770
+ options: [
771
+ {
772
+ value: "preferred_username",
773
+ label: "preferred_username"
774
+ },
775
+ {
776
+ value: "email",
777
+ label: "email"
778
+ },
779
+ {
780
+ value: "sub",
781
+ label: "sub (user id)"
782
+ }
783
+ ]
784
+ }
785
+ ]
786
+ }] };
787
+ //#endregion
788
+ //#region src/oidc/oidc-connections.ts
789
+ /**
790
+ * Pure helpers for OIDC connection rows in `config.providers`.
791
+ */
792
+ var INSTANCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
793
+ var DEFAULT_OIDC_PROVIDER = {
794
+ id: "default",
795
+ displayName: "OpenID Connect",
796
+ icon: "shield-check",
797
+ issuerUrl: "",
798
+ clientId: "",
799
+ clientSecret: "",
800
+ redirectUri: "",
801
+ scopes: "openid profile email",
802
+ usernameClaim: "preferred_username"
803
+ };
804
+ function findOidcProvider(config, id) {
805
+ return config.providers.find((p) => p.id === id) ?? null;
806
+ }
807
+ function connectionConfigured(row) {
808
+ const secret = typeof row.clientSecret === "string" ? row.clientSecret.trim() : "";
809
+ return row.clientId.trim() !== "" && secret !== "";
810
+ }
811
+ function clientSecretView(clientSecret, secrets, connectionId) {
812
+ if (require_dist.isSecretMarker(clientSecret)) return require_dist.toSecretFieldView(clientSecret);
813
+ const plain = typeof clientSecret === "string" ? clientSecret.trim() : "";
814
+ if (plain === "") return null;
815
+ if (secrets === void 0) return require_dist.toSecretFieldView(plain);
816
+ return {
817
+ $secret: secrets.ref(require_dist.globalSecretScope(), require_dist.arrayItemFieldPath("providers", connectionId, "clientSecret")),
818
+ v: 1,
819
+ set: true
820
+ };
821
+ }
822
+ function createOidcConnection(config, input) {
823
+ const id = input.id.trim();
824
+ if (!INSTANCE_ID_PATTERN.test(id)) return {
825
+ ok: false,
826
+ reason: "invalid-id"
827
+ };
828
+ if (config.providers.some((p) => p.id === id)) return {
829
+ ok: false,
830
+ reason: "duplicate-id"
831
+ };
832
+ const row = {
833
+ ...DEFAULT_OIDC_PROVIDER,
834
+ id,
835
+ displayName: input.displayName.trim() || DEFAULT_OIDC_PROVIDER.displayName
836
+ };
837
+ return {
838
+ ok: true,
839
+ id,
840
+ config: { providers: [...config.providers, row] }
841
+ };
842
+ }
843
+ function updateOidcConnection(config, id, patch) {
844
+ const idx = config.providers.findIndex((p) => p.id === id);
845
+ if (idx < 0) return {
846
+ ok: false,
847
+ reason: "not-found"
848
+ };
849
+ const prev = config.providers[idx];
850
+ if (prev === void 0) return {
851
+ ok: false,
852
+ reason: "not-found"
853
+ };
854
+ const next = {
855
+ ...prev,
856
+ displayName: patch.displayName.trim() || prev.displayName,
857
+ ...patch.icon !== void 0 ? { icon: patch.icon } : {},
858
+ ...patch.issuerUrl !== void 0 ? { issuerUrl: patch.issuerUrl } : {},
859
+ ...patch.clientId !== void 0 ? { clientId: patch.clientId } : {},
860
+ ...patch.clientSecret !== void 0 ? { clientSecret: patch.clientSecret } : {},
861
+ ...patch.redirectUri !== void 0 ? { redirectUri: patch.redirectUri } : {},
862
+ ...patch.scopes !== void 0 ? { scopes: patch.scopes } : {},
863
+ ...patch.usernameClaim !== void 0 ? { usernameClaim: patch.usernameClaim } : {}
864
+ };
865
+ return {
866
+ ok: true,
867
+ config: { providers: config.providers.map((p, i) => i === idx ? next : p) },
868
+ clearClientSecret: patch.clearClientSecret === true
869
+ };
870
+ }
871
+ function deleteOidcConnection(config, id) {
872
+ if (!config.providers.some((p) => p.id === id)) return {
873
+ ok: false,
874
+ reason: "not-found"
875
+ };
876
+ return {
877
+ ok: true,
878
+ config: { providers: config.providers.filter((p) => p.id !== id) }
879
+ };
880
+ }
881
+ /** Map a wire `clientSecret` field into an update patch fragment. */
882
+ function clientSecretPatchOf(value) {
883
+ if (value === void 0) return {};
884
+ if (require_dist.isSecretClear(value)) return { clearClientSecret: true };
885
+ if (value === "") return {};
886
+ return { clientSecret: value };
887
+ }
888
+ //#endregion
707
889
  //#region src/oidc/request-origin.ts
708
890
  /** host[:port] — a DNS name, IPv4 or bracketed IPv6. No userinfo, path or spaces. */
709
891
  var AUTHORITY_RE = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::\d{1,5})?$/i;
@@ -797,6 +979,8 @@ function pickRequestOrigin(headers, allowed) {
797
979
  * `/<providerId>/callback` per configured provider. The handler
798
980
  * reads `:providerId` from the route params, picks the matching
799
981
  * config entry, and runs the OIDC flow against THAT IdP.
982
+ * - `oidc-connections` (singleton) — list / create / update / delete for
983
+ * IdP rows; Settings UI is DataTable + dialog (no editable-array).
800
984
  *
801
985
  * Each provider entry independently performs:
802
986
  * - PKCE S256 + nonce
@@ -819,17 +1003,7 @@ function pickRequestOrigin(headers, allowed) {
819
1003
  * - Refresh token rotation (offline_access).
820
1004
  * - Persist state + nonce + PKCE across restart (multi-replica).
821
1005
  */
822
- var DEFAULT_PROVIDER = {
823
- id: "default",
824
- displayName: "OpenID Connect",
825
- icon: "shield-check",
826
- issuerUrl: "",
827
- clientId: "",
828
- clientSecret: "",
829
- redirectUri: "",
830
- scopes: "openid profile email",
831
- usernameClaim: "preferred_username"
832
- };
1006
+ var DEFAULT_PROVIDER = DEFAULT_OIDC_PROVIDER;
833
1007
  var DEFAULT_CONFIG = { providers: [] };
834
1008
  var STATE_TTL_MS = 5 * 6e4;
835
1009
  /**
@@ -838,12 +1012,6 @@ var STATE_TTL_MS = 5 * 6e4;
838
1012
  * so a junk value is refused here rather than carried into a signed bridge.
839
1013
  */
840
1014
  var LINK_INTENT_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
841
- /**
842
- * An instance id appears in URLs and in the identity `oidc:<id>:<sub>`; a `:`
843
- * would make two identities read the same, so ids are slugs. A configured row
844
- * whose id is not one is skipped.
845
- */
846
- var INSTANCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
847
1015
  /** Bound on in-flight logins: `/start` is public, so a flood must not grow the map. */
848
1016
  var MAX_PENDING_STATES = 1e3;
849
1017
  /**
@@ -865,9 +1033,14 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
865
1033
  /** Pending OAuth states across ALL providers. The pending entry
866
1034
  * carries `providerId` so the /callback handler picks the right config. */
867
1035
  pendingStates = /* @__PURE__ */ new Map();
1036
+ /** Keychain path for each IdP's client secret (D787 / D828 sibling). */
1037
+ secretStatePaths = ["providers[id].clientSecret"];
868
1038
  constructor() {
869
1039
  super({ ...DEFAULT_CONFIG });
870
1040
  }
1041
+ async onConfigChanged() {
1042
+ this.rebuildInstances();
1043
+ }
871
1044
  async onInitialize() {
872
1045
  this.rebuildInstances();
873
1046
  const authProvider = {
@@ -914,9 +1087,84 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
914
1087
  {
915
1088
  capability: require_dist.loginMethodCapability,
916
1089
  provider: loginMethodProvider
1090
+ },
1091
+ {
1092
+ capability: require_dist.oidcConnectionsCapability,
1093
+ provider: this.connectionsProvider()
917
1094
  }
918
1095
  ];
919
1096
  }
1097
+ connectionsProvider() {
1098
+ return {
1099
+ connectionFormSchema: async () => OIDC_CONNECTION_FORM_SCHEMA,
1100
+ listConnections: async () => {
1101
+ const secrets = this.ctx.secrets;
1102
+ return this.config.providers.map((row) => {
1103
+ const merged = {
1104
+ ...DEFAULT_PROVIDER,
1105
+ ...row
1106
+ };
1107
+ return {
1108
+ id: merged.id,
1109
+ displayName: merged.displayName,
1110
+ icon: require_dist.isDisplayIconKey(merged.icon) ? merged.icon : DEFAULT_OIDC_ICON,
1111
+ issuerUrl: merged.issuerUrl,
1112
+ clientId: merged.clientId,
1113
+ configured: connectionConfigured(merged),
1114
+ formValues: {
1115
+ displayName: merged.displayName,
1116
+ icon: merged.icon,
1117
+ issuerUrl: merged.issuerUrl,
1118
+ clientId: merged.clientId,
1119
+ clientSecret: clientSecretView(merged.clientSecret, secrets, merged.id) ?? "",
1120
+ redirectUri: merged.redirectUri,
1121
+ scopes: merged.scopes,
1122
+ usernameClaim: merged.usernameClaim
1123
+ }
1124
+ };
1125
+ });
1126
+ },
1127
+ createConnection: async (input) => {
1128
+ const result = createOidcConnection(this.config, input);
1129
+ if (!result.ok) throw new Error(result.reason === "duplicate-id" ? `An OIDC connection with id "${input.id}" already exists` : `Invalid connection id "${input.id}" — use a lowercase slug`);
1130
+ await this.updateGlobalSettings(result.config);
1131
+ this.rebuildInstances();
1132
+ return { id: result.id };
1133
+ },
1134
+ updateConnection: async (input) => {
1135
+ const secretPatch = clientSecretPatchOf(input.clientSecret);
1136
+ const result = updateOidcConnection(this.config, input.id, {
1137
+ displayName: input.displayName,
1138
+ ...input.icon !== void 0 ? { icon: input.icon } : {},
1139
+ ...input.issuerUrl !== void 0 ? { issuerUrl: input.issuerUrl } : {},
1140
+ ...input.clientId !== void 0 ? { clientId: input.clientId } : {},
1141
+ ...secretPatch,
1142
+ ...input.redirectUri !== void 0 ? { redirectUri: input.redirectUri } : {},
1143
+ ...input.scopes !== void 0 ? { scopes: input.scopes } : {},
1144
+ ...input.usernameClaim !== void 0 ? { usernameClaim: input.usernameClaim } : {}
1145
+ });
1146
+ if (!result.ok) throw new Error(`Unknown OIDC connection "${input.id}"`);
1147
+ if (result.clearClientSecret) {
1148
+ const cleared = result.config.providers.map((p) => p.id === input.id ? {
1149
+ ...p,
1150
+ clientSecret: require_dist.SECRET_CLEAR
1151
+ } : p);
1152
+ await this.updateGlobalSettings({ providers: cleared });
1153
+ } else await this.updateGlobalSettings(result.config);
1154
+ this.rebuildInstances();
1155
+ return { ok: true };
1156
+ },
1157
+ deleteConnection: async (input) => {
1158
+ if (findOidcProvider(this.config, input.id) === null) throw new Error(`Unknown OIDC connection "${input.id}"`);
1159
+ const result = deleteOidcConnection(this.config, input.id);
1160
+ if (!result.ok) throw new Error(`Unknown OIDC connection "${input.id}"`);
1161
+ await this.updateGlobalSettings(result.config);
1162
+ await this.clearSecrets([`providers[${input.id}].clientSecret`]);
1163
+ this.rebuildInstances();
1164
+ return { ok: true };
1165
+ }
1166
+ };
1167
+ }
920
1168
  /**
921
1169
  * Build one `redirect` login-method per configured provider instance.
922
1170
  * `startUrl` is the addon's public `/start` route for that instance;
@@ -941,92 +1189,9 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
941
1189
  globalSettingsSchema() {
942
1190
  return this.schema({ sections: [{
943
1191
  id: "oidc",
944
- title: "OIDC Providers",
945
- description: "Configure one or more IdPs (Google, Microsoft, Okta, Keycloak, …). Each entry produces an independent login button. The redirect URI shown on the IdP-side must include the provider id in the path: `<origin>/addon/auth-oidc/<id>/callback`.",
946
- columns: 1,
947
- fields: [this.field({
948
- type: "editable-array",
949
- key: "providers",
950
- label: "Providers",
951
- description: "Each row is a distinct OIDC IdP. Click + to add.",
952
- default: [],
953
- itemFields: [
954
- {
955
- type: "text",
956
- key: "id",
957
- label: "Instance id",
958
- description: "URL-friendly slug — appears in the redirect URI as /addon/auth-oidc/<id>/callback. Stable; renaming breaks any IdP-side registrations.",
959
- default: ""
960
- },
961
- {
962
- type: "text",
963
- key: "displayName",
964
- label: "Display name",
965
- description: "Shown on the login button.",
966
- default: "OpenID Connect"
967
- },
968
- {
969
- type: "icon",
970
- key: "icon",
971
- label: "Icon",
972
- description: "Shown on the login button.",
973
- default: DEFAULT_PROVIDER.icon
974
- },
975
- {
976
- type: "text",
977
- key: "issuerUrl",
978
- label: "Issuer URL",
979
- description: "IdP base URL — discovery doc lives at <issuer>/.well-known/openid-configuration.",
980
- default: ""
981
- },
982
- {
983
- type: "text",
984
- key: "clientId",
985
- label: "Client ID",
986
- default: ""
987
- },
988
- {
989
- type: "password",
990
- key: "clientSecret",
991
- label: "Client Secret",
992
- default: ""
993
- },
994
- {
995
- type: "text",
996
- key: "redirectUri",
997
- label: "Redirect URI override",
998
- description: "Optional — leave empty. Empty: a sign-in returns to the hub address it was started from (LAN, mesh or public, as listed on the Network page), so register one callback per address you sign in from at the IdP. Set: it PINS that one callback — every sign-in returns to its address, wherever it started.",
999
- default: ""
1000
- },
1001
- {
1002
- type: "text",
1003
- key: "scopes",
1004
- label: "Scopes",
1005
- description: "Space-separated.",
1006
- default: "openid profile email"
1007
- },
1008
- {
1009
- type: "select",
1010
- key: "usernameClaim",
1011
- label: "Username claim",
1012
- default: "preferred_username",
1013
- options: [
1014
- {
1015
- value: "preferred_username",
1016
- label: "preferred_username"
1017
- },
1018
- {
1019
- value: "email",
1020
- label: "email"
1021
- },
1022
- {
1023
- value: "sub",
1024
- label: "sub (user id)"
1025
- }
1026
- ]
1027
- }
1028
- ]
1029
- })]
1192
+ title: "OpenID Connect",
1193
+ description: "Manage IdP connections in this page’s Settings table (Add / Edit). Each connection’s redirect URI is `<origin>/addon/auth-oidc/<id>/callback`. Credentials stay in the CamStack keychain.",
1194
+ fields: []
1030
1195
  }] });
1031
1196
  }
1032
1197
  rebuildInstances() {