@ekanos/sdk 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,174 @@
2
2
 
3
3
  The official SDK for building Ekanos integrations.
4
4
 
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @ekanos/sdk
9
+ ```
10
+
11
+ `@ekanos/integration-schema` and `@ekanos/ui` come along as dependencies. You
12
+ provide the peers: `react` (^19.2.8), `zod` (^3.25.76), and — if you render
13
+ widgets or an activation form — `@tanstack/react-query` (^5.101.4),
14
+ `react-hook-form` (^7.68.0), `@hookform/resolvers` (^5.2.2), and `next`
15
+ (^16.0.0).
16
+
17
+ ## Quick start
18
+
19
+ An integration is one call to `defineIntegration()`. It validates at import
20
+ time and hands back a deep-frozen definition; the host re-parses the same
21
+ object against the same schema when it registers you.
22
+
23
+ ```ts
24
+ // integration.ts
25
+ import { defineIntegration } from '@ekanos/sdk/integration';
26
+ import { z } from 'zod';
27
+
28
+ import { CurrentWeatherWidget } from './current-weather-widget';
29
+
30
+ const LocationSchema = z.object({
31
+ label: z.string().min(1),
32
+ latitude: z.number(),
33
+ longitude: z.number(),
34
+ });
35
+
36
+ export const weather = defineIntegration({
37
+ slug: 'acme-weather', // kebab-case; becomes the route + MCP namespace
38
+ name: 'Acme Weather',
39
+ description: 'Conditions for the places your team cares about.',
40
+ version: '1.0.0', // semver; promotion diffs definitions by it
41
+
42
+ // Origin-only https allowlist. `ctx.fetch` refuses anything else BEFORE it
43
+ // opens a socket — redirect targets included.
44
+ egress: ['https://api.open-meteo.com'],
45
+
46
+ // Every storage key declares a zod schema. `clientReadable` (default false)
47
+ // is what lets a widget read the key back through the host storage route.
48
+ storage: {
49
+ account: {
50
+ 'settings/location': { schema: LocationSchema, clientReadable: true },
51
+ 'cache/forecast': z.object({ fetchedAt: z.string() }),
52
+ },
53
+ },
54
+
55
+ components: {
56
+ widgets: [
57
+ {
58
+ id: 'acme-weather-current',
59
+ name: 'Current conditions',
60
+ component: CurrentWeatherWidget,
61
+ widgetState: 'active',
62
+ },
63
+ ],
64
+ },
65
+
66
+ tools: [
67
+ {
68
+ name: 'get_current_weather', // lowercase snake_case
69
+ description: 'Current conditions for the saved location.',
70
+ async run(ctx) {
71
+ const saved = await ctx.storage.account.get('settings/location');
72
+ if (!saved) return { error: 'No location saved.' };
73
+
74
+ const response = await ctx.fetch(
75
+ `https://api.open-meteo.com/v1/forecast?latitude=${saved.data.latitude}` +
76
+ `&longitude=${saved.data.longitude}&current=temperature_2m`,
77
+ );
78
+
79
+ return response.json();
80
+ },
81
+ },
82
+ ],
83
+ });
84
+ ```
85
+
86
+ `ctx` is the whole platform surface a handler gets: `ctx.storage` (already
87
+ bound to this account and product, schema-validated on every read and write),
88
+ `ctx.secrets`, `ctx.fetch` (egress-enforced), `ctx.logger`, plus identity
89
+ facts. There is no raw database client and no request object.
90
+
91
+ ### A widget
92
+
93
+ Widgets are client components that render through the `Widget.*` compound API.
94
+ Read `WidgetContext` first if you want to skip fetching while the integration
95
+ is inactive.
96
+
97
+ ```tsx
98
+ 'use client';
99
+
100
+ import { use } from 'react';
101
+
102
+ import type { IntegrationComponentProps } from '@ekanos/sdk';
103
+ import { Widget, WidgetContext } from '@ekanos/sdk/components';
104
+
105
+ export function CurrentWeatherWidget({ accountId }: IntegrationComponentProps) {
106
+ const ctx = use(WidgetContext);
107
+ const query = useMyData(accountId, ctx?.state.state === 'active');
108
+
109
+ return (
110
+ <Widget.DataState loading={query.isLoading} error={query.isError}>
111
+ <Widget.Card>
112
+ <Widget.Header title="Current conditions" />
113
+ <Widget.Loading />
114
+ <Widget.Error />
115
+ <Widget.Active>{/* … */}</Widget.Active>
116
+ </Widget.Card>
117
+ </Widget.DataState>
118
+ );
119
+ }
120
+ ```
121
+
122
+ Widget settings live in `ctx.storage`, which is server-side, so a widget reads
123
+ them back over HTTP with `fetchIntegrationStorage()` from `@ekanos/sdk/hooks`
124
+ — only keys declared `clientReadable: true` come back, and a key that did not
125
+ opt in is simply absent.
126
+
127
+ To render a widget anywhere other than the Fusion dashboard — your own app, a
128
+ story, a component test — wrap it in `WidgetPreviewProvider`, which fills the
129
+ same `WidgetContext` the host fills, with local state and no persistence:
130
+
131
+ ```tsx
132
+ import { WidgetPreviewProvider } from '@ekanos/sdk/components';
133
+
134
+ <WidgetPreviewProvider widgetId="acme-weather-current" title="Current conditions">
135
+ <CurrentWeatherWidget accountId={accountId} productSlug="acme-weather" />
136
+ </WidgetPreviewProvider>;
137
+ ```
138
+
139
+ ### Testing
140
+
141
+ `@ekanos/sdk/testing` gives you the same context production builds, in memory:
142
+ same schema validation, same egress allowlist, no network.
143
+
144
+ ```ts
145
+ import { createMockContext } from '@ekanos/sdk/testing';
146
+
147
+ const ctx = createMockContext({
148
+ integration: { slug: 'acme-weather' },
149
+ storageSchemas: weather.storage,
150
+ egress: weather.egress,
151
+ storage: { account: { 'settings/location': { label: 'HQ', latitude: 45, longitude: -122 } } },
152
+ fetchHandlers: [
153
+ { match: 'https://api.open-meteo.com', respond: () => Response.json({ current: {} }) },
154
+ ],
155
+ });
156
+
157
+ const result = await weather.tools[0].run(ctx, {});
158
+
159
+ expect(ctx.fetchCalls).toHaveLength(1);
160
+ ```
161
+
162
+ `invokeWebhook()` and `invokeSchedule()` do the same for declared event
163
+ surfaces, building the context FROM the definition so a handler can never be
164
+ tested against schemas or an allowlist it does not declare.
165
+
166
+ ### Styling
167
+
168
+ `@ekanos/ui` ships no CSS. Its components emit Tailwind class strings against
169
+ Fusion's semantic tokens, and icons are Font Awesome glyphs the host loads. In
170
+ your own app they render as correct but unstyled HTML, and icons render as
171
+ nothing at all — that is expected, not a bug in your code.
172
+
5
173
  ## Entrypoints
6
174
 
7
175
  | Import | Runs on | Contents |
@@ -18,7 +186,7 @@ The official SDK for building Ekanos integrations.
18
186
  carry no `server-only` marker — they are self-contained by construction and
19
187
  the pack test typechecks all three in a clean room (alongside the root). The
20
188
  capability context spec is
21
- [`docs/devex/capability-context-proposal.md`](../../docs/devex/capability-context-proposal.md).
189
+ [`docs/devex/capability-context-proposal.md`](https://github.com/companydotcom/fusion/blob/dev/docs/devex/capability-context-proposal.md).
22
190
  Partners never extend `BaseIntegration` — `defineIntegration()` is the v1
23
191
  contract, and the host adapts the definition via
24
192
  `registerPartnerIntegration` in `@kit/integrations-core` (typed there as a
@@ -30,7 +198,7 @@ structural twin, held in parity by
30
198
  - The root entrypoint is types-only and must stay importable from client code.
31
199
  Never add a value export to it.
32
200
  - Additions anywhere require a second consumer and an entry in
33
- [`docs/devex/sdk-export-map.md`](../../docs/devex/sdk-export-map.md) — the
201
+ [`docs/devex/sdk-export-map.md`](https://github.com/companydotcom/fusion/blob/dev/docs/devex/sdk-export-map.md) — the
34
202
  export map is the source of truth for what is public and why.
35
203
  - The inversion landed: this package OWNS the `/components`, `/hooks`, and
36
204
  `/mcp` implementations and `@kit/integrations-core` re-exports them. Never
@@ -67,12 +235,11 @@ pulled transitively) and executing a real
67
235
  | `pnpm --filter @ekanos/sdk pack:test` | Clean-room test: pack the SDK (and `@ekanos/integration-schema` + `@ekanos/ui`), install ONLY the SDK tarball outside the workspace with the declared peers, typecheck a consumer of EVERY published entrypoint (`skipLibCheck: false`), esbuild-bundle the client and server graphs, grep dist for `@kit/*` import specifiers, and EXECUTE `import("@ekanos/sdk/integration")` in real Node |
68
236
 
69
237
  The pack test was the forcing function for the SDK inversion
70
- ([export map](../../docs/devex/sdk-export-map.md), review outcome 6), and the
238
+ ([export map](https://github.com/companydotcom/fusion/blob/dev/docs/devex/sdk-export-map.md), review outcome 6), and the
71
239
  inversion landed: every published entrypoint (root, `/components`, `/hooks`,
72
240
  `/mcp`, `/context`, `/testing`, `/integration`) is typechecked in the clean
73
241
  room and must be GREEN — no skips remain. Nothing is a v1 contract until it
74
242
  passes.
75
243
 
76
- Publishing itself is a deliberate human step: flip `"private": true` and run
77
- `pnpm publish` (`publishConfig.access` is already `public`, version stays
78
- `0.0.0` until then).
244
+ Publishing is a deliberate human step, gated on `pack:test` being GREEN.
245
+ `publishConfig.access` is already `public`.
@@ -1,6 +1,17 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import Link from 'next/link';
3
+ // `next/link.js`, not `next/link`. `next` ships no `exports` map, so the
4
+ // extensionless subpath only resolves under a bundler — Node's own ESM loader
5
+ // throws ERR_MODULE_NOT_FOUND, and any partner running vitest or plain Node
6
+ // against the published tarball loses this whole module. The `.js` form is not
7
+ // a bypass of Next's resolution: `create-compiler-aliases.js` keys its aliases
8
+ // on the RESOLVED FILE PATH (`<next>/link.js`, with the comment "Handle fully
9
+ // specified imports like `next/image.js`"), so an App Router build still gets
10
+ // `next/dist/client/app-dir/link`, exactly as `next/link` would. Next's dist
11
+ // also ends with `Object.assign(exports.default, exports); module.exports =
12
+ // exports.default`, so Node's CJS-default interop and a bundler's `__esModule`
13
+ // interop land on the same component object. Do not "tidy" the extension away.
14
+ import Link from 'next/link.js';
4
15
  import { Badge } from '@ekanos/ui/badge';
5
16
  import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@ekanos/ui/card';
6
17
  import { Icon } from '@ekanos/ui/icon';
@@ -1 +1 @@
1
- {"version":3,"file":"base-marketplace-tile.js","sourceRoot":"","sources":["../../src/components/base-marketplace-tile.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport Link from 'next/link';\n\nimport { Badge } from '@ekanos/ui/badge';\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@ekanos/ui/card';\nimport { Icon } from '@ekanos/ui/icon';\nimport { If } from '@ekanos/ui/if';\nimport { cn } from '@ekanos/ui/utils';\n\nimport type { MarketplaceTileProps } from '../types/integration';\n\n// Type for product category\nexport interface ProductCategory {\n id: string;\n name: string;\n slug: string;\n description?: string;\n}\n\nexport type BaseMarketplaceTileProps = MarketplaceTileProps & {\n /** The display name of the product */\n name: string;\n /** Full description of the product (used for detail pages) */\n description: string;\n /** URL-friendly identifier for the product */\n slug: string;\n /** Short summary text displayed on the tile (1-2 sentences) */\n summary?: string;\n /** Name of the company/brand that provides this product */\n brandName?: string;\n /** Path to the product logo image */\n logoPath?: string;\n /** Text for the call-to-action button */\n ctaText?: string;\n /** External URL for the CTA (opens in new tab when set) */\n ctaLink?: string;\n\n /** Array of categories this product belongs to */\n categories?: ProductCategory[];\n\n /** Hex color from product schema for tile theming */\n productColor?: string;\n /** Additional CSS classes to apply to the tile */\n className?: string;\n\n /** Internal navigation URL for the integration detail page */\n href?: string;\n};\n\nexport function BaseMarketplaceTile(props: BaseMarketplaceTileProps) {\n const {\n name,\n summary,\n brandName,\n logoPath,\n categories = [],\n productColor,\n className,\n href,\n ctaLink,\n } = props;\n\n const displayTitle = name || 'Unknown Product';\n const displaySummary = summary;\n const displayBrandName = brandName || displayTitle;\n\n const cardContent = (\n <Card\n className={cn(\n 'group bg-card relative flex h-full w-full cursor-pointer flex-col overflow-hidden border-0 pt-3 pb-2 shadow-sm transition-all duration-300 hover:shadow-md',\n className,\n )}\n >\n {/* Header: Centered Logo, Title, Categories */}\n <CardHeader className=\"flex flex-col items-center pt-6 pb-3 text-center\">\n {/* Logo */}\n <div className=\"mb-3\">\n {logoPath ? (\n <div className=\"flex h-16 w-16 items-center justify-center\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={logoPath}\n alt={`${displayBrandName} logo`}\n width={64}\n height={64}\n className=\"max-h-full max-w-full object-contain\"\n onError={(e) => {\n const target = e.target as HTMLImageElement;\n target.style.display = 'none';\n if (target.nextSibling) {\n (target.nextSibling as HTMLElement).style.display = 'flex';\n }\n }}\n />\n <div className=\"bg-border ring-border/50 hidden h-full w-full items-center justify-center rounded-lg text-2xl font-semibold ring-1\">\n {displayBrandName.charAt(0).toUpperCase()}\n </div>\n </div>\n ) : (\n <div className=\"bg-border ring-border/50 flex h-16 w-16 items-center justify-center rounded-lg border-2 text-2xl font-semibold ring-1\">\n {displayBrandName.charAt(0).toUpperCase()}\n </div>\n )}\n </div>\n\n {/* Title */}\n <CardTitle className=\"line-clamp-2 text-xl leading-tight font-semibold\">\n {displayTitle}\n </CardTitle>\n\n {/* Category Badges - Under Title */}\n <If condition={categories.length > 0}>\n <div className=\"mt-2 flex flex-wrap justify-center gap-1\">\n {categories.slice(0, 2).map((cat) => (\n <Badge\n key={cat.id}\n variant=\"secondary\"\n className=\"px-2 py-0.5 text-xs font-medium\"\n style={{\n backgroundColor: productColor\n ? `${productColor}66`\n : undefined,\n }}\n >\n {cat.name}\n </Badge>\n ))}\n <If condition={categories.length > 2}>\n <Badge\n variant=\"secondary\"\n className=\"px-2 py-0.5 text-xs font-medium\"\n style={{\n backgroundColor: productColor\n ? `${productColor}66`\n : undefined,\n }}\n >\n +{categories.length - 2}\n </Badge>\n </If>\n </div>\n </If>\n </CardHeader>\n\n {/* Content: Summary */}\n <CardContent className=\"flex-1 pb-8 text-center\">\n <If condition={displaySummary}>\n <CardDescription className=\"text-muted-foreground line-clamp-2 text-base leading-relaxed\">\n {displaySummary}\n </CardDescription>\n </If>\n </CardContent>\n\n {/* Brand Name - Bottom Right */}\n <If condition={displayBrandName !== displayTitle}>\n <p className=\"text-muted-foreground/70 absolute right-3 bottom-3 text-sm\">\n {displayBrandName}\n </p>\n </If>\n\n {/* External Link Indicator */}\n <If condition={!!ctaLink}>\n <div\n className=\"text-muted-foreground/60 group-hover:text-foreground absolute bottom-3 left-3 transition-colors\"\n aria-hidden=\"true\"\n >\n <Icon\n name=\"fa-light fa-arrow-up-right-from-square\"\n className=\"h-4 w-4\"\n />\n </div>\n </If>\n </Card>\n );\n\n // External link: opens in new tab\n if (ctaLink) {\n return (\n <a\n href={ctaLink}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"block h-full\"\n aria-label={`${displayTitle} (opens in new tab)`}\n >\n {cardContent}\n </a>\n );\n }\n\n // Internal link: uses Next.js Link\n if (href) {\n return (\n <Link href={href} className=\"block h-full\">\n {cardContent}\n </Link>\n );\n }\n\n // Fallback: render without Link (for backwards compatibility)\n return cardContent;\n}\n"]}
1
+ {"version":3,"file":"base-marketplace-tile.js","sourceRoot":"","sources":["../../src/components/base-marketplace-tile.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\n// `next/link.js`, not `next/link`. `next` ships no `exports` map, so the\n// extensionless subpath only resolves under a bundler — Node's own ESM loader\n// throws ERR_MODULE_NOT_FOUND, and any partner running vitest or plain Node\n// against the published tarball loses this whole module. The `.js` form is not\n// a bypass of Next's resolution: `create-compiler-aliases.js` keys its aliases\n// on the RESOLVED FILE PATH (`<next>/link.js`, with the comment \"Handle fully\n// specified imports like `next/image.js`\"), so an App Router build still gets\n// `next/dist/client/app-dir/link`, exactly as `next/link` would. Next's dist\n// also ends with `Object.assign(exports.default, exports); module.exports =\n// exports.default`, so Node's CJS-default interop and a bundler's `__esModule`\n// interop land on the same component object. Do not \"tidy\" the extension away.\nimport Link from 'next/link.js';\n\nimport { Badge } from '@ekanos/ui/badge';\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@ekanos/ui/card';\nimport { Icon } from '@ekanos/ui/icon';\nimport { If } from '@ekanos/ui/if';\nimport { cn } from '@ekanos/ui/utils';\n\nimport type { MarketplaceTileProps } from '../types/integration';\n\n// Type for product category\nexport interface ProductCategory {\n id: string;\n name: string;\n slug: string;\n description?: string;\n}\n\nexport type BaseMarketplaceTileProps = MarketplaceTileProps & {\n /** The display name of the product */\n name: string;\n /** Full description of the product (used for detail pages) */\n description: string;\n /** URL-friendly identifier for the product */\n slug: string;\n /** Short summary text displayed on the tile (1-2 sentences) */\n summary?: string;\n /** Name of the company/brand that provides this product */\n brandName?: string;\n /** Path to the product logo image */\n logoPath?: string;\n /** Text for the call-to-action button */\n ctaText?: string;\n /** External URL for the CTA (opens in new tab when set) */\n ctaLink?: string;\n\n /** Array of categories this product belongs to */\n categories?: ProductCategory[];\n\n /** Hex color from product schema for tile theming */\n productColor?: string;\n /** Additional CSS classes to apply to the tile */\n className?: string;\n\n /** Internal navigation URL for the integration detail page */\n href?: string;\n};\n\nexport function BaseMarketplaceTile(props: BaseMarketplaceTileProps) {\n const {\n name,\n summary,\n brandName,\n logoPath,\n categories = [],\n productColor,\n className,\n href,\n ctaLink,\n } = props;\n\n const displayTitle = name || 'Unknown Product';\n const displaySummary = summary;\n const displayBrandName = brandName || displayTitle;\n\n const cardContent = (\n <Card\n className={cn(\n 'group bg-card relative flex h-full w-full cursor-pointer flex-col overflow-hidden border-0 pt-3 pb-2 shadow-sm transition-all duration-300 hover:shadow-md',\n className,\n )}\n >\n {/* Header: Centered Logo, Title, Categories */}\n <CardHeader className=\"flex flex-col items-center pt-6 pb-3 text-center\">\n {/* Logo */}\n <div className=\"mb-3\">\n {logoPath ? (\n <div className=\"flex h-16 w-16 items-center justify-center\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={logoPath}\n alt={`${displayBrandName} logo`}\n width={64}\n height={64}\n className=\"max-h-full max-w-full object-contain\"\n onError={(e) => {\n const target = e.target as HTMLImageElement;\n target.style.display = 'none';\n if (target.nextSibling) {\n (target.nextSibling as HTMLElement).style.display = 'flex';\n }\n }}\n />\n <div className=\"bg-border ring-border/50 hidden h-full w-full items-center justify-center rounded-lg text-2xl font-semibold ring-1\">\n {displayBrandName.charAt(0).toUpperCase()}\n </div>\n </div>\n ) : (\n <div className=\"bg-border ring-border/50 flex h-16 w-16 items-center justify-center rounded-lg border-2 text-2xl font-semibold ring-1\">\n {displayBrandName.charAt(0).toUpperCase()}\n </div>\n )}\n </div>\n\n {/* Title */}\n <CardTitle className=\"line-clamp-2 text-xl leading-tight font-semibold\">\n {displayTitle}\n </CardTitle>\n\n {/* Category Badges - Under Title */}\n <If condition={categories.length > 0}>\n <div className=\"mt-2 flex flex-wrap justify-center gap-1\">\n {categories.slice(0, 2).map((cat) => (\n <Badge\n key={cat.id}\n variant=\"secondary\"\n className=\"px-2 py-0.5 text-xs font-medium\"\n style={{\n backgroundColor: productColor\n ? `${productColor}66`\n : undefined,\n }}\n >\n {cat.name}\n </Badge>\n ))}\n <If condition={categories.length > 2}>\n <Badge\n variant=\"secondary\"\n className=\"px-2 py-0.5 text-xs font-medium\"\n style={{\n backgroundColor: productColor\n ? `${productColor}66`\n : undefined,\n }}\n >\n +{categories.length - 2}\n </Badge>\n </If>\n </div>\n </If>\n </CardHeader>\n\n {/* Content: Summary */}\n <CardContent className=\"flex-1 pb-8 text-center\">\n <If condition={displaySummary}>\n <CardDescription className=\"text-muted-foreground line-clamp-2 text-base leading-relaxed\">\n {displaySummary}\n </CardDescription>\n </If>\n </CardContent>\n\n {/* Brand Name - Bottom Right */}\n <If condition={displayBrandName !== displayTitle}>\n <p className=\"text-muted-foreground/70 absolute right-3 bottom-3 text-sm\">\n {displayBrandName}\n </p>\n </If>\n\n {/* External Link Indicator */}\n <If condition={!!ctaLink}>\n <div\n className=\"text-muted-foreground/60 group-hover:text-foreground absolute bottom-3 left-3 transition-colors\"\n aria-hidden=\"true\"\n >\n <Icon\n name=\"fa-light fa-arrow-up-right-from-square\"\n className=\"h-4 w-4\"\n />\n </div>\n </If>\n </Card>\n );\n\n // External link: opens in new tab\n if (ctaLink) {\n return (\n <a\n href={ctaLink}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"block h-full\"\n aria-label={`${displayTitle} (opens in new tab)`}\n >\n {cardContent}\n </a>\n );\n }\n\n // Internal link: uses Next.js Link\n if (href) {\n return (\n <Link href={href} className=\"block h-full\">\n {cardContent}\n </Link>\n );\n }\n\n // Fallback: render without Link (for backwards compatibility)\n return cardContent;\n}\n"]}
@@ -26,6 +26,8 @@
26
26
  */
