@opengeni/react 4.0.2 → 5.0.2-canary.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.
Files changed (48) hide show
  1. package/README.md +42 -0
  2. package/dist/artifacts.js +24 -285
  3. package/dist/artifacts.js.map +1 -1
  4. package/dist/chunk-BRFMGY4L.js +274 -0
  5. package/dist/chunk-BRFMGY4L.js.map +1 -0
  6. package/dist/chunk-IWIDV7SB.js +1 -0
  7. package/dist/chunk-IWIDV7SB.js.map +1 -0
  8. package/dist/{chunk-CQG7YUJB.js → chunk-WRVKY242.js} +1169 -1017
  9. package/dist/chunk-WRVKY242.js.map +1 -0
  10. package/dist/clipboard.js +1 -0
  11. package/dist/components/message-timeline.d.ts +7 -3
  12. package/dist/connect-accounts.d.ts +10 -0
  13. package/dist/connect-chooser.d.ts +11 -0
  14. package/dist/connect-panel.d.ts +10 -0
  15. package/dist/connect-setup.d.ts +12 -0
  16. package/dist/connect.d.ts +30 -0
  17. package/dist/connect.js +831 -0
  18. package/dist/connect.js.map +1 -0
  19. package/dist/device-authorization.d.ts +15 -0
  20. package/dist/identity-link-accounts.d.ts +11 -0
  21. package/dist/identity-link-consent.d.ts +18 -0
  22. package/dist/index.js +523 -663
  23. package/dist/index.js.map +1 -1
  24. package/dist/session-ui.d.ts +2 -0
  25. package/dist/session-ui.js +3 -1
  26. package/dist/sites-ui.d.ts +33 -0
  27. package/dist/sites.d.ts +3 -0
  28. package/dist/sites.js +288 -0
  29. package/dist/sites.js.map +1 -0
  30. package/package.json +16 -2
  31. package/src/components/message-timeline.tsx +22 -5
  32. package/src/components/sandbox-workspace.tsx +9 -2
  33. package/src/connect-accounts.tsx +171 -0
  34. package/src/connect-chooser.tsx +144 -0
  35. package/src/connect-panel.tsx +29 -0
  36. package/src/connect-setup.tsx +260 -0
  37. package/src/connect.ts +48 -0
  38. package/src/device-authorization.tsx +90 -0
  39. package/src/hooks/use-workspace-capture.ts +3 -2
  40. package/src/identity-link-accounts.tsx +144 -0
  41. package/src/identity-link-consent.tsx +192 -0
  42. package/src/session-ui.ts +2 -0
  43. package/src/sites-ui.tsx +385 -0
  44. package/src/sites.ts +3 -0
  45. package/src/timeline/tool-renderers.tsx +1 -1
  46. package/styles/connect.css +48 -0
  47. package/styles/connect.d.ts +1 -0
  48. package/dist/chunk-CQG7YUJB.js.map +0 -1
