@stigmer/react 0.0.89 → 0.0.90

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.
Files changed (128) hide show
  1. package/identity-provider/CreateIdentityProviderForm.d.ts.map +1 -1
  2. package/identity-provider/CreateIdentityProviderForm.js +60 -2
  3. package/identity-provider/CreateIdentityProviderForm.js.map +1 -1
  4. package/identity-provider/IdentityProviderDetailPanel.d.ts.map +1 -1
  5. package/identity-provider/IdentityProviderDetailPanel.js +87 -4
  6. package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
  7. package/identity-provider/IdentityProviderListPanel.js +5 -3
  8. package/identity-provider/IdentityProviderListPanel.js.map +1 -1
  9. package/identity-provider/IdentityProviderWizard.d.ts.map +1 -1
  10. package/identity-provider/IdentityProviderWizard.js +59 -4
  11. package/identity-provider/IdentityProviderWizard.js.map +1 -1
  12. package/index.d.ts +2 -0
  13. package/index.d.ts.map +1 -1
  14. package/index.js +2 -0
  15. package/index.js.map +1 -1
  16. package/package.json +7 -7
  17. package/platform-client/CreatePlatformClientForm.d.ts +42 -0
  18. package/platform-client/CreatePlatformClientForm.d.ts.map +1 -0
  19. package/platform-client/CreatePlatformClientForm.js +148 -0
  20. package/platform-client/CreatePlatformClientForm.js.map +1 -0
  21. package/platform-client/PlatformClientDetailPanel.d.ts +51 -0
  22. package/platform-client/PlatformClientDetailPanel.d.ts.map +1 -0
  23. package/platform-client/PlatformClientDetailPanel.js +247 -0
  24. package/platform-client/PlatformClientDetailPanel.js.map +1 -0
  25. package/platform-client/PlatformClientListPanel.d.ts +41 -0
  26. package/platform-client/PlatformClientListPanel.d.ts.map +1 -0
  27. package/platform-client/PlatformClientListPanel.js +123 -0
  28. package/platform-client/PlatformClientListPanel.js.map +1 -0
  29. package/platform-client/PlatformClientSecretAlert.d.ts +39 -0
  30. package/platform-client/PlatformClientSecretAlert.d.ts.map +1 -0
  31. package/platform-client/PlatformClientSecretAlert.js +74 -0
  32. package/platform-client/PlatformClientSecretAlert.js.map +1 -0
  33. package/platform-client/index.d.ts +11 -0
  34. package/platform-client/index.d.ts.map +1 -0
  35. package/platform-client/index.js +11 -0
  36. package/platform-client/index.js.map +1 -0
  37. package/platform-client/useCreatePlatformClient.d.ts +42 -0
  38. package/platform-client/useCreatePlatformClient.d.ts.map +1 -0
  39. package/platform-client/useCreatePlatformClient.js +49 -0
  40. package/platform-client/useCreatePlatformClient.js.map +1 -0
  41. package/platform-client/useDeletePlatformClient.d.ts +31 -0
  42. package/platform-client/useDeletePlatformClient.d.ts.map +1 -0
  43. package/platform-client/useDeletePlatformClient.js +42 -0
  44. package/platform-client/useDeletePlatformClient.js.map +1 -0
  45. package/platform-client/usePlatformClient.d.ts +37 -0
  46. package/platform-client/usePlatformClient.d.ts.map +1 -0
  47. package/platform-client/usePlatformClient.js +62 -0
  48. package/platform-client/usePlatformClient.js.map +1 -0
  49. package/platform-client/usePlatformClientList.d.ts +42 -0
  50. package/platform-client/usePlatformClientList.d.ts.map +1 -0
  51. package/platform-client/usePlatformClientList.js +71 -0
  52. package/platform-client/usePlatformClientList.js.map +1 -0
  53. package/platform-client/useRotatePlatformClientSecret.d.ts +35 -0
  54. package/platform-client/useRotatePlatformClientSecret.d.ts.map +1 -0
  55. package/platform-client/useRotatePlatformClientSecret.js +43 -0
  56. package/platform-client/useRotatePlatformClientSecret.js.map +1 -0
  57. package/platform-client/useUpdatePlatformClient.d.ts +39 -0
  58. package/platform-client/useUpdatePlatformClient.d.ts.map +1 -0
  59. package/platform-client/useUpdatePlatformClient.js +50 -0
  60. package/platform-client/useUpdatePlatformClient.js.map +1 -0
  61. package/src/identity-provider/CreateIdentityProviderForm.tsx +220 -0
  62. package/src/identity-provider/IdentityProviderDetailPanel.tsx +288 -6
  63. package/src/identity-provider/IdentityProviderListPanel.tsx +9 -2
  64. package/src/identity-provider/IdentityProviderWizard.tsx +231 -25
  65. package/src/index.ts +26 -0
  66. package/src/platform-client/CreatePlatformClientForm.tsx +519 -0
  67. package/src/platform-client/PlatformClientDetailPanel.tsx +898 -0
  68. package/src/platform-client/PlatformClientListPanel.tsx +413 -0
  69. package/src/platform-client/PlatformClientSecretAlert.tsx +252 -0
  70. package/src/platform-client/index.ts +49 -0
  71. package/src/platform-client/useCreatePlatformClient.ts +77 -0
  72. package/src/platform-client/useDeletePlatformClient.ts +64 -0
  73. package/src/platform-client/usePlatformClient.ts +86 -0
  74. package/src/platform-client/usePlatformClientList.ts +96 -0
  75. package/src/platform-client/useRotatePlatformClientSecret.ts +68 -0
  76. package/src/platform-client/useUpdatePlatformClient.ts +70 -0
  77. package/src/test/index.ts +6 -0
  78. package/src/{demo → test}/samples.ts +1 -1
  79. package/styles.css +1 -1
  80. package/test/__tests__/samples.test.d.ts.map +1 -0
  81. package/{demo → test}/__tests__/samples.test.js.map +1 -1
  82. package/test/index.d.ts +2 -0
  83. package/test/index.d.ts.map +1 -0
  84. package/test/index.js +6 -0
  85. package/test/index.js.map +1 -0
  86. package/{demo → test}/samples.d.ts +1 -1
  87. package/{demo → test}/samples.d.ts.map +1 -1
  88. package/{demo → test}/samples.js +1 -1
  89. package/{demo → test}/samples.js.map +1 -1
  90. package/demo/__tests__/demo-client.test.d.ts +0 -2
  91. package/demo/__tests__/demo-client.test.d.ts.map +0 -1
  92. package/demo/__tests__/demo-client.test.js +0 -133
  93. package/demo/__tests__/demo-client.test.js.map +0 -1
  94. package/demo/__tests__/fixtures.test.d.ts +0 -2
  95. package/demo/__tests__/fixtures.test.d.ts.map +0 -1
  96. package/demo/__tests__/fixtures.test.js +0 -135
  97. package/demo/__tests__/fixtures.test.js.map +0 -1
  98. package/demo/__tests__/samples.test.d.ts.map +0 -1
  99. package/demo/client.d.ts +0 -29
  100. package/demo/client.d.ts.map +0 -1
  101. package/demo/client.js +0 -52
  102. package/demo/client.js.map +0 -1
  103. package/demo/fixtures.d.ts +0 -194
  104. package/demo/fixtures.d.ts.map +0 -1
  105. package/demo/fixtures.js +0 -267
  106. package/demo/fixtures.js.map +0 -1
  107. package/demo/index.d.ts +0 -6
  108. package/demo/index.d.ts.map +0 -1
  109. package/demo/index.js +0 -6
  110. package/demo/index.js.map +0 -1
  111. package/demo/transport.d.ts +0 -59
  112. package/demo/transport.d.ts.map +0 -1
  113. package/demo/transport.js +0 -75
  114. package/demo/transport.js.map +0 -1
  115. package/demo/types.d.ts +0 -62
  116. package/demo/types.d.ts.map +0 -1
  117. package/demo/types.js +0 -16
  118. package/demo/types.js.map +0 -1
  119. package/src/demo/__tests__/demo-client.test.tsx +0 -213
  120. package/src/demo/__tests__/fixtures.test.ts +0 -214
  121. package/src/demo/client.ts +0 -78
  122. package/src/demo/fixtures.ts +0 -409
  123. package/src/demo/index.ts +0 -12
  124. package/src/demo/transport.ts +0 -116
  125. package/src/demo/types.ts +0 -69
  126. /package/src/{demo → test}/__tests__/samples.test.ts +0 -0
  127. /package/{demo → test}/__tests__/samples.test.d.ts +0 -0
  128. /package/{demo → test}/__tests__/samples.test.js +0 -0