27
27
  export { Widget } from './widgets/widget.js';
28
28
  export { WidgetContext, type WidgetActions, type WidgetContextValue, type WidgetHealth, type WidgetMeta, type WidgetRenderState, type WidgetState, } from './widgets/widget-context.js';
29
+ export { WidgetPreviewProvider } from './widgets/widget-preview-provider.js';
30
+ export type { WidgetPreviewProviderProps } from './widgets/widget-preview-provider.js';
29
31
  export { AiPromptChip } from './widgets/ai-prompt-chip.js';
30
32
  export { AskIcon } from './widgets/ask-icon.js';
31
33
  export { WidgetAskBar } from './widgets/widget-ask-bar.js';
@@ -26,6 +26,7 @@
26
26
  */
27
27
  export { Widget } from './widgets/widget.js';
28
28
  export { WidgetContext, } from './widgets/widget-context.js';
29
+ export { WidgetPreviewProvider } from './widgets/widget-preview-provider.js';
29
30
  export { AiPromptChip } from './widgets/ai-prompt-chip.js';
30
31
  export { AskIcon } from './widgets/ask-icon.js';
31
32
  export { WidgetAskBar } from './widgets/widget-ask-bar.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EACL,aAAa,GAOd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAEtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAG9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["/**\n * @ekanos/sdk/components — client-side building blocks.\n *\n * Import from client components only. `Widget` is the compound API every\n * dashboard widget renders through; the activation forms are the two\n * sanctioned activation UX bases (credential-style and OAuth-style), and\n * `BaseMarketplaceTile` is the matching base for the marketplace tile.\n *\n * The SDK OWNS these implementations — the pre-inversion re-exports through\n * `@kit/integrations-core/components/*` are gone; core now re-exports FROM\n * here for first-party code (one source of truth, two consumption paths —\n * DEVELOPER_EXPERIENCE_PLAN.md §5.1). UI primitives come from `@ekanos/ui`\n * (the published slice of the host design system), never `@kit/ui`.\n *\n * Server seam: nothing here imports a server module. `OAuthActivationForm`\n * performs activation through the host-injected actions seam\n * (`IntegrationActivationProvider`, @ekanos/sdk/hooks) — a server-action\n * REFERENCE bound by the host, not server code shipped to the browser.\n *\n * `WidgetContext` is the single context identity both sides share: the\n * host's `DashboardWidgetProvider` (host chrome — persistence callbacks,\n * config hierarchy) provides it, the SDK's `Widget.*` reads it.\n *\n * Surface discipline: additions require an entry in\n * docs/devex/sdk-export-map.md (\"`@ekanos/sdk/components`\").\n */\n\nexport { Widget } from './widgets/widget';\nexport {\n WidgetContext,\n type WidgetActions,\n type WidgetContextValue,\n type WidgetHealth,\n type WidgetMeta,\n type WidgetRenderState,\n type WidgetState,\n} from './widgets/widget-context';\nexport { AiPromptChip } from './widgets/ai-prompt-chip';\nexport { AskIcon } from './widgets/ask-icon';\nexport { WidgetAskBar } from './widgets/widget-ask-bar';\nexport { WidgetDataLoading } from './widgets/widget-state-components';\n\nexport { BaseActivationForm } from './base-activation-form';\nexport type { BaseActivationFormProps } from './base-activation-form';\nexport {\n ActivationDialog,\n ActivationInline,\n BaseActivationDialog,\n} from './base-activation-dialog';\n\nexport { BaseMarketplaceTile } from './base-marketplace-tile';\nexport type { BaseMarketplaceTileProps } from './base-marketplace-tile';\n\nexport { OAuthActivationForm } from './oauth-activation-form';\nexport type { OAuthActivationFormConfig } from './oauth-activation-form';\n\nexport { currentReturnPath } from './current-return-path';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EACL,aAAa,GAOd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAE1E,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAEtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAG9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["/**\n * @ekanos/sdk/components — client-side building blocks.\n *\n * Import from client components only. `Widget` is the compound API every\n * dashboard widget renders through; the activation forms are the two\n * sanctioned activation UX bases (credential-style and OAuth-style), and\n * `BaseMarketplaceTile` is the matching base for the marketplace tile.\n *\n * The SDK OWNS these implementations — the pre-inversion re-exports through\n * `@kit/integrations-core/components/*` are gone; core now re-exports FROM\n * here for first-party code (one source of truth, two consumption paths —\n * DEVELOPER_EXPERIENCE_PLAN.md §5.1). UI primitives come from `@ekanos/ui`\n * (the published slice of the host design system), never `@kit/ui`.\n *\n * Server seam: nothing here imports a server module. `OAuthActivationForm`\n * performs activation through the host-injected actions seam\n * (`IntegrationActivationProvider`, @ekanos/sdk/hooks) — a server-action\n * REFERENCE bound by the host, not server code shipped to the browser.\n *\n * `WidgetContext` is the single context identity both sides share: the\n * host's `DashboardWidgetProvider` (host chrome — persistence callbacks,\n * config hierarchy) provides it, the SDK's `Widget.*` reads it.\n *\n * Surface discipline: additions require an entry in\n * docs/devex/sdk-export-map.md (\"`@ekanos/sdk/components`\").\n */\n\nexport { Widget } from './widgets/widget';\nexport {\n WidgetContext,\n type WidgetActions,\n type WidgetContextValue,\n type WidgetHealth,\n type WidgetMeta,\n type WidgetRenderState,\n type WidgetState,\n} from './widgets/widget-context';\nexport { WidgetPreviewProvider } from './widgets/widget-preview-provider';\nexport type { WidgetPreviewProviderProps } from './widgets/widget-preview-provider';\nexport { AiPromptChip } from './widgets/ai-prompt-chip';\nexport { AskIcon } from './widgets/ask-icon';\nexport { WidgetAskBar } from './widgets/widget-ask-bar';\nexport { WidgetDataLoading } from './widgets/widget-state-components';\n\nexport { BaseActivationForm } from './base-activation-form';\nexport type { BaseActivationFormProps } from './base-activation-form';\nexport {\n ActivationDialog,\n ActivationInline,\n BaseActivationDialog,\n} from './base-activation-dialog';\n\nexport { BaseMarketplaceTile } from './base-marketplace-tile';\nexport type { BaseMarketplaceTileProps } from './base-marketplace-tile';\n\nexport { OAuthActivationForm } from './oauth-activation-form';\nexport type { OAuthActivationFormConfig } from './oauth-activation-form';\n\nexport { currentReturnPath } from './current-return-path';\n"]}
