@ekanos/sdk 0.3.0 → 0.5.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 (63) hide show
  1. package/README.md +171 -75
  2. package/api-report.md +8 -3
  3. package/dist/components/base-activation-dialog.d.ts +15 -5
  4. package/dist/components/base-activation-dialog.js +31 -5
  5. package/dist/components/base-activation-dialog.js.map +1 -1
  6. package/dist/components/base-activation-form.d.ts +20 -5
  7. package/dist/components/base-activation-form.js +12 -5
  8. package/dist/components/base-activation-form.js.map +1 -1
  9. package/dist/components/base-marketplace-tile.js +12 -10
  10. package/dist/components/base-marketplace-tile.js.map +1 -1
  11. package/dist/components/index.d.ts +1 -1
  12. package/dist/components/index.js.map +1 -1
  13. package/dist/components/oauth-activation-form.d.ts +23 -16
  14. package/dist/components/oauth-activation-form.js +34 -20
  15. package/dist/components/oauth-activation-form.js.map +1 -1
  16. package/dist/components/widgets/ai-prompt-chip.js +2 -2
  17. package/dist/components/widgets/ai-prompt-chip.js.map +1 -1
  18. package/dist/components/widgets/use-relative-time.d.ts +7 -0
  19. package/dist/components/widgets/use-relative-time.js +55 -0
  20. package/dist/components/widgets/use-relative-time.js.map +1 -0
  21. package/dist/components/widgets/widget-ask-bar.js +1 -1
  22. package/dist/components/widgets/widget-ask-bar.js.map +1 -1
  23. package/dist/components/widgets/widget-context.d.ts +21 -0
  24. package/dist/components/widgets/widget-context.js.map +1 -1
  25. package/dist/components/widgets/widget-preview-provider.d.ts +1 -1
  26. package/dist/components/widgets/widget-preview-provider.js +1 -1
  27. package/dist/components/widgets/widget-preview-provider.js.map +1 -1
  28. package/dist/components/widgets/widget-state-components.d.ts +4 -1
  29. package/dist/components/widgets/widget-state-components.js +6 -4
  30. package/dist/components/widgets/widget-state-components.js.map +1 -1
  31. package/dist/components/widgets/widget.d.ts +47 -2
  32. package/dist/components/widgets/widget.js +111 -23
  33. package/dist/components/widgets/widget.js.map +1 -1
  34. package/dist/context/egress.js.map +1 -1
  35. package/dist/context/storage-validation.d.ts +24 -4
  36. package/dist/context/storage-validation.js +5 -2
  37. package/dist/context/storage-validation.js.map +1 -1
  38. package/dist/hooks/ask-assistant-context.js.map +1 -1
  39. package/dist/hooks/fetch-integration-storage.js.map +1 -1
  40. package/dist/hooks/partner-oauth.js +3 -0
  41. package/dist/hooks/partner-oauth.js.map +1 -1
  42. package/dist/hooks/use-integration-action.js.map +1 -1
  43. package/dist/hooks/use-oauth-connection-status.d.ts +78 -78
  44. package/dist/hooks/use-oauth-connection-status.js +17 -10
  45. package/dist/hooks/use-oauth-connection-status.js.map +1 -1
  46. package/dist/hooks/widget-event-context.js.map +1 -1
  47. package/dist/integration/event-surfaces.js.map +1 -1
  48. package/dist/integration/index.d.ts +2 -2
  49. package/dist/integration/index.js +1 -1
  50. package/dist/integration/index.js.map +1 -1
  51. package/dist/testing/invoke.js +9 -5
  52. package/dist/testing/invoke.js.map +1 -1
  53. package/dist/testing/mock-context.js +43 -8
  54. package/dist/testing/mock-context.js.map +1 -1
  55. package/dist/testing/mock-fetch-provider.js.map +1 -1
  56. package/dist/types/integration.d.ts +27 -21
  57. package/dist/types/integration.js.map +1 -1
  58. package/dist/types/widget-ask-context.d.ts +9 -0
  59. package/dist/types/widget-ask-context.js.map +1 -1
  60. package/dist/types/workspace-target.d.ts +3 -2
  61. package/dist/types/workspace-target.js.map +1 -1
  62. package/eslint.preset.mjs +9 -8
  63. package/package.json +37 -41
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @ekanos/sdk
2
2
 
3
3
  The official SDK for building Ekanos integrations — the packages that add
4
- widgets, assistant tools, and event handlers to a Fusion dashboard.
4
+ widgets, assistant tools, and event handlers to an Ekanos dashboard.
5
5
 
6
6
  **Start here.** This README is the complete authoring reference: everything you
7
7
  can declare, with a worked example for each surface. There is no separate docs
@@ -11,7 +11,7 @@ where to say so.
11
11
  The other packages, so you know when to leave this page: **`@ekanos/cli`** is
12
12
  the toolchain (`init`, `validate`, `dev`, `test`) and owns `ekanos.json`;
13
13
  **`@ekanos/harness`** is the local dev harness that renders your surfaces in
14
- real Fusion chrome; **`@ekanos/ui`** is the component library; and
14
+ real Ekanos chrome; **`@ekanos/ui`** is the component library; and
15
15
  **`@ekanos/integration-schema`** is the validation contract underneath this
16
16
  one. Each has its own README.
17
17
 
@@ -79,48 +79,44 @@ Three of five icons rendered as `□`. The monorepo could not reproduce it at al
79
79
  visible from an installed tarball. If you bundle `@ekanos/*` into your own
80
80
  output, keep these singletons.
81
81
 
82
- ### What is published, and what is not
82
+ ### What is published
83
83
 
84
- Being straight about this up front, because the alternative is you finding out
85
- by running a command that 404s:
84
+ Everything you need is on npm:
86
85
 
87
- | Package | On npm |
86
+ | Package | Role |
88
87
  |---|---|
89
- | `@ekanos/sdk`, `@ekanos/ui`, `@ekanos/integration-schema` | **yes** — `0.1.2` |
90
- | `@ekanos/cli`, `@ekanos/harness` | **not yet** |
88
+ | `@ekanos/sdk`, `@ekanos/ui`, `@ekanos/integration-schema` | the authoring API, the widget UI slice, the schema |
89
+ | `@ekanos/cli` | `ekanos init`, `dev`, `test`, `validate`, `publish` |
90
+ | `@ekanos/harness` | the local dev harness `ekanos dev` runs |
91
91
 