@@ -4,6 +4,7 @@ import { useCallback, useState, type FormEvent } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import { getUserMessage } from "@stigmer/sdk";
6
6
  import type { IdentityProvider } from "@stigmer/protos/ai/stigmer/iam/identityprovider/v1/api_pb";
7
+ import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
7
8
  import { useCreateIdentityProvider } from "./useCreateIdentityProvider";
8
9
 
9
10
  /** Props for {@link CreateIdentityProviderForm}. */
@@ -59,6 +60,30 @@ export function CreateIdentityProviderForm({
59
60
  const [isSso, setIsSso] = useState(false);
60
61
  const [oidcClientId, setOidcClientId] = useState("");
61
62
 
63
+ // JIT provisioning
64
+ const [autoProvision, setAutoProvision] = useState(false);
65
+ const [autoGrant, setAutoGrant] = useState(false);
66
+ const [autoGrantRole, setAutoGrantRole] = useState<IamRole>(IamRole.iam_role_unspecified);
67
+ const [tenantOrgClaim, setTenantOrgClaim] = useState("");
68
+
69
+ const handleAutoProvisionChange = useCallback((v: boolean) => {
70
+ setAutoProvision(v);
71
+ if (!v) {
72
+ setAutoGrant(false);
73
+ setAutoGrantRole(IamRole.iam_role_unspecified);
74
+ setTenantOrgClaim("");
75
+ }
76
+ }, []);
77
+
78
+ const handleAutoGrantChange = useCallback((v: boolean) => {
79
+ setAutoGrant(v);
80
+ if (v) setAutoProvision(true);
81
+ if (!v) {
82
+ setAutoGrantRole(IamRole.iam_role_unspecified);
83
+ setTenantOrgClaim("");
84
+ }
85
+ }, []);
86
+
62
87
  const trimmedName = name.trim();
63
88
  const trimmedJwksUri = jwksUri.trim();
64
89
  const trimmedIssuers = issuers.trim();
@@ -91,6 +116,16 @@ export function CreateIdentityProviderForm({
91
116
  isSsoProvider: true,
92
117
  oidcClientId: oidcClientId.trim(),
93
118
  }),
119
+ ...(!isSso && {
120
+ autoProvisionAccounts: autoProvision,
121
+ autoGrantOnOrg: autoGrant,
122
+ ...(autoGrant && autoGrantRole !== IamRole.iam_role_unspecified && {
123
+ autoGrantRole,
124
+ }),
125
+ ...(autoGrant && tenantOrgClaim.trim() && {
126
+ tenantOrgClaim: tenantOrgClaim.trim(),
127
+ }),
128
+ }),
94
129
  });