@@ -0,0 +1,41 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { WidgetAskContext } from '../../types/widget-ask-context.js';
3
+ import { type WidgetHealth, type WidgetRenderState } from './widget-context.js';
4
+ export interface WidgetPreviewProviderProps {
5
+ widgetId: string;
6
+ title?: string;
7
+ subtitle?: string;
8
+ description?: string;
9
+ className?: string;
10
+ /** Defaults to `'active'` — the state you almost always want to look at. */
11
+ state?: WidgetRenderState;
12
+ isCollapsible?: boolean;
13
+ isPinnable?: boolean;
14
+ defaultCollapsed?: boolean;
15
+ defaultPinned?: boolean;
16
+ maxContentHeight?: string;
17
+ askContext?: WidgetAskContext;
18
+ aiFooterEnabled?: boolean;
19
+ health?: WidgetHealth;
20
+ children: ReactNode;
21
+ }
22
+ /**
23
+ * Renders a widget outside the Fusion dashboard.
24
+ *
25
+ * `Widget.*` reads everything — render state, collapse/pin, title — from
26
+ * `WidgetContext`, which in production is filled by the host's
27
+ * `DashboardWidgetProvider` (config hierarchy, persistence callbacks, health
28
+ * resolution). None of that exists in a partner's own app, a Storybook story,
29
+ * or a component test, and without SOME provider every `Widget.*` throws. This
30
+ * is that provider: same context value, local state only, nothing persisted.
31
+ *
32
+ * ```tsx
33
+ * <WidgetPreviewProvider widgetId="my-widget" title="My widget">
34
+ * <MyWidget accountId={accountId} productSlug="my-integration" />
35
+ * </WidgetPreviewProvider>
36
+ * ```
37
+ *
38
+ * Flip `state` to walk every branch your widget renders (`'loading'`,
39
+ * `'error'`, `'inactive'`, `'disabled'`) without faking a network condition.
40
+ */
41
+ export declare function WidgetPreviewProvider({ widgetId, title, subtitle, description, className, state, isCollapsible, isPinnable, defaultCollapsed, defaultPinned, maxContentHeight, askContext, aiFooterEnabled, health, children, }: WidgetPreviewProviderProps): import("react").JSX.Element;
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useCallback, useMemo, useState } from 'react';
4
+ import { WidgetContext, } from './widget-context.js';
5
+ /**
6
+ * Renders a widget outside the Fusion dashboard.
7
+ *
8
+ * `Widget.*` reads everything — render state, collapse/pin, title — from
9
+ * `WidgetContext`, which in production is filled by the host's
10
+ * `DashboardWidgetProvider` (config hierarchy, persistence callbacks, health
11
+ * resolution). None of that exists in a partner's own app, a Storybook story,
12
+ * or a component test, and without SOME provider every `Widget.*` throws. This
13
+ * is that provider: same context value, local state only, nothing persisted.
14
+ *
15
+ * ```tsx
16
+ * <WidgetPreviewProvider widgetId="my-widget" title="My widget">
17
+ * <MyWidget accountId={accountId} productSlug="my-integration" />
18
+ * </WidgetPreviewProvider>
19
+ * ```
20
+ *
21
+ * Flip `state` to walk every branch your widget renders (`'loading'`,
22
+ * `'error'`, `'inactive'`, `'disabled'`) without faking a network condition.
23
+ */
24
+ export function WidgetPreviewProvider({ widgetId, title, subtitle, description, className, state = 'active', isCollapsible = false, isPinnable = false, defaultCollapsed = false, defaultPinned = false, maxContentHeight, askContext, aiFooterEnabled = false, health, children, }) {
25
+ const [collapsed, setCollapsed] = useState(defaultCollapsed);
26
+ const [pinned, setPinned] = useState(defaultPinned);
27
+ const toggleCollapsed = useCallback(() => setCollapsed((previous) => !previous), []);
28
+ const togglePinned = useCallback(() => setPinned((previous) => !previous), []);
29
+ const value = useMemo(() => ({
30
+ state: {
31
+ state,
32
+ collapsed,
33
+ pinned,
34
+ pendingCollapse: false,
35
+ pendingPin: false,
36
+ },
37
+ actions: { toggleCollapsed, togglePinned },
38
+ meta: {
39
+ widgetId,
40
+ title,
41
+ subtitle,
42
+ description,
43
+ className,
44
+ maxContentHeight,
45
+ isCollapsible,
46
+ isPinnable,
47
+ askContext,
48
+ aiFooterEnabled,
49
+ health,
50
+ },
51
+ }), [
52
+ aiFooterEnabled,
53
+ askContext,
54
+ className,
55
+ collapsed,
56
+ description,
57
+ health,
58
+ isCollapsible,
59
+ isPinnable,
60
+ maxContentHeight,
61
+ pinned,
62
+ state,
63
+ subtitle,
64
+ title,
65
+ toggleCollapsed,
66
+ togglePinned,
67
+ widgetId,
68
+ ]);
69
+ return _jsx(WidgetContext, { value: value, children: children });
70
+ }
71
+ //# sourceMappingURL=widget-preview-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-preview-provider.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-preview-provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAkB,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGvE,OAAO,EACL,aAAa,GAId,MAAM,kBAAkB,CAAC;AAqB1B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,KAAK,GAAG,QAAQ,EAChB,aAAa,GAAG,KAAK,EACrB,UAAU,GAAG,KAAK,EAClB,gBAAgB,GAAG,KAAK,EACxB,aAAa,GAAG,KAAK,EACrB,gBAAgB,EAChB,UAAU,EACV,eAAe,GAAG,KAAK,EACvB,MAAM,EACN,QAAQ,GACmB;IAC3B,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAC7D,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;IAEpD,MAAM,eAAe,GAAG,WAAW,CACjC,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAC3C,EAAE,CACH,CAAC;IAEF,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;IAE/E,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,KAAK,EAAE;YACL,KAAK;YACL,SAAS;YACT,MAAM;YACN,eAAe,EAAE,KAAK;YACtB,UAAU,EAAE,KAAK;SAClB;QACD,OAAO,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;QAC1C,IAAI,EAAE;YACJ,QAAQ;YACR,KAAK;YACL,QAAQ;YACR,WAAW;YACX,SAAS;YACT,gBAAgB;YAChB,aAAa;YACb,UAAU;YACV,UAAU;YACV,eAAe;YACf,MAAM;SACP;KACF,CAAC,EACF;QACE,eAAe;QACf,UAAU;QACV,SAAS;QACT,SAAS;QACT,WAAW;QACX,MAAM;QACN,aAAa;QACb,UAAU;QACV,gBAAgB;QAChB,MAAM;QACN,KAAK;QACL,QAAQ;QACR,KAAK;QACL,eAAe;QACf,YAAY;QACZ,QAAQ;KACT,CACF,CAAC;IAEF,OAAO,KAAC,aAAa,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAiB,CAAC;AACjE,CAAC","sourcesContent":["'use client';\n\nimport { type ReactNode, useCallback, useMemo, useState } from 'react';\n\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport {\n WidgetContext,\n type WidgetContextValue,\n type WidgetHealth,\n type WidgetRenderState,\n} from './widget-context';\n\nexport interface WidgetPreviewProviderProps {\n widgetId: string;\n title?: string;\n subtitle?: string;\n description?: string;\n className?: string;\n /** Defaults to `'active'` — the state you almost always want to look at. */\n state?: WidgetRenderState;\n isCollapsible?: boolean;\n isPinnable?: boolean;\n defaultCollapsed?: boolean;\n defaultPinned?: boolean;\n maxContentHeight?: string;\n askContext?: WidgetAskContext;\n aiFooterEnabled?: boolean;\n health?: WidgetHealth;\n children: ReactNode;\n}\n\n/**\n * Renders a widget outside the Fusion dashboard.\n *\n * `Widget.*` reads everything — render state, collapse/pin, title — from\n * `WidgetContext`, which in production is filled by the host's\n * `DashboardWidgetProvider` (config hierarchy, persistence callbacks, health\n * resolution). None of that exists in a partner's own app, a Storybook story,\n * or a component test, and without SOME provider every `Widget.*` throws. This\n * is that provider: same context value, local state only, nothing persisted.\n *\n * ```tsx\n * <WidgetPreviewProvider widgetId=\"my-widget\" title=\"My widget\">\n * <MyWidget accountId={accountId} productSlug=\"my-integration\" />\n * </WidgetPreviewProvider>\n * ```\n *\n * Flip `state` to walk every branch your widget renders (`'loading'`,\n * `'error'`, `'inactive'`, `'disabled'`) without faking a network condition.\n */\nexport function WidgetPreviewProvider({\n widgetId,\n title,\n subtitle,\n description,\n className,\n state = 'active',\n isCollapsible = false,\n isPinnable = false,\n defaultCollapsed = false,\n defaultPinned = false,\n maxContentHeight,\n askContext,\n aiFooterEnabled = false,\n health,\n children,\n}: WidgetPreviewProviderProps) {\n const [collapsed, setCollapsed] = useState(defaultCollapsed);\n const [pinned, setPinned] = useState(defaultPinned);\n\n const toggleCollapsed = useCallback(\n () => setCollapsed((previous) => !previous),\n [],\n );\n\n const togglePinned = useCallback(() => setPinned((previous) => !previous), []);\n\n const value = useMemo<WidgetContextValue>(\n () => ({\n state: {\n state,\n collapsed,\n pinned,\n pendingCollapse: false,\n pendingPin: false,\n },\n actions: { toggleCollapsed, togglePinned },\n meta: {\n widgetId,\n title,\n subtitle,\n description,\n className,\n maxContentHeight,\n isCollapsible,\n isPinnable,\n askContext,\n aiFooterEnabled,\n health,\n },\n }),\n [\n aiFooterEnabled,\n askContext,\n className,\n collapsed,\n description,\n health,\n isCollapsible,\n isPinnable,\n maxContentHeight,\n pinned,\n state,\n subtitle,\n title,\n toggleCollapsed,\n togglePinned,\n widgetId,\n ],\n );\n\n return <WidgetContext value={value}>{children}</WidgetContext>;\n}\n"]}
@@ -34,7 +34,10 @@ class WidgetErrorBoundary extends Component {
34
34
  function useWidget() {
35
35
  const ctx = use(WidgetContext);
36
36
  if (!ctx) {
37
- throw new Error('Widget.* compound components must be rendered inside a WidgetProvider.');
37
+ throw new Error('Widget.* compound components must be rendered inside a widget provider. ' +
38
+ 'On the Fusion dashboard the host supplies one. Anywhere else — your own ' +
39
+ 'app, a story, a component test — wrap the widget in ' +
40
+ "`WidgetPreviewProvider` from '@ekanos/sdk/components'.");
38
41
  }
39
42
  return ctx;
40
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"widget.js","sourceRoot":"","sources":["../../../src/components/widgets/widget.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,SAAS,EAAkB,GAAG,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAEhE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EACL,IAAI,EACJ,WAAW,EACX,UAAU,EACV,UAAU,EACV,SAAS,GACV,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EACL,OAAO,EACP,cAAc,EACd,eAAe,EACf,cAAc,GACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAGtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,aAAa,EAA0B,MAAM,kBAAkB,CAAC;AACzE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,2BAA2B,CAAC;AAEnC,+EAA+E;AAE/E,MAAM,mBAAoB,SAAQ,SAGjC;IAHD;;QAIE,UAAK,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAkC9B,CAAC;IAjCC,MAAM,CAAC,wBAAwB;QAC7B,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC;IACD,iBAAiB,CAAC,KAAY,EAAE,IAAqB;QACnD,OAAO,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,QAAQ,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IACzE,CAAC;IACD,MAAM;QACJ,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACxB,OAAO,CACL,eAAK,SAAS,EAAC,4DAA4D,aACzE,cAAK,SAAS,EAAC,2EAA2E,YACxF,KAAC,IAAI,IACH,IAAI,EAAC,gCAAgC,EACrC,SAAS,EAAC,0BAA0B,GACpC,GACE,EACN,YAAG,SAAS,EAAC,6CAA6C,YACxD,KAAC,KAAK,IACJ,OAAO,EAAC,8BAA8B,EACtC,QAAQ,EAAC,sBAAsB,GAC/B,GACA,EACJ,YAAG,SAAS,EAAC,oDAAoD,YAC/D,KAAC,KAAK,IACJ,OAAO,EAAC,6BAA6B,EACrC,QAAQ,EAAC,4DAA4D,GACrE,GACA,IACA,CACP,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;IAC7B,CAAC;CACF;AAED,+EAA+E;AAE/E,SAAS,SAAS;IAChB,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,+EAA+E;AAE/E,SAAS,UAAU,CAAC,EAAE,QAAQ,EAA2B;IACvD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,EAAE,CAAC;IAC7B,OAAO,CACL,KAAC,IAAI,IACH,SAAS,EAAE,EAAE;QACX,iEAAiE;QACjE,kEAAkE;QAClE,4FAA4F,EAC5F,IAAI,CAAC,SAAS,CACf,oBACe,IAAI,CAAC,QAAQ,YAE5B,QAAQ,GACJ,CACR,CAAC;AACJ,CAAC;AAED,+EAA+E;AAE/E,SAAS,cAAc;;IACrB,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,SAAS,EAAE,CAAC;IAC7C,OAAO,CACL,KAAC,MAAM,IACL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,MAAM,EACX,OAAO,EAAE,OAAO,CAAC,eAAe,EAChC,QAAQ,EAAE,KAAK,CAAC,eAAe,EAC/B,SAAS,EAAC,0MAA0M,mBACrM,CAAC,KAAK,CAAC,SAAS,mBAChB,kBAAkB,IAAI,CAAC,QAAQ,EAAE,gBAE9C,KAAK,CAAC,SAAS;YACb,CAAC,CAAC,UAAU,MAAA,IAAI,CAAC,KAAK,mCAAI,QAAQ,EAAE;YACpC,CAAC,CAAC,YAAY,MAAA,IAAI,CAAC,KAAK,mCAAI,QAAQ,EAAE,YAG1C,KAAC,IAAI,IACH,IAAI,EAAC,2BAA2B,EAChC,SAAS,EAAE,EAAE,CACX,2GAA2G,EAC3G,CAAC,KAAK,CAAC,SAAS,IAAI,WAAW,CAChC,GACD,GACK,CACV,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,EACpB,KAAK,EACL,QAAQ,EACR,WAAW,EACX,QAAQ,GAST;IACC,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,EAAE,CAAC;IAC7B,MAAM,cAAc,GAAG,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,IAAI,CAAC,KAAK,CAAC;IAC3C,MAAM,iBAAiB,GAAG,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,IAAI,CAAC,QAAQ,CAAC;IACpD,MAAM,oBAAoB,GAAG,WAAW,aAAX,WAAW,cAAX,WAAW,GAAI,IAAI,CAAC,WAAW,CAAC;IAC7D,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,CACL,KAAC,UAAU,IAAC,SAAS,EAAC,mDAAmD,YACvE,eAAK,SAAS,EAAC,wCAAwC,aACrD,eAAK,SAAS,EAAC,yBAAyB,aACrC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,KAAC,cAAc,KAAG,CAAC,CAAC,CAAC,IAAI,EAC9C,oBAAoB,CAAC,CAAC,CAAC,CACtB,8BACE,KAAC,eAAe,cACd,MAAC,OAAO,eACN,KAAC,cAAc,IACb,MAAM,EACJ,KAAC,SAAS,IAAC,SAAS,EAAC,mCAAmC,GAAG,YAG5D,cAAc,GACA,EACjB,KAAC,cAAc,cACb,sBAAI,oBAAoB,GAAK,GACd,IACT,GACM,EAClB,eAAM,SAAS,EAAC,SAAS,YAAE,oBAAoB,GAAQ,IACtD,CACJ,CAAC,CAAC,CAAC,CACF,KAAC,SAAS,IAAC,SAAS,EAAC,uBAAuB,YACzC,cAAc,GACL,CACb,EACA,iBAAiB,IAAI,CACpB,eAAM,SAAS,EAAC,6CAA6C,YAC1D,iBAAiB,GACb,CACR,IACG,EACL,QAAQ,IAAI,CACX,cAAK,SAAS,EAAC,kCAAkC,YAAE,QAAQ,GAAO,CACnE,IACG,GACK,CACd,CAAC;AACJ,CAAC;AAED,+EAA+E;AAE/E,SAAS,eAAe,CAAC,EACvB,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,QAAQ,GAMT;IACC,OAAO,CACL,cACE,SAAS,EAAE,EAAE,CACX,kIAAkI,EAClI,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,iBAAiB,CAClD,YAED,cAAK,SAAS,EAAC,yBAAyB,YACtC,KAAC,WAAW,IACV,EAAE,EAAE,kBAAkB,QAAQ,EAAE,EAChC,SAAS,EAAE,EAAE,CACX,yDAAyD,EACzD,iGAAiG,EACjG,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,CACxC,EACD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,SAAS,YAEpE,QAAQ,GACG,GACV,GACF,CACP,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,EAClB,gBAAgB,EAChB,QAAQ,GAIT;IACC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,iBAAiB,EAAE,CAAC;IACjD,OAAO,CACL,KAAC,WAAW,IAAC,SAAS,EAAC,kCAAkC,YACvD,cACE,SAAS,EAAC,yFAAyF,EACnG,KAAK,EAAE;gBACL,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAE,MAAgB,CAAC,CAAC,CAAC,MAAM;gBACtD,SAAS,EAAE,gBAAgB,aAAhB,gBAAgB,cAAhB,gBAAgB,GAAI,SAAS;aACzC,YAED,cAAK,GAAG,EAAE,QAAQ,YAAG,QAAQ,GAAO,GAChC,GACM,CACf,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,EAAE,QAAQ,EAA2B;IACzD,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,SAAS,EAAE,CAAC;IACpC,IAAI,KAAK,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,IAAI,GAAG,CACX,KAAC,mBAAmB,IAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,YACzC,QAAQ,GACW,CACvB,CAAC;IACF,OAAO,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAC1B,KAAC,eAAe,IACd,SAAS,EAAE,KAAK,CAAC,SAAS,EAC1B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EACvC,QAAQ,EAAE,IAAI,CAAC,QAAQ,YAEtB,IAAI,GACW,CACnB,CAAC,CAAC,CAAC,CACF,KAAC,UAAU,IAAC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,YAAG,IAAI,GAAc,CACzE,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,EAAE,QAAQ,EAA4B;IAC3D,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;IAC9B,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC3C,OAAO,CACL,KAAC,WAAW,IAAC,SAAS,EAAC,kCAAkC,YACtD,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,KAAC,kBAAkB,KAAG,GACvB,CACf,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,QAAQ,EAA4B;IACzD,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;IAC9B,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,CACL,KAAC,WAAW,IAAC,SAAS,EAAC,kCAAkC,YACtD,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,KAAC,gBAAgB,KAAG,GACrB,CACf,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,EAAE,QAAQ,EAA4B;IAC5D,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;IAC9B,IAAI,KAAK,CAAC,KAAK,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,CACL,KAAC,WAAW,IAAC,SAAS,EAAC,kCAAkC,YACtD,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,KAAC,mBAAmB,KAAG,GACxB,CACf,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,EAAE,QAAQ,EAA4B;IAC5D,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;IAC9B,IAAI,KAAK,CAAC,KAAK,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,CACL,KAAC,WAAW,IAAC,SAAS,EAAC,kCAAkC,YACtD,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,KAAC,mBAAmB,KAAG,GACxB,CACf,CAAC;AACJ,CAAC;AAED,+EAA+E;AAE/E,SAAS,YAAY,CAAC,EAAE,QAAQ,EAA2B;IACzD,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,SAAS,EAAE,CAAC;IACpC,IAAI,KAAK,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,OAAO,CACL,KAAC,UAAU,IACT,SAAS,EAAE,EAAE,CACX,oHAAoH,EACpH,IAAI,CAAC,aAAa,IAAI,KAAK,CAAC,SAAS;YACnC,CAAC,CAAC,+BAA+B;YACjC,CAAC,CAAC,aAAa,CAClB,iBACY,IAAI,CAAC,aAAa,IAAI,KAAK,CAAC,SAAS,YAEjD,QAAQ,GACE,CACd,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,EAAE,QAAQ,EAA2B;IAC1D,OAAO,CACL,KAAC,UAAU,IAAC,SAAS,EAAC,kDAAkD,YACrE,QAAQ,GACE,CACd,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,EAAE,OAAO,EAAkC;;IAClE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,SAAS,EAAE,CAAC;IACpC,MAAM,gBAAgB,GAAG,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,IAAI,CAAC,UAAU,CAAC;IACpD,MAAM,UAAU,GACd,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,CAAC,gBAAgB,CAAC;QAC5C,KAAK,CAAC,KAAK,KAAK,QAAQ;QACxB,CAAC,KAAK,CAAC,SAAS,CAAC;IACnB,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7B,OAAO,CACL,KAAC,YAAY,IACX,OAAO,EACL,gBAAgB,aAAhB,gBAAgB,cAAhB,gBAAgB,GAAI;YAClB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,KAAK,EAAE,MAAA,IAAI,CAAC,KAAK,mCAAI,IAAI,CAAC,QAAQ;YAClC,QAAQ,EAAE,IAAI;YACd,eAAe,EAAE,iBAAiB,MAAA,IAAI,CAAC,KAAK,mCAAI,aAAa,GAAG;SACjE,GAEH,CACH,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB;;IACzB,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,SAAS,EAAE,CAAC;IACpC,MAAM,UAAU,GACd,KAAK,CAAC,KAAK,KAAK,QAAQ;QACxB,CAAC,KAAK,CAAC,SAAS;QAChB,CAAA,MAAA,IAAI,CAAC,MAAM,0CAAE,MAAM,MAAK,UAAU,CAAC;IACrC,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7B,OAAO,CACL,eAAK,SAAS,EAAC,gGAAgG,aAC7G,KAAC,IAAI,IACH,IAAI,EAAC,gCAAgC,EACrC,SAAS,EAAC,kBAAkB,wBAE5B,EACF,yBACE,KAAC,KAAK,IACJ,OAAO,EAAC,+CAA+C,EACvD,QAAQ,EAAC,wDAAmD,GAC5D,GACG,IACH,CACP,CAAC;AACJ,CAAC;AAED,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,eAAe,CAAC,EACvB,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAQ,GAYT;IACC,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,aAAa,GACjB,MAAM,CAAC,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC7B,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK;QACpB,CAAC,CAAC,QAAQ;YACR,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,OAAO;gBACP,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,KAAK;oBACL,CAAC,CAAC,OAAO;oBACT,CAAC,CAAC,QAAQ,CAAC;IACrB,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,KAAK,kCAAO,MAAM,CAAC,KAAK,KAAE,KAAK,EAAE,aAAa,GAAE;QAChD,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,IAAI,EAAE,MAAM,CAAC,IAAI;KAClB,CAAC,EACF,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAC3D,CAAC;IACF,OAAO,KAAC,aAAa,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAiB,CAAC;AACjE,CAAC;AAED,+EAA+E;AAE/E,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,YAAY;IACpB,SAAS,EAAE,eAAe;IAC1B,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,aAAa;IACtB,KAAK,EAAE,WAAW;IAClB,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,cAAc;IACxB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,aAAa;IACtB,SAAS,EAAE,eAAe;IAC1B,YAAY,EAAE,kBAAkB;CACjC,CAAC","sourcesContent":["'use client';\n\nimport { Component, type ReactNode, use, useMemo } from 'react';\n\nimport { Button } from '@ekanos/ui/button';\nimport {\n Card,\n CardContent,\n CardFooter,\n CardHeader,\n CardTitle,\n} from '@ekanos/ui/card';\nimport { Icon } from '@ekanos/ui/icon';\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from '@ekanos/ui/tooltip';\nimport { Trans } from '@ekanos/ui/trans';\nimport { cn } from '@ekanos/ui/utils';\n\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport { useAnimatedHeight } from './use-animated-height';\nimport { WidgetAskBar } from './widget-ask-bar';\nimport { WidgetContext, type WidgetRenderState } from './widget-context';\nimport {\n WidgetDisabledState,\n WidgetErrorState,\n WidgetInactiveState,\n WidgetLoadingState,\n} from './widget-state-components';\n\n// ─── Error Boundary ─────────────────────────────────────────────────────────\n\nclass WidgetErrorBoundary extends Component<\n { widgetId: string; children: ReactNode },\n { hasError: boolean }\n> {\n state = { hasError: false };\n static getDerivedStateFromError() {\n return { hasError: true };\n }\n componentDidCatch(error: Error, info: React.ErrorInfo) {\n console.error(`Widget \"${this.props.widgetId}\" crashed:`, error, info);\n }\n render() {\n if (this.state.hasError) {\n return (\n <div className=\"flex flex-1 flex-col items-center justify-center gap-3 p-8\">\n <div className=\"bg-destructive/10 flex h-16 w-16 items-center justify-center rounded-full\">\n <Icon\n name=\"fa-light fa-circle-exclamation\"\n className=\"text-destructive h-8 w-8\"\n />\n </div>\n <p className=\"text-muted-foreground text-base font-medium\">\n <Trans\n i18nKey=\"dashboard:widgetCrashedTitle\"\n defaults=\"Something went wrong\"\n />\n </p>\n <p className=\"text-muted-foreground max-w-xs text-center text-sm\">\n <Trans\n i18nKey=\"dashboard:widgetCrashedBody\"\n defaults=\"This widget encountered an error. Try refreshing the page.\"\n />\n </p>\n </div>\n );\n }\n return this.props.children;\n }\n}\n\n// ─── Hook ───────────────────────────────────────────────────────────────────\n\nfunction useWidget() {\n const ctx = use(WidgetContext);\n if (!ctx) {\n throw new Error(\n 'Widget.* compound components must be rendered inside a WidgetProvider.',\n );\n }\n return ctx;\n}\n\n// ─── Card ───────────────────────────────────────────────────────────────────\n\nfunction WidgetCard({ children }: { children: ReactNode }) {\n const { meta } = useWidget();\n return (\n <Card\n className={cn(\n // Neutralize the shadcn card's own py-6/gap-6 box model — widget\n // chrome owns all vertical spacing via its header/content/footer.\n 'shadow-widget relative flex h-full flex-col gap-0 overflow-hidden rounded-lg border-0 py-0',\n meta.className,\n )}\n data-widget-id={meta.widgetId}\n >\n {children}\n </Card>\n );\n}\n\n// ─── Header ─────────────────────────────────────────────────────────────────\n\nfunction CollapseButton() {\n const { state, actions, meta } = useWidget();\n return (\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={actions.toggleCollapsed}\n disabled={state.pendingCollapse}\n className=\"bg-background dark:bg-secondary relative h-6 w-6 rounded-full before:absolute before:top-1/2 before:left-1/2 before:h-11 before:w-11 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']\"\n aria-expanded={!state.collapsed}\n aria-controls={`widget-content-${meta.widgetId}`}\n aria-label={\n state.collapsed\n ? `Expand ${meta.title ?? 'widget'}`\n : `Collapse ${meta.title ?? 'widget'}`\n }\n >\n <Icon\n name=\"fa-light fa-chevron-right\"\n className={cn(\n 'h-4 w-4 transition-transform duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none',\n !state.collapsed && 'rotate-90',\n )}\n />\n </Button>\n );\n}\n\nfunction WidgetHeader({\n title,\n subtitle,\n description,\n children,\n}: {\n /** Override the provider's title — useful when the rendered name differs from the config name. */\n title?: string;\n /** Override the provider's subtitle — useful for widget-computed values. */\n subtitle?: string;\n /** Override the provider's description — useful for widget-computed values. */\n description?: string;\n children?: ReactNode;\n}) {\n const { meta } = useWidget();\n const effectiveTitle = title ?? meta.title;\n const effectiveSubtitle = subtitle ?? meta.subtitle;\n const effectiveDescription = description ?? meta.description;\n if (!effectiveTitle) return null;\n return (\n <CardHeader className=\"flex-shrink-0 border-b pt-6 pb-4 [.border-b]:pb-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2\">\n {meta.isCollapsible ? <CollapseButton /> : null}\n {effectiveDescription ? (\n <>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger\n render={\n <CardTitle className=\"cursor-help text-lg font-semibold\" />\n }\n >\n {effectiveTitle}\n </TooltipTrigger>\n <TooltipContent>\n <p>{effectiveDescription}</p>\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n <span className=\"sr-only\">{effectiveDescription}</span>\n </>\n ) : (\n <CardTitle className=\"text-lg font-semibold\">\n {effectiveTitle}\n </CardTitle>\n )}\n {effectiveSubtitle && (\n <span className=\"text-muted-foreground text-base font-medium\">\n {effectiveSubtitle}\n </span>\n )}\n </div>\n {children && (\n <div className=\"flex shrink-0 items-center gap-2\">{children}</div>\n )}\n </div>\n </CardHeader>\n );\n}\n\n// ─── State-gated content ────────────────────────────────────────────────────\n\nfunction CollapsibleBody({\n collapsed,\n maxContentHeight,\n widgetId,\n children,\n}: {\n collapsed: boolean;\n maxContentHeight?: string;\n widgetId: string;\n children: ReactNode;\n}) {\n return (\n <div\n className={cn(\n 'grid min-h-0 flex-1 transition-[grid-template-rows] duration-300 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none',\n collapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]',\n )}\n >\n <div className=\"min-h-0 overflow-hidden\">\n <CardContent\n id={`widget-content-${widgetId}`}\n className={cn(\n 'flex h-full min-h-0 flex-1 flex-col overflow-y-auto p-0',\n 'transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none',\n collapsed ? 'opacity-0' : 'opacity-100',\n )}\n style={maxContentHeight ? { maxHeight: maxContentHeight } : undefined}\n >\n {children}\n </CardContent>\n </div>\n </div>\n );\n}\n\nfunction StaticBody({\n maxContentHeight,\n children,\n}: {\n maxContentHeight?: string;\n children: ReactNode;\n}) {\n const { innerRef, height } = useAnimatedHeight();\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n <div\n className=\"overflow-hidden transition-[height] duration-250 ease-out motion-reduce:transition-none\"\n style={{\n height: height === 'auto' ? ('auto' as const) : height,\n maxHeight: maxContentHeight ?? undefined,\n }}\n >\n <div ref={innerRef}>{children}</div>\n </div>\n </CardContent>\n );\n}\n\nfunction WidgetActive({ children }: { children: ReactNode }) {\n const { state, meta } = useWidget();\n if (state.state !== 'active') return null;\n const body = (\n <WidgetErrorBoundary widgetId={meta.widgetId}>\n {children}\n </WidgetErrorBoundary>\n );\n return meta.isCollapsible ? (\n <CollapsibleBody\n collapsed={state.collapsed}\n maxContentHeight={meta.maxContentHeight}\n widgetId={meta.widgetId}\n >\n {body}\n </CollapsibleBody>\n ) : (\n <StaticBody maxContentHeight={meta.maxContentHeight}>{body}</StaticBody>\n );\n}\n\nfunction WidgetLoading({ children }: { children?: ReactNode }) {\n const { state } = useWidget();\n if (state.state !== 'loading') return null;\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n {children ?? <WidgetLoadingState />}\n </CardContent>\n );\n}\n\nfunction WidgetError({ children }: { children?: ReactNode }) {\n const { state } = useWidget();\n if (state.state !== 'error') return null;\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n {children ?? <WidgetErrorState />}\n </CardContent>\n );\n}\n\nfunction WidgetInactive({ children }: { children?: ReactNode }) {\n const { state } = useWidget();\n if (state.state !== 'inactive') return null;\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n {children ?? <WidgetInactiveState />}\n </CardContent>\n );\n}\n\nfunction WidgetDisabled({ children }: { children?: ReactNode }) {\n const { state } = useWidget();\n if (state.state !== 'disabled') return null;\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n {children ?? <WidgetDisabledState />}\n </CardContent>\n );\n}\n\n// ─── Footers ────────────────────────────────────────────────────────────────\n\nfunction WidgetFooter({ children }: { children: ReactNode }) {\n const { state, meta } = useWidget();\n if (state.state !== 'active') return null;\n return (\n <CardFooter\n className={cn(\n 'flex-shrink-0 pb-6 transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none',\n meta.isCollapsible && state.collapsed\n ? 'pointer-events-none opacity-0'\n : 'opacity-100',\n )}\n aria-hidden={meta.isCollapsible && state.collapsed}\n >\n {children}\n </CardFooter>\n );\n}\n\nfunction WidgetActions({ children }: { children: ReactNode }) {\n return (\n <CardFooter className=\"bg-muted flex-shrink-0 justify-end border-t py-3\">\n {children}\n </CardFooter>\n );\n}\n\nfunction WidgetAskFooter({ context }: { context?: WidgetAskContext }) {\n const { state, meta } = useWidget();\n const effectiveContext = context ?? meta.askContext;\n const shouldShow =\n (meta.aiFooterEnabled || !!effectiveContext) &&\n state.state === 'active' &&\n !state.collapsed;\n if (!shouldShow) return null;\n return (\n <WidgetAskBar\n context={\n effectiveContext ?? {\n widgetId: meta.widgetId,\n title: meta.title ?? meta.widgetId,\n snapshot: null,\n suggestedPrompt: `Tell me about ${meta.title ?? 'this widget'}.`,\n }\n }\n />\n );\n}\n\n/**\n * Renders a subtle \"data may be stale\" banner when the widget's integration is\n * `degraded` (but still active). Unhealthy integrations route to `disabled`\n * instead, so this only fires for the degraded middle ground.\n */\nfunction WidgetHealthFooter() {\n const { state, meta } = useWidget();\n const shouldShow =\n state.state === 'active' &&\n !state.collapsed &&\n meta.health?.status === 'degraded';\n if (!shouldShow) return null;\n return (\n <div className=\"text-warning border-warning/30 bg-warning/5 flex items-center gap-2 border-t px-4 py-2 text-xs\">\n <Icon\n name=\"fa-light fa-circle-exclamation\"\n className=\"h-4 w-4 shrink-0\"\n aria-hidden\n />\n <span>\n <Trans\n i18nKey=\"common:integrationHealth.degradedWidgetNotice\"\n defaults=\"This integration is degraded — data may be stale.\"\n />\n </span>\n </div>\n );\n}\n\n// ─── Data state override ────────────────────────────────────────────────────\n\n/**\n * Wraps children in a sub-provider that overrides the effective render state\n * with the widget's own data loading / error state. Only overrides when the\n * parent state is `active` — so the integration's `inactive` / `disabled` /\n * `loading` (initial) states still win.\n *\n * Read the parent integration state BEFORE wrapping in this component if you\n * need it to gate data fetching:\n *\n * ```tsx\n * const { state } = use(WidgetContext);\n * const integrationActive = state.state === 'active';\n * const { isLoading, isError } = useMyData(accountId, integrationActive);\n * return (\n * <Widget.DataState loading={isLoading} error={isError}>\n * <Widget.Card>…</Widget.Card>\n * </Widget.DataState>\n * );\n * ```\n */\nfunction WidgetDataState({\n loading,\n error,\n inactive,\n children,\n}: {\n loading?: boolean;\n error?: boolean;\n /**\n * Force the `inactive` render state even though the integration itself is\n * active. Use for widget-level \"not connected\" states (e.g. the product is\n * activated but the upstream OAuth connection is missing). Takes priority\n * over `loading` / `error`.\n */\n inactive?: boolean;\n children: ReactNode;\n}) {\n const parent = useWidget();\n const overrideState: WidgetRenderState =\n parent.state.state !== 'active'\n ? parent.state.state\n : inactive\n ? 'inactive'\n : loading\n ? 'loading'\n : error\n ? 'error'\n : 'active';\n const value = useMemo(\n () => ({\n state: { ...parent.state, state: overrideState },\n actions: parent.actions,\n meta: parent.meta,\n }),\n [parent.state, parent.actions, parent.meta, overrideState],\n );\n return <WidgetContext value={value}>{children}</WidgetContext>;\n}\n\n// ─── Compound export ────────────────────────────────────────────────────────\n\nexport const Widget = {\n Card: WidgetCard,\n Header: WidgetHeader,\n DataState: WidgetDataState,\n Active: WidgetActive,\n Loading: WidgetLoading,\n Error: WidgetError,\n Inactive: WidgetInactive,\n Disabled: WidgetDisabled,\n Footer: WidgetFooter,\n Actions: WidgetActions,\n AskFooter: WidgetAskFooter,\n HealthFooter: WidgetHealthFooter,\n};\n"]}
1
+ {"version":3,"file":"widget.js","sourceRoot":"","sources":["../../../src/components/widgets/widget.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { Component, type ReactNode, use, useMemo } from 'react';\n\nimport { Button } from '@ekanos/ui/button';\nimport {\n Card,\n CardContent,\n CardFooter,\n CardHeader,\n CardTitle,\n} from '@ekanos/ui/card';\nimport { Icon } from '@ekanos/ui/icon';\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from '@ekanos/ui/tooltip';\nimport { Trans } from '@ekanos/ui/trans';\nimport { cn } from '@ekanos/ui/utils';\n\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport { useAnimatedHeight } from './use-animated-height';\nimport { WidgetAskBar } from './widget-ask-bar';\nimport { WidgetContext, type WidgetRenderState } from './widget-context';\nimport {\n WidgetDisabledState,\n WidgetErrorState,\n WidgetInactiveState,\n WidgetLoadingState,\n} from './widget-state-components';\n\n// ─── Error Boundary ─────────────────────────────────────────────────────────\n\nclass WidgetErrorBoundary extends Component<\n { widgetId: string; children: ReactNode },\n { hasError: boolean }\n> {\n state = { hasError: false };\n static getDerivedStateFromError() {\n return { hasError: true };\n }\n componentDidCatch(error: Error, info: React.ErrorInfo) {\n console.error(`Widget \"${this.props.widgetId}\" crashed:`, error, info);\n }\n render() {\n if (this.state.hasError) {\n return (\n <div className=\"flex flex-1 flex-col items-center justify-center gap-3 p-8\">\n <div className=\"bg-destructive/10 flex h-16 w-16 items-center justify-center rounded-full\">\n <Icon\n name=\"fa-light fa-circle-exclamation\"\n className=\"text-destructive h-8 w-8\"\n />\n </div>\n <p className=\"text-muted-foreground text-base font-medium\">\n <Trans\n i18nKey=\"dashboard:widgetCrashedTitle\"\n defaults=\"Something went wrong\"\n />\n </p>\n <p className=\"text-muted-foreground max-w-xs text-center text-sm\">\n <Trans\n i18nKey=\"dashboard:widgetCrashedBody\"\n defaults=\"This widget encountered an error. Try refreshing the page.\"\n />\n </p>\n </div>\n );\n }\n return this.props.children;\n }\n}\n\n// ─── Hook ───────────────────────────────────────────────────────────────────\n\nfunction useWidget() {\n const ctx = use(WidgetContext);\n if (!ctx) {\n throw new Error(\n 'Widget.* compound components must be rendered inside a widget provider. ' +\n 'On the Fusion dashboard the host supplies one. Anywhere else — your own ' +\n 'app, a story, a component test — wrap the widget in ' +\n \"`WidgetPreviewProvider` from '@ekanos/sdk/components'.\",\n );\n }\n return ctx;\n}\n\n// ─── Card ───────────────────────────────────────────────────────────────────\n\nfunction WidgetCard({ children }: { children: ReactNode }) {\n const { meta } = useWidget();\n return (\n <Card\n className={cn(\n // Neutralize the shadcn card's own py-6/gap-6 box model — widget\n // chrome owns all vertical spacing via its header/content/footer.\n 'shadow-widget relative flex h-full flex-col gap-0 overflow-hidden rounded-lg border-0 py-0',\n meta.className,\n )}\n data-widget-id={meta.widgetId}\n >\n {children}\n </Card>\n );\n}\n\n// ─── Header ─────────────────────────────────────────────────────────────────\n\nfunction CollapseButton() {\n const { state, actions, meta } = useWidget();\n return (\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={actions.toggleCollapsed}\n disabled={state.pendingCollapse}\n className=\"bg-background dark:bg-secondary relative h-6 w-6 rounded-full before:absolute before:top-1/2 before:left-1/2 before:h-11 before:w-11 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']\"\n aria-expanded={!state.collapsed}\n aria-controls={`widget-content-${meta.widgetId}`}\n aria-label={\n state.collapsed\n ? `Expand ${meta.title ?? 'widget'}`\n : `Collapse ${meta.title ?? 'widget'}`\n }\n >\n <Icon\n name=\"fa-light fa-chevron-right\"\n className={cn(\n 'h-4 w-4 transition-transform duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none',\n !state.collapsed && 'rotate-90',\n )}\n />\n </Button>\n );\n}\n\nfunction WidgetHeader({\n title,\n subtitle,\n description,\n children,\n}: {\n /** Override the provider's title — useful when the rendered name differs from the config name. */\n title?: string;\n /** Override the provider's subtitle — useful for widget-computed values. */\n subtitle?: string;\n /** Override the provider's description — useful for widget-computed values. */\n description?: string;\n children?: ReactNode;\n}) {\n const { meta } = useWidget();\n const effectiveTitle = title ?? meta.title;\n const effectiveSubtitle = subtitle ?? meta.subtitle;\n const effectiveDescription = description ?? meta.description;\n if (!effectiveTitle) return null;\n return (\n <CardHeader className=\"flex-shrink-0 border-b pt-6 pb-4 [.border-b]:pb-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2\">\n {meta.isCollapsible ? <CollapseButton /> : null}\n {effectiveDescription ? (\n <>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger\n render={\n <CardTitle className=\"cursor-help text-lg font-semibold\" />\n }\n >\n {effectiveTitle}\n </TooltipTrigger>\n <TooltipContent>\n <p>{effectiveDescription}</p>\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n <span className=\"sr-only\">{effectiveDescription}</span>\n </>\n ) : (\n <CardTitle className=\"text-lg font-semibold\">\n {effectiveTitle}\n </CardTitle>\n )}\n {effectiveSubtitle && (\n <span className=\"text-muted-foreground text-base font-medium\">\n {effectiveSubtitle}\n </span>\n )}\n </div>\n {children && (\n <div className=\"flex shrink-0 items-center gap-2\">{children}</div>\n )}\n </div>\n </CardHeader>\n );\n}\n\n// ─── State-gated content ────────────────────────────────────────────────────\n\nfunction CollapsibleBody({\n collapsed,\n maxContentHeight,\n widgetId,\n children,\n}: {\n collapsed: boolean;\n maxContentHeight?: string;\n widgetId: string;\n children: ReactNode;\n}) {\n return (\n <div\n className={cn(\n 'grid min-h-0 flex-1 transition-[grid-template-rows] duration-300 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none',\n collapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]',\n )}\n >\n <div className=\"min-h-0 overflow-hidden\">\n <CardContent\n id={`widget-content-${widgetId}`}\n className={cn(\n 'flex h-full min-h-0 flex-1 flex-col overflow-y-auto p-0',\n 'transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none',\n collapsed ? 'opacity-0' : 'opacity-100',\n )}\n style={maxContentHeight ? { maxHeight: maxContentHeight } : undefined}\n >\n {children}\n </CardContent>\n </div>\n </div>\n );\n}\n\nfunction StaticBody({\n maxContentHeight,\n children,\n}: {\n maxContentHeight?: string;\n children: ReactNode;\n}) {\n const { innerRef, height } = useAnimatedHeight();\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n <div\n className=\"overflow-hidden transition-[height] duration-250 ease-out motion-reduce:transition-none\"\n style={{\n height: height === 'auto' ? ('auto' as const) : height,\n maxHeight: maxContentHeight ?? undefined,\n }}\n >\n <div ref={innerRef}>{children}</div>\n </div>\n </CardContent>\n );\n}\n\nfunction WidgetActive({ children }: { children: ReactNode }) {\n const { state, meta } = useWidget();\n if (state.state !== 'active') return null;\n const body = (\n <WidgetErrorBoundary widgetId={meta.widgetId}>\n {children}\n </WidgetErrorBoundary>\n );\n return meta.isCollapsible ? (\n <CollapsibleBody\n collapsed={state.collapsed}\n maxContentHeight={meta.maxContentHeight}\n widgetId={meta.widgetId}\n >\n {body}\n </CollapsibleBody>\n ) : (\n <StaticBody maxContentHeight={meta.maxContentHeight}>{body}</StaticBody>\n );\n}\n\nfunction WidgetLoading({ children }: { children?: ReactNode }) {\n const { state } = useWidget();\n if (state.state !== 'loading') return null;\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n {children ?? <WidgetLoadingState />}\n </CardContent>\n );\n}\n\nfunction WidgetError({ children }: { children?: ReactNode }) {\n const { state } = useWidget();\n if (state.state !== 'error') return null;\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n {children ?? <WidgetErrorState />}\n </CardContent>\n );\n}\n\nfunction WidgetInactive({ children }: { children?: ReactNode }) {\n const { state } = useWidget();\n if (state.state !== 'inactive') return null;\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n {children ?? <WidgetInactiveState />}\n </CardContent>\n );\n}\n\nfunction WidgetDisabled({ children }: { children?: ReactNode }) {\n const { state } = useWidget();\n if (state.state !== 'disabled') return null;\n return (\n <CardContent className=\"flex min-h-0 flex-1 flex-col p-0\">\n {children ?? <WidgetDisabledState />}\n </CardContent>\n );\n}\n\n// ─── Footers ────────────────────────────────────────────────────────────────\n\nfunction WidgetFooter({ children }: { children: ReactNode }) {\n const { state, meta } = useWidget();\n if (state.state !== 'active') return null;\n return (\n <CardFooter\n className={cn(\n 'flex-shrink-0 pb-6 transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none',\n meta.isCollapsible && state.collapsed\n ? 'pointer-events-none opacity-0'\n : 'opacity-100',\n )}\n aria-hidden={meta.isCollapsible && state.collapsed}\n >\n {children}\n </CardFooter>\n );\n}\n\nfunction WidgetActions({ children }: { children: ReactNode }) {\n return (\n <CardFooter className=\"bg-muted flex-shrink-0 justify-end border-t py-3\">\n {children}\n </CardFooter>\n );\n}\n\nfunction WidgetAskFooter({ context }: { context?: WidgetAskContext }) {\n const { state, meta } = useWidget();\n const effectiveContext = context ?? meta.askContext;\n const shouldShow =\n (meta.aiFooterEnabled || !!effectiveContext) &&\n state.state === 'active' &&\n !state.collapsed;\n if (!shouldShow) return null;\n return (\n <WidgetAskBar\n context={\n effectiveContext ?? {\n widgetId: meta.widgetId,\n title: meta.title ?? meta.widgetId,\n snapshot: null,\n suggestedPrompt: `Tell me about ${meta.title ?? 'this widget'}.`,\n }\n }\n />\n );\n}\n\n/**\n * Renders a subtle \"data may be stale\" banner when the widget's integration is\n * `degraded` (but still active). Unhealthy integrations route to `disabled`\n * instead, so this only fires for the degraded middle ground.\n */\nfunction WidgetHealthFooter() {\n const { state, meta } = useWidget();\n const shouldShow =\n state.state === 'active' &&\n !state.collapsed &&\n meta.health?.status === 'degraded';\n if (!shouldShow) return null;\n return (\n <div className=\"text-warning border-warning/30 bg-warning/5 flex items-center gap-2 border-t px-4 py-2 text-xs\">\n <Icon\n name=\"fa-light fa-circle-exclamation\"\n className=\"h-4 w-4 shrink-0\"\n aria-hidden\n />\n <span>\n <Trans\n i18nKey=\"common:integrationHealth.degradedWidgetNotice\"\n defaults=\"This integration is degraded — data may be stale.\"\n />\n </span>\n </div>\n );\n}\n\n// ─── Data state override ────────────────────────────────────────────────────\n\n/**\n * Wraps children in a sub-provider that overrides the effective render state\n * with the widget's own data loading / error state. Only overrides when the\n * parent state is `active` — so the integration's `inactive` / `disabled` /\n * `loading` (initial) states still win.\n *\n * Read the parent integration state BEFORE wrapping in this component if you\n * need it to gate data fetching:\n *\n * ```tsx\n * const { state } = use(WidgetContext);\n * const integrationActive = state.state === 'active';\n * const { isLoading, isError } = useMyData(accountId, integrationActive);\n * return (\n * <Widget.DataState loading={isLoading} error={isError}>\n * <Widget.Card>…</Widget.Card>\n * </Widget.DataState>\n * );\n * ```\n */\nfunction WidgetDataState({\n loading,\n error,\n inactive,\n children,\n}: {\n loading?: boolean;\n error?: boolean;\n /**\n * Force the `inactive` render state even though the integration itself is\n * active. Use for widget-level \"not connected\" states (e.g. the product is\n * activated but the upstream OAuth connection is missing). Takes priority\n * over `loading` / `error`.\n */\n inactive?: boolean;\n children: ReactNode;\n}) {\n const parent = useWidget();\n const overrideState: WidgetRenderState =\n parent.state.state !== 'active'\n ? parent.state.state\n : inactive\n ? 'inactive'\n : loading\n ? 'loading'\n : error\n ? 'error'\n : 'active';\n const value = useMemo(\n () => ({\n state: { ...parent.state, state: overrideState },\n actions: parent.actions,\n meta: parent.meta,\n }),\n [parent.state, parent.actions, parent.meta, overrideState],\n );\n return <WidgetContext value={value}>{children}</WidgetContext>;\n}\n\n// ─── Compound export ────────────────────────────────────────────────────────\n\nexport const Widget = {\n Card: WidgetCard,\n Header: WidgetHeader,\n DataState: WidgetDataState,\n Active: WidgetActive,\n Loading: WidgetLoading,\n Error: WidgetError,\n Inactive: WidgetInactive,\n Disabled: WidgetDisabled,\n Footer: WidgetFooter,\n Actions: WidgetActions,\n AskFooter: WidgetAskFooter,\n HealthFooter: WidgetHealthFooter,\n};\n"]}
@@ -14,8 +14,17 @@
14
14
  * callers must treat a missing key as "no value", never as an error.
15
15
  *
16
16
  * Values are returned raw and unvalidated: the integration owns the shape of
17
- * its own storage, so it validates with the same zod schemas it declared.
18
- * See the Open-Meteo example's `fetchWeatherSettings` for the pattern.
17
+ * its own storage, so it validates with the same zod schemas it declared:
18
+ *
19
+ * ```ts
20
+ * const raw = await fetchIntegrationStorage('acme-weather', {
21
+ * accountId,
22
+ * keys: ['settings/location'],
23
+ * });
24
+ *
25
+ * const parsed = LocationSchema.safeParse(raw['settings/location']);
26
+ * const location = parsed.success ? parsed.data : null;
27
+ * ```
19
28
  */
20
29
  export interface FetchIntegrationStorageOptions {
21
30
  /** The account whose storage to read. Must be one the caller belongs to. */
@@ -14,8 +14,17 @@
14
14
  * callers must treat a missing key as "no value", never as an error.
15
15
  *
16
16
  * Values are returned raw and unvalidated: the integration owns the shape of
17
- * its own storage, so it validates with the same zod schemas it declared.
18
- * See the Open-Meteo example's `fetchWeatherSettings` for the pattern.
17
+ * its own storage, so it validates with the same zod schemas it declared:
18
+ *
19
+ * ```ts
20
+ * const raw = await fetchIntegrationStorage('acme-weather', {
21
+ * accountId,
22
+ * keys: ['settings/location'],
23
+ * });
24
+ *
25
+ * const parsed = LocationSchema.safeParse(raw['settings/location']);
26
+ * const location = parsed.success ? parsed.data : null;
27
+ * ```
19
28
  */
20
29
  /** Mirrors the host route's own per-request cap. */
21
30
  const MAX_KEYS_PER_REQUEST = 20;
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-integration-storage.js","sourceRoot":"","sources":["../../src/hooks/fetch-integration-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,oDAAoD;AACpD,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAgBhC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,eAAuB,EACvB,OAEC;;IAED,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC;QAChC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;KAC7B,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,mBAAmB,CAAC;IAEpD,OAAO,GAAG,IAAI,IAAI,eAAe,YAAY,KAAK,EAAE,CAAC;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,eAAuB,EACvB,OAAuC;;IAEvC,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,oCAAoC,CAChF,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,oBAAoB,OAAO,CAAC,IAAI,CAAC,MAAM,SAAS;YACzF,gCAAgC,oBAAoB,+BAA+B,CACtF,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,qBAAqB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAE5D,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;QAC3C,MAAM,EAAE,MAAA,OAAO,CAAC,MAAM,mCAAI,IAAI;QAC9B,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;KACxC,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,mBAAmB,eAAe,cAAc,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAE/C,IACE,OAAO,KAAK,IAAI;QAChB,OAAO,OAAO,KAAK,QAAQ;QAC3B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EACtB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,QAAQ,eAAe,gDAAgD,CACxE,CAAC;IACJ,CAAC;IAED,OAAO,OAAkC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * The browser half of the generic storage bridge.\n *\n * `ctx.storage` is a server capability and widgets are client components, so\n * a saved setting has to round-trip through HTTP. The host serves that\n * round-trip once, generically, at\n * `GET /api/integrations/[slug]/storage?accountId=…&keys=a,b`; this is the\n * client that calls it, so an integration never hand-writes the URL, the\n * query string, or the not-ok handling.\n *\n * Only keys whose declaration opted in with `clientReadable: true` come back.\n * A key that did not opt in, was never declared, or simply has no stored row\n * is ABSENT from the result — all three are indistinguishable by design, so\n * callers must treat a missing key as \"no value\", never as an error.\n *\n * Values are returned raw and unvalidated: the integration owns the shape of\n * its own storage, so it validates with the same zod schemas it declared.\n * See the Open-Meteo example's `fetchWeatherSettings` for the pattern.\n */\n\n/** Mirrors the host route's own per-request cap. */\nconst MAX_KEYS_PER_REQUEST = 20;\n\nexport interface FetchIntegrationStorageOptions {\n /** The account whose storage to read. Must be one the caller belongs to. */\n accountId: string;\n /** Declared, `clientReadable` account-scope keys, e.g. `['settings/units']`. */\n keys: readonly string[];\n signal?: AbortSignal;\n /**\n * Overrides the base path. Exists for harnesses and tests that serve the\n * route from somewhere other than the host's own origin; production code\n * should leave it alone.\n */\n baseUrl?: string;\n}\n\n/**\n * Builds the host storage-route URL for one integration. Exported because a\n * harness or a test double needs to recognize the exact URL this client will\n * request.\n */\nexport function integrationStorageUrl(\n integrationSlug: string,\n options: Pick<FetchIntegrationStorageOptions, 'accountId' | 'keys'> & {\n baseUrl?: string;\n },\n): string {\n const query = new URLSearchParams({\n accountId: options.accountId,\n keys: options.keys.join(','),\n });\n\n const base = options.baseUrl ?? '/api/integrations';\n\n return `${base}/${integrationSlug}/storage?${query}`;\n}\n\n/**\n * Reads declared, client-readable account-scope storage for one integration.\n *\n * Throws when the request itself fails — a 403 (not signed in, not a member,\n * not activated, or no such integration, all deliberately indistinguishable)\n * or a transport error. It does NOT throw for a key that came back absent.\n */\nexport async function fetchIntegrationStorage(\n integrationSlug: string,\n options: FetchIntegrationStorageOptions,\n): Promise<Record<string, unknown>> {\n if (options.keys.length === 0) {\n throw new Error(\n `fetchIntegrationStorage('${integrationSlug}') needs at least one storage key.`,\n );\n }\n\n if (options.keys.length > MAX_KEYS_PER_REQUEST) {\n throw new Error(\n `fetchIntegrationStorage('${integrationSlug}') was asked for ${options.keys.length} keys; ` +\n `the host route reads at most ${MAX_KEYS_PER_REQUEST} per request. Split the read.`,\n );\n }\n\n const url = integrationStorageUrl(integrationSlug, options);\n\n const response = await globalThis.fetch(url, {\n signal: options.signal ?? null,\n headers: { accept: 'application/json' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Could not read '${integrationSlug}' storage (${response.status} ${response.statusText}).`,\n );\n }\n\n const payload: unknown = await response.json();\n\n if (\n payload === null ||\n typeof payload !== 'object' ||\n Array.isArray(payload)\n ) {\n throw new Error(\n `The '${integrationSlug}' storage route returned a non-object payload.`,\n );\n }\n\n return payload as Record<string, unknown>;\n}\n"]}
1
+ {"version":3,"file":"fetch-integration-storage.js","sourceRoot":"","sources":["../../src/hooks/fetch-integration-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,oDAAoD;AACpD,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAgBhC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,eAAuB,EACvB,OAEC;;IAED,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC;QAChC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;KAC7B,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,mBAAmB,CAAC;IAEpD,OAAO,GAAG,IAAI,IAAI,eAAe,YAAY,KAAK,EAAE,CAAC;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,eAAuB,EACvB,OAAuC;;IAEvC,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,oCAAoC,CAChF,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,oBAAoB,OAAO,CAAC,IAAI,CAAC,MAAM,SAAS;YACzF,gCAAgC,oBAAoB,+BAA+B,CACtF,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,qBAAqB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAE5D,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;QAC3C,MAAM,EAAE,MAAA,OAAO,CAAC,MAAM,mCAAI,IAAI;QAC9B,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;KACxC,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,mBAAmB,eAAe,cAAc,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAE/C,IACE,OAAO,KAAK,IAAI;QAChB,OAAO,OAAO,KAAK,QAAQ;QAC3B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EACtB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,QAAQ,eAAe,gDAAgD,CACxE,CAAC;IACJ,CAAC;IAED,OAAO,OAAkC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * The browser half of the generic storage bridge.\n *\n * `ctx.storage` is a server capability and widgets are client components, so\n * a saved setting has to round-trip through HTTP. The host serves that\n * round-trip once, generically, at\n * `GET /api/integrations/[slug]/storage?accountId=…&keys=a,b`; this is the\n * client that calls it, so an integration never hand-writes the URL, the\n * query string, or the not-ok handling.\n *\n * Only keys whose declaration opted in with `clientReadable: true` come back.\n * A key that did not opt in, was never declared, or simply has no stored row\n * is ABSENT from the result — all three are indistinguishable by design, so\n * callers must treat a missing key as \"no value\", never as an error.\n *\n * Values are returned raw and unvalidated: the integration owns the shape of\n * its own storage, so it validates with the same zod schemas it declared:\n *\n * ```ts\n * const raw = await fetchIntegrationStorage('acme-weather', {\n * accountId,\n * keys: ['settings/location'],\n * });\n *\n * const parsed = LocationSchema.safeParse(raw['settings/location']);\n * const location = parsed.success ? parsed.data : null;\n * ```\n */\n\n/** Mirrors the host route's own per-request cap. */\nconst MAX_KEYS_PER_REQUEST = 20;\n\nexport interface FetchIntegrationStorageOptions {\n /** The account whose storage to read. Must be one the caller belongs to. */\n accountId: string;\n /** Declared, `clientReadable` account-scope keys, e.g. `['settings/units']`. */\n keys: readonly string[];\n signal?: AbortSignal;\n /**\n * Overrides the base path. Exists for harnesses and tests that serve the\n * route from somewhere other than the host's own origin; production code\n * should leave it alone.\n */\n baseUrl?: string;\n}\n\n/**\n * Builds the host storage-route URL for one integration. Exported because a\n * harness or a test double needs to recognize the exact URL this client will\n * request.\n */\nexport function integrationStorageUrl(\n integrationSlug: string,\n options: Pick<FetchIntegrationStorageOptions, 'accountId' | 'keys'> & {\n baseUrl?: string;\n },\n): string {\n const query = new URLSearchParams({\n accountId: options.accountId,\n keys: options.keys.join(','),\n });\n\n const base = options.baseUrl ?? '/api/integrations';\n\n return `${base}/${integrationSlug}/storage?${query}`;\n}\n\n/**\n * Reads declared, client-readable account-scope storage for one integration.\n *\n * Throws when the request itself fails — a 403 (not signed in, not a member,\n * not activated, or no such integration, all deliberately indistinguishable)\n * or a transport error. It does NOT throw for a key that came back absent.\n */\nexport async function fetchIntegrationStorage(\n integrationSlug: string,\n options: FetchIntegrationStorageOptions,\n): Promise<Record<string, unknown>> {\n if (options.keys.length === 0) {\n throw new Error(\n `fetchIntegrationStorage('${integrationSlug}') needs at least one storage key.`,\n );\n }\n\n if (options.keys.length > MAX_KEYS_PER_REQUEST) {\n throw new Error(\n `fetchIntegrationStorage('${integrationSlug}') was asked for ${options.keys.length} keys; ` +\n `the host route reads at most ${MAX_KEYS_PER_REQUEST} per request. Split the read.`,\n );\n }\n\n const url = integrationStorageUrl(integrationSlug, options);\n\n const response = await globalThis.fetch(url, {\n signal: options.signal ?? null,\n headers: { accept: 'application/json' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Could not read '${integrationSlug}' storage (${response.status} ${response.statusText}).`,\n );\n }\n\n const payload: unknown = await response.json();\n\n if (\n payload === null ||\n typeof payload !== 'object' ||\n Array.isArray(payload)\n ) {\n throw new Error(\n `The '${integrationSlug}' storage route returned a non-object payload.`,\n );\n }\n\n return payload as Record<string, unknown>;\n}\n"]}
@@ -0,0 +1,375 @@
1
+ /**
2
+ * @ekanos/sdk/eslint — the capability ESLint preset ("R5").
3
+ *
4
+ * This is a SECURITY CONTROL, not a style preset. Partner integration code
5
+ * runs UNSANDBOXED, in-process, inside the host: the generated
6
+ * `partner-integration-bootstrap.ts` statically imports each partner package,
7
+ * so a partner module graph evaluates with full host authority before any
8
+ * validation runs (see the "Trust model (F1, accepted T1)" header in
9
+ * `apps/web/scripts/generate-partner-integration-bootstrap.ts`).
10
+ *
11
+ * The accepted T1 trust tier rests on exactly two controls:
12
+ *
13
+ * 1. the promotion gate — a human reads the SOURCE before it is built, and
14
+ * 2. THIS PRESET — which bans the escapes that route around the capability
15
+ * layer, so "reads the source" is a tractable review rather than an
16
+ * invitation to spot `globalThis.fetch` by eye.
17
+ *
18
+ * Runtime SANDBOXING of handlers is the deferred T3 tier. Until it exists,
19
+ * every rule below is load-bearing. Docs: `docs/devex/sdk-export-map.md`
20
+ * (review outcome F1), `docs/devex/adversarial-review-2026-08-30.md`.
21
+ *
22
+ * ── Why it lives here ────────────────────────────────────────────────────
23
+ * A partner already installs `@ekanos/sdk`, so the control ships with the
24
+ * surface it protects and versions in lockstep with it: the day `ctx.fetch`
25
+ * grows a documented client-side twin, the fetch rule relaxes in the same
26
+ * release. It is dependency-free — every rule below is core ESLint — so it
27
+ * adds nothing to the SDK's build, `api-report`, or `pack:test`.
28
+ *
29
+ * ── Usage (a partner's eslint.config.mjs) ────────────────────────────────
30
+ *
31
+ * import capabilityPreset from '@ekanos/sdk/eslint';
32
+ *
33
+ * export default [
34
+ * ...someBaseConfig,
35
+ * ...capabilityPreset,
36
+ * ];
37
+ *
38
+ * ── Inline disables do not work, by design ───────────────────────────────
39
+ * The preset sets `linterOptions.noInlineConfig` over the source glob, so
40
+ * `// eslint-disable-next-line` cannot silence these rules from partner
41
+ * source — otherwise the control would be opt-out by the very code it
42
+ * constrains. A genuine exception must be a FILE-SCOPED OVERRIDE in the
43
+ * package's own `eslint.config.mjs`, where a reviewer reads it. Both
44
+ * exceptions currently granted in this repo live in the example packages'
45
+ * configs and are recorded there with the reason.
46
+ */
47
+
48
+ /**
49
+ * Glob for partner integration SOURCE. Build tooling — `vitest.config.ts`,
50
+ * `eslint.config.mjs`, `tsup.config.ts` — is deliberately OUT of scope: it
51
+ * runs on the partner's build machine, never in the host process, and it
52
+ * legitimately imports `node:path`/`node:url`. Narrowing the preset to source
53
+ * is what keeps it free of the false positives that get a control disabled.
54
+ */
55
+ const SOURCE_GLOB = ['src/**/*.ts', 'src/**/*.tsx'];
56
+
57
+ const CTX_FETCH_REMEDIATION =
58
+ 'Use `ctx.fetch` from the capability context (`IntegrationContext`) instead, ' +
59
+ "and declare every origin you call in your integration's `egress` " +
60
+ 'allowlist — `defineIntegration({ egress: [...] })`. `ctx.fetch` refuses an ' +
61
+ 'undeclared origin before any I/O, re-checks every redirect hop, and pins ' +
62
+ 'the vetted IP addresses so an allowlisted hostname cannot be rebound to a ' +
63
+ 'host-internal address. A global fetch has none of that, so it is an egress ' +
64
+ 'bypass. Widening `egress` is a reviewed security-posture change, not a ' +
65
+ 'config tweak.';
66
+
67
+ const CTX_SECRETS_REMEDIATION =
68
+ 'Use `ctx.secrets.get(name)` from the capability context instead, and ' +
69
+ "declare the credential in your integration's activation schema. The host " +
70
+ "process environment holds the platform's own credentials — it is not your " +
71
+ 'configuration store, and reading it is how an integration gets a secret it ' +
72
+ 'was never granted.';
73
+
74
+ /**
75
+ * Node builtins a partner integration must never reach, each with the
76
+ * capability that replaces it. Every entry is listed twice — bare and
77
+ * `node:`-prefixed — because both resolve.
78
+ *
79
+ * This is an explicit list rather than a blanket `node:*` ban on purpose: the
80
+ * SDK surface is isomorphic, and `node:crypto`/`node:url`-shaped imports are
81
+ * neither an escape nor worth training a partner to reach for a disable
82
+ * comment over. The generic escape hatch — a computed dynamic `import()` — is
83
+ * closed separately in `no-restricted-syntax` below, so the list cannot be
84
+ * routed around by building the specifier at runtime.
85
+ */
86
+ const BANNED_MODULES = [
87
+ {
88
+ names: ['fs', 'node:fs', 'fs/promises', 'node:fs/promises'],
89
+ message:
90
+ 'The host filesystem is not part of the capability surface. Use ' +
91
+ '`ctx.storage.account` / `ctx.storage.user` for integration state — it ' +
92
+ 'is scoped to the account the request is authorized for, quota-bounded, ' +
93
+ 'and schema-validated. Filesystem access reads and writes host state ' +
94
+ 'that no account owns.',
95
+ },
96
+ {
97
+ names: ['child_process', 'node:child_process'],
98
+ message:
99
+ 'Spawning a process escapes every capability control at once. There is ' +
100
+ 'no replacement: an integration is data-in / data-out. If you need work ' +
101
+ "the SDK cannot express, raise it as an SDK gap in your integration's " +
102
+ 'README rather than shelling out.',
103
+ },
104
+ {
105
+ names: [
106
+ 'net',
107
+ 'node:net',
108
+ 'tls',
109
+ 'node:tls',
110
+ 'dgram',
111
+ 'node:dgram',
112
+ 'http',
113
+ 'node:http',
114
+ 'https',
115
+ 'node:https',
116
+ 'http2',
117
+ 'node:http2',
118
+ ],
119
+ message:
120
+ 'Raw sockets and the `http`/`https` clients bypass the egress allowlist ' +
121
+ 'and its address guard entirely. ' +
122
+ CTX_FETCH_REMEDIATION,
123
+ },
124
+ {
125
+ names: ['dns', 'node:dns', 'dns/promises', 'node:dns/promises'],
126
+ message:
127
+ 'Resolving hostnames yourself is the first half of a DNS-rebinding ' +
128
+ 'egress bypass — `ctx.fetch` resolves ONCE and pins the vetted ' +
129
+ 'addresses precisely so that a name cannot resolve differently between ' +
130
+ 'the check and the connection. ' +
131
+ CTX_FETCH_REMEDIATION,
132
+ },
133
+ {
134
+ names: ['worker_threads', 'node:worker_threads'],
135
+ message:
136
+ 'A worker thread runs on a path where the capability context does not ' +
137
+ 'exist, so nothing inside it can reach `ctx.fetch`, `ctx.secrets` or ' +
138
+ '`ctx.storage`. Do the work in the handler, with `ctx`, and let the ' +
139
+ 'host own concurrency — batch or paginate rather than fanning out.',
140
+ },
141
+ {
142
+ names: ['vm', 'node:vm'],
143
+ message:
144
+ '`vm` executes code from a string, which makes the source a reviewer ' +
145
+ 'reads no longer the code that runs — the assumption the T1 trust tier ' +
146
+ 'is built on. Write the logic as source.',
147
+ },
148
+ {
149
+ names: ['module', 'node:module'],
150
+ message:
151
+ '`node:module` (`createRequire`) is a loader escape hatch: it resolves ' +
152
+ 'modules this preset bans by name at runtime. Use a static `import` of ' +
153
+ 'a package you declare in `dependencies`.',
154
+ },
155
+ {
156
+ names: ['process', 'node:process'],
157
+ message:
158
+ 'Importing `process` reaches the host environment. ' +
159
+ CTX_SECRETS_REMEDIATION,
160
+ },
161
+ {
162
+ names: ['os', 'node:os'],
163
+ message:
164
+ '`node:os` reports the host machine — hostname, users, network ' +
165
+ 'interfaces, home directories. None of it describes the account your ' +
166
+ 'integration is running for, and all of it is host reconnaissance. ' +
167
+ 'Read what you need from the capability context instead: ' +
168
+ '`ctx.accountId` for who this run is for, `ctx.storage` for state you ' +
169
+ 'persisted, `ctx.logger` for diagnostics.',
170
+ },
171
+ {
172
+ names: ['undici', 'node-fetch', 'axios', 'got', 'superagent', 'request'],
173
+ message:
174
+ 'A third-party HTTP client issues requests the egress allowlist never ' +
175
+ 'sees, which is the same bypass as a global `fetch` with an extra ' +
176
+ 'dependency. ' +
177
+ CTX_FETCH_REMEDIATION,
178
+ },
179
+ {
180
+ names: ['react-i18next'],
181
+ importNames: ['Trans'],
182
+ message:
183
+ 'Use `Trans` from `@ekanos/ui/trans` instead — it is wired to the ' +
184
+ "host's i18n instance, so your strings resolve against the same " +
185
+ 'namespaces and language the surrounding page already loaded.',
186
+ },
187
+ ];
188
+
189
+ /** Globals that reach the network without passing through `ctx.fetch`. */
190
+ const EGRESS_GLOBALS = ['fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource'];
191
+
192
+ /** Objects a `globalThis.fetch`-style member access can hide behind. */
193
+ const GLOBAL_OBJECTS = 'globalThis|window|self|global';
194
+
195
+ /** Members of those objects that are the very escapes banned as bare globals. */
196
+ const GLOBAL_ESCAPE_MEMBERS =
197
+ 'fetch|XMLHttpRequest|WebSocket|EventSource|process|eval|require|Function';
198
+
199
+ /**
200
+ * `@kit/*` is checked with a PATTERN rather than a path so every subpath is
201
+ * covered (`@kit/ui/badge`, `@kit/next/actions`, …). This is the rule that
202
+ * catches, at lint time in the partner's own editor, the failure that
203
+ * otherwise surfaces as an unresolvable module at install time — which is
204
+ * exactly how our own reference example turned out to be unbuildable outside
205
+ * the monorepo.
206
+ */
207
+ const KIT_PATTERN = {
208
+ group: ['@kit/*', '@kit/*/**'],
209
+ message:
210
+ '`@kit/*` packages are workspace-internal to the Fusion monorepo and do ' +
211
+ 'not exist on any registry, so this import cannot resolve for anyone ' +
212
+ 'outside it — including in the clean-room build your submission is gated ' +
213
+ 'on. Import UI primitives from `@ekanos/ui/*` (badge, button, card, form, ' +
214
+ 'input, select, icon, utils, …), integration surfaces from `@ekanos/sdk` ' +
215
+ 'and its `/components`, `/hooks`, `/mcp`, `/context`, `/integration` ' +
216
+ 'entrypoints, and nothing else from the host.',
217
+ };
218
+
219
+ const SUPABASE_PATTERN = {
220
+ group: ['@supabase/*', '@supabase/*/**'],
221
+ message:
222
+ 'A direct Supabase client talks to the database with whatever key it is ' +
223
+ 'given, outside the account scoping and RLS the host applies for you. Use ' +
224
+ '`ctx.storage.account` / `ctx.storage.user` for integration state — ' +
225
+ 'authorized for the account in the request and validated against your ' +
226
+ 'declared schema.',
227
+ };
228
+
229
+ /** Flattens BANNED_MODULES into `no-restricted-imports` `paths` entries. */
230
+ function bannedPaths() {
231
+ return BANNED_MODULES.flatMap((entry) =>
232
+ entry.names.map((name) => ({
233
+ name,
234
+ message: entry.message,
235
+ ...(entry.importNames ? { importNames: entry.importNames } : {}),
236
+ })),
237
+ );
238
+ }
239
+
240
+ /**
241
+ * The rules, exported separately so a consumer can compose them into its own
242
+ * config block (a different `files` glob, say) without re-deriving them.
243
+ */
244
+ export const capabilityRules = {
245
+ /**
246
+ * Bare global references only. ESLint resolves scope first, so a parameter
247
+ * or local named `fetch` — the SDK's own `IntegrationFetch` injection
248
+ * pattern, `getJson(fetch, url)` — is NOT reported. That is the intended
249
+ * shape: pass the mediated fetch in as a value.
250
+ */
251
+ 'no-restricted-globals': [
252
+ 'error',
253
+ ...EGRESS_GLOBALS.map((name) => ({
254
+ name,
255
+ message: `\`${name}\` reaches the network outside the capability layer. ${CTX_FETCH_REMEDIATION}`,
256
+ })),
257
+ {
258
+ name: 'process',
259
+ message: `\`process\` (including \`process.env\`) is host state, not integration state. ${CTX_SECRETS_REMEDIATION}`,
260
+ },
261
+ {
262
+ name: 'require',
263
+ message:
264
+ '`require` resolves modules at runtime, so no reviewer and no import ' +
265
+ 'rule can see what is actually loaded. Use a static `import` of a ' +
266
+ 'package declared in `dependencies`.',
267
+ },
268
+ {
269
+ name: 'eval',
270
+ message:
271
+ '`eval` runs code built at runtime, so the source a reviewer reads ' +
272
+ 'stops being the code that runs — and human review of source is half ' +
273
+ 'of what makes running your integration in-process acceptable. Write ' +
274
+ 'the logic as source.',
275
+ },
276
+ ],
277
+
278
+ 'no-restricted-imports': [
279
+ 'error',
280
+ { paths: bannedPaths(), patterns: [KIT_PATTERN, SUPABASE_PATTERN] },
281
+ ],
282
+
283
+ 'no-restricted-syntax': [
284
+ 'error',
285
+ {
286
+ // Closes the hole in no-restricted-globals: that rule resolves
287
+ // identifiers, so `globalThis.fetch` (a member access, not a global
288
+ // reference) is invisible to it.
289
+ selector: `MemberExpression[object.name=/^(${GLOBAL_OBJECTS})$/][property.name=/^(${GLOBAL_ESCAPE_MEMBERS})$/]`,
290
+ message:
291
+ 'Reaching an escape through the global object is the same bypass as ' +
292
+ 'naming it directly, and it is how a capability control gets routed ' +
293
+ 'around without tripping an import rule. Use the capability context: ' +
294
+ '`ctx.fetch` for network calls (with the origin declared in `egress`) ' +
295
+ 'and `ctx.secrets` for credentials.',
296
+ },
297
+ {
298
+ // Computed-string member access, e.g. `globalThis['fetch']`.
299
+ selector: `MemberExpression[computed=true][object.name=/^(${GLOBAL_OBJECTS})$/]`,
300
+ message:
301
+ 'Indexing the global object with a computed key hides which global is ' +
302
+ 'being reached, which defeats every rule in this preset. Reference ' +
303
+ 'what you need directly, through the capability context — `ctx.fetch` ' +
304
+ 'for network calls, `ctx.secrets` for credentials, `ctx.storage` for ' +
305
+ 'state.',
306
+ },
307
+ {
308
+ selector: 'NewExpression[callee.name="Function"]',
309
+ message:
310
+ '`new Function` compiles code from a string — the same problem as ' +
311
+ '`eval`: the source under review is no longer the code that runs. ' +
312
+ 'Write the logic as source.',
313
+ },
314
+ {
315
+ selector: 'CallExpression[callee.name="Function"]',
316
+ message:
317
+ '`Function(...)` compiles code from a string — the same problem as ' +
318
+ '`eval`: the source under review is no longer the code that runs. ' +
319
+ 'Write the logic as source.',
320
+ },
321
+ {
322
+ // The generic route around no-restricted-imports: build the specifier
323
+ // at runtime and the import rule has nothing to match.
324
+ selector: 'ImportExpression[source.type!="Literal"]',
325
+ message:
326
+ 'A dynamic `import()` with a computed specifier hides what is loaded ' +
327
+ 'from both the import rules and the human promotion review. Use a ' +
328
+ 'static `import`, or `import("literal-specifier")` if you genuinely ' +
329
+ 'need lazy loading.',
330
+ },
331
+ {
332
+ // `no-restricted-imports` does not inspect dynamic import specifiers,
333
+ // so the banned module list is re-applied to literal ones here.
334
+ selector:
335
+ 'ImportExpression[source.value=/^(node:)?(fs|child_process|net|tls|dgram|dns|http|https|http2|worker_threads|vm|module|process|os)$/]',
336
+ message:
337
+ 'This Node builtin is banned for integration code whether it is ' +
338
+ 'imported statically or dynamically — a dynamic `import()` of it is ' +
339
+ 'the same capability escape. See the static-import message for the ' +
340
+ 'capability that replaces it (`ctx.fetch`, `ctx.secrets`, ' +
341
+ '`ctx.storage`).',
342
+ },
343
+ {
344
+ // The `/promises` faces of the same builtins. Split out because a `/`
345
+ // cannot appear inside an esquery regex-literal attribute value.
346
+ selector:
347
+ 'ImportExpression[source.value=/^(node:)?(fs|dns)\\u002Fpromises$/]',
348
+ message:
349
+ 'This Node builtin is banned for integration code whether it is ' +
350
+ 'imported statically or dynamically. Use `ctx.storage` for state and ' +
351
+ '`ctx.fetch` for network calls.',
352
+ },
353
+ ],
354
+ };
355
+
356
+ /**
357
+ * The preset. Spread it AFTER any base config: it deliberately replaces a
358
+ * base `no-restricted-imports` / `no-restricted-globals` /
359
+ * `no-restricted-syntax` setting rather than merging with it (flat config does
360
+ * not merge rule options), and it re-states the one entry the Fusion base
361
+ * config carries — `react-i18next`'s `Trans` — in its partner-correct form.
362
+ */
363
+ export default [
364
+ {
365
+ name: '@ekanos/sdk/eslint:capabilities',
366
+ files: SOURCE_GLOB,
367
+ linterOptions: {
368
+ // A control the constrained code can switch off is not a control.
369
+ // Exceptions belong in the consumer's config, where review sees them.
370
+ // See the header.
371
+ noInlineConfig: true,
372
+ },
373
+ rules: capabilityRules,
374
+ },
375
+ ];
package/package.json CHANGED
@@ -1,17 +1,11 @@
1
1
  {
2
2
  "name": "@ekanos/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "The official SDK for building Ekanos integrations.",
6
6
  "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/companydotcom/fusion.git",
10
- "directory": "packages/integration-sdk"
11
- },
12
- "homepage": "https://github.com/companydotcom/fusion/tree/dev/packages/integration-sdk#readme",
13
7
  "bugs": {
14
- "url": "https://github.com/companydotcom/fusion/issues"
8
+ "email": "npm@govastly.com"
15
9
  },
16
10
  "sideEffects": [
17
11
  "**/mcp/**"
@@ -19,7 +13,8 @@
19
13
  "files": [
20
14
  "dist",
21
15
  "README.md",
22
- "LICENSE"
16
+ "LICENSE",
17
+ "eslint.preset.mjs"
23
18
  ],
24
19
  "exports": {
25
20
  ".": {
@@ -49,7 +44,8 @@
49
44
  "./integration": {
50
45
  "types": "./dist/integration/index.d.ts",
51
46
  "default": "./dist/integration/index.js"
52
- }
47
+ },
48
+ "./eslint": "./eslint.preset.mjs"
53
49
  },
54
50
  "publishConfig": {
55
51
  "access": "public"
@@ -57,8 +53,8 @@
57
53
  "dependencies": {
58
54
  "@supabase/supabase-js": "2.87.1",
59
55
  "server-only": "^0.0.1",
60
- "@ekanos/integration-schema": "0.1.0",
61
- "@ekanos/ui": "0.1.0"
56
+ "@ekanos/integration-schema": "0.1.2",
57
+ "@ekanos/ui": "0.1.2"
62
58
  },
63
59
  "peerDependencies": {
64
60
  "@hookform/resolvers": "^5.2.2",
@@ -78,9 +74,9 @@
78
74
  "typescript": "^5.9.3",
79
75
  "vitest": "4.1.10",
80
76
  "zod": "^3.25.74",
77
+ "@kit/prettier-config": "0.1.0",
81
78
  "@kit/tsconfig": "0.1.0",
82
- "@kit/eslint-config": "0.2.0",
83
- "@kit/prettier-config": "0.1.0"
79
+ "@kit/eslint-config": "0.2.0"
84
80
  },
85
81
  "prettier": "@kit/prettier-config",
86
82
  "typesVersions": {