92
- So `npx @ekanos/cli init` and `pnpm add -D @ekanos/harness` do not resolve
93
- today. The toolchain and the local dev harness are written and documented —
94
- their READMEs are in this repo and describe what will ship — but you cannot
95
- install them yet.
92
+ Start a project with:
96
93
 
97
- **What that costs you is seeing, not building.** Everything in this document
98
- works against `@ekanos/sdk` alone:
94
+ ```bash
95
+ npx @ekanos/cli init
96
+ ```
97
+
98
+ `ekanos init` scaffolds a project that already depends on the matching
99
+ versions, and `ekanos dev` renders every surface of your integration in real
100
+ Ekanos chrome — the dashboard grid, the marketplace tile, the activation form,
101
+ the Triggers panel — from fixtures, or against your real vendor API in live
102
+ mode. The `@ekanos/harness` and `@ekanos/cli` READMEs document both.
103
+
104
+ You can also use `@ekanos/sdk` on its own:
99
105
 
100
106
  ```bash
101
107
  npm install @ekanos/sdk zod
102
108
  ```
103
109
 
104
110
  - `defineIntegration()` validates at import time and throws with a remediation
105
- message, so your definition is its own feedback loop — you do not need
106
- `ekanos validate` to know whether it is correct.
111
+ message, so your definition is its own feedback loop.
107
112
  - The whole [Testing](#testing) section needs nothing but vitest.
108
113
  `createMockContext()` gives you the same context production builds, and
109
- `invokeWebhook()` / `invokeSchedule()` run your event handlers with the same
110
- payload validation and egress allowlist the real transports will.
111
-
112
- **What you genuinely cannot do yet** is see any of it rendered — widgets in the
113
- dashboard grid, the marketplace tile, the activation form, the Triggers panel.
114
- Those are the harness, and the harness is not installable. Webhook, schedule
115
- and OAuth work collapses back to unit tests, which is a real loss but a
116
- recoverable one. Widget *appearance* does not: you will be writing it blind
117
- until the harness ships.
118
-
119
- One hard stop worth naming: `ekanos.json`, the project file the CLI reads. Its
120
- schema and the full generated-shell file list are documented in the
121
- `@ekanos/cli` README — which ships inside the CLI tarball, so today you can
122
- only read it if you have this repo. Nothing in this document needs that file;
123
- if you are hand-rolling something that does, wait for the CLI.
114
+ `invokeWebhook()` / `invokeSchedule()` / `invokeActivate()` run your handlers
115
+ with the same payload validation and egress allowlist the real transports
116
+ will.
117
+
118
+ `ekanos.json` is the project file the CLI reads; its schema and the generated
119
+ shell's file list are documented in the `@ekanos/cli` README.
124
120
 
125
121
  ## The shape of an integration
126
122
 
@@ -316,6 +312,15 @@ Four things the signature does not tell you:
316
312
  Key names are `[a-z0-9_-]+` with at most one `/` segment: `config`,
317
313
  `settings/location`, `cache/recent-payments`. Two slashes is a validation error.
318
314
 
315
+ **Limits.** The host caps one stored value at **64 KB serialized**, one
316
+ secret value at 64 KB, the number of distinct secret names an integration
317
+ writes at **32**, and a key segment at 100 characters. The same numbers are
318
+ exported as `PARTNER_STORAGE_LIMITS` from `@ekanos/integration-schema` and
319
+ enforced by `createMockContext()`, so a test that writes an oversized value
320
+ fails locally the way the host would. A snapshot of vendor records reaches
321
+ 64 KB around 120–150 modest rows: trim to a byte budget and flag it rather
322
+ than finding out on the host.
323
+
319
324
  ### `egress`
320
325
 
321
326
  An origin-only https allowlist. `ctx.fetch` refuses anything not covered
@@ -409,7 +414,7 @@ const apiKey = await ctx.secrets.get(ACME_API_KEY);
409
414
 
410
415
  ### `tools`
411
416
 
412
- Tools are what the Fusion assistant (and, where the host allows it, the workflow
417
+ Tools are what the Ekanos assistant (and, where the host allows it, the workflow
413
418
  engine) can call. Each declares a name, a description the model reads, an
414
419
  optional JSON-Schema parameter object, and an async `run`.
415
420
 
@@ -489,7 +494,15 @@ export function CurrentWeatherWidget({ accountId }: IntegrationComponentProps) {
489
494
  const query = useCurrentWeather(accountId, ctx?.state.state === 'active');
490
495
 
491
496
  return (
492
- <Widget.DataState loading={query.isLoading} error={query.isError}>
497
+ <Widget.DataState
498
+ loading={query.isLoading}
499
+ error={query.isError}
500
+ // Optional. They add the header menu's Refresh item, a Try again
501
+ // button on the error view, and an "Updated …" footer.
502
+ onRefresh={() => void query.refetch()}
503
+ refreshing={query.isFetching}
504
+ updatedAt={query.dataUpdatedAt}
505
+ >
493
506
  <Widget.Card>
494
507
  <Widget.Header title="Current conditions" />
495
508
  <Widget.Loading />
@@ -549,7 +562,7 @@ Widget settings live in `ctx.storage`, which is server-side, so a widget reads
549
562
  them back over HTTP with `fetchIntegrationStorage()` from `@ekanos/sdk/hooks` —
550
563
  subject to `clientReadable`, above.
551
564
 
552
- To render a widget anywhere other than the Fusion dashboard — your own app, a
565
+ To render a widget anywhere other than the Ekanos dashboard — your own app, a
553
566
  story, a component test — wrap it in `WidgetPreviewProvider`, which fills the
554
567
  same `WidgetContext` the host fills, with local state and no persistence:
555
568
 
@@ -620,9 +633,17 @@ product — the logo is assigned host-side, not declared here.
620
633
 
621
634
  ### `components.activationForm`
622
635
 
623
- What a user fills in to connect. `BaseActivationForm` owns the dialog chrome,
624
- the submit button, and the error/success states; you supply the
625
- `react-hook-form` instance and the fields.
636
+ What a user fills in to connect — **only** that. The host renders a small
637
+ frame around your form: your product's logo, name and one-line summary, then
638
+ your form. Nothing sits between the two. The host also owns the success
639
+ moment: it closes and toasts, naming the workspace your widgets landed in.
640
+ `BaseActivationForm` renders the action — **Connect** and **Cancel**
641
+ right-aligned under your fields, or, for a form with no fields, a single
642
+ full-width button — plus "Connecting…" while busy and the inline error. You
643
+ supply the `react-hook-form` instance and the fields. Nothing else belongs
644
+ in a form: no title, feature list, permission badge, connected banner or
645
+ success screen. The connect step is kept to the one decision the person
646
+ already made by clicking your product.
626
647
 
627
648
  ```tsx
628
649
  'use client';
@@ -654,7 +675,6 @@ export function AcmePaymentsActivationForm({
654
675
  inline = false, // pass this through — see below
655
676
  }: ActivationFormProps) {
656
677
  const [error, setError] = useState<string | null>(null);
657
- const [success, setSuccess] = useState(false);
658
678
 
659
679
  const form = useForm({ resolver: zodResolver(ActivationSchema),
660
680
  defaultValues: { [ACME_API_KEY]: '', merchantId: '' } });
@@ -670,7 +690,8 @@ export function AcmePaymentsActivationForm({
670
690
  integrationSlug: 'acme-payments',
671
691
  activationData: data,
672
692
  });
673
- setSuccess(true);
693
+ // Report once, straight to the host. It closes and toasts.
694
+ onSuccess?.({ success: true, externalId: data.merchantId as string });
674
695
  } catch (cause) {
675
696
  setError(cause instanceof Error ? cause.message : 'Could not connect.');
676
697
  }
@@ -686,15 +707,6 @@ export function AcmePaymentsActivationForm({
686
707
  onSubmit={handleSubmit}
687
708
  isLoading={isLoading}
688
709
  error={error}
689
- success={success}
690
- successTitle="Acme Payments is connected"
691
- onSuccessClose={() => onSuccess?.({
692
- success: true,
693
- configData: { merchantId: form.getValues('merchantId') },
694
- externalId: form.getValues('merchantId'),
695
- })}
696
- submitButtonText="Connect Acme Payments"
697
- submitButtonLoadingText="Connecting…"
698
710
  >
699
711
  <FormField control={form.control} name={ACME_API_KEY} render={({ field }) => (
700
712
  <FormItem>
@@ -731,36 +743,70 @@ Three things worth knowing:
731
743
  export const ACME_API_KEY = 'acme_api_key';
732
744
  ```
733
745
 
734
- Values you want back on the client instead go in `configData` on the
735
- `ActivationResult` you hand `onSuccess`.
746
+ Values you want persisted as **settings** (not secrets) go in `configData`
747
+ on the `ActivationResult` you hand `onSuccess`, keyed by the `settings/*`
748
+ storage keys you declared. Never put raw field values there: the host
749
+ rejects any key you did not declare, and the user sees a warning that
750
+ "some settings could not be saved".
736
751
  - **`useActivateIntegration` needs the host's injected actions.** It reads them
737
752
  from `IntegrationActivationProvider`, which the host mounts. Outside the host
738
753
  — in the dev harness, or your own app — mount that provider yourself, or the
739
754
  hook has nothing to call.
740
755
 
741
- `OAuthActivationForm` is the connect-button variant for OAuth integrations, and
756
+ `OAuthActivationForm` is the one-button variant for OAuth integrations: it
757
+ renders **Continue with Acme** with a line explaining the user will sign in on
758
+ Acme's site and come straight back, then activates on return. Its `config`
759
+ needs only `slug` and `displayName` (plus `connectPath`, `errorParam` or
760
+ `autoActivateOnConnect: false` when you genuinely need them).
742
761
  `useOAuthConnectionStatus()` reports whether the account is connected.
743
762
 
763
+ Both primitives accept a `footnote` — one quiet line under the action row for
764
+ what happens next — and nothing else decorative.
765
+
766
+ **Reconnecting.** The host mounts your form a second time, with `reconnect`
767
+ set, when a connected integration needs its credentials renewed — after a
768
+ failed health check, or when the owner chooses Reconnect.
769
+ `OAuthActivationForm` handles it for you: the button reads **Reconnect with
770
+ Acme** and always goes through sign-in again, even with a token stored. A
771
+ credential form needs nothing special: it collects new values and activates
772
+ again, which replaces the old ones and resets the integration's health to
773
+ unknown until the next check. A bespoke OAuth form should treat `reconnect`
774
+ the same way — send the person through sign-in rather than skipping it
775
+ because a token exists.
776
+
777
+ **Behavior change for existing OAuth forms:** `OAuthActivationForm`'s
778
+ `children` render only **after** the user has signed in, and only for a form
779
+ with `autoActivateOnConnect: false` is there a moment to see them. They are
780
+ for post-sign-in fields, not for feature lists or permission copy — the
781
+ connect step carries neither. The primitive does not read those
782
+ fields: a form that needs values after sign-in owns its own `useForm` on
783
+ `BaseActivationForm` and saves them before activating (the GitHub form is
784
+ the reference). If your form passed such a child, delete it; the same copy
785
+ belongs in `permissions` / `capabilities`.
786
+
744
787
  A form is optional. Omit it and the host renders a generic one — a plain field
745
788
  per value it needs — rather than nothing.
746
789
 
747
790
  ### `capabilities` and `permissions`
748
791
 
749
- Both are copy for the integration detail page, and both are pure data. **Every
750
- string here is read by the person connecting the integration** — they render on
751
- the activation dialog, not just in a catalogue listing. Write `detail` as a
752
- sentence you would be happy for a customer's security reviewer to read, because
753
- that is who reads it.
792
+ Both are pure data describing what your integration does and touches. They
793
+ are **not** shown on the connect dialog — that step is only your name,
794
+ summary and form. They appear in the platform's product catalog, where a
795
+ source's administrators review an integration before offering it to teams.
796
+ Write `detail` as a sentence you would be happy for a customer's security
797
+ reviewer to read, because that is who reads it. For an OAuth integration,
798
+ make them match the scopes you request: the provider's consent screen is
799
+ what the person connecting actually sees.
754
800
 
755
- Exactly what appears, so you can write to the shape:
801
+ The shape each entry takes:
756
802
 
757
803
  | Section | Renders |
758
804
  |---|---|
759
805
  | **What it does** | `capability.label`, then ` — ` + `capability.description` when present. `capability.icon` is **not** rendered here |
760
806
  | **What it can access** | `permission.label`, a badge reading `Read` or `Write` from `permission.type`, then `permission.detail` on the line below |
761
807
 
762
- Both sections are omitted entirely when their array is empty. `type` drives
763
- only the badge — it does not reorder or group anything.
808
+ An empty array simply declares nothing. `type` drives only the Read/Write
809
+ label — it does not reorder or group anything.
764
810
 
765
811
  ```ts
766
812
  capabilities: [
@@ -810,7 +856,8 @@ export const paymentUpdatedWebhook: PartnerWebhookDeclaration<MyStorage> = {
810
856
  payloadSchema: PaymentUpdatedPayloadSchema,
811
857
 
812
858
  // How the TRANSPORT verifies. `'none'` states explicitly that the source
813
- // is unsigned. Your handler never checks a signature.
859
+ // is unsigned. Your handler never checks a signature. Add `scheme` when the
860
+ // provider is not a plain HMAC-SHA256 digest of the body — see below.
814
861
  signature: { header: 'x-acme-signature', secretName: 'acme_webhook_secret' },
815
862
 
816
863
  // Seeds the harness's payload editor and documents the shape beside the
@@ -871,6 +918,51 @@ Signature verification is the transport's job in every environment — the host
871
918
  ingress enforces it, the local harness logs it as skipped. Do not verify it
872
919
  yourself; you would be verifying a header the transport already consumed.
873
920
 
921
+ If a vendor's scheme is not expressible yet and you declare `'none'`, the
922
+ delivery is authenticated only by the endpoint token in its URL. Treat the
923
+ body as a hint: take the ids from it and re-fetch the item through
924
+ `ctx.fetch` before writing anything a widget renders, and confirm removals
925
+ with the vendor too. A forged delivery then costs one read instead of a
926
+ planted name or link on the dashboard.
927
+
928
+ #### `signature.scheme` — when the provider is not a plain body digest
929
+
930
+ Without `scheme`, the transport expects the header to be an HMAC-SHA256 of the
931
+ raw body, hex or base64, optionally labelled `sha256=` (GitHub, Linear).
932
+ Most other providers differ in one of three ways: they
933
+ pack the digest into `k=v` pairs, they sign a timestamp along with the body, or
934
+ they use SHA-1. `scheme` describes that declaratively; the transport does the
935
+ arithmetic and your handler still contains no verification code.
936
+
937
+ | Provider | `scheme` |
938
+ |---|---|
939
+ | Stripe, Calendly (`t=…,v1=…`, HMAC over `"<t>.<body>"`) | `{ format: 'keyed', digestKey: 'v1', timestampKey: 't', signedPayload: '{timestamp}.{body}', toleranceSeconds: 300 }` |
940
+ | Retell (`v=<unix_ms>,d=<hex>`, HMAC over `body + timestamp`) | `{ format: 'keyed', digestKey: 'd', timestampKey: 'v', signedPayload: '{body}{timestamp}' }` |
941
+ | Intercom (`X-Hub-Signature: sha1=…`) | `{ algorithm: 'sha1' }` |
942
+ | Svix-style single signature (`svix-timestamp` header, HMAC over `"<ts>.<body>"`) | `{ timestampHeader: 'svix-timestamp', signedPayload: '{timestamp}.{body}' }` |
943
+
944
+ Fields: `algorithm` (`'sha256'` default, or `'sha1'`), `format` (`'digest'`
945
+ default, or `'keyed'` for `k=v,k=v` headers), `digestKey` / `timestampKey`
946
+ (which pairs, keyed only), `timestampHeader` (a separate header instead of a
947
+ pair), `signedPayload` (a template over `{body}` and `{timestamp}`; default
948
+ `'{body}'`), `toleranceSeconds` (replay window whenever a timestamp source is
949
+ declared; default 300, `0` disables). Seconds and milliseconds are both
950
+ accepted from the sender. `defineIntegration()` rejects a scheme that cannot be
951
+ verified — a keyed format with no `digestKey`, a `{timestamp}` with no source,
952
+ a timestamp source whose value is **not** in `signedPayload` (a replay window
953
+ over an unsigned timestamp protects nothing), a transport header name such as
954
+ `authorization` — so the mistake surfaces before the first delivery, not as a
955
+ 401 on it. Linear's replay guard is a field inside the signed body, so for
956
+ Linear you omit `scheme` and check `webhookTimestamp` in the handler.
957
+
958
+ Two things the scheme never does: it never takes the algorithm from the
959
+ header (a `sha1=` label under a `sha256` declaration fails), and it never lets
960
+ a stale delivery through because the digest was right — freshness is checked
961
+ first. Svix-style multi-signature headers (`v1,<b64> v1,<b64>`, space-separated
962
+ with a `whsec_` base64 secret) are not covered yet; declare `'none'` for those
963
+ and say so in the description. Slack (`v0=` label over `v0:{ts}:{body}`) is
964
+ not covered either.
965
+
874
966
  ### `schedules`
875
967
 
876
968
  A cron-driven run. Same handler shape, same `ctx`.
@@ -1031,17 +1123,22 @@ either be ignored or aim the host's callback somewhere it should not go.
1031
1123
  Register this with your provider, against whichever host will run the flow:
1032
1124
 
1033
1125
  ```
1034
- https://<your-fusion-host>/api/integrations/<your-slug>/callback
1126
+ https://<your-ekanos-host>/api/integrations/<your-slug>/callback
1035
1127
  ```
1036
1128
 
1037
- The dev harness does not serve that route, so you cannot complete a round trip
1038
- locally. What it *can* check is that both endpoints you declared are inside
1039
- your egress list.
1129
+ The dev harness serves its own loopback callback, so you can complete a real
1130
+ round trip locally in live mode: register
1131
+ `http://localhost:3100/api/integrations/<your-slug>/oauth/callback` with your
1132
+ provider as well. See "Local OAuth" in the `@ekanos/harness` README for the
1133
+ secrets file and the connect flow.
1040
1134
 
1041
- **Honest limitation:** there is no local OAuth loop yet. The harness's Triggers
1042
- panel renders the declaration and checks its egress coverage, but it cannot walk
1043
- you through an authorize redirect. Until that lands, unit-test `onTokens`
1044
- directly against `createMockContext()` — it is an ordinary async function.
1135
+ In **live mode** the harness runs this loop for real on localhost — authorize
1136
+ redirect, single-use `state`, code exchange through your declared egress, then
1137
+ your `onTokens`. Client credentials come from `.ekanos/local-secrets.json`,
1138
+ which the harness generates and ignores in version control. In fixtures mode it
1139
+ answers connection status from the harness entry instead, and you can still
1140
+ unit-test `onTokens` directly against `createMockContext()` — it is an ordinary
1141
+ async function.
1045
1142
 
1046
1143
  ### `proposes`
1047
1144
 
@@ -1189,7 +1286,7 @@ and the host adapts the definition internally.
1189
1286
 
1190
1287
  ### Styling
1191
1288
 
1192
- `@ekanos/ui` components emit Tailwind class strings against Fusion's semantic
1289
+ `@ekanos/ui` components emit Tailwind class strings against Ekanos's semantic
1193
1290
  tokens, and icons are Font Awesome glyphs the host loads. Import
1194
1291
  `@ekanos/ui/styles.css` (or the narrower `tokens.css` / `theme.css` /
1195
1292
  `base.css`) to get the token layer — that also draws a `□` placeholder for
@@ -1232,12 +1329,11 @@ whether it is you or us costs an afternoon.
1232
1329
  get an allowlisted fetch, so it needs a small seam of its own — a React
1233
1330
  context whose default is the browser's `fetch`. The `@ekanos/harness` README
1234
1331
  has the whole ~20-line pattern; the dev harness's live mode mounts it for you.
1235
- - **No local OAuth loop.** See [`oauth`](#oauth) above.
1236
- - **`onActivate` is declared and testable, but the host does not call it yet.**
1237
- It runs in the `@ekanos/harness` activation surface and under
1238
- `invokeActivate()`, but production has no wired call site as of this
1239
- writing — declare it, test it locally, and it starts running the moment host
1240
- wiring lands with no change on your side.
1332
+ - **`onActivate` errors are non-fatal.** The host runs it after an activation
1333
+ is first persisted and again when activation data changes, but a throw is
1334
+ logged and the activation stays connected — use it to seed caches and
1335
+ validate eagerly, never as a connect gate. It also runs in the
1336
+ `@ekanos/harness` activation surface and under `invokeActivate()`.
1241
1337
  - **`useActivateIntegration()` is inert without the host's provider.** It reads
1242
1338
  server actions out of `IntegrationActivationProvider`. Nothing warns you at
1243
1339
  build time.
package/api-report.md CHANGED
@@ -9,7 +9,7 @@ This is the complete surface of every entrypoint. The authoring guide
9
9
  for these symbols is README.md. Entrypoints marked WORKSPACE-ONLY are
10
10
  not reachable from a published install — see the note in each section.
11
11
 
12
- Total exported names: **167** across 7 entrypoints.
12
+ Total exported names: **172** across 7 entrypoints.
13
13
 
14
14
  ## `@ekanos/sdk` (32 exports)
15
15
 
@@ -48,7 +48,7 @@ Total exported names: **167** across 7 entrypoints.
48
48
  | `WidgetSuggestedPrompt` | interface |
49
49
  | `WorkspaceTargetDefinition` | interface |
50
50
 
51
- ## `@ekanos/sdk/components` (24 exports)
51
+ ## `@ekanos/sdk/components` (25 exports)
52
52
 
53
53
  | Export | Kind |
54
54
  | --- | --- |
@@ -68,6 +68,7 @@ Total exported names: **167** across 7 entrypoints.
68
68
  | `WidgetAskBar` | function |
69
69
  | `WidgetContext` | variable |
70
70
  | `WidgetContextValue` | interface |
71
+ | `WidgetData` | interface |
71
72
  | `WidgetDataLoading` | function |
72
73
  | `WidgetHealth` | interface |
73
74
  | `WidgetMeta` | interface |
@@ -190,10 +191,11 @@ Total exported names: **167** across 7 entrypoints.
190
191
  | `invokeSchedule` | function |
191
192
  | `invokeWebhook` | function |
192
193
 
193
- ## `@ekanos/sdk/integration` (27 exports)
194
+ ## `@ekanos/sdk/integration` (31 exports)
194
195
 
195
196
  | Export | Kind |
196
197
  | --- | --- |
198
+ | `DEFAULT_SIGNATURE_TOLERANCE_SECONDS` | variable |
197
199
  | `DefinitionCollisionInput` | interface |
198
200
  | `FirstPartyInventory` | interface |
199
201
  | `IntegrationCapabilityDeclaration` | type alias |
@@ -205,6 +207,8 @@ Total exported names: **167** across 7 entrypoints.
205
207
  | `OAuthProviderDeclaration` | interface |
206
208
  | `OAuthTokens` | interface |
207
209
  | `OnActivateHandler` | type alias |
210
+ | `PARTNER_STORAGE_LIMITS` | variable |
211
+ | `PartnerActionDeclaration` | interface |
208
212
  | `PartnerOAuthDeclaration` | interface |
209
213
  | `PartnerScheduleDeclaration` | interface |
210
214
  | `PartnerToolModule` | interface |
@@ -218,6 +222,7 @@ Total exported names: **167** across 7 entrypoints.
218
222
  | `WebhookEvent` | interface |
219
223
  | `WebhookResult` | interface |
220
224
  | `WebhookSignatureDeclaration` | type alias |
225
+ | `WebhookSignatureScheme` | interface |
221
226
  | `defineIntegration` | function |
222
227
  | `parseCronExpression` | function |
223
228
  | `validateIntegrationDefinitions` | function |
@@ -16,15 +16,25 @@ interface BaseActivationDialogProps {
16
16
  inline?: boolean;
17
17
  }
18
18
  export declare function BaseActivationDialog({ isOpen, onClose, title, description, children, isLoading, error, success, successTitle, successMessage, onSuccessClose, maxWidth, inline, }: BaseActivationDialogProps): import("react").JSX.Element;
19
- export declare function ActivationInline({ isLoading, error, success, successTitle, successMessage, onClose, children, }: {
19
+ /**
20
+ * The form's body when it sits inside chrome the HOST owns (the connect
21
+ * dialog, the workspace notice, first-use). The host already shows the
22
+ * integration's name, what it does and what it can access, and it owns
23
+ * the success moment (it closes and toasts) — so this renders the fields,
24
+ * the error, and nothing else.
25
+ *
26
+ * `success` is a hand-off, not a screen: the moment it turns true,
27
+ * `onSuccessClose` runs once, which is what lets a form written for the
28
+ * old "Continue" panel finish the same way as one that reports success
29
+ * directly. Nothing renders after it fires; the host takes over.
30
+ */
31
+ export declare function ActivationInline({ isLoading, error, success, onSuccessClose, children, }: {
20
32
  isLoading?: boolean;
21
33
  error?: string | null;
22
34
  success?: boolean;
23
- successTitle?: string;
24
- successMessage?: string;
25
- onClose: () => void;
35
+ onSuccessClose?: () => void;
26
36
  children: ReactNode;
27
- }): import("react").JSX.Element;
37
+ }): import("react").JSX.Element | null;
28
38
  export declare function ActivationDialog({ isOpen, onClose, title, description, children, isLoading, error, success, successTitle, successMessage, maxWidth, }: {
29
39
  isOpen: boolean;
30
40
  onClose: () => void;
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from 'react';
3
4
  import { Alert, AlertDescription } from '@ekanos/ui/alert';
4
5
  import { Button } from '@ekanos/ui/button';
5
6
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@ekanos/ui/dialog';
@@ -12,10 +13,10 @@ function SuccessContent({ successTitle = 'Integration Activated!', successMessag
12
13
  return (_jsxs("div", { className: "flex flex-col items-center space-y-4 py-6", children: [_jsx("div", { className: "bg-success/10 flex h-16 w-16 items-center justify-center rounded-full", children: _jsx(Icon, { name: "fa-light fa-circle-check", className: "text-success h-8 w-8" }) }), _jsxs("div", { className: "text-center", children: [_jsx("h3", { className: "text-lg font-semibold", children: successTitle }), _jsx("p", { className: "text-muted-foreground mt-1 text-sm", children: successMessage })] }), showSeparator && _jsx(Separator, {}), _jsxs("div", { className: "flex gap-2", children: [showSeparator && (_jsx(Button, { variant: "outline", onClick: onClose, children: _jsx(Trans, { i18nKey: "common:stayOnMarketplace", defaults: "Stay on Marketplace" }) })), _jsx(Button, { onClick: onClose, children: _jsx(Trans, { i18nKey: "common:continue", defaults: "Continue" }) })] })] }));
13
14
  }
14
15
  function ErrorAlert({ error }) {
15
- return (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: error }) }));
16
+ return (_jsx(Alert, { variant: "destructive", role: "alert", children: _jsx(AlertDescription, { children: error }) }));
16
17
  }
17
18
  function LoadingOverlay() {
18
- return (_jsx("div", { className: "bg-background/80 fixed inset-0 z-[1000] flex items-center justify-center", children: _jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(Icon, { name: "fa-light fa-spinner", className: "text-primary h-8 w-8 animate-spin" }), _jsx("span", { className: "text-muted-foreground text-sm", children: "Loading..." })] }) }));
19
+ return (_jsx("div", { className: "bg-background/80 fixed inset-0 z-[1000] flex items-center justify-center", children: _jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(Icon, { name: "fa-light fa-spinner-third", className: "text-primary h-8 w-8 animate-spin" }), _jsx("span", { className: "text-muted-foreground text-sm", children: "Loading..." })] }) }));
19
20
  }