95
130
  onCreated?.(idp);
96
131
  } catch {
@@ -106,6 +141,10 @@ export function CreateIdentityProviderForm({
106
141
  trimmedAudience,
107
142
  isSso,
108
143
  oidcClientId,
144
+ autoProvision,
145
+ autoGrant,
146
+ autoGrantRole,
147
+ tenantOrgClaim,
109
148
  create,
110
149
  clearError,
111
150
  onCreated,
@@ -194,6 +233,20 @@ export function CreateIdentityProviderForm({
194
233
  required
195
234
  />
196
235
  )}
236
+
237
+ {/* JIT provisioning */}
238
+ <JitSection
239
+ isSso={isSso}
240
+ autoProvision={autoProvision}
241
+ onAutoProvisionChange={handleAutoProvisionChange}
242
+ autoGrant={autoGrant}
243
+ onAutoGrantChange={handleAutoGrantChange}
244
+ autoGrantRole={autoGrantRole}
245
+ onAutoGrantRoleChange={setAutoGrantRole}
246
+ tenantOrgClaim={tenantOrgClaim}
247
+ onTenantOrgClaimChange={setTenantOrgClaim}
248
+ disabled={isCreating}
249
+ />
197
250
  </div>
198
251
 
199
252
  {error && (
@@ -285,6 +338,173 @@ function FormField({
285
338
  );
286
339
  }
287
340
 
341
+ // ---------------------------------------------------------------------------
342
+ // JIT provisioning section
343
+ // ---------------------------------------------------------------------------
344
+
345
+ const JIT_ROLE_OPTIONS: readonly { readonly value: string; readonly label: string }[] = [
346
+ { value: String(IamRole.iam_role_unspecified), label: "Default (viewer)" },
347
+ { value: String(IamRole.viewer), label: "Viewer" },
348
+ { value: String(IamRole.member), label: "Member" },
349
+ { value: String(IamRole.admin), label: "Admin" },
350
+ ];
351
+
352
+ function JitSection({
353
+ isSso,
354
+ autoProvision,
355
+ onAutoProvisionChange,
356
+ autoGrant,
357
+ onAutoGrantChange,
358
+ autoGrantRole,
359
+ onAutoGrantRoleChange,
360
+ tenantOrgClaim,
361
+ onTenantOrgClaimChange,
362
+ disabled,
363
+ }: {
364
+ isSso: boolean;
365
+ autoProvision: boolean;
366
+ onAutoProvisionChange: (v: boolean) => void;
367
+ autoGrant: boolean;
368
+ onAutoGrantChange: (v: boolean) => void;
369
+ autoGrantRole: IamRole;
370
+ onAutoGrantRoleChange: (v: IamRole) => void;
371
+ tenantOrgClaim: string;
372
+ onTenantOrgClaimChange: (v: string) => void;
373
+ disabled: boolean;
374
+ }) {
375
+ if (isSso) {
376
+ return (
377
+ <div className="rounded-md border border-border/60 bg-muted/30 px-3 py-2">
378
+ <p className="text-[0.65rem] text-muted-foreground">
379
+ SSO providers automatically provision accounts and grant the{" "}
380
+ <span className="font-medium text-foreground">viewer</span> role on
381
+ the owning organization. JIT provisioning settings are not applicable.
382
+ </p>
383
+ </div>
384
+ );
385
+ }
386
+
387
+ return (
388
+ <fieldset className="space-y-2.5" disabled={disabled}>
389
+ <hr className="border-border/40" />
390
+ <legend className="text-xs font-medium text-foreground">
391
+ JIT provisioning
392
+ </legend>
393
+ <p className="text-[0.65rem] text-muted-foreground">
394
+ Configure automatic account creation and role assignment for users
395
+ authenticating with this provider.
396
+ </p>
397
+
398
+ <ToggleSwitch
399
+ checked={autoProvision}
400
+ onChange={onAutoProvisionChange}
401
+ label="Auto-provision accounts"
402
+ hint="Create a federated account automatically on first authentication"
403
+ disabled={disabled}
404
+ />
405
+
406
+ <ToggleSwitch
407
+ checked={autoGrant}
408
+ onChange={onAutoGrantChange}
409
+ label="Auto-grant on organization"
410
+ hint="Grant a role on the owning organization when an account is provisioned"
411
+ disabled={disabled || !autoProvision}
412
+ />
413
+
414
+ {autoGrant && (
415
+ <>
416
+ <div className="space-y-1">
417
+ <label
418
+ htmlFor="stgm-idp-grant-role"
419
+ className="text-xs font-medium text-foreground"
420
+ >
421
+ Auto-grant role
422
+ </label>
423
+ <select
424
+ id="stgm-idp-grant-role"
425
+ value={String(autoGrantRole)}
426
+ onChange={(e) => onAutoGrantRoleChange(Number(e.target.value) as IamRole)}
427
+ disabled={disabled}
428
+ className={cn(
429
+ "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
430
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
431
+ "disabled:pointer-events-none disabled:opacity-50",
432
+ )}
433
+ >
434
+ {JIT_ROLE_OPTIONS.map((opt) => (
435
+ <option key={opt.value} value={opt.value}>
436
+ {opt.label}
437
+ </option>
438
+ ))}
439
+ </select>
440
+ <p className="text-[0.65rem] text-muted-foreground">
441
+ Role granted automatically — org admins can upgrade later
442
+ </p>
443
+ </div>
444
+
445
+ <FormField
446
+ id="stgm-idp-tenant-claim"
447
+ label="Tenant org claim"
448
+ value={tenantOrgClaim}
449
+ onChange={onTenantOrgClaimChange}
450
+ placeholder="e.g., org_id"
451
+ hint="JWT claim name that maps to a platform-managed organization (max 256 chars)"
452
+ disabled={disabled}
453
+ />
454
+ </>
455
+ )}
456
+ </fieldset>
457
+ );
458
+ }
459
+
460
+ // ---------------------------------------------------------------------------
461
+ // Toggle switch
462
+ // ---------------------------------------------------------------------------
463
+
464
+ function ToggleSwitch({
465
+ checked,
466
+ onChange,
467
+ label,
468
+ hint,
469
+ disabled,
470
+ }: {
471
+ checked: boolean;
472
+ onChange: (v: boolean) => void;
473
+ label: string;
474
+ hint?: string;
475
+ disabled?: boolean;
476
+ }) {
477
+ return (
478
+ <div className="space-y-0.5">
479
+ <div className="flex items-center gap-2">
480
+ <button
481
+ type="button"
482
+ role="switch"
483
+ aria-checked={checked}
484
+ onClick={() => onChange(!checked)}
485
+ disabled={disabled}
486
+ className={cn(
487
+ "relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors",
488
+ checked ? "bg-primary" : "bg-muted",
489
+ "disabled:pointer-events-none disabled:opacity-50",
490
+ )}
491
+ >
492
+ <span
493
+ className={cn(
494
+ "pointer-events-none inline-block h-4 w-4 rounded-full bg-background shadow-sm ring-0 transition-transform",
495
+ checked ? "translate-x-4" : "translate-x-0",
496
+ )}
497
+ />
498
+ </button>
499
+ <span className="text-xs font-medium text-foreground">{label}</span>
500
+ </div>
501
+ {hint && (
502
+ <p className="pl-11 text-[0.65rem] text-muted-foreground">{hint}</p>
503
+ )}
504
+ </div>
505
+ );
506
+ }
507
+
288
508
  // ---------------------------------------------------------------------------
289
509
  // Icons
290
510
  // ---------------------------------------------------------------------------
@@ -4,6 +4,7 @@ import { useCallback, useState, type FormEvent } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import { getUserMessage } from "@stigmer/sdk";
6
6
  import type { IdentityProvider } from "@stigmer/protos/ai/stigmer/iam/identityprovider/v1/api_pb";
7
+ import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
7
8
  import { timestampDate, type Timestamp } from "@bufbuild/protobuf/wkt";
8
9
  import { useUpdateIdentityProvider } from "./useUpdateIdentityProvider";
9
10
 
@@ -76,6 +77,30 @@ export function IdentityProviderDetailPanel({
76
77
  spec?.oidcClientId ?? "",
77
78
  );
78
79
 
80
+ // JIT provisioning
81
+ const [autoProvision, setAutoProvision] = useState(spec?.autoProvisionAccounts ?? false);
82
+ const [autoGrant, setAutoGrant] = useState(spec?.autoGrantOnOrg ?? false);
83
+ const [autoGrantRole, setAutoGrantRole] = useState<IamRole>(spec?.autoGrantRole ?? IamRole.iam_role_unspecified);
84
+ const [tenantOrgClaim, setTenantOrgClaim] = useState(spec?.tenantOrgClaim ?? "");
85
+
86
+ const handleAutoProvisionChange = useCallback((v: boolean) => {
87
+ setAutoProvision(v);
88
+ if (!v) {
89
+ setAutoGrant(false);
90
+ setAutoGrantRole(IamRole.iam_role_unspecified);
91
+ setTenantOrgClaim("");
92
+ }
93
+ }, []);
94
+
95
+ const handleAutoGrantChange = useCallback((v: boolean) => {
96
+ setAutoGrant(v);
97
+ if (v) setAutoProvision(true);
98
+ if (!v) {
99
+ setAutoGrantRole(IamRole.iam_role_unspecified);
100
+ setTenantOrgClaim("");
101
+ }
102
+ }, []);
103
+
79
104
  const enterEdit = useCallback(() => {
80
105
  setDisplayName(spec?.displayName ?? "");
81
106
  setJwksUri(spec?.jwksUri ?? "");
@@ -84,6 +109,10 @@ export function IdentityProviderDetailPanel({
84
109
  setUserinfoEndpoint(spec?.userinfoEndpoint ?? "");
85
110
  setIsSso(spec?.isSsoProvider ?? false);
86
111
  setOidcClientId(spec?.oidcClientId ?? "");
112
+ setAutoProvision(spec?.autoProvisionAccounts ?? false);
113
+ setAutoGrant(spec?.autoGrantOnOrg ?? false);
114
+ setAutoGrantRole(spec?.autoGrantRole ?? IamRole.iam_role_unspecified);
115
+ setTenantOrgClaim(spec?.tenantOrgClaim ?? "");
87
116
  clearError();
88
117
  setMode("edit");
89
118
  }, [spec, clearError]);
@@ -112,6 +141,16 @@ export function IdentityProviderDetailPanel({
112
141
  userinfoEndpoint: userinfoEndpoint.trim() || undefined,
113
142
  isSsoProvider: isSso,
114
143
  oidcClientId: isSso ? oidcClientId.trim() : undefined,
144
+ ...(!isSso && {
145
+ autoProvisionAccounts: autoProvision,
146
+ autoGrantOnOrg: autoGrant,
147
+ ...(autoGrant && autoGrantRole !== IamRole.iam_role_unspecified && {
148
+ autoGrantRole,
149
+ }),
150
+ ...(autoGrant && tenantOrgClaim.trim() && {
151
+ tenantOrgClaim: tenantOrgClaim.trim(),
152
+ }),
153
+ }),
115
154
  });
116
155
  setMode("view");
117
156
  onUpdated?.(updated);
@@ -121,7 +160,8 @@ export function IdentityProviderDetailPanel({
121
160
  },
122
161
  [
123
162
  meta, displayName, jwksUri, issuers, audience,
124
- userinfoEndpoint, isSso, oidcClientId, update, clearError, onUpdated,
163
+ userinfoEndpoint, isSso, oidcClientId, autoProvision, autoGrant,
164
+ autoGrantRole, tenantOrgClaim, update, clearError, onUpdated,
125
165
  ],
126
166
  );
127
167
 
@@ -160,11 +200,7 @@ export function IdentityProviderDetailPanel({
160
200
  {meta.slug}
161
201
  </span>
162
202
  )}
163
- {spec?.isSsoProvider && (
164
- <span className="inline-flex items-center rounded-full border border-primary/30 bg-primary-subtle px-2 py-0.5 text-[0.65rem] font-medium text-primary">
165
- SSO
166
- </span>
167
- )}
203
+ <ProvisioningModeBadge spec={spec} />
168
204
  </div>
169
205
  </div>
170
206
 
@@ -279,6 +315,20 @@ export function IdentityProviderDetailPanel({
279
315
  />
280
316
  )}
281
317
 
318
+ {/* JIT provisioning */}
319
+ <JitEditSection
320
+ isSso={isSso}
321
+ autoProvision={autoProvision}
322
+ onAutoProvisionChange={handleAutoProvisionChange}
323
+ autoGrant={autoGrant}
324
+ onAutoGrantChange={handleAutoGrantChange}
325
+ autoGrantRole={autoGrantRole}
326
+ onAutoGrantRoleChange={setAutoGrantRole}
327
+ tenantOrgClaim={tenantOrgClaim}
328
+ onTenantOrgClaimChange={setTenantOrgClaim}
329
+ disabled={isUpdating}
330
+ />
331
+
282
332
  {error && (
283
333
  <p className="text-destructive text-[0.65rem]" role="alert">
284
334
  {getUserMessage(error)}
@@ -360,6 +410,35 @@ function ViewMode({
360
410
  value={`${spec!.rateLimitBudget} req/min`}
361
411
  />
362
412
  )}
413
+
414
+ {/* JIT provisioning fields */}
415
+ {!spec?.isSsoProvider && spec?.autoProvisionAccounts && (
416
+ <>
417
+ <hr className="border-border/40" />
418
+ <Field
419
+ label="Auto-provision accounts"
420
+ value={spec.autoProvisionAccounts ? "Enabled" : "Disabled"}
421
+ />
422
+ <Field
423
+ label="Auto-grant on organization"
424
+ value={spec.autoGrantOnOrg ? "Enabled" : "Disabled"}
425
+ />
426
+ {spec.autoGrantOnOrg && (
427
+ <Field
428
+ label="Auto-grant role"
429
+ value={formatIamRole(spec.autoGrantRole)}
430
+ />
431
+ )}
432
+ {spec.autoGrantOnOrg && spec.tenantOrgClaim && (
433
+ <Field
434
+ label="Tenant org claim"
435
+ value={spec.tenantOrgClaim}
436
+ mono
437
+ />
438
+ )}
439
+ </>
440
+ )}
441
+
363
442
  <div className="flex gap-6">
364
443
  {createdAt && (
365
444
  <Field
@@ -534,6 +613,209 @@ function FieldInput({
534
613
  );
535
614
  }
536
615
 
616
+ // ---------------------------------------------------------------------------
617
+ // Provisioning mode badge
618
+ // ---------------------------------------------------------------------------
619
+
620
+ function ProvisioningModeBadge({ spec }: { spec: IdentityProvider["spec"] }) {
621
+ if (spec?.isSsoProvider) {
622
+ return (
623
+ <span className="inline-flex items-center rounded-full border border-primary/30 bg-primary-subtle px-2 py-0.5 text-[0.65rem] font-medium text-primary">
624
+ SSO
625
+ </span>
626
+ );
627
+ }
628
+ if (spec?.autoProvisionAccounts) {
629
+ return (
630
+ <span className="inline-flex items-center rounded-full border border-primary/30 bg-primary-subtle px-2 py-0.5 text-[0.65rem] font-medium text-primary">
631
+ JIT
632
+ </span>
633
+ );
634
+ }
635
+ return null;
636
+ }
637
+
638
+ // ---------------------------------------------------------------------------
639
+ // JIT edit section
640
+ // ---------------------------------------------------------------------------
641
+
642
+ const JIT_ROLE_OPTIONS: readonly { readonly value: string; readonly label: string }[] = [
643
+ { value: String(IamRole.iam_role_unspecified), label: "Default (viewer)" },
644
+ { value: String(IamRole.viewer), label: "Viewer" },
645
+ { value: String(IamRole.member), label: "Member" },
646
+ { value: String(IamRole.admin), label: "Admin" },
647
+ ];
648
+
649
+ function JitEditSection({
650
+ isSso,
651
+ autoProvision,
652
+ onAutoProvisionChange,
653
+ autoGrant,
654
+ onAutoGrantChange,
655
+ autoGrantRole,
656
+ onAutoGrantRoleChange,
657
+ tenantOrgClaim,
658
+ onTenantOrgClaimChange,
659
+ disabled,
660
+ }: {
661
+ isSso: boolean;
662
+ autoProvision: boolean;
663
+ onAutoProvisionChange: (v: boolean) => void;
664
+ autoGrant: boolean;
665
+ onAutoGrantChange: (v: boolean) => void;
666
+ autoGrantRole: IamRole;
667
+ onAutoGrantRoleChange: (v: IamRole) => void;
668
+ tenantOrgClaim: string;
669
+ onTenantOrgClaimChange: (v: string) => void;
670
+ disabled?: boolean;
671
+ }) {
672
+ if (isSso) {
673
+ return (
674
+ <div className="rounded-md border border-border/60 bg-muted/30 px-3 py-2">
675
+ <p className="text-[0.65rem] text-muted-foreground">
676
+ SSO providers automatically provision accounts and grant the{" "}
677
+ <span className="font-medium text-foreground">viewer</span> role on
678
+ the owning organization. JIT provisioning settings are not applicable.
679
+ </p>
680
+ </div>
681
+ );
682
+ }
683
+
684
+ return (
685
+ <fieldset className="space-y-2.5" disabled={disabled}>
686
+ <hr className="border-border/40" />
687
+ <legend className="text-xs font-medium text-foreground">
688
+ JIT provisioning
689
+ </legend>
690
+ <p className="text-[0.65rem] text-muted-foreground">
691
+ Configure automatic account creation and role assignment for users
692
+ authenticating with this provider.
693
+ </p>
694
+
695
+ <ToggleSwitch
696
+ checked={autoProvision}
697
+ onChange={onAutoProvisionChange}
698
+ label="Auto-provision accounts"
699
+ hint="Create a federated account automatically on first authentication"
700
+ disabled={disabled}
701
+ />
702
+
703
+ <ToggleSwitch
704
+ checked={autoGrant}
705
+ onChange={onAutoGrantChange}
706
+ label="Auto-grant on organization"
707
+ hint="Grant a role on the owning organization when an account is provisioned"
708
+ disabled={disabled || !autoProvision}
709
+ />
710
+
711
+ {autoGrant && (
712
+ <>
713
+ <div className="space-y-1">
714
+ <label
715
+ htmlFor="stgm-idp-edit-grant-role"
716
+ className="text-xs font-medium text-foreground"
717
+ >
718
+ Auto-grant role
719
+ </label>
720
+ <select
721
+ id="stgm-idp-edit-grant-role"
722
+ value={String(autoGrantRole)}
723
+ onChange={(e) => onAutoGrantRoleChange(Number(e.target.value) as IamRole)}
724
+ disabled={disabled}
725
+ className={cn(
726
+ "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
727
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
728
+ "disabled:pointer-events-none disabled:opacity-50",
729
+ )}
730
+ >
731
+ {JIT_ROLE_OPTIONS.map((opt) => (
732
+ <option key={opt.value} value={opt.value}>
733
+ {opt.label}
734
+ </option>
735
+ ))}
736
+ </select>
737
+ <p className="text-[0.65rem] text-muted-foreground">
738
+ Role granted automatically — org admins can upgrade later
739
+ </p>
740
+ </div>
741
+
742
+ <FieldInput
743
+ id="stgm-idp-edit-tenant-claim"
744
+ label="Tenant org claim"
745
+ value={tenantOrgClaim}
746
+ onChange={onTenantOrgClaimChange}
747
+ placeholder="e.g., org_id"
748
+ hint="JWT claim name that maps to a platform-managed organization (max 256 chars)"
749
+ disabled={disabled}
750
+ />
751
+ </>
752
+ )}
753
+ </fieldset>
754
+ );
755
+ }
756
+
757
+ // ---------------------------------------------------------------------------
758
+ // Toggle switch
759
+ // ---------------------------------------------------------------------------
760
+
761
+ function ToggleSwitch({
762
+ checked,
763
+ onChange,
764
+ label,
765
+ hint,
766
+ disabled,
767
+ }: {
768
+ checked: boolean;
769
+ onChange: (v: boolean) => void;
770
+ label: string;
771
+ hint?: string;
772
+ disabled?: boolean;
773
+ }) {
774
+ return (
775
+ <div className="space-y-0.5">
776
+ <div className="flex items-center gap-2">
777
+ <button
778
+ type="button"
779
+ role="switch"
780
+ aria-checked={checked}
781
+ onClick={() => onChange(!checked)}
782
+ disabled={disabled}
783
+ className={cn(
784
+ "relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors",
785
+ checked ? "bg-primary" : "bg-muted",
786
+ "disabled:pointer-events-none disabled:opacity-50",
787
+ )}
788
+ >
789
+ <span
790
+ className={cn(
791
+ "pointer-events-none inline-block h-4 w-4 rounded-full bg-background shadow-sm ring-0 transition-transform",
792
+ checked ? "translate-x-4" : "translate-x-0",
793
+ )}
794
+ />
795
+ </button>
796
+ <span className="text-xs font-medium text-foreground">{label}</span>
797
+ </div>
798
+ {hint && (
799
+ <p className="pl-11 text-[0.65rem] text-muted-foreground">{hint}</p>
800
+ )}
801
+ </div>
802
+ );
803
+ }
804
+
805
+ // ---------------------------------------------------------------------------
806
+ // Helpers
807
+ // ---------------------------------------------------------------------------
808
+
809
+ function formatIamRole(role: IamRole): string {
810
+ switch (role) {
811
+ case IamRole.viewer: return "Viewer";
812
+ case IamRole.member: return "Member";
813
+ case IamRole.admin: return "Admin";
814
+ case IamRole.owner: return "Owner";
815
+ default: return "Viewer (default)";
816
+ }
817
+ }
818
+
537
819
  function formatDate(date: Date): string {
538
820
  return date.toLocaleDateString(undefined, {
539
821
  month: "short",
@@ -148,12 +148,14 @@ function IdpRow({
148
148
  const { deleteProvider, isDeleting, error } = useDeleteIdentityProvider();
149
149
 
150
150
  const id = identityProvider.metadata?.id ?? "";
151
+ const spec = identityProvider.spec;
151
152
  const name =
152
- identityProvider.spec?.displayName ||
153
+ spec?.displayName ||
153
154
  identityProvider.metadata?.name ||
154
155
  "Unnamed provider";
155
156
  const slug = identityProvider.metadata?.slug;
156
- const isSso = identityProvider.spec?.isSsoProvider;
157
+ const isSso = spec?.isSsoProvider;
158
+ const isJit = !isSso && spec?.autoProvisionAccounts;
157
159
  const createdAt =
158
160
  identityProvider.status?.audit?.specAudit?.createdAt;
159
161
 
@@ -238,6 +240,11 @@ function IdpRow({
238
240
  SSO
239
241
  </span>
240
242
  )}
243
+ {isJit && (
244
+ <span className="inline-flex items-center rounded-full border border-primary/30 bg-primary-subtle px-2 py-0.5 text-[0.65rem] font-medium text-primary">
245
+ JIT
246
+ </span>
247
+ )}
241
248
  {createdAt && (
242
249
  <span title={`Created ${timestampDate(createdAt).toISOString()}`}>
243
250
  {formatShortDate(timestampDate(createdAt))}