@ekanos/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/components/base-activation-dialog.d.ts +41 -0
- package/dist/components/base-activation-dialog.js +41 -0
- package/dist/components/base-activation-dialog.js.map +1 -0
- package/dist/components/base-activation-form.d.ts +36 -0
- package/dist/components/base-activation-form.js +21 -0
- package/dist/components/base-activation-form.js.map +1 -0
- package/dist/components/base-marketplace-tile.d.ts +34 -0
- package/dist/components/base-marketplace-tile.js +40 -0
- package/dist/components/base-marketplace-tile.js.map +1 -0
- package/dist/components/current-return-path.d.ts +19 -0
- package/dist/components/current-return-path.js +26 -0
- package/dist/components/current-return-path.js.map +1 -0
- package/dist/components/index.d.ts +40 -0
- package/dist/components/index.js +38 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/oauth-activation-form.d.ts +50 -0
- package/dist/components/oauth-activation-form.js +119 -0
- package/dist/components/oauth-activation-form.js.map +1 -0
- package/dist/components/widgets/ai-prompt-chip.d.ts +31 -0
- package/dist/components/widgets/ai-prompt-chip.js +38 -0
- package/dist/components/widgets/ai-prompt-chip.js.map +1 -0
- package/dist/components/widgets/ask-icon.d.ts +3 -0
- package/dist/components/widgets/ask-icon.js +5 -0
- package/dist/components/widgets/ask-icon.js.map +1 -0
- package/dist/components/widgets/use-animated-height.d.ts +12 -0
- package/dist/components/widgets/use-animated-height.js +39 -0
- package/dist/components/widgets/use-animated-height.js.map +1 -0
- package/dist/components/widgets/widget-ask-bar.d.ts +18 -0
- package/dist/components/widgets/widget-ask-bar.js +62 -0
- package/dist/components/widgets/widget-ask-bar.js.map +1 -0
- package/dist/components/widgets/widget-context.d.ts +38 -0
- package/dist/components/widgets/widget-context.js +4 -0
- package/dist/components/widgets/widget-context.js.map +1 -0
- package/dist/components/widgets/widget-state-components.d.ts +5 -0
- package/dist/components/widgets/widget-state-components.js +19 -0
- package/dist/components/widgets/widget-state-components.js.map +1 -0
- package/dist/components/widgets/widget.d.ts +91 -0
- package/dist/components/widgets/widget.js +205 -0
- package/dist/components/widgets/widget.js.map +1 -0
- package/dist/context/egress.d.ts +30 -0
- package/dist/context/egress.js +81 -0
- package/dist/context/egress.js.map +1 -0
- package/dist/context/errors.d.ts +57 -0
- package/dist/context/errors.js +78 -0
- package/dist/context/errors.js.map +1 -0
- package/dist/context/index.d.ts +19 -0
- package/dist/context/index.js +20 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/require-context.d.ts +26 -0
- package/dist/context/require-context.js +36 -0
- package/dist/context/require-context.js.map +1 -0
- package/dist/context/storage-validation.d.ts +71 -0
- package/dist/context/storage-validation.js +132 -0
- package/dist/context/storage-validation.js.map +1 -0
- package/dist/context/types.d.ts +11 -0
- package/dist/context/types.js +2 -0
- package/dist/context/types.js.map +1 -0
- package/dist/hooks/activation-actions-context.d.ts +80 -0
- package/dist/hooks/activation-actions-context.js +31 -0
- package/dist/hooks/activation-actions-context.js.map +1 -0
- package/dist/hooks/ask-assistant-context.d.ts +32 -0
- package/dist/hooks/ask-assistant-context.js +56 -0
- package/dist/hooks/ask-assistant-context.js.map +1 -0
- package/dist/hooks/fetch-integration-storage.d.ts +48 -0
- package/dist/hooks/fetch-integration-storage.js +68 -0
- package/dist/hooks/fetch-integration-storage.js.map +1 -0
- package/dist/hooks/index.d.ts +28 -0
- package/dist/hooks/index.js +32 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-activate-integration.d.ts +15 -0
- package/dist/hooks/use-activate-integration.js +61 -0
- package/dist/hooks/use-activate-integration.js.map +1 -0
- package/dist/hooks/use-oauth-connection-status.d.ts +360 -0
- package/dist/hooks/use-oauth-connection-status.js +108 -0
- package/dist/hooks/use-oauth-connection-status.js.map +1 -0
- package/dist/hooks/use-widget-events.d.ts +20 -0
- package/dist/hooks/use-widget-events.js +51 -0
- package/dist/hooks/use-widget-events.js.map +1 -0
- package/dist/hooks/widget-event-context.d.ts +22 -0
- package/dist/hooks/widget-event-context.js +75 -0
- package/dist/hooks/widget-event-context.js.map +1 -0
- package/dist/hooks/widget-event-types.d.ts +55 -0
- package/dist/hooks/widget-event-types.js +9 -0
- package/dist/hooks/widget-event-types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/cron.d.ts +29 -0
- package/dist/integration/cron.js +102 -0
- package/dist/integration/cron.js.map +1 -0
- package/dist/integration/define-integration.d.ts +28 -0
- package/dist/integration/define-integration.js +41 -0
- package/dist/integration/define-integration.js.map +1 -0
- package/dist/integration/event-surfaces.d.ts +15 -0
- package/dist/integration/event-surfaces.js +45 -0
- package/dist/integration/event-surfaces.js.map +1 -0
- package/dist/integration/index.d.ts +20 -0
- package/dist/integration/index.js +23 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/types.d.ts +7 -0
- package/dist/integration/types.js +2 -0
- package/dist/integration/types.js.map +1 -0
- package/dist/mcp/guards.d.ts +40 -0
- package/dist/mcp/guards.js +99 -0
- package/dist/mcp/guards.js.map +1 -0
- package/dist/mcp/index.d.ts +22 -0
- package/dist/mcp/index.js +22 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/ownership.d.ts +74 -0
- package/dist/mcp/ownership.js +83 -0
- package/dist/mcp/ownership.js.map +1 -0
- package/dist/mcp/types.d.ts +145 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/invoke.d.ts +77 -0
- package/dist/testing/invoke.js +83 -0
- package/dist/testing/invoke.js.map +1 -0
- package/dist/testing/mock-context.d.ts +79 -0
- package/dist/testing/mock-context.js +206 -0
- package/dist/testing/mock-context.js.map +1 -0
- package/dist/types/icon.d.ts +12 -0
- package/dist/types/icon.js +2 -0
- package/dist/types/icon.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/integration.d.ts +258 -0
- package/dist/types/integration.js +2 -0
- package/dist/types/integration.js.map +1 -0
- package/dist/types/widget-ask-context.d.ts +36 -0
- package/dist/types/widget-ask-context.js +2 -0
- package/dist/types/widget-ask-context.js.map +1 -0
- package/dist/types/workspace-target.d.ts +33 -0
- package/dist/types/workspace-target.js +2 -0
- package/dist/types/workspace-target.js.map +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Component, use, useMemo } from 'react';
|
|
4
|
+
import { Button } from '@ekanos/ui/button';
|
|
5
|
+
import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from '@ekanos/ui/card';
|
|
6
|
+
import { Icon } from '@ekanos/ui/icon';
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@ekanos/ui/tooltip';
|
|
8
|
+
import { Trans } from '@ekanos/ui/trans';
|
|
9
|
+
import { cn } from '@ekanos/ui/utils';
|
|
10
|
+
import { useAnimatedHeight } from './use-animated-height.js';
|
|
11
|
+
import { WidgetAskBar } from './widget-ask-bar.js';
|
|
12
|
+
import { WidgetContext } from './widget-context.js';
|
|
13
|
+
import { WidgetDisabledState, WidgetErrorState, WidgetInactiveState, WidgetLoadingState, } from './widget-state-components.js';
|
|
14
|
+
// ─── Error Boundary ─────────────────────────────────────────────────────────
|
|
15
|
+
class WidgetErrorBoundary extends Component {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(...arguments);
|
|
18
|
+
this.state = { hasError: false };
|
|
19
|
+
}
|
|
20
|
+
static getDerivedStateFromError() {
|
|
21
|
+
return { hasError: true };
|
|
22
|
+
}
|
|
23
|
+
componentDidCatch(error, info) {
|
|
24
|
+
console.error(`Widget "${this.props.widgetId}" crashed:`, error, info);
|
|
25
|
+
}
|
|
26
|
+
render() {
|
|
27
|
+
if (this.state.hasError) {
|
|
28
|
+
return (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 p-8", children: [_jsx("div", { className: "bg-destructive/10 flex h-16 w-16 items-center justify-center rounded-full", children: _jsx(Icon, { name: "fa-light fa-circle-exclamation", className: "text-destructive h-8 w-8" }) }), _jsx("p", { className: "text-muted-foreground text-base font-medium", children: _jsx(Trans, { i18nKey: "dashboard:widgetCrashedTitle", defaults: "Something went wrong" }) }), _jsx("p", { className: "text-muted-foreground max-w-xs text-center text-sm", children: _jsx(Trans, { i18nKey: "dashboard:widgetCrashedBody", defaults: "This widget encountered an error. Try refreshing the page." }) })] }));
|
|
29
|
+
}
|
|
30
|
+
return this.props.children;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ─── Hook ───────────────────────────────────────────────────────────────────
|
|
34
|
+
function useWidget() {
|
|
35
|
+
const ctx = use(WidgetContext);
|
|
36
|
+
if (!ctx) {
|
|
37
|
+
throw new Error('Widget.* compound components must be rendered inside a WidgetProvider.');
|
|
38
|
+
}
|
|
39
|
+
return ctx;
|
|
40
|
+
}
|
|
41
|
+
// ─── Card ───────────────────────────────────────────────────────────────────
|
|
42
|
+
function WidgetCard({ children }) {
|
|
43
|
+
const { meta } = useWidget();
|
|
44
|
+
return (_jsx(Card, { className: cn(
|
|
45
|
+
// Neutralize the shadcn card's own py-6/gap-6 box model — widget
|
|
46
|
+
// chrome owns all vertical spacing via its header/content/footer.
|
|
47
|
+
'shadow-widget relative flex h-full flex-col gap-0 overflow-hidden rounded-lg border-0 py-0', meta.className), "data-widget-id": meta.widgetId, children: children }));
|
|
48
|
+
}
|
|
49
|
+
// ─── Header ─────────────────────────────────────────────────────────────────
|
|
50
|
+
function CollapseButton() {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
const { state, actions, meta } = useWidget();
|
|
53
|
+
return (_jsx(Button, { variant: "outline", size: "icon", onClick: actions.toggleCollapsed, disabled: state.pendingCollapse, 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-['']", "aria-expanded": !state.collapsed, "aria-controls": `widget-content-${meta.widgetId}`, "aria-label": state.collapsed
|
|
54
|
+
? `Expand ${(_a = meta.title) !== null && _a !== void 0 ? _a : 'widget'}`
|
|
55
|
+
: `Collapse ${(_b = meta.title) !== null && _b !== void 0 ? _b : 'widget'}`, children: _jsx(Icon, { name: "fa-light fa-chevron-right", className: cn('h-4 w-4 transition-transform duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none', !state.collapsed && 'rotate-90') }) }));
|
|
56
|
+
}
|
|
57
|
+
function WidgetHeader({ title, subtitle, description, children, }) {
|
|
58
|
+
const { meta } = useWidget();
|
|
59
|
+
const effectiveTitle = title !== null && title !== void 0 ? title : meta.title;
|
|
60
|
+
const effectiveSubtitle = subtitle !== null && subtitle !== void 0 ? subtitle : meta.subtitle;
|
|
61
|
+
const effectiveDescription = description !== null && description !== void 0 ? description : meta.description;
|
|
62
|
+
if (!effectiveTitle)
|
|
63
|
+
return null;
|
|
64
|
+
return (_jsx(CardHeader, { className: "flex-shrink-0 border-b pt-6 pb-4 [.border-b]:pb-4", children: _jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [meta.isCollapsible ? _jsx(CollapseButton, {}) : null, effectiveDescription ? (_jsxs(_Fragment, { children: [_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx(CardTitle, { className: "cursor-help text-lg font-semibold" }), children: effectiveTitle }), _jsx(TooltipContent, { children: _jsx("p", { children: effectiveDescription }) })] }) }), _jsx("span", { className: "sr-only", children: effectiveDescription })] })) : (_jsx(CardTitle, { className: "text-lg font-semibold", children: effectiveTitle })), effectiveSubtitle && (_jsx("span", { className: "text-muted-foreground text-base font-medium", children: effectiveSubtitle }))] }), children && (_jsx("div", { className: "flex shrink-0 items-center gap-2", children: children }))] }) }));
|
|
65
|
+
}
|
|
66
|
+
// ─── State-gated content ────────────────────────────────────────────────────
|
|
67
|
+
function CollapsibleBody({ collapsed, maxContentHeight, widgetId, children, }) {
|
|
68
|
+
return (_jsx("div", { className: cn('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', collapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'), children: _jsx("div", { className: "min-h-0 overflow-hidden", children: _jsx(CardContent, { id: `widget-content-${widgetId}`, className: cn('flex h-full min-h-0 flex-1 flex-col overflow-y-auto p-0', 'transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none', collapsed ? 'opacity-0' : 'opacity-100'), style: maxContentHeight ? { maxHeight: maxContentHeight } : undefined, children: children }) }) }));
|
|
69
|
+
}
|
|
70
|
+
function StaticBody({ maxContentHeight, children, }) {
|
|
71
|
+
const { innerRef, height } = useAnimatedHeight();
|
|
72
|
+
return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: _jsx("div", { className: "overflow-hidden transition-[height] duration-250 ease-out motion-reduce:transition-none", style: {
|
|
73
|
+
height: height === 'auto' ? 'auto' : height,
|
|
74
|
+
maxHeight: maxContentHeight !== null && maxContentHeight !== void 0 ? maxContentHeight : undefined,
|
|
75
|
+
}, children: _jsx("div", { ref: innerRef, children: children }) }) }));
|
|
76
|
+
}
|
|
77
|
+
function WidgetActive({ children }) {
|
|
78
|
+
const { state, meta } = useWidget();
|
|
79
|
+
if (state.state !== 'active')
|
|
80
|
+
return null;
|
|
81
|
+
const body = (_jsx(WidgetErrorBoundary, { widgetId: meta.widgetId, children: children }));
|
|
82
|
+
return meta.isCollapsible ? (_jsx(CollapsibleBody, { collapsed: state.collapsed, maxContentHeight: meta.maxContentHeight, widgetId: meta.widgetId, children: body })) : (_jsx(StaticBody, { maxContentHeight: meta.maxContentHeight, children: body }));
|
|
83
|
+
}
|
|
84
|
+
function WidgetLoading({ children }) {
|
|
85
|
+
const { state } = useWidget();
|
|
86
|
+
if (state.state !== 'loading')
|
|
87
|
+
return null;
|
|
88
|
+
return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: children !== null && children !== void 0 ? children : _jsx(WidgetLoadingState, {}) }));
|
|
89
|
+
}
|
|
90
|
+
function WidgetError({ children }) {
|
|
91
|
+
const { state } = useWidget();
|
|
92
|
+
if (state.state !== 'error')
|
|
93
|
+
return null;
|
|
94
|
+
return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: children !== null && children !== void 0 ? children : _jsx(WidgetErrorState, {}) }));
|
|
95
|
+
}
|
|
96
|
+
function WidgetInactive({ children }) {
|
|
97
|
+
const { state } = useWidget();
|
|
98
|
+
if (state.state !== 'inactive')
|
|
99
|
+
return null;
|
|
100
|
+
return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: children !== null && children !== void 0 ? children : _jsx(WidgetInactiveState, {}) }));
|
|
101
|
+
}
|
|
102
|
+
function WidgetDisabled({ children }) {
|
|
103
|
+
const { state } = useWidget();
|
|
104
|
+
if (state.state !== 'disabled')
|
|
105
|
+
return null;
|
|
106
|
+
return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: children !== null && children !== void 0 ? children : _jsx(WidgetDisabledState, {}) }));
|
|
107
|
+
}
|
|
108
|
+
// ─── Footers ────────────────────────────────────────────────────────────────
|
|
109
|
+
function WidgetFooter({ children }) {
|
|
110
|
+
const { state, meta } = useWidget();
|
|
111
|
+
if (state.state !== 'active')
|
|
112
|
+
return null;
|
|
113
|
+
return (_jsx(CardFooter, { className: cn('flex-shrink-0 pb-6 transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none', meta.isCollapsible && state.collapsed
|
|
114
|
+
? 'pointer-events-none opacity-0'
|
|
115
|
+
: 'opacity-100'), "aria-hidden": meta.isCollapsible && state.collapsed, children: children }));
|
|
116
|
+
}
|
|
117
|
+
function WidgetActions({ children }) {
|
|
118
|
+
return (_jsx(CardFooter, { className: "bg-muted flex-shrink-0 justify-end border-t py-3", children: children }));
|
|
119
|
+
}
|
|
120
|
+
function WidgetAskFooter({ context }) {
|
|
121
|
+
var _a, _b;
|
|
122
|
+
const { state, meta } = useWidget();
|
|
123
|
+
const effectiveContext = context !== null && context !== void 0 ? context : meta.askContext;
|
|
124
|
+
const shouldShow = (meta.aiFooterEnabled || !!effectiveContext) &&
|
|
125
|
+
state.state === 'active' &&
|
|
126
|
+
!state.collapsed;
|
|
127
|
+
if (!shouldShow)
|
|
128
|
+
return null;
|
|
129
|
+
return (_jsx(WidgetAskBar, { context: effectiveContext !== null && effectiveContext !== void 0 ? effectiveContext : {
|
|
130
|
+
widgetId: meta.widgetId,
|
|
131
|
+
title: (_a = meta.title) !== null && _a !== void 0 ? _a : meta.widgetId,
|
|
132
|
+
snapshot: null,
|
|
133
|
+
suggestedPrompt: `Tell me about ${(_b = meta.title) !== null && _b !== void 0 ? _b : 'this widget'}.`,
|
|
134
|
+
} }));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Renders a subtle "data may be stale" banner when the widget's integration is
|
|
138
|
+
* `degraded` (but still active). Unhealthy integrations route to `disabled`
|
|
139
|
+
* instead, so this only fires for the degraded middle ground.
|
|
140
|
+
*/
|
|
141
|
+
function WidgetHealthFooter() {
|
|
142
|
+
var _a;
|
|
143
|
+
const { state, meta } = useWidget();
|
|
144
|
+
const shouldShow = state.state === 'active' &&
|
|
145
|
+
!state.collapsed &&
|
|
146
|
+
((_a = meta.health) === null || _a === void 0 ? void 0 : _a.status) === 'degraded';
|
|
147
|
+
if (!shouldShow)
|
|
148
|
+
return null;
|
|
149
|
+
return (_jsxs("div", { className: "text-warning border-warning/30 bg-warning/5 flex items-center gap-2 border-t px-4 py-2 text-xs", children: [_jsx(Icon, { name: "fa-light fa-circle-exclamation", className: "h-4 w-4 shrink-0", "aria-hidden": true }), _jsx("span", { children: _jsx(Trans, { i18nKey: "common:integrationHealth.degradedWidgetNotice", defaults: "This integration is degraded \u2014 data may be stale." }) })] }));
|
|
150
|
+
}
|
|
151
|
+
// ─── Data state override ────────────────────────────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* Wraps children in a sub-provider that overrides the effective render state
|
|
154
|
+
* with the widget's own data loading / error state. Only overrides when the
|
|
155
|
+
* parent state is `active` — so the integration's `inactive` / `disabled` /
|
|
156
|
+
* `loading` (initial) states still win.
|
|
157
|
+
*
|
|
158
|
+
* Read the parent integration state BEFORE wrapping in this component if you
|
|
159
|
+
* need it to gate data fetching:
|
|
160
|
+
*
|
|
161
|
+
* ```tsx
|
|
162
|
+
* const { state } = use(WidgetContext);
|
|
163
|
+
* const integrationActive = state.state === 'active';
|
|
164
|
+
* const { isLoading, isError } = useMyData(accountId, integrationActive);
|
|
165
|
+
* return (
|
|
166
|
+
* <Widget.DataState loading={isLoading} error={isError}>
|
|
167
|
+
* <Widget.Card>…</Widget.Card>
|
|
168
|
+
* </Widget.DataState>
|
|
169
|
+
* );
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
function WidgetDataState({ loading, error, inactive, children, }) {
|
|
173
|
+
const parent = useWidget();
|
|
174
|
+
const overrideState = parent.state.state !== 'active'
|
|
175
|
+
? parent.state.state
|
|
176
|
+
: inactive
|
|
177
|
+
? 'inactive'
|
|
178
|
+
: loading
|
|
179
|
+
? 'loading'
|
|
180
|
+
: error
|
|
181
|
+
? 'error'
|
|
182
|
+
: 'active';
|
|
183
|
+
const value = useMemo(() => ({
|
|
184
|
+
state: Object.assign(Object.assign({}, parent.state), { state: overrideState }),
|
|
185
|
+
actions: parent.actions,
|
|
186
|
+
meta: parent.meta,
|
|
187
|
+
}), [parent.state, parent.actions, parent.meta, overrideState]);
|
|
188
|
+
return _jsx(WidgetContext, { value: value, children: children });
|
|
189
|
+
}
|
|
190
|
+
// ─── Compound export ────────────────────────────────────────────────────────
|
|
191
|
+
export const Widget = {
|
|
192
|
+
Card: WidgetCard,
|
|
193
|
+
Header: WidgetHeader,
|
|
194
|
+
DataState: WidgetDataState,
|
|
195
|
+
Active: WidgetActive,
|
|
196
|
+
Loading: WidgetLoading,
|
|
197
|
+
Error: WidgetError,
|
|
198
|
+
Inactive: WidgetInactive,
|
|
199
|
+
Disabled: WidgetDisabled,
|
|
200
|
+
Footer: WidgetFooter,
|
|
201
|
+
Actions: WidgetActions,
|
|
202
|
+
AskFooter: WidgetAskFooter,
|
|
203
|
+
HealthFooter: WidgetHealthFooter,
|
|
204
|
+
};
|
|
205
|
+
//# sourceMappingURL=widget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 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"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The egress allowlist matcher (ruling 3) — ONE pure function, shared by the
|
|
3
|
+
* harness mock, the promote-gate check, and the production `ctx.fetch`
|
|
4
|
+
* wrapper, so a test that passes the mock cannot be an egress surprise later.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Returns whether `url`'s ORIGIN is covered by the egress allowlist.
|
|
8
|
+
*
|
|
9
|
+
* Semantics (ruling 3 — origin-only, explicit subdomain wildcards):
|
|
10
|
+
* - Matching is on the origin (scheme + host + port) of the RESOLVED URL;
|
|
11
|
+
* path, query, and hash never participate. Default ports are normalized
|
|
12
|
+
* (`https://x.com:443` and `https://x.com` are the same origin).
|
|
13
|
+
* - An exact entry (`https://api.example.com`) matches that origin only.
|
|
14
|
+
* - A wildcard entry (`https://*.example.com`) matches subdomains at any
|
|
15
|
+
* depth (`api.example.com`, `a.b.example.com`) but NOT the apex
|
|
16
|
+
* (`example.com`) — list the apex explicitly if it is also allowed. This
|
|
17
|
+
* keeps the reviewed surface literal: a wildcard grant is a statement
|
|
18
|
+
* about hosts UNDER a domain, never silently about the domain itself.
|
|
19
|
+
* - Scheme and port must match exactly; a wildcard never crosses either.
|
|
20
|
+
*
|
|
21
|
+
* Redirects: this function checks one URL. The caller (the fetch wrapper)
|
|
22
|
+
* must re-check the target origin of every redirect hop — a redirect to a
|
|
23
|
+
* non-allowlisted origin is also a denial.
|
|
24
|
+
*
|
|
25
|
+
* Throws `TypeError` (with remediation) on a malformed `url` — the same
|
|
26
|
+
* input `fetch` would reject — or on a malformed allowlist entry, so a typo
|
|
27
|
+
* in the egress declaration fails loudly in the harness rather than
|
|
28
|
+
* silently never matching.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isEgressAllowed(url: string | URL, allowlist: readonly string[]): boolean;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The egress allowlist matcher (ruling 3) — ONE pure function, shared by the
|
|
3
|
+
* harness mock, the promote-gate check, and the production `ctx.fetch`
|
|
4
|
+
* wrapper, so a test that passes the mock cannot be an egress surprise later.
|
|
5
|
+
*/
|
|
6
|
+
const WILDCARD_MARKER = '://*.';
|
|
7
|
+
function invalidEntry(entry, detail) {
|
|
8
|
+
return new TypeError(`Egress allowlist entry "${entry}" is invalid: ${detail}. ` +
|
|
9
|
+
`Entries are origins only — "https://api.example.com" (exact) or ` +
|
|
10
|
+
`"https://*.example.com" (subdomain wildcard); scheme + host + optional ` +
|
|
11
|
+
`port, no path, query, hash, or credentials. Fix the entry in the ` +
|
|
12
|
+
`integration's egress declaration.`);
|
|
13
|
+
}
|
|
14
|
+
function parseEntry(entry) {
|
|
15
|
+
const wildcard = entry.includes(WILDCARD_MARKER);
|
|
16
|
+
const candidate = wildcard ? entry.replace(WILDCARD_MARKER, '://') : entry;
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = new URL(candidate);
|
|
20
|
+
}
|
|
21
|
+
catch (_a) {
|
|
22
|
+
throw invalidEntry(entry, 'it does not parse as a URL origin');
|
|
23
|
+
}
|
|
24
|
+
if (parsed.hostname.includes('*')) {
|
|
25
|
+
throw invalidEntry(entry, 'wildcards are only supported as a leading "*." on the hostname');
|
|
26
|
+
}
|
|
27
|
+
if (parsed.pathname !== '/' || parsed.search !== '' || parsed.hash !== '') {
|
|
28
|
+
throw invalidEntry(entry, 'it carries a path, query, or hash');
|
|
29
|
+
}
|
|
30
|
+
if (parsed.username !== '' || parsed.password !== '') {
|
|
31
|
+
throw invalidEntry(entry, 'it carries credentials');
|
|
32
|
+
}
|
|
33
|
+
if (parsed.hostname === '') {
|
|
34
|
+
throw invalidEntry(entry, 'it has no hostname');
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
protocol: parsed.protocol,
|
|
38
|
+
hostname: parsed.hostname,
|
|
39
|
+
port: parsed.port,
|
|
40
|
+
wildcard,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns whether `url`'s ORIGIN is covered by the egress allowlist.
|
|
45
|
+
*
|
|
46
|
+
* Semantics (ruling 3 — origin-only, explicit subdomain wildcards):
|
|
47
|
+
* - Matching is on the origin (scheme + host + port) of the RESOLVED URL;
|
|
48
|
+
* path, query, and hash never participate. Default ports are normalized
|
|
49
|
+
* (`https://x.com:443` and `https://x.com` are the same origin).
|
|
50
|
+
* - An exact entry (`https://api.example.com`) matches that origin only.
|
|
51
|
+
* - A wildcard entry (`https://*.example.com`) matches subdomains at any
|
|
52
|
+
* depth (`api.example.com`, `a.b.example.com`) but NOT the apex
|
|
53
|
+
* (`example.com`) — list the apex explicitly if it is also allowed. This
|
|
54
|
+
* keeps the reviewed surface literal: a wildcard grant is a statement
|
|
55
|
+
* about hosts UNDER a domain, never silently about the domain itself.
|
|
56
|
+
* - Scheme and port must match exactly; a wildcard never crosses either.
|
|
57
|
+
*
|
|
58
|
+
* Redirects: this function checks one URL. The caller (the fetch wrapper)
|
|
59
|
+
* must re-check the target origin of every redirect hop — a redirect to a
|
|
60
|
+
* non-allowlisted origin is also a denial.
|
|
61
|
+
*
|
|
62
|
+
* Throws `TypeError` (with remediation) on a malformed `url` — the same
|
|
63
|
+
* input `fetch` would reject — or on a malformed allowlist entry, so a typo
|
|
64
|
+
* in the egress declaration fails loudly in the harness rather than
|
|
65
|
+
* silently never matching.
|
|
66
|
+
*/
|
|
67
|
+
export function isEgressAllowed(url, allowlist) {
|
|
68
|
+
const target = new URL(url);
|
|
69
|
+
return allowlist.some((entry) => {
|
|
70
|
+
const parsed = parseEntry(entry);
|
|
71
|
+
if (parsed.protocol !== target.protocol)
|
|
72
|
+
return false;
|
|
73
|
+
if (parsed.port !== target.port)
|
|
74
|
+
return false;
|
|
75
|
+
if (parsed.wildcard) {
|
|
76
|
+
return target.hostname.endsWith(`.${parsed.hostname}`);
|
|
77
|
+
}
|
|
78
|
+
return target.hostname === parsed.hostname;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=egress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress.js","sourceRoot":"","sources":["../../src/context/egress.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,eAAe,GAAG,OAAO,CAAC;AAShC,SAAS,YAAY,CAAC,KAAa,EAAE,MAAc;IACjD,OAAO,IAAI,SAAS,CAClB,2BAA2B,KAAK,iBAAiB,MAAM,IAAI;QACzD,kEAAkE;QAClE,yEAAyE;QACzE,mEAAmE;QACnE,mCAAmC,CACtC,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACjD,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAE3E,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;IAC9B,CAAC;IAAC,WAAM,CAAC;QACP,MAAM,YAAY,CAAC,KAAK,EAAE,mCAAmC,CAAC,CAAC;IACjE,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAClC,MAAM,YAAY,CAChB,KAAK,EACL,gEAAgE,CACjE,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,IAAI,MAAM,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;QAC1E,MAAM,YAAY,CAAC,KAAK,EAAE,mCAAmC,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,EAAE,IAAI,MAAM,CAAC,QAAQ,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,YAAY,CAAC,KAAK,EAAE,wBAAwB,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,EAAE,EAAE,CAAC;QAC3B,MAAM,YAAY,CAAC,KAAK,EAAE,oBAAoB,CAAC,CAAC;IAClD,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,QAAQ;KACT,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAiB,EACjB,SAA4B;IAE5B,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IAE5B,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;QAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QACjC,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QACtD,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC;QAC9C,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACpB,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * The egress allowlist matcher (ruling 3) — ONE pure function, shared by the\n * harness mock, the promote-gate check, and the production `ctx.fetch`\n * wrapper, so a test that passes the mock cannot be an egress surprise later.\n */\n\nconst WILDCARD_MARKER = '://*.';\n\ninterface ParsedEntry {\n protocol: string;\n hostname: string;\n port: string;\n wildcard: boolean;\n}\n\nfunction invalidEntry(entry: string, detail: string): TypeError {\n return new TypeError(\n `Egress allowlist entry \"${entry}\" is invalid: ${detail}. ` +\n `Entries are origins only — \"https://api.example.com\" (exact) or ` +\n `\"https://*.example.com\" (subdomain wildcard); scheme + host + optional ` +\n `port, no path, query, hash, or credentials. Fix the entry in the ` +\n `integration's egress declaration.`,\n );\n}\n\nfunction parseEntry(entry: string): ParsedEntry {\n const wildcard = entry.includes(WILDCARD_MARKER);\n const candidate = wildcard ? entry.replace(WILDCARD_MARKER, '://') : entry;\n\n let parsed: URL;\n try {\n parsed = new URL(candidate);\n } catch {\n throw invalidEntry(entry, 'it does not parse as a URL origin');\n }\n\n if (parsed.hostname.includes('*')) {\n throw invalidEntry(\n entry,\n 'wildcards are only supported as a leading \"*.\" on the hostname',\n );\n }\n if (parsed.pathname !== '/' || parsed.search !== '' || parsed.hash !== '') {\n throw invalidEntry(entry, 'it carries a path, query, or hash');\n }\n if (parsed.username !== '' || parsed.password !== '') {\n throw invalidEntry(entry, 'it carries credentials');\n }\n if (parsed.hostname === '') {\n throw invalidEntry(entry, 'it has no hostname');\n }\n\n return {\n protocol: parsed.protocol,\n hostname: parsed.hostname,\n port: parsed.port,\n wildcard,\n };\n}\n\n/**\n * Returns whether `url`'s ORIGIN is covered by the egress allowlist.\n *\n * Semantics (ruling 3 — origin-only, explicit subdomain wildcards):\n * - Matching is on the origin (scheme + host + port) of the RESOLVED URL;\n * path, query, and hash never participate. Default ports are normalized\n * (`https://x.com:443` and `https://x.com` are the same origin).\n * - An exact entry (`https://api.example.com`) matches that origin only.\n * - A wildcard entry (`https://*.example.com`) matches subdomains at any\n * depth (`api.example.com`, `a.b.example.com`) but NOT the apex\n * (`example.com`) — list the apex explicitly if it is also allowed. This\n * keeps the reviewed surface literal: a wildcard grant is a statement\n * about hosts UNDER a domain, never silently about the domain itself.\n * - Scheme and port must match exactly; a wildcard never crosses either.\n *\n * Redirects: this function checks one URL. The caller (the fetch wrapper)\n * must re-check the target origin of every redirect hop — a redirect to a\n * non-allowlisted origin is also a denial.\n *\n * Throws `TypeError` (with remediation) on a malformed `url` — the same\n * input `fetch` would reject — or on a malformed allowlist entry, so a typo\n * in the egress declaration fails loudly in the harness rather than\n * silently never matching.\n */\nexport function isEgressAllowed(\n url: string | URL,\n allowlist: readonly string[],\n): boolean {\n const target = new URL(url);\n\n return allowlist.some((entry) => {\n const parsed = parseEntry(entry);\n if (parsed.protocol !== target.protocol) return false;\n if (parsed.port !== target.port) return false;\n if (parsed.wildcard) {\n return target.hostname.endsWith(`.${parsed.hostname}`);\n }\n return target.hostname === parsed.hostname;\n });\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability-context error classes. Dependency-pure and isomorphic — the
|
|
3
|
+
* production wrapper and the `@ekanos/sdk/testing` mock throw these same
|
|
4
|
+
* classes, so `instanceof` checks written against the mock hold in prod.
|
|
5
|
+
* Every message is a remediation instruction: what was wrong + what to do.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Thrown by `ctx.fetch` when a request is refused by egress policy.
|
|
9
|
+
*
|
|
10
|
+
* The default case is the allowlist: the resolved URL's origin is not in the
|
|
11
|
+
* integration's egress list (proposal §2.3, ruling 3), and it throws
|
|
12
|
+
* synchronously, before any network I/O. Never a synthetic 403 `Response`: a
|
|
13
|
+
* fake response gets retried, parsed, and mis-attributed to the remote API.
|
|
14
|
+
*
|
|
15
|
+
* `reason` covers the network-level denials the host layer adds on top of the
|
|
16
|
+
* string match — an allowlisted name that resolves into a private, loopback,
|
|
17
|
+
* or link-local range, on the first hop or any redirect hop. Those are found
|
|
18
|
+
* after DNS, so they reject the returned promise instead. `reason` is a
|
|
19
|
+
* CATEGORY, never an address: it tells a partner what class of destination
|
|
20
|
+
* was refused without describing the host's network.
|
|
21
|
+
*/
|
|
22
|
+
export declare class EgressDeniedError extends Error {
|
|
23
|
+
readonly origin: string;
|
|
24
|
+
readonly reason?: string | undefined;
|
|
25
|
+
readonly code = "EGRESS_DENIED";
|
|
26
|
+
constructor(origin: string, reason?: string | undefined);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Thrown by `secrets.set()` when the name resolves only from an admin-issued
|
|
30
|
+
* tier (ruling 2). Writes are tier-bound: only the account-tier store bound
|
|
31
|
+
* to {account, product} at construction is writable.
|
|
32
|
+
*/
|
|
33
|
+
export declare class SecretAccessError extends Error {
|
|
34
|
+
readonly secretName: string;
|
|
35
|
+
readonly code = "SECRET_ACCESS_DENIED";
|
|
36
|
+
constructor(secretName: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Thrown by `ctx.storage` on any of (ruling 1):
|
|
40
|
+
* - access to a key with no declared schema (strict keys),
|
|
41
|
+
* - a write whose payload fails the declared schema,
|
|
42
|
+
* - a read whose stored data no longer matches the declared schema
|
|
43
|
+
* (schema evolution is explicit — versioned keys or `z.union` — never silent).
|
|
44
|
+
*
|
|
45
|
+
* `hint` carries the remediation, with zod issues formatted per path.
|
|
46
|
+
*/
|
|
47
|
+
export declare class StorageValidationError extends Error {
|
|
48
|
+
readonly code = "STORAGE_VALIDATION_FAILED";
|
|
49
|
+
readonly scope: 'account' | 'user';
|
|
50
|
+
readonly key: string;
|
|
51
|
+
readonly hint: string;
|
|
52
|
+
constructor(details: {
|
|
53
|
+
scope: 'account' | 'user';
|
|
54
|
+
key: string;
|
|
55
|
+
hint: string;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability-context error classes. Dependency-pure and isomorphic — the
|
|
3
|
+
* production wrapper and the `@ekanos/sdk/testing` mock throw these same
|
|
4
|
+
* classes, so `instanceof` checks written against the mock hold in prod.
|
|
5
|
+
* Every message is a remediation instruction: what was wrong + what to do.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Thrown by `ctx.fetch` when a request is refused by egress policy.
|
|
9
|
+
*
|
|
10
|
+
* The default case is the allowlist: the resolved URL's origin is not in the
|
|
11
|
+
* integration's egress list (proposal §2.3, ruling 3), and it throws
|
|
12
|
+
* synchronously, before any network I/O. Never a synthetic 403 `Response`: a
|
|
13
|
+
* fake response gets retried, parsed, and mis-attributed to the remote API.
|
|
14
|
+
*
|
|
15
|
+
* `reason` covers the network-level denials the host layer adds on top of the
|
|
16
|
+
* string match — an allowlisted name that resolves into a private, loopback,
|
|
17
|
+
* or link-local range, on the first hop or any redirect hop. Those are found
|
|
18
|
+
* after DNS, so they reject the returned promise instead. `reason` is a
|
|
19
|
+
* CATEGORY, never an address: it tells a partner what class of destination
|
|
20
|
+
* was refused without describing the host's network.
|
|
21
|
+
*/
|
|
22
|
+
export class EgressDeniedError extends Error {
|
|
23
|
+
constructor(origin, reason) {
|
|
24
|
+
super(reason
|
|
25
|
+
? `Egress to ${origin} is denied: ${reason}. Integration egress may ` +
|
|
26
|
+
`only reach publicly routable addresses — private, loopback, ` +
|
|
27
|
+
`link-local, and other non-public ranges are refused on the ` +
|
|
28
|
+
`initial request and on every redirect hop. Point the ` +
|
|
29
|
+
`integration at a public host.`
|
|
30
|
+
: `Egress to ${origin} is not in this integration's allowlist. ` +
|
|
31
|
+
`Declare the origin in the integration's egress list — an exact ` +
|
|
32
|
+
`origin ("https://api.example.com") or a "*." subdomain wildcard ` +
|
|
33
|
+
`("https://*.example.com") — or call an allowed origin instead. ` +
|
|
34
|
+
`Allowlist changes are a security-posture change reviewed at ` +
|
|
35
|
+
`promotion.`);
|
|
36
|
+
this.origin = origin;
|
|
37
|
+
this.reason = reason;
|
|
38
|
+
this.code = 'EGRESS_DENIED';
|
|
39
|
+
this.name = 'EgressDeniedError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Thrown by `secrets.set()` when the name resolves only from an admin-issued
|
|
44
|
+
* tier (ruling 2). Writes are tier-bound: only the account-tier store bound
|
|
45
|
+
* to {account, product} at construction is writable.
|
|
46
|
+
*/
|
|
47
|
+
export class SecretAccessError extends Error {
|
|
48
|
+
constructor(secretName) {
|
|
49
|
+
super(`Secret "${secretName}" resolves only from an admin-issued tier (source ` +
|
|
50
|
+
`or global) and cannot be written from integration code. secrets.set() ` +
|
|
51
|
+
`rotates account-tier values only — the ones this integration stored for ` +
|
|
52
|
+
`the current account. If this credential needs a new value, an admin ` +
|
|
53
|
+
`must update it where it was issued.`);
|
|
54
|
+
this.secretName = secretName;
|
|
55
|
+
this.code = 'SECRET_ACCESS_DENIED';
|
|
56
|
+
this.name = 'SecretAccessError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Thrown by `ctx.storage` on any of (ruling 1):
|
|
61
|
+
* - access to a key with no declared schema (strict keys),
|
|
62
|
+
* - a write whose payload fails the declared schema,
|
|
63
|
+
* - a read whose stored data no longer matches the declared schema
|
|
64
|
+
* (schema evolution is explicit — versioned keys or `z.union` — never silent).
|
|
65
|
+
*
|
|
66
|
+
* `hint` carries the remediation, with zod issues formatted per path.
|
|
67
|
+
*/
|
|
68
|
+
export class StorageValidationError extends Error {
|
|
69
|
+
constructor(details) {
|
|
70
|
+
super(details.hint);
|
|
71
|
+
this.code = 'STORAGE_VALIDATION_FAILED';
|
|
72
|
+
this.name = 'StorageValidationError';
|
|
73
|
+
this.scope = details.scope;
|
|
74
|
+
this.key = details.key;
|
|
75
|
+
this.hint = details.hint;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/context/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAG1C,YACW,MAAc,EACd,MAAe;QAExB,KAAK,CACH,MAAM;YACJ,CAAC,CAAC,aAAa,MAAM,eAAe,MAAM,2BAA2B;gBACjE,8DAA8D;gBAC9D,6DAA6D;gBAC7D,uDAAuD;gBACvD,+BAA+B;YACnC,CAAC,CAAC,aAAa,MAAM,2CAA2C;gBAC5D,iEAAiE;gBACjE,kEAAkE;gBAClE,iEAAiE;gBACjE,8DAA8D;gBAC9D,YAAY,CACnB,CAAC;QAhBO,WAAM,GAAN,MAAM,CAAQ;QACd,WAAM,GAAN,MAAM,CAAS;QAJjB,SAAI,GAAG,eAAe,CAAC;QAoB9B,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAG1C,YAAqB,UAAkB;QACrC,KAAK,CACH,WAAW,UAAU,oDAAoD;YACvE,wEAAwE;YACxE,0EAA0E;YAC1E,sEAAsE;YACtE,qCAAqC,CACxC,CAAC;QAPiB,eAAU,GAAV,UAAU,CAAQ;QAF9B,SAAI,GAAG,sBAAsB,CAAC;QAUrC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAM/C,YAAY,OAIX;QACC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAVb,SAAI,GAAG,2BAA2B,CAAC;QAW1C,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;CACF","sourcesContent":["/**\n * Capability-context error classes. Dependency-pure and isomorphic — the\n * production wrapper and the `@ekanos/sdk/testing` mock throw these same\n * classes, so `instanceof` checks written against the mock hold in prod.\n * Every message is a remediation instruction: what was wrong + what to do.\n */\n\n/**\n * Thrown by `ctx.fetch` when a request is refused by egress policy.\n *\n * The default case is the allowlist: the resolved URL's origin is not in the\n * integration's egress list (proposal §2.3, ruling 3), and it throws\n * synchronously, before any network I/O. Never a synthetic 403 `Response`: a\n * fake response gets retried, parsed, and mis-attributed to the remote API.\n *\n * `reason` covers the network-level denials the host layer adds on top of the\n * string match — an allowlisted name that resolves into a private, loopback,\n * or link-local range, on the first hop or any redirect hop. Those are found\n * after DNS, so they reject the returned promise instead. `reason` is a\n * CATEGORY, never an address: it tells a partner what class of destination\n * was refused without describing the host's network.\n */\nexport class EgressDeniedError extends Error {\n readonly code = 'EGRESS_DENIED';\n\n constructor(\n readonly origin: string,\n readonly reason?: string,\n ) {\n super(\n reason\n ? `Egress to ${origin} is denied: ${reason}. Integration egress may ` +\n `only reach publicly routable addresses — private, loopback, ` +\n `link-local, and other non-public ranges are refused on the ` +\n `initial request and on every redirect hop. Point the ` +\n `integration at a public host.`\n : `Egress to ${origin} is not in this integration's allowlist. ` +\n `Declare the origin in the integration's egress list — an exact ` +\n `origin (\"https://api.example.com\") or a \"*.\" subdomain wildcard ` +\n `(\"https://*.example.com\") — or call an allowed origin instead. ` +\n `Allowlist changes are a security-posture change reviewed at ` +\n `promotion.`,\n );\n this.name = 'EgressDeniedError';\n }\n}\n\n/**\n * Thrown by `secrets.set()` when the name resolves only from an admin-issued\n * tier (ruling 2). Writes are tier-bound: only the account-tier store bound\n * to {account, product} at construction is writable.\n */\nexport class SecretAccessError extends Error {\n readonly code = 'SECRET_ACCESS_DENIED';\n\n constructor(readonly secretName: string) {\n super(\n `Secret \"${secretName}\" resolves only from an admin-issued tier (source ` +\n `or global) and cannot be written from integration code. secrets.set() ` +\n `rotates account-tier values only — the ones this integration stored for ` +\n `the current account. If this credential needs a new value, an admin ` +\n `must update it where it was issued.`,\n );\n this.name = 'SecretAccessError';\n }\n}\n\n/**\n * Thrown by `ctx.storage` on any of (ruling 1):\n * - access to a key with no declared schema (strict keys),\n * - a write whose payload fails the declared schema,\n * - a read whose stored data no longer matches the declared schema\n * (schema evolution is explicit — versioned keys or `z.union` — never silent).\n *\n * `hint` carries the remediation, with zod issues formatted per path.\n */\nexport class StorageValidationError extends Error {\n readonly code = 'STORAGE_VALIDATION_FAILED';\n readonly scope: 'account' | 'user';\n readonly key: string;\n readonly hint: string;\n\n constructor(details: {\n scope: 'account' | 'user';\n key: string;\n hint: string;\n }) {\n super(details.hint);\n this.name = 'StorageValidationError';\n this.scope = details.scope;\n this.key = details.key;\n this.hint = details.hint;\n }\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ekanos/sdk/context — the capability context's pure shared runtime.
|
|
3
|
+
*
|
|
4
|
+
* Dependency-pure and isomorphic (zod only — no `@kit/*`, no `server-only`):
|
|
5
|
+
* the error classes, the egress allowlist matcher, and the storage
|
|
6
|
+
* validators are ONE implementation shared by the `@ekanos/sdk/testing`
|
|
7
|
+
* mock and the production host wrapper, so behavior proven against the mock
|
|
8
|
+
* holds in prod.
|
|
9
|
+
*
|
|
10
|
+
* The context TYPES (`IntegrationContext` and friends) are exported
|
|
11
|
+
* type-only from the SDK root — import them from `@ekanos/sdk`.
|
|
12
|
+
*
|
|
13
|
+
* Surface discipline: additions require an entry in
|
|
14
|
+
* docs/devex/sdk-export-map.md ("Context & testing entrypoints (P1)").
|
|
15
|
+
*/
|
|
16
|
+
export { EgressDeniedError, SecretAccessError, StorageValidationError, } from './errors.js';
|
|
17
|
+
export { isEgressAllowed } from './egress.js';
|
|
18
|
+
export { assertStorageKeyDeclared, assertStorageKeyWritable, isStorageKeyClientReadable, parseStorageValue, resolveStorageKeyDeclaration, } from './storage-validation.js';
|
|
19
|
+
export { requireContext, type IntegrationContextCarrier, } from './require-context.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ekanos/sdk/context — the capability context's pure shared runtime.
|
|
3
|
+
*
|
|
4
|
+
* Dependency-pure and isomorphic (zod only — no `@kit/*`, no `server-only`):
|
|
5
|
+
* the error classes, the egress allowlist matcher, and the storage
|
|
6
|
+
* validators are ONE implementation shared by the `@ekanos/sdk/testing`
|
|
7
|
+
* mock and the production host wrapper, so behavior proven against the mock
|
|
8
|
+
* holds in prod.
|
|
9
|
+
*
|
|
10
|
+
* The context TYPES (`IntegrationContext` and friends) are exported
|
|
11
|
+
* type-only from the SDK root — import them from `@ekanos/sdk`.
|
|
12
|
+
*
|
|
13
|
+
* Surface discipline: additions require an entry in
|
|
14
|
+
* docs/devex/sdk-export-map.md ("Context & testing entrypoints (P1)").
|
|
15
|
+
*/
|
|
16
|
+
export { EgressDeniedError, SecretAccessError, StorageValidationError, } from './errors.js';
|
|
17
|
+
export { isEgressAllowed } from './egress.js';
|
|
18
|
+
export { assertStorageKeyDeclared, assertStorageKeyWritable, isStorageKeyClientReadable, parseStorageValue, resolveStorageKeyDeclaration, } from './storage-validation.js';
|
|
19
|
+
export { requireContext, } from './require-context.js';
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,0BAA0B,EAC1B,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,cAAc,GAEf,MAAM,mBAAmB,CAAC","sourcesContent":["/**\n * @ekanos/sdk/context — the capability context's pure shared runtime.\n *\n * Dependency-pure and isomorphic (zod only — no `@kit/*`, no `server-only`):\n * the error classes, the egress allowlist matcher, and the storage\n * validators are ONE implementation shared by the `@ekanos/sdk/testing`\n * mock and the production host wrapper, so behavior proven against the mock\n * holds in prod.\n *\n * The context TYPES (`IntegrationContext` and friends) are exported\n * type-only from the SDK root — import them from `@ekanos/sdk`.\n *\n * Surface discipline: additions require an entry in\n * docs/devex/sdk-export-map.md (\"Context & testing entrypoints (P1)\").\n */\n\nexport {\n EgressDeniedError,\n SecretAccessError,\n StorageValidationError,\n} from './errors';\nexport { isEgressAllowed } from './egress';\nexport {\n assertStorageKeyDeclared,\n assertStorageKeyWritable,\n isStorageKeyClientReadable,\n parseStorageValue,\n resolveStorageKeyDeclaration,\n} from './storage-validation';\nexport {\n requireContext,\n type IntegrationContextCarrier,\n} from './require-context';\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `requireContext()` — the typed accessor migrated tools use to read the
|
|
3
|
+
* host-built capability context off the internal `ToolContext`.
|
|
4
|
+
*
|
|
5
|
+
* The host attaches the context ADDITIVELY as an untyped passthrough field
|
|
6
|
+
* (`integrationContext?: unknown` on `@kit/integrations-core`'s ToolContext —
|
|
7
|
+
* core cannot import the SDK without a package cycle). This function is the
|
|
8
|
+
* SDK-side narrowing: structural assertion + remediation error, so first-party
|
|
9
|
+
* tools get full typing from the package they already depend on.
|
|
10
|
+
*
|
|
11
|
+
* Dependency-pure and isomorphic like the rest of `/context`.
|
|
12
|
+
*/
|
|
13
|
+
import type { IntegrationContext, StorageSchemas } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* The one field `requireContext` reads. `@kit/integrations-core`'s
|
|
16
|
+
* `ToolContext` satisfies this structurally without core importing the SDK.
|
|
17
|
+
*/
|
|
18
|
+
export interface IntegrationContextCarrier {
|
|
19
|
+
integrationContext?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Narrow the internal tool context's `integrationContext` passthrough to the
|
|
23
|
+
* SDK's `IntegrationContext`. Throws with a remediation message when the host
|
|
24
|
+
* path did not attach one — a migrated tool must only run on wired paths.
|
|
25
|
+
*/
|
|
26
|
+
export declare function requireContext<Schemas extends StorageSchemas = StorageSchemas>(toolContext: IntegrationContextCarrier): IntegrationContext<Schemas>;
|