20
21
  export function BaseActivationDialog({ isOpen, onClose, title, description, children, isLoading = false, error = null, success = false, successTitle, successMessage, onSuccessClose, maxWidth = 'sm:max-w-[600px]', inline = false, }) {
21
22
  const handleClose = () => {
@@ -27,13 +28,38 @@ export function BaseActivationDialog({ isOpen, onClose, title, description, chil
27
28
  }
28
29
  };
29
30
  if (inline) {
30
- return (_jsx(ActivationInline, { isLoading: isLoading, error: error, success: success, successTitle: successTitle, successMessage: successMessage, onClose: handleClose, children: children }));
31
+ return (_jsx(ActivationInline, { isLoading: isLoading, error: error, success: success, onSuccessClose: success ? handleClose : undefined, children: children }));
31
32
  }
32
33
  return (_jsx(ActivationDialog, { isOpen: isOpen, onClose: handleClose, title: title, description: description, isLoading: isLoading, error: error, success: success, successTitle: successTitle, successMessage: successMessage, maxWidth: maxWidth, children: children }));
33
34
  }
34
35
  // --- Explicit Variants ---
35
- export function ActivationInline({ isLoading = false, error = null, success = false, successTitle, successMessage, onClose, children, }) {
36
- return (_jsxs("div", { className: cn('space-y-4', isLoading && 'pointer-events-none opacity-60'), "aria-busy": isLoading, children: [_jsx(If, { condition: !!error, children: _jsx(ErrorAlert, { error: error }) }), _jsx(If, { condition: success, fallback: children, children: _jsx(SuccessContent, { successTitle: successTitle, successMessage: successMessage, onClose: onClose }) })] }));
36
+ /**
37
+ * The form's body when it sits inside chrome the HOST owns (the connect
38
+ * dialog, the workspace notice, first-use). The host already shows the
39
+ * integration's name, what it does and what it can access, and it owns
40
+ * the success moment (it closes and toasts) — so this renders the fields,
41
+ * the error, and nothing else.
42
+ *
43
+ * `success` is a hand-off, not a screen: the moment it turns true,
44
+ * `onSuccessClose` runs once, which is what lets a form written for the
45
+ * old "Continue" panel finish the same way as one that reports success
46
+ * directly. Nothing renders after it fires; the host takes over.
47
+ */
48
+ export function ActivationInline({ isLoading = false, error = null, success = false, onSuccessClose, children, }) {
49
+ const handedOff = useRef(false);
50
+ // Hands the success state to the host exactly once. An effect is the
51
+ // right tool here: this reacts to a state transition that the form's own
52
+ // submit handler set, and the host callback it calls navigates.
53
+ useEffect(() => {
54
+ if (!success || !onSuccessClose || handedOff.current)
55
+ return;
56
+ handedOff.current = true;
57
+ onSuccessClose();
58
+ }, [success, onSuccessClose]);
59
+ if (success) {
60
+ return null;
61
+ }
62
+ return (_jsxs("div", { className: cn('flex flex-col gap-4', isLoading && 'opacity-70'), "aria-busy": isLoading, children: [_jsx(If, { condition: !!error, children: _jsx(ErrorAlert, { error: error }) }), children] }));
37
63
  }
38
64
  export function ActivationDialog({ isOpen, onClose, title, description, children, isLoading = false, error = null, success = false, successTitle, successMessage, maxWidth = 'sm:max-w-[600px]', }) {
39
65
  return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: _jsxs(DialogContent, { className: `${maxWidth} max-h-[80vh] overflow-y-auto`, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: description })] }), _jsx(If, { condition: !!error, children: _jsx(ErrorAlert, { error: error }) }), _jsx(If, { condition: success, fallback: children, children: _jsx(SuccessContent, { successTitle: successTitle, successMessage: successMessage, onClose: onClose, showSeparator: true }) }), isLoading && _jsx(LoadingOverlay, {})] }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"base-activation-dialog.js","sourceRoot":"","sources":["../../src/components/base-activation-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { ReactNode } from 'react';\n\nimport { Alert, AlertDescription } from '@ekanos/ui/alert';\nimport { Button } from '@ekanos/ui/button';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@ekanos/ui/dialog';\nimport { Icon } from '@ekanos/ui/icon';\nimport { If } from '@ekanos/ui/if';\nimport { Separator } from '@ekanos/ui/separator';\nimport { Trans } from '@ekanos/ui/trans';\nimport { cn } from '@ekanos/ui/utils';\n\ninterface BaseActivationDialogProps {\n isOpen: boolean;\n onClose: () => void;\n title: string;\n description: string;\n children: ReactNode;\n isLoading?: boolean;\n error?: string | null;\n success?: boolean;\n successTitle?: string;\n successMessage?: string;\n onSuccessClose?: () => void;\n maxWidth?: string;\n /** @deprecated Use `ActivationInline` instead for inline rendering */\n inline?: boolean;\n}\n\nfunction SuccessContent({\n successTitle = 'Integration Activated!',\n successMessage = 'Your integration is now active and ready to use.',\n onClose,\n showSeparator = false,\n}: {\n successTitle?: string;\n successMessage?: string;\n onClose: () => void;\n showSeparator?: boolean;\n}) {\n return (\n <div className=\"flex flex-col items-center space-y-4 py-6\">\n <div className=\"bg-success/10 flex h-16 w-16 items-center justify-center rounded-full\">\n <Icon\n name=\"fa-light fa-circle-check\"\n className=\"text-success h-8 w-8\"\n />\n </div>\n <div className=\"text-center\">\n <h3 className=\"text-lg font-semibold\">{successTitle}</h3>\n <p className=\"text-muted-foreground mt-1 text-sm\">{successMessage}</p>\n </div>\n {showSeparator && <Separator />}\n <div className=\"flex gap-2\">\n {showSeparator && (\n <Button variant=\"outline\" onClick={onClose}>\n <Trans\n i18nKey=\"common:stayOnMarketplace\"\n defaults=\"Stay on Marketplace\"\n />\n </Button>\n )}\n <Button onClick={onClose}>\n <Trans i18nKey=\"common:continue\" defaults=\"Continue\" />\n </Button>\n </div>\n </div>\n );\n}\n\nfunction ErrorAlert({ error }: { error: string }) {\n return (\n <Alert variant=\"destructive\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n );\n}\n\nfunction LoadingOverlay() {\n return (\n <div className=\"bg-background/80 fixed inset-0 z-[1000] flex items-center justify-center\">\n <div className=\"flex flex-col items-center gap-2\">\n <Icon\n name=\"fa-light fa-spinner\"\n className=\"text-primary h-8 w-8 animate-spin\"\n />\n <span className=\"text-muted-foreground text-sm\">Loading...</span>\n </div>\n </div>\n );\n}\n\nexport function BaseActivationDialog({\n isOpen,\n onClose,\n title,\n description,\n children,\n isLoading = false,\n error = null,\n success = false,\n successTitle,\n successMessage,\n onSuccessClose,\n maxWidth = 'sm:max-w-[600px]',\n inline = false,\n}: BaseActivationDialogProps) {\n const handleClose = () => {\n if (success && onSuccessClose) {\n onSuccessClose();\n } else {\n onClose();\n }\n };\n\n if (inline) {\n return (\n <ActivationInline\n isLoading={isLoading}\n error={error}\n success={success}\n successTitle={successTitle}\n successMessage={successMessage}\n onClose={handleClose}\n >\n {children}\n </ActivationInline>\n );\n }\n\n return (\n <ActivationDialog\n isOpen={isOpen}\n onClose={handleClose}\n title={title}\n description={description}\n isLoading={isLoading}\n error={error}\n success={success}\n successTitle={successTitle}\n successMessage={successMessage}\n maxWidth={maxWidth}\n >\n {children}\n </ActivationDialog>\n );\n}\n\n// --- Explicit Variants ---\n\nexport function ActivationInline({\n isLoading = false,\n error = null,\n success = false,\n successTitle,\n successMessage,\n onClose,\n children,\n}: {\n isLoading?: boolean;\n error?: string | null;\n success?: boolean;\n successTitle?: string;\n successMessage?: string;\n onClose: () => void;\n children: ReactNode;\n}) {\n return (\n <div\n className={cn('space-y-4', isLoading && 'pointer-events-none opacity-60')}\n aria-busy={isLoading}\n >\n <If condition={!!error}>\n <ErrorAlert error={error!} />\n </If>\n <If condition={success} fallback={children}>\n <SuccessContent\n successTitle={successTitle}\n successMessage={successMessage}\n onClose={onClose}\n />\n </If>\n </div>\n );\n}\n\nexport function ActivationDialog({\n isOpen,\n onClose,\n title,\n description,\n children,\n isLoading = false,\n error = null,\n success = false,\n successTitle,\n successMessage,\n maxWidth = 'sm:max-w-[600px]',\n}: {\n isOpen: boolean;\n onClose: () => void;\n title: string;\n description: string;\n children: ReactNode;\n isLoading?: boolean;\n error?: string | null;\n success?: boolean;\n successTitle?: string;\n successMessage?: string;\n maxWidth?: string;\n}) {\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>\n <DialogContent className={`${maxWidth} max-h-[80vh] overflow-y-auto`}>\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n\n <If condition={!!error}>\n <ErrorAlert error={error!} />\n </If>\n\n <If condition={success} fallback={children}>\n <SuccessContent\n successTitle={successTitle}\n successMessage={successMessage}\n onClose={onClose}\n showSeparator\n />\n </If>\n\n {isLoading && <LoadingOverlay />}\n </DialogContent>\n </Dialog>\n );\n}\n"]}