@@ -0,0 +1,831 @@
1
+ import "./chunk-IWIDV7SB.js";
2
+ import {
3
+ copyTextToClipboard
4
+ } from "./chunk-JVYQRRC4.js";
5
+
6
+ // src/connect.ts
7
+ import { useMemo, useSyncExternalStore } from "react";
8
+
9
+ // src/connect-setup.tsx
10
+ import { useState } from "react";
11
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
+ var setupStatus = {
13
+ ready: "Ready to connect",
14
+ requires_user_action: "Authorize your account",
15
+ credential_input: "Enter connection details",
16
+ provider_wait: "Waiting for authorization",
17
+ account_selection: "Choose an account",
18
+ resource_selection: "Choose resources",
19
+ preview: "Review available operations",
20
+ installing: "Installing selected operations\u2026",
21
+ connected_but_incomplete: "Account connected \u2014 finish setup",
22
+ complete: "Connection ready",
23
+ cancelled: "Setup cancelled",
24
+ expired: "Setup expired",
25
+ failed: "Setup could not finish",
26
+ uncertain: "Setup outcome needs checking"
27
+ };
28
+ function deviceVerificationUrl(action) {
29
+ if (action.type !== "wait" || !action.verificationUrl) return null;
30
+ try {
31
+ const url = new URL(action.verificationUrl);
32
+ return url.protocol === "https:" && !url.username && !url.password ? action.verificationUrl : null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function ConnectSetup(props) {
38
+ const [scope, setScope] = useState(props.controller);
39
+ const [generation, setGeneration] = useState(0);
40
+ if (scope !== props.controller) {
41
+ setScope(props.controller);
42
+ setGeneration(generation + 1);
43
+ }
44
+ return /* @__PURE__ */ jsx(ScopedSetup, { ...props }, generation);
45
+ }
46
+ function ScopedSetup({ controller, onAuthorize, onBrowseResources, className }) {
47
+ const view = useConnect(controller);
48
+ const [localError, setLocalError] = useState(false);
49
+ const invoke = (operation) => {
50
+ setLocalError(false);
51
+ try {
52
+ void Promise.resolve(operation()).catch(() => setLocalError(true));
53
+ } catch {
54
+ setLocalError(true);
55
+ }
56
+ };
57
+ const attempt = view.attempt;
58
+ if (!attempt)
59
+ return /* @__PURE__ */ jsx("section", { className, "aria-label": "Connection setup", children: /* @__PURE__ */ jsx("p", { role: "status", children: "Choose a connection to begin." }) });
60
+ const action = attempt.nextAction;
61
+ const verificationUrl = deviceVerificationUrl(action);
62
+ const terminal = ["complete", "cancelled", "expired"].includes(attempt.state);
63
+ const submit = (event) => {
64
+ event.preventDefault();
65
+ const form = event.currentTarget;
66
+ const data = new FormData(form);
67
+ if (view.busy) return;
68
+ const key = crypto.randomUUID();
69
+ if (action.type === "credentials") {
70
+ const values = Object.fromEntries(
71
+ action.fields.map((field, i) => [field.name, String(data.get(`field-${i}`) ?? "")])
72
+ );
73
+ form.reset();
74
+ invoke(() => view.advance({ type: "credentials", values }, key));
75
+ } else if (action.type === "select_account") {
76
+ const accountId = String(data.get("account") ?? "");
77
+ if (action.accounts.some((account) => account.id === accountId && account.status !== "disabled"))
78
+ invoke(() => view.advance({ type: "account", accountId }, key));
79
+ } else if (action.type === "select_resources") {
80
+ if (action.cursor) return;
81
+ const resourceIds = data.getAll("resource").map(String);
82
+ invoke(() => view.advance({ type: "resources", resourceIds }, key));
83
+ } else if (action.type === "preview") {
84
+ const operationIds = data.getAll("operation").map(String);
85
+ invoke(
86
+ () => view.advance(
87
+ {
88
+ type: "install",
89
+ previewId: action.previewId,
90
+ contentHash: action.contentHash,
91
+ operationIds
92
+ },
93
+ key
94
+ )
95
+ );
96
+ }
97
+ };
98
+ return /* @__PURE__ */ jsxs("section", { className, "aria-label": "Connection setup", "aria-busy": view.busy, children: [
99
+ /* @__PURE__ */ jsxs("p", { children: [
100
+ "Ownership: ",
101
+ attempt.ownership === "personal" ? "Personal" : "Workspace"
102
+ ] }),
103
+ /* @__PURE__ */ jsx("p", { role: "status", children: setupStatus[attempt.state] }),
104
+ attempt.account && /* @__PURE__ */ jsxs("p", { children: [
105
+ "Account: ",
106
+ attempt.account.label
107
+ ] }),
108
+ (localError || view.error || attempt.error) && /* @__PURE__ */ jsx("p", { role: "alert", children: attempt.error?.code === "source_changed" ? "The integration source changed. Review the new operations before installing." : "Connection setup could not continue. Refresh its status before trying again." }),
109
+ !terminal && /* @__PURE__ */ jsx("form", { onSubmit: submit, autoComplete: "off", children: /* @__PURE__ */ jsxs("fieldset", { disabled: view.busy, children: [
110
+ /* @__PURE__ */ jsx("legend", { children: "Connection details" }),
111
+ attempt.state === "connected_but_incomplete" && action.type === "none" && /* @__PURE__ */ jsx(
112
+ "button",
113
+ {
114
+ type: "button",
115
+ onClick: () => invoke(() => view.advance({ type: "retry" }, crypto.randomUUID())),
116
+ children: "Review integration operations"
117
+ }
118
+ ),
119
+ action.type === "credentials" && action.fields.map((field, i) => /* @__PURE__ */ jsxs("label", { children: [
120
+ field.label,
121
+ field.options ? /* @__PURE__ */ jsxs("select", { name: `field-${i}`, required: field.required, defaultValue: "", children: [
122
+ /* @__PURE__ */ jsx("option", { value: "", children: field.required ? "Choose an option" : "No account (public service)" }),
123
+ field.options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
124
+ ] }) : /* @__PURE__ */ jsx(
125
+ "input",
126
+ {
127
+ name: `field-${i}`,
128
+ type: field.secret ? "password" : "text",
129
+ required: field.required,
130
+ autoComplete: "off"
131
+ }
132
+ )
133
+ ] }, field.name)),
134
+ action.type === "select_account" && /* @__PURE__ */ jsxs("label", { children: [
135
+ "Account",
136
+ /* @__PURE__ */ jsxs("select", { name: "account", required: true, defaultValue: "", children: [
137
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Choose an account" }),
138
+ action.accounts.map((account) => /* @__PURE__ */ jsxs(
139
+ "option",
140
+ {
141
+ value: account.id,
142
+ disabled: account.status === "disabled",
143
+ children: [
144
+ account.label,
145
+ " \u2014 ",
146
+ account.id,
147
+ " (",
148
+ account.ownership,
149
+ ")"
150
+ ]
151
+ },
152
+ account.id
153
+ ))
154
+ ] })
155
+ ] }),
156
+ action.type === "select_resources" && action.resources.map((resource) => /* @__PURE__ */ jsxs("label", { children: [
157
+ /* @__PURE__ */ jsx("input", { type: "checkbox", name: "resource", value: resource.id }),
158
+ resource.label,
159
+ " (",
160
+ resource.kind,
161
+ ")"
162
+ ] }, resource.id)),
163
+ action.type === "select_resources" && action.cursor && /* @__PURE__ */ jsxs(Fragment, { children: [
164
+ /* @__PURE__ */ jsx("p", { children: "More resources are available. Review all pages before submitting a selection." }),
165
+ onBrowseResources && /* @__PURE__ */ jsx(
166
+ "button",
167
+ {
168
+ type: "button",
169
+ onClick: () => invoke(() => onBrowseResources(structuredClone(attempt))),
170
+ children: "Browse all resources"
171
+ }
172
+ )
173
+ ] }),
174
+ action.type === "preview" && /* @__PURE__ */ jsxs(Fragment, { children: [
175
+ /* @__PURE__ */ jsx("p", { children: "Select the operations to install." }),
176
+ action.operations.map((operation) => /* @__PURE__ */ jsxs("label", { children: [
177
+ /* @__PURE__ */ jsx("input", { type: "checkbox", name: "operation", value: operation.id }),
178
+ operation.label,
179
+ " (",
180
+ operation.kind,
181
+ ")"
182
+ ] }, operation.id))
183
+ ] }),
184
+ ["credentials", "select_account", "select_resources", "preview"].includes(
185
+ action.type
186
+ ) && /* @__PURE__ */ jsx(
187
+ "button",
188
+ {
189
+ type: "submit",
190
+ disabled: action.type === "select_resources" && Boolean(action.cursor),
191
+ children: action.type === "preview" ? "Install selected operations" : "Continue"
192
+ }
193
+ ),
194
+ action.type === "authorize" && /* @__PURE__ */ jsx(
195
+ "button",
196
+ {
197
+ type: "button",
198
+ onClick: () => invoke(() => onAuthorize(structuredClone(attempt))),
199
+ children: "Authorize connection"
200
+ }
201
+ ),
202
+ action.type === "wait" && /* @__PURE__ */ jsxs(Fragment, { children: [
203
+ action.userCode && /* @__PURE__ */ jsxs("p", { children: [
204
+ "Verification code: ",
205
+ /* @__PURE__ */ jsx("code", { children: action.userCode })
206
+ ] }),
207
+ /* @__PURE__ */ jsx("p", { children: "Complete provider authorization, then check the connection status." }),
208
+ verificationUrl && /* @__PURE__ */ jsx("a", { href: verificationUrl, target: "_blank", rel: "noopener noreferrer", children: "Open provider verification page (new tab)" })
209
+ ] }),
210
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => invoke(() => view.refresh()), children: "Check status" }),
211
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => invoke(() => view.cancel(crypto.randomUUID())), children: "Cancel setup" })
212
+ ] }) }, `${attempt.id}:${attempt.revision}`)
213
+ ] });
214
+ }
215
+
216
+ // src/connect-chooser.tsx
217
+ import { useEffect, useState as useState2 } from "react";
218
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
219
+ function ConnectChooser(props) {
220
+ const [scope, setScope] = useState2(props.controller);
221
+ const [generation, setGeneration] = useState2(0);
222
+ if (scope !== props.controller) {
223
+ setScope(props.controller);
224
+ setGeneration(generation + 1);
225
+ }
226
+ return /* @__PURE__ */ jsx2(ScopedChooser, { ...props }, generation);
227
+ }
228
+ function ScopedChooser({ controller, returnUrl, className }) {
229
+ const view = useConnect(controller);
230
+ const [catalog, setCatalog] = useState2(null);
231
+ const [failed, setFailed] = useState2(false);
232
+ const [load, setLoad] = useState2(0);
233
+ const [providerId, setProviderId] = useState2("");
234
+ const [ownership, setOwnership] = useState2("");
235
+ useEffect(() => {
236
+ const abort = new AbortController();
237
+ setCatalog(null);
238
+ setFailed(false);
239
+ setProviderId("");
240
+ setOwnership("");
241
+ void Promise.resolve().then(() => {
242
+ abort.signal.throwIfAborted();
243
+ return controller.transport.catalog(controller.workspaceId, { signal: abort.signal });
244
+ }).then((providers) => {
245
+ if (!abort.signal.aborted) setCatalog(structuredClone(providers));
246
+ }).catch(() => {
247
+ if (!abort.signal.aborted) setFailed(true);
248
+ });
249
+ return () => abort.abort();
250
+ }, [controller, load]);
251
+ const provider = catalog?.find((entry) => entry.id === providerId);
252
+ const canBegin = provider?.readiness === "available" && (ownership === "personal" || ownership === "workspace") && provider.ownership.includes(ownership);
253
+ const submit = (event) => {
254
+ event.preventDefault();
255
+ if (!canBegin || view.busy || ownership !== "personal" && ownership !== "workspace") return;
256
+ setFailed(false);
257
+ try {
258
+ void controller.begin({ providerId, ownership, returnUrl, idempotencyKey: crypto.randomUUID() }).catch(() => setFailed(true));
259
+ } catch {
260
+ setFailed(true);
261
+ }
262
+ };
263
+ return /* @__PURE__ */ jsxs2(
264
+ "section",
265
+ {
266
+ className,
267
+ "aria-label": "Choose a connection",
268
+ "aria-busy": view.busy || !catalog && !failed,
269
+ children: [
270
+ failed && /* @__PURE__ */ jsx2("p", { role: "alert", children: "Connections could not be loaded or started. Check setup status before retrying." }),
271
+ !catalog && !failed && /* @__PURE__ */ jsx2("p", { role: "status", children: "Loading connections\u2026" }),
272
+ catalog?.length === 0 && /* @__PURE__ */ jsx2("p", { role: "status", children: "No connections are available." }),
273
+ Boolean(catalog?.length) && /* @__PURE__ */ jsx2("form", { onSubmit: submit, children: /* @__PURE__ */ jsxs2("fieldset", { disabled: view.busy, children: [
274
+ /* @__PURE__ */ jsx2("legend", { children: "Provider and ownership" }),
275
+ /* @__PURE__ */ jsxs2("label", { children: [
276
+ "Provider",
277
+ /* @__PURE__ */ jsxs2(
278
+ "select",
279
+ {
280
+ required: true,
281
+ value: providerId,
282
+ onChange: (event) => {
283
+ setProviderId(event.target.value);
284
+ setOwnership("");
285
+ },
286
+ children: [
287
+ /* @__PURE__ */ jsx2("option", { value: "", disabled: true, children: "Choose a provider" }),
288
+ catalog.map((entry) => /* @__PURE__ */ jsxs2(
289
+ "option",
290
+ {
291
+ value: entry.id,
292
+ disabled: entry.readiness !== "available" || entry.ownership.length === 0,
293
+ children: [
294
+ entry.label,
295
+ " \u2014 ",
296
+ entry.id,
297
+ " (",
298
+ entry.readiness.replaceAll("_", " "),
299
+ ")"
300
+ ]
301
+ },
302
+ entry.id
303
+ ))
304
+ ]
305
+ }
306
+ )
307
+ ] }),
308
+ provider && /* @__PURE__ */ jsxs2("label", { children: [
309
+ "Ownership",
310
+ /* @__PURE__ */ jsxs2(
311
+ "select",
312
+ {
313
+ required: true,
314
+ value: ownership,
315
+ onChange: (event) => setOwnership(event.target.value),
316
+ children: [
317
+ /* @__PURE__ */ jsx2("option", { value: "", disabled: true, children: "Choose ownership" }),
318
+ provider.ownership.map((choice) => /* @__PURE__ */ jsx2("option", { value: choice, children: choice === "personal" ? "Personal \u2014 owned by you" : "Workspace \u2014 shared connection" }, choice))
319
+ ]
320
+ }
321
+ )
322
+ ] }),
323
+ /* @__PURE__ */ jsx2("button", { type: "submit", disabled: !canBegin, children: "Start setup" })
324
+ ] }) }),
325
+ /* @__PURE__ */ jsx2("button", { type: "button", disabled: view.busy, onClick: () => setLoad(load + 1), children: "Reload providers" })
326
+ ]
327
+ }
328
+ );
329
+ }
330
+
331
+ // src/connect-accounts.tsx
332
+ import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
333
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
334
+ function ConnectAccounts(props) {
335
+ const [scope, setScope] = useState3(props.controller);
336
+ const [generation, setGeneration] = useState3(0);
337
+ if (scope !== props.controller) {
338
+ setScope(props.controller);
339
+ setGeneration(generation + 1);
340
+ }
341
+ return /* @__PURE__ */ jsx3(ScopedAccounts, { ...props }, generation);
342
+ }
343
+ function ScopedAccounts({ controller, className, returnUrl }) {
344
+ const view = useConnect(controller);
345
+ const [accounts, setAccounts] = useState3(null);
346
+ const [selected, setSelected] = useState3(null);
347
+ const [busy, setBusy] = useState3(false);
348
+ const [error, setError] = useState3(false);
349
+ const [reload, setReload] = useState3(0);
350
+ const mutation = useRef(null);
351
+ useEffect2(() => () => mutation.current?.abort(), []);
352
+ useEffect2(() => {
353
+ const abort = new AbortController();
354
+ setAccounts(null);
355
+ setSelected(null);
356
+ setError(false);
357
+ void Promise.resolve().then(() => {
358
+ abort.signal.throwIfAborted();
359
+ return controller.transport.accounts(controller.workspaceId, { signal: abort.signal });
360
+ }).then((result) => {
361
+ if (!abort.signal.aborted) setAccounts(structuredClone(result));
362
+ }).catch(() => {
363
+ if (!abort.signal.aborted) setError(true);
364
+ });
365
+ return () => abort.abort();
366
+ }, [controller, reload]);
367
+ const disconnect = async (account) => {
368
+ const expectedVersion = account.version;
369
+ if (mutation.current || view.busy || expectedVersion === void 0 || !Number.isSafeInteger(expectedVersion) || expectedVersion < 1)
370
+ return;
371
+ const abort = new AbortController();
372
+ mutation.current = abort;
373
+ setBusy(true);
374
+ setError(false);
375
+ try {
376
+ await controller.transport.disconnect(controller.workspaceId, account.id, {
377
+ expectedVersion,
378
+ signal: abort.signal
379
+ });
380
+ if (!abort.signal.aborted) setReload((value) => value + 1);
381
+ } catch {
382
+ if (!abort.signal.aborted) {
383
+ setAccounts(null);
384
+ setError(true);
385
+ }
386
+ } finally {
387
+ if (!abort.signal.aborted) {
388
+ mutation.current = null;
389
+ setBusy(false);
390
+ setSelected(null);
391
+ }
392
+ }
393
+ };
394
+ const reconnect = async (account) => {
395
+ if (!returnUrl || mutation.current || view.busy) return;
396
+ setError(false);
397
+ try {
398
+ await controller.begin({
399
+ providerId: account.providerId,
400
+ ownership: account.ownership,
401
+ reconnectAccountId: account.id,
402
+ returnUrl,
403
+ idempotencyKey: crypto.randomUUID()
404
+ });
405
+ } catch {
406
+ setError(true);
407
+ }
408
+ };
409
+ return /* @__PURE__ */ jsxs3(
410
+ "section",
411
+ {
412
+ className,
413
+ "aria-label": "Connected accounts",
414
+ "aria-busy": busy || view.busy || !accounts && !error,
415
+ children: [
416
+ error && /* @__PURE__ */ jsx3("p", { role: "alert", children: "Account state could not be confirmed. Reload before trying again." }),
417
+ !accounts && !error && /* @__PURE__ */ jsx3("p", { role: "status", children: "Loading accounts\u2026" }),
418
+ accounts?.length === 0 && /* @__PURE__ */ jsx3("p", { role: "status", children: "No connected accounts." }),
419
+ accounts && accounts.length > 0 && /* @__PURE__ */ jsx3("ul", { children: accounts.map((account) => /* @__PURE__ */ jsxs3("li", { children: [
420
+ /* @__PURE__ */ jsxs3("span", { children: [
421
+ account.label,
422
+ " \u2014 ",
423
+ account.providerId,
424
+ " \u2014 ",
425
+ account.ownership,
426
+ " \u2014",
427
+ " ",
428
+ account.status.replaceAll("_", " ")
429
+ ] }),
430
+ returnUrl && /* @__PURE__ */ jsxs3(
431
+ "button",
432
+ {
433
+ type: "button",
434
+ disabled: busy || view.busy || account.status === "disabled",
435
+ onClick: () => void reconnect(account),
436
+ children: [
437
+ "Reconnect ",
438
+ account.label
439
+ ]
440
+ }
441
+ ),
442
+ selected === `${account.providerId}:${account.id}` ? /* @__PURE__ */ jsxs3("div", { role: "group", "aria-label": `Disconnect ${account.label}`, children: [
443
+ /* @__PURE__ */ jsx3("p", { children: "This removes local OpenGeni access. It does not revoke consent at the provider." }),
444
+ /* @__PURE__ */ jsx3(
445
+ "button",
446
+ {
447
+ type: "button",
448
+ disabled: busy || view.busy,
449
+ onClick: () => void disconnect(account),
450
+ children: "Confirm disconnect"
451
+ }
452
+ ),
453
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: busy, onClick: () => setSelected(null), children: "Keep account" })
454
+ ] }) : /* @__PURE__ */ jsxs3(
455
+ "button",
456
+ {
457
+ type: "button",
458
+ disabled: busy || view.busy || account.status === "disabled" || !Number.isSafeInteger(account.version) || account.version < 1,
459
+ onClick: () => setSelected(`${account.providerId}:${account.id}`),
460
+ children: [
461
+ "Disconnect ",
462
+ account.label
463
+ ]
464
+ }
465
+ )
466
+ ] }, `${account.providerId}:${account.id}`)) }),
467
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: busy, onClick: () => setReload((value) => value + 1), children: "Reload accounts" })
468
+ ]
469
+ }
470
+ );
471
+ }
472
+
473
+ // src/connect-panel.tsx
474
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
475
+ function ConnectPanel({
476
+ returnUrl,
477
+ title = "Connections",
478
+ showAccounts = true,
479
+ className,
480
+ ...setup
481
+ }) {
482
+ return /* @__PURE__ */ jsxs4("div", { className: ["og-connect", className].filter(Boolean).join(" "), children: [
483
+ /* @__PURE__ */ jsx4("h2", { children: title }),
484
+ /* @__PURE__ */ jsx4(ConnectChooser, { controller: setup.controller, returnUrl }),
485
+ /* @__PURE__ */ jsx4(ConnectSetup, { ...setup }),
486
+ showAccounts && /* @__PURE__ */ jsx4(ConnectAccounts, { controller: setup.controller, returnUrl })
487
+ ] });
488
+ }
489
+
490
+ // src/device-authorization.tsx
491
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState4 } from "react";
492
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
493
+ function DeviceAuthorization(props) {
494
+ const [copied, setCopied] = useState4(false);
495
+ const [copyError, setCopyError] = useState4(false);
496
+ const generation = useRef2(0);
497
+ const timer = useRef2(null);
498
+ useEffect3(() => {
499
+ generation.current++;
500
+ setCopied(false);
501
+ setCopyError(false);
502
+ return () => {
503
+ generation.current++;
504
+ if (timer.current) clearTimeout(timer.current);
505
+ };
506
+ }, [props.userCode]);
507
+ async function copy() {
508
+ const revision = ++generation.current;
509
+ if (timer.current) clearTimeout(timer.current);
510
+ setCopied(false);
511
+ setCopyError(false);
512
+ try {
513
+ const clipboard = props.loadClipboard ? await props.loadClipboard() : { copyTextToClipboard };
514
+ if (revision !== generation.current) return;
515
+ const success = await clipboard.copyTextToClipboard(props.userCode);
516
+ if (revision !== generation.current) return;
517
+ setCopied(success);
518
+ setCopyError(!success);
519
+ props.onCopyResult?.(success);
520
+ if (success)
521
+ timer.current = setTimeout(() => {
522
+ if (revision === generation.current) setCopied(false);
523
+ }, 1600);
524
+ } catch {
525
+ if (revision !== generation.current) return;
526
+ setCopyError(true);
527
+ props.onCopyResult?.(false);
528
+ }
529
+ }
530
+ let href;
531
+ try {
532
+ const url = new URL(props.verificationUri);
533
+ if (["https:", "http:"].includes(url.protocol) && !url.username && !url.password)
534
+ href = props.verificationUri;
535
+ } catch {
536
+ }
537
+ return /* @__PURE__ */ jsxs5("section", { className: `og-connect ${props.className ?? ""}`, "aria-label": "Device authorization", children: [
538
+ /* @__PURE__ */ jsx5("p", { children: props.description ?? `Enter this code at ${props.providerLabel ?? "the provider"}.` }),
539
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", flexWrap: "wrap", gap: "0.5rem", alignItems: "center" }, children: [
540
+ /* @__PURE__ */ jsx5("code", { ...props.codeAttributes, style: { overflowWrap: "anywhere" }, children: props.userCode }),
541
+ /* @__PURE__ */ jsx5(
542
+ "button",
543
+ {
544
+ type: "button",
545
+ "aria-label": copied ? "Code copied" : "Copy code",
546
+ onClick: () => void copy(),
547
+ children: copied ? "Copied" : "Copy code"
548
+ }
549
+ ),
550
+ href ? /* @__PURE__ */ jsxs5("a", { href, target: "_blank", rel: "noopener noreferrer", children: [
551
+ "Open ",
552
+ props.providerLabel ?? "auth page"
553
+ ] }) : /* @__PURE__ */ jsx5("span", { role: "alert", children: "Authorization address unavailable." })
554
+ ] }),
555
+ copyError ? /* @__PURE__ */ jsx5("p", { role: "alert", children: "Couldn't copy the code. Copy it manually instead." }) : null,
556
+ /* @__PURE__ */ jsx5("p", { role: "status", children: "Waiting for authorization\u2026" })
557
+ ] });
558
+ }
559
+
560
+ // src/identity-link-consent.tsx
561
+ import { useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
562
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
563
+ function IdentityLinkConsent({
564
+ client,
565
+ workspaceId,
566
+ linkId,
567
+ challenge,
568
+ className,
569
+ onComplete
570
+ }) {
571
+ const [link, setLink] = useState5(null);
572
+ const [preview, setPreview] = useState5(null);
573
+ const [selected, setSelected] = useState5([]);
574
+ const [busy, setBusy] = useState5(false);
575
+ const [error, setError] = useState5(false);
576
+ const [retry, setRetry] = useState5(0);
577
+ const generation = useRef3(0);
578
+ useEffect4(() => {
579
+ const current = ++generation.current;
580
+ setLink(null);
581
+ setPreview(null);
582
+ setSelected([]);
583
+ setBusy(true);
584
+ setError(false);
585
+ void client.previewIdentityLink(workspaceId, linkId, challenge).then(
586
+ (result) => {
587
+ if (generation.current !== current) return;
588
+ setPreview(result);
589
+ setLink(result.link);
590
+ setSelected(result.link.permissions);
591
+ },
592
+ () => {
593
+ if (generation.current === current) setError(true);
594
+ }
595
+ ).finally(() => {
596
+ if (generation.current === current) setBusy(false);
597
+ });
598
+ return () => {
599
+ generation.current++;
600
+ };
601
+ }, [client, workspaceId, linkId, challenge, retry]);
602
+ async function mutate(revoke) {
603
+ if (!link || busy) return;
604
+ const current = generation.current;
605
+ setBusy(true);
606
+ setError(false);
607
+ try {
608
+ const result = revoke ? await client.revokeIdentityLink(workspaceId, linkId, link.revision) : await client.confirmIdentityLink(workspaceId, linkId, {
609
+ challenge,
610
+ expectedRevision: link.revision,
611
+ permissions: selected
612
+ });
613
+ if (generation.current !== current) return;
614
+ setLink(result);
615
+ onComplete?.(result);
616
+ } catch {
617
+ if (generation.current === current) setError(true);
618
+ } finally {
619
+ if (generation.current === current) setBusy(false);
620
+ }
621
+ }
622
+ return /* @__PURE__ */ jsxs6(
623
+ "section",
624
+ {
625
+ className: ["og-connect og-identity-link", className].filter(Boolean).join(" "),
626
+ "aria-label": "Link your account",
627
+ "aria-busy": busy,
628
+ children: [
629
+ /* @__PURE__ */ jsx6("h2", { children: "Link your OpenGeni account" }),
630
+ /* @__PURE__ */ jsx6("p", { children: "Only continue if you started this request in a product you trust. Linking lets that product act as your account with the permissions you select. It does not merge accounts or move existing work." }),
631
+ preview && /* @__PURE__ */ jsxs6("dl", { children: [
632
+ /* @__PURE__ */ jsx6("dt", { children: "Organization" }),
633
+ /* @__PURE__ */ jsx6("dd", { children: preview.organizationId }),
634
+ /* @__PURE__ */ jsx6("dt", { children: "Product user" }),
635
+ /* @__PURE__ */ jsxs6("dd", { children: [
636
+ preview.externalIdentity.externalId,
637
+ " (",
638
+ preview.externalIdentity.source,
639
+ ")"
640
+ ] }),
641
+ /* @__PURE__ */ jsx6("dt", { children: "Your OpenGeni identity" }),
642
+ /* @__PURE__ */ jsx6("dd", { children: preview.nativeSubjectId })
643
+ ] }),
644
+ error && /* @__PURE__ */ jsxs6("div", { role: "alert", children: [
645
+ "This request could not be completed. It may have expired, changed, or require you to sign in to the right organization.",
646
+ !link && /* @__PURE__ */ jsx6("button", { type: "button", disabled: busy, onClick: () => setRetry((value) => value + 1), children: "Try again" })
647
+ ] }),
648
+ !link && busy && /* @__PURE__ */ jsx6("p", { role: "status", children: "Loading the account-link request\u2026" }),
649
+ link?.status === "pending" && /* @__PURE__ */ jsxs6(Fragment2, { children: [
650
+ /* @__PURE__ */ jsxs6("p", { children: [
651
+ link.expiresAt ? `Access ends ${new Date(link.expiresAt).toLocaleString()}.` : "Access lasts until you or the product revoke this link.",
652
+ " ",
653
+ "Your current permissions still apply."
654
+ ] }),
655
+ /* @__PURE__ */ jsxs6("fieldset", { disabled: busy, children: [
656
+ /* @__PURE__ */ jsx6("legend", { children: "Allow these permissions" }),
657
+ link.permissions.map((permission) => /* @__PURE__ */ jsxs6("label", { children: [
658
+ /* @__PURE__ */ jsx6(
659
+ "input",
660
+ {
661
+ type: "checkbox",
662
+ checked: selected.includes(permission),
663
+ onChange: (event) => setSelected(
664
+ (values) => event.target.checked ? [...values, permission] : values.filter((value) => value !== permission)
665
+ )
666
+ }
667
+ ),
668
+ /* @__PURE__ */ jsx6("span", { children: permission.replaceAll(":", " \xB7 ") })
669
+ ] }, permission))
670
+ ] }),
671
+ /* @__PURE__ */ jsx6(
672
+ "button",
673
+ {
674
+ type: "button",
675
+ disabled: busy || selected.length === 0,
676
+ onClick: () => void mutate(false),
677
+ children: busy ? "Saving\u2026" : "Allow selected access"
678
+ }
679
+ ),
680
+ /* @__PURE__ */ jsx6("p", { children: "You can close this page without granting access." })
681
+ ] }),
682
+ link?.status === "active" && /* @__PURE__ */ jsxs6(Fragment2, { children: [
683
+ /* @__PURE__ */ jsx6("p", { role: "status", children: "Account linked. Return to your product to continue." }),
684
+ /* @__PURE__ */ jsx6("button", { type: "button", disabled: busy, onClick: () => void mutate(true), children: "Revoke this link" })
685
+ ] }),
686
+ link?.status === "revoked" && /* @__PURE__ */ jsx6("p", { role: "status", children: "Link revoked. Your OpenGeni account and existing work are unchanged." }),
687
+ link?.status === "expired" && /* @__PURE__ */ jsx6("p", { role: "status", children: "This request has expired. Start a new request from your product." })
688
+ ]
689
+ }
690
+ );
691
+ }
692
+
693
+ // src/identity-link-accounts.tsx
694
+ import { useEffect as useEffect5, useRef as useRef4, useState as useState6 } from "react";
695
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
696
+ function IdentityLinkAccounts({
697
+ client,
698
+ workspaceId
699
+ }) {
700
+ const [links, setLinks] = useState6([]);
701
+ const [cursor, setCursor] = useState6(null);
702
+ const [busy, setBusy] = useState6(true);
703
+ const [error, setError] = useState6(false);
704
+ const [revision, setRevision] = useState6(0);
705
+ const generation = useRef4(0);
706
+ const inFlight = useRef4(false);
707
+ useEffect5(() => {
708
+ const current = ++generation.current;
709
+ inFlight.current = true;
710
+ setLinks([]);
711
+ setCursor(null);
712
+ setBusy(true);
713
+ setError(false);
714
+ void client.listIdentityLinks(workspaceId).then(
715
+ (page) => {
716
+ if (current !== generation.current) return;
717
+ setLinks(page.links);
718
+ setCursor(page.nextCursor);
719
+ },
720
+ () => {
721
+ if (current === generation.current) setError(true);
722
+ }
723
+ ).finally(() => {
724
+ if (current === generation.current) {
725
+ inFlight.current = false;
726
+ setBusy(false);
727
+ }
728
+ });
729
+ return () => {
730
+ generation.current++;
731
+ };
732
+ }, [client, workspaceId, revision]);
733
+ async function act(link) {
734
+ if (inFlight.current || !link && !cursor) return;
735
+ const current = generation.current;
736
+ inFlight.current = true;
737
+ setBusy(true);
738
+ setError(false);
739
+ try {
740
+ if (link) {
741
+ const updated = await client.revokeIdentityLink(workspaceId, link.id, link.revision);
742
+ if (current === generation.current)
743
+ setLinks(
744
+ (values) => values.map((value) => value.id === updated.id ? { ...value, ...updated } : value)
745
+ );
746
+ } else {
747
+ const page = await client.listIdentityLinks(workspaceId, cursor);
748
+ if (current === generation.current) {
749
+ setLinks((values) => [
750
+ ...new Map([...values, ...page.links].map((value) => [value.id, value])).values()
751
+ ]);
752
+ setCursor(page.nextCursor);
753
+ }
754
+ }
755
+ } catch {
756
+ if (current === generation.current) setError(true);
757
+ } finally {
758
+ if (current === generation.current) {
759
+ inFlight.current = false;
760
+ setBusy(false);
761
+ }
762
+ }
763
+ }
764
+ return /* @__PURE__ */ jsxs7(
765
+ "section",
766
+ {
767
+ className: "og-connect og-identity-link",
768
+ "aria-label": "Linked product access",
769
+ "aria-busy": busy,
770
+ children: [
771
+ /* @__PURE__ */ jsx7("h2", { children: "Linked product access" }),
772
+ /* @__PURE__ */ jsx7("p", { children: "Manage products allowed to act as your account in this organization. Revoking a link stops future linked access; it does not delete your work or undo an operation already started." }),
773
+ error && /* @__PURE__ */ jsxs7("div", { role: "alert", children: [
774
+ "Could not update account links. Reload to see current access.",
775
+ /* @__PURE__ */ jsx7("button", { type: "button", disabled: busy, onClick: () => setRevision((value) => value + 1), children: "Reload links" })
776
+ ] }),
777
+ busy && /* @__PURE__ */ jsx7("p", { role: "status", children: "Loading account links\u2026" }),
778
+ !busy && !error && links.length === 0 && /* @__PURE__ */ jsx7("p", { children: "No products have linked access to this account." }),
779
+ /* @__PURE__ */ jsx7("ul", { children: links.map((link) => /* @__PURE__ */ jsxs7("li", { children: [
780
+ /* @__PURE__ */ jsxs7("p", { children: [
781
+ link.externalIdentity?.source || "Product identity",
782
+ " ",
783
+ /* @__PURE__ */ jsx7("code", { children: link.externalIdentity?.externalId ?? link.externalIdentityId })
784
+ ] }),
785
+ /* @__PURE__ */ jsxs7("p", { children: [
786
+ "Status: ",
787
+ link.status,
788
+ ".",
789
+ " ",
790
+ link.expiresAt ? `Expires ${new Date(link.expiresAt).toLocaleString()}.` : "No automatic expiry."
791
+ ] }),
792
+ /* @__PURE__ */ jsx7("p", { children: link.permissions.join(", ") }),
793
+ (link.status === "active" || link.status === "pending") && /* @__PURE__ */ jsx7("button", { type: "button", disabled: busy, onClick: () => void act(link), children: "Revoke access" })
794
+ ] }, link.id)) }),
795
+ cursor && /* @__PURE__ */ jsx7("button", { type: "button", disabled: busy, onClick: () => void act(), children: "Load more links" })
796
+ ]
797
+ }
798
+ );
799
+ }
800
+
801
+ // src/connect.ts
802
+ function useConnect(controller) {
803
+ const snapshot = useSyncExternalStore(
804
+ controller.subscribe,
805
+ controller.getSnapshot,
806
+ controller.getSnapshot
807
+ );
808
+ const actions = useMemo(
809
+ () => ({
810
+ begin: controller.begin.bind(controller),
811
+ recover: controller.recover.bind(controller),
812
+ refresh: controller.refresh.bind(controller),
813
+ waitForAction: controller.waitForAction.bind(controller),
814
+ advance: controller.advance.bind(controller),
815
+ cancel: controller.cancel.bind(controller)
816
+ }),
817
+ [controller]
818
+ );
819
+ return useMemo(() => ({ ...snapshot, ...actions }), [snapshot, actions]);
820
+ }
821
+ export {
822
+ ConnectAccounts,
823
+ ConnectChooser,
824
+ ConnectPanel,
825
+ ConnectSetup,
826
+ DeviceAuthorization,
827
+ IdentityLinkAccounts,
828
+ IdentityLinkConsent,
829
+ useConnect
830
+ };
831
+ //# sourceMappingURL=connect.js.map