1
+ {"version":3,"file":"base-activation-dialog.js","sourceRoot":"","sources":["../../src/components/base-activation-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { ReactNode, useEffect, useRef } from 'react';\n\nimport { Alert, AlertDescription } from '@ekanos/ui/alert';\nimport { Button } from '@ekanos/ui/button';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@ekanos/ui/dialog';\nimport { Icon } from '@ekanos/ui/icon';\nimport { If } from '@ekanos/ui/if';\nimport { Separator } from '@ekanos/ui/separator';\nimport { Trans } from '@ekanos/ui/trans';\nimport { cn } from '@ekanos/ui/utils';\n\ninterface BaseActivationDialogProps {\n isOpen: boolean;\n onClose: () => void;\n title: string;\n description: string;\n children: ReactNode;\n isLoading?: boolean;\n error?: string | null;\n success?: boolean;\n successTitle?: string;\n successMessage?: string;\n onSuccessClose?: () => void;\n maxWidth?: string;\n /** @deprecated Use `ActivationInline` instead for inline rendering */\n inline?: boolean;\n}\n\nfunction SuccessContent({\n successTitle = 'Integration Activated!',\n successMessage = 'Your integration is now active and ready to use.',\n onClose,\n showSeparator = false,\n}: {\n successTitle?: string;\n successMessage?: string;\n onClose: () => void;\n showSeparator?: boolean;\n}) {\n return (\n <div className=\"flex flex-col items-center space-y-4 py-6\">\n <div className=\"bg-success/10 flex h-16 w-16 items-center justify-center rounded-full\">\n <Icon\n name=\"fa-light fa-circle-check\"\n className=\"text-success h-8 w-8\"\n />\n </div>\n <div className=\"text-center\">\n <h3 className=\"text-lg font-semibold\">{successTitle}</h3>\n <p className=\"text-muted-foreground mt-1 text-sm\">{successMessage}</p>\n </div>\n {showSeparator && <Separator />}\n <div className=\"flex gap-2\">\n {showSeparator && (\n <Button variant=\"outline\" onClick={onClose}>\n <Trans\n i18nKey=\"common:stayOnMarketplace\"\n defaults=\"Stay on Marketplace\"\n />\n </Button>\n )}\n <Button onClick={onClose}>\n <Trans i18nKey=\"common:continue\" defaults=\"Continue\" />\n </Button>\n </div>\n </div>\n );\n}\n\nfunction ErrorAlert({ error }: { error: string }) {\n return (\n <Alert variant=\"destructive\" role=\"alert\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n );\n}\n\nfunction LoadingOverlay() {\n return (\n <div className=\"bg-background/80 fixed inset-0 z-[1000] flex items-center justify-center\">\n <div className=\"flex flex-col items-center gap-2\">\n <Icon\n name=\"fa-light fa-spinner-third\"\n className=\"text-primary h-8 w-8 animate-spin\"\n />\n <span className=\"text-muted-foreground text-sm\">Loading...</span>\n </div>\n </div>\n );\n}\n\nexport function BaseActivationDialog({\n isOpen,\n onClose,\n title,\n description,\n children,\n isLoading = false,\n error = null,\n success = false,\n successTitle,\n successMessage,\n onSuccessClose,\n maxWidth = 'sm:max-w-[600px]',\n inline = false,\n}: BaseActivationDialogProps) {\n const handleClose = () => {\n if (success && onSuccessClose) {\n onSuccessClose();\n } else {\n onClose();\n }\n };\n\n if (inline) {\n return (\n <ActivationInline\n isLoading={isLoading}\n error={error}\n success={success}\n onSuccessClose={success ? handleClose : undefined}\n >\n {children}\n </ActivationInline>\n );\n }\n\n return (\n <ActivationDialog\n isOpen={isOpen}\n onClose={handleClose}\n title={title}\n description={description}\n isLoading={isLoading}\n error={error}\n success={success}\n successTitle={successTitle}\n successMessage={successMessage}\n maxWidth={maxWidth}\n >\n {children}\n </ActivationDialog>\n );\n}\n\n// --- Explicit Variants ---\n\n/**\n * The form's body when it sits inside chrome the HOST owns (the connect\n * dialog, the workspace notice, first-use). The host already shows the\n * integration's name, what it does and what it can access, and it owns\n * the success moment (it closes and toasts) — so this renders the fields,\n * the error, and nothing else.\n *\n * `success` is a hand-off, not a screen: the moment it turns true,\n * `onSuccessClose` runs once, which is what lets a form written for the\n * old \"Continue\" panel finish the same way as one that reports success\n * directly. Nothing renders after it fires; the host takes over.\n */\nexport function ActivationInline({\n isLoading = false,\n error = null,\n success = false,\n onSuccessClose,\n children,\n}: {\n isLoading?: boolean;\n error?: string | null;\n success?: boolean;\n onSuccessClose?: () => void;\n children: ReactNode;\n}) {\n const handedOff = useRef(false);\n\n // Hands the success state to the host exactly once. An effect is the\n // right tool here: this reacts to a state transition that the form's own\n // submit handler set, and the host callback it calls navigates.\n useEffect(() => {\n if (!success || !onSuccessClose || handedOff.current) return;\n\n handedOff.current = true;\n onSuccessClose();\n }, [success, onSuccessClose]);\n\n if (success) {\n return null;\n }\n\n return (\n <div\n className={cn('flex flex-col gap-4', isLoading && 'opacity-70')}\n aria-busy={isLoading}\n >\n <If condition={!!error}>\n <ErrorAlert error={error!} />\n </If>\n {children}\n </div>\n );\n}\n\nexport function ActivationDialog({\n isOpen,\n onClose,\n title,\n description,\n children,\n isLoading = false,\n error = null,\n success = false,\n successTitle,\n successMessage,\n maxWidth = 'sm:max-w-[600px]',\n}: {\n isOpen: boolean;\n onClose: () => void;\n title: string;\n description: string;\n children: ReactNode;\n isLoading?: boolean;\n error?: string | null;\n success?: boolean;\n successTitle?: string;\n successMessage?: string;\n maxWidth?: string;\n}) {\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>\n <DialogContent className={`${maxWidth} max-h-[80vh] overflow-y-auto`}>\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n\n <If condition={!!error}>\n <ErrorAlert error={error!} />\n </If>\n\n <If condition={success} fallback={children}>\n <SuccessContent\n successTitle={successTitle}\n successMessage={successMessage}\n onClose={onClose}\n showSeparator\n />\n </If>\n\n {isLoading && <LoadingOverlay />}\n </DialogContent>\n </Dialog>\n );\n}\n"]}