@ekanos/sdk 0.1.5 → 0.2.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/api-report.md CHANGED
@@ -9,7 +9,7 @@ This is the complete surface of every entrypoint. The authoring guide
9
9
  for these symbols is README.md. Entrypoints marked WORKSPACE-ONLY are
10
10
  not reachable from a published install — see the note in each section.
11
11
 
12
- Total exported names: **150** across 7 entrypoints.
12
+ Total exported names: **160** across 7 entrypoints.
13
13
 
14
14
  ## `@ekanos/sdk` (32 exports)
15
15
 
@@ -77,7 +77,7 @@ Total exported names: **150** across 7 entrypoints.
77
77
  | `WidgetState` | interface |
78
78
  | `currentReturnPath` | function |
79
79
 
80
- ## `@ekanos/sdk/hooks` (28 exports)
80
+ ## `@ekanos/sdk/hooks` (37 exports)
81
81
 
82
82
  | Export | Kind |
83
83
  | --- | --- |
@@ -91,19 +91,28 @@ Total exported names: **150** across 7 entrypoints.
91
91
  | `FetchIntegrationStorageOptions` | interface |
92
92
  | `IntegrationActionResult` | type alias |
93
93
  | `IntegrationActivationActions` | interface |
94
+ | `IntegrationActivationDataProvider` | function |
94
95
  | `IntegrationActivationProvider` | function |
96
+ | `IntegrationFetchProvider` | function |
95
97
  | `OAuthErrorParam` | interface |
98
+ | `PartnerOAuthPathOptions` | interface |
96
99
  | `PublishEventInput` | interface |
97
100
  | `UseOAuthConnectionStatusOptions` | interface |
98
101
  | `WidgetEvent` | interface |
99
102
  | `WidgetEventFilter` | interface |
100
103
  | `WidgetEventProvider` | function |
101
104
  | `WidgetEventSubscriber` | type alias |
105
+ | `createPartnerConnectionCheck` | function |
102
106
  | `fetchIntegrationStorage` | function |
103
107
  | `integrationStorageUrl` | function |
108
+ | `partnerOAuthConnectPath` | function |
109
+ | `partnerOAuthErrorParam` | function |
110
+ | `partnerOAuthStatusUrl` | function |
104
111
  | `useActivateIntegration` | function |
112
+ | `useActivationData` | function |
105
113
  | `useAskAssistant` | function |
106
114
  | `useDeactivateIntegration` | function |
115
+ | `useIntegrationFetch` | function |
107
116
  | `useOAuthConnectionStatus` | function |
108
117
  | `useOAuthWidgetGate` | function |
109
118
  | `useRegisterAskAssistant` | function |
@@ -153,7 +162,7 @@ Total exported names: **150** across 7 entrypoints.
153
162
  | `requireContext` | function |
154
163
  | `resolveStorageKeyDeclaration` | function |
155
164
 
156
- ## `@ekanos/sdk/testing` (16 exports)
165
+ ## `@ekanos/sdk/testing` (17 exports)
157
166
 
158
167
  | Export | Kind |
159
168
  | --- | --- |
@@ -170,6 +179,7 @@ Total exported names: **150** across 7 entrypoints.
170
179
  | `ScheduleInvocationOutcome` | interface |
171
180
  | `WebhookInvocationOutcome` | interface |
172
181
  | `createMockContext` | function |
182
+ | `createMockFetchProvider` | function |
173
183
  | `invokeActivate` | function |
174
184
  | `invokeSchedule` | function |
175
185
  | `invokeWebhook` | function |
@@ -1 +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"]}
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,CAC9B,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EACxC,EAAE,CACH,CAAC;IAEF,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(\n () => setPinned((previous) => !previous),\n [],\n );\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"]}
@@ -22,7 +22,11 @@ export { fetchIntegrationStorage, integrationStorageUrl, } from './fetch-integra
22
22
  export type { FetchIntegrationStorageOptions } from './fetch-integration-storage.js';
23
23
  export { useOAuthConnectionStatus, useOAuthWidgetGate, } from './use-oauth-connection-status.js';
24
24
  export type { ConnectionCheckAction, ConnectionCheckResult, OAuthErrorParam, UseOAuthConnectionStatusOptions, } from './use-oauth-connection-status.js';
25
+ export { createPartnerConnectionCheck, partnerOAuthConnectPath, partnerOAuthErrorParam, partnerOAuthStatusUrl, } from './partner-oauth.js';
26
+ export type { PartnerOAuthPathOptions } from './partner-oauth.js';
25
27
  export { WidgetEventProvider, useWidgetEventBus } from './widget-event-context.js';
26
28
  export { useWidgetEvents } from './use-widget-events.js';
27
29
  export type { PublishEventInput, WidgetEvent, WidgetEventFilter, WidgetEventSubscriber, } from './widget-event-types.js';
28
30
  export { AskAssistantProvider, useAskAssistant, useRegisterAskAssistant, } from './ask-assistant-context.js';
31
+ export { IntegrationFetchProvider, useIntegrationFetch, } from './integration-fetch-context.js';
32
+ export { IntegrationActivationDataProvider, useActivationData, } from './integration-activation-data-context.js';
@@ -24,9 +24,21 @@ export { IntegrationActivationProvider } from './activation-actions-context.js';
24
24
  export { fetchIntegrationStorage, integrationStorageUrl, } from './fetch-integration-storage.js';
25
25
  // OAuth connection status
26
26
  export { useOAuthConnectionStatus, useOAuthWidgetGate, } from './use-oauth-connection-status.js';
27
+ // Hosted partner OAuth transport (host routes:
28
+ // GET /api/integrations/[slug]/oauth/{connect,callback,status}). A partner
29
+ // passes `connectPath: partnerOAuthConnectPath(slug)` to OAuthActivationForm
30
+ // and `createPartnerConnectionCheck(slug)` as its `checkConnection`.
31
+ export { createPartnerConnectionCheck, partnerOAuthConnectPath, partnerOAuthErrorParam, partnerOAuthStatusUrl, } from './partner-oauth.js';
27
32
  // Widget Event Bus
28
33
  export { WidgetEventProvider, useWidgetEventBus } from './widget-event-context.js';
29
34
  export { useWidgetEvents } from './use-widget-events.js';
30
35
  // Widget Ask-Bar Bridge
31
36
  export { AskAssistantProvider, useAskAssistant, useRegisterAskAssistant, } from './ask-assistant-context.js';
37
+ // Client-side fetch (host routes: none — this mediates a widget's own
38
+ // vendor calls, mirroring `ctx.fetch` on the server). Default outside a
39
+ // provider is a plain `globalThis.fetch` passthrough.
40
+ export { IntegrationFetchProvider, useIntegrationFetch, } from './integration-fetch-context.js';
41
+ // Activation data (the credential half of the client-fetch pattern).
42
+ // Default outside a provider is `null` — no host mounts this yet.
43
+ export { IntegrationActivationDataProvider, useActivationData, } from './integration-activation-data-context.js';
32
44
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,4CAA4C;AAC5C,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAU7E,kDAAkD;AAClD,mEAAmE;AACnE,qEAAqE;AACrE,OAAO,EACL,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AAGrC,0BAA0B;AAC1B,OAAO,EACL,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,+BAA+B,CAAC;AAQvC,mBAAmB;AACnB,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAQtD,wBAAwB;AACxB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,uBAAuB,GACxB,MAAM,yBAAyB,CAAC","sourcesContent":["/**\n * @ekanos/sdk/hooks — client-side hooks.\n *\n * Import from client components only. The SDK OWNS these implementations;\n * `@kit/integrations-core/hooks` re-exports them for first-party code, so\n * there is one source of truth with two consumption paths\n * so there is one source of truth with two consumption paths.\n *\n * Server seam: nothing here imports a server module. The activation hooks\n * reach the host's `'use server'` actions ONLY through the\n * `IntegrationActivationProvider` injection seam\n * (activation-actions-context.tsx) — that is what keeps this entrypoint free\n * of `server-only` in a partner's module graph.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\n// Activation (host-injected server actions)\nexport {\n useActivateIntegration,\n useDeactivateIntegration,\n} from './use-activate-integration';\nexport { IntegrationActivationProvider } from './activation-actions-context';\nexport type {\n ActivateIntegrationAction,\n ActivateIntegrationInput,\n DeactivateIntegrationAction,\n DeactivateIntegrationInput,\n IntegrationActionResult,\n IntegrationActivationActions,\n} from './activation-actions-context';\n\n// Generic client-side storage bridge (host route:\n// GET /api/integrations/[slug]/storage). Serves only keys declared\n// `clientReadable: true`; a key that did not opt in reads as absent.\nexport {\n fetchIntegrationStorage,\n integrationStorageUrl,\n} from './fetch-integration-storage';\nexport type { FetchIntegrationStorageOptions } from './fetch-integration-storage';\n\n// OAuth connection status\nexport {\n useOAuthConnectionStatus,\n useOAuthWidgetGate,\n} from './use-oauth-connection-status';\nexport type {\n ConnectionCheckAction,\n ConnectionCheckResult,\n OAuthErrorParam,\n UseOAuthConnectionStatusOptions,\n} from './use-oauth-connection-status';\n\n// Widget Event Bus\nexport { WidgetEventProvider, useWidgetEventBus } from './widget-event-context';\nexport { useWidgetEvents } from './use-widget-events';\nexport type {\n PublishEventInput,\n WidgetEvent,\n WidgetEventFilter,\n WidgetEventSubscriber,\n} from './widget-event-types';\n\n// Widget Ask-Bar Bridge\nexport {\n AskAssistantProvider,\n useAskAssistant,\n useRegisterAskAssistant,\n} from './ask-assistant-context';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,4CAA4C;AAC5C,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAU7E,kDAAkD;AAClD,mEAAmE;AACnE,qEAAqE;AACrE,OAAO,EACL,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AAGrC,0BAA0B;AAC1B,OAAO,EACL,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,+BAA+B,CAAC;AAQvC,+CAA+C;AAC/C,2EAA2E;AAC3E,6EAA6E;AAC7E,qEAAqE;AACrE,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AAGzB,mBAAmB;AACnB,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAQtD,wBAAwB;AACxB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AAEjC,sEAAsE;AACtE,wEAAwE;AACxE,sDAAsD;AACtD,OAAO,EACL,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AAErC,qEAAqE;AACrE,kEAAkE;AAClE,OAAO,EACL,iCAAiC,EACjC,iBAAiB,GAClB,MAAM,uCAAuC,CAAC","sourcesContent":["/**\n * @ekanos/sdk/hooks — client-side hooks.\n *\n * Import from client components only. The SDK OWNS these implementations;\n * `@kit/integrations-core/hooks` re-exports them for first-party code, so\n * there is one source of truth with two consumption paths\n * so there is one source of truth with two consumption paths.\n *\n * Server seam: nothing here imports a server module. The activation hooks\n * reach the host's `'use server'` actions ONLY through the\n * `IntegrationActivationProvider` injection seam\n * (activation-actions-context.tsx) — that is what keeps this entrypoint free\n * of `server-only` in a partner's module graph.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\n// Activation (host-injected server actions)\nexport {\n useActivateIntegration,\n useDeactivateIntegration,\n} from './use-activate-integration';\nexport { IntegrationActivationProvider } from './activation-actions-context';\nexport type {\n ActivateIntegrationAction,\n ActivateIntegrationInput,\n DeactivateIntegrationAction,\n DeactivateIntegrationInput,\n IntegrationActionResult,\n IntegrationActivationActions,\n} from './activation-actions-context';\n\n// Generic client-side storage bridge (host route:\n// GET /api/integrations/[slug]/storage). Serves only keys declared\n// `clientReadable: true`; a key that did not opt in reads as absent.\nexport {\n fetchIntegrationStorage,\n integrationStorageUrl,\n} from './fetch-integration-storage';\nexport type { FetchIntegrationStorageOptions } from './fetch-integration-storage';\n\n// OAuth connection status\nexport {\n useOAuthConnectionStatus,\n useOAuthWidgetGate,\n} from './use-oauth-connection-status';\nexport type {\n ConnectionCheckAction,\n ConnectionCheckResult,\n OAuthErrorParam,\n UseOAuthConnectionStatusOptions,\n} from './use-oauth-connection-status';\n\n// Hosted partner OAuth transport (host routes:\n// GET /api/integrations/[slug]/oauth/{connect,callback,status}). A partner\n// passes `connectPath: partnerOAuthConnectPath(slug)` to OAuthActivationForm\n// and `createPartnerConnectionCheck(slug)` as its `checkConnection`.\nexport {\n createPartnerConnectionCheck,\n partnerOAuthConnectPath,\n partnerOAuthErrorParam,\n partnerOAuthStatusUrl,\n} from './partner-oauth';\nexport type { PartnerOAuthPathOptions } from './partner-oauth';\n\n// Widget Event Bus\nexport { WidgetEventProvider, useWidgetEventBus } from './widget-event-context';\nexport { useWidgetEvents } from './use-widget-events';\nexport type {\n PublishEventInput,\n WidgetEvent,\n WidgetEventFilter,\n WidgetEventSubscriber,\n} from './widget-event-types';\n\n// Widget Ask-Bar Bridge\nexport {\n AskAssistantProvider,\n useAskAssistant,\n useRegisterAskAssistant,\n} from './ask-assistant-context';\n\n// Client-side fetch (host routes: none — this mediates a widget's own\n// vendor calls, mirroring `ctx.fetch` on the server). Default outside a\n// provider is a plain `globalThis.fetch` passthrough.\nexport {\n IntegrationFetchProvider,\n useIntegrationFetch,\n} from './integration-fetch-context';\n\n// Activation data (the credential half of the client-fetch pattern).\n// Default outside a provider is `null` — no host mounts this yet.\nexport {\n IntegrationActivationDataProvider,\n useActivationData,\n} from './integration-activation-data-context';\n"]}
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare function IntegrationActivationDataProvider({ value, children, }: {
3
+ /** Whatever the host/harness chooses to expose. `null` if nothing has. */
4
+ value: unknown;
5
+ children: ReactNode;
6
+ }): import("react").JSX.Element;
7
+ /**
8
+ * `null` unless something mounted `IntegrationActivationDataProvider` above
9
+ * this widget. In production today, nothing does — this hook exists so
10
+ * partner code has one stable call site to reach for, not because the host
11
+ * currently has anything to hand it.
12
+ */
13
+ export declare function useActivationData<T = unknown>(): T | null;
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, use } from 'react';
4
+ /**
5
+ * The stable call site for reading whatever the host chooses to expose about
6
+ * an integration's activation — the credential half of the client-fetch
7
+ * pattern (`useIntegrationFetch` is the transport half). In production
8
+ * today, no host mounts `IntegrationActivationDataProvider` and nothing is
9
+ * client-visible about an activation beyond
10
+ * `IntegrationComponentProps` — this hook's default is `null`, exactly like
11
+ * today. It exists so partner code has one place to reach for this, ahead of
12
+ * a host-side mechanism (a schema-level `clientReadable` flag on activation
13
+ * fields, mirroring storage's own) that is deliberately out of scope here.
14
+ */
15
+ const IntegrationActivationDataContext = createContext(null);
16
+ export function IntegrationActivationDataProvider({ value, children, }) {
17
+ return (_jsx(IntegrationActivationDataContext, { value: value, children: children }));
18
+ }
19
+ /**
20
+ * `null` unless something mounted `IntegrationActivationDataProvider` above
21
+ * this widget. In production today, nothing does — this hook exists so
22
+ * partner code has one stable call site to reach for, not because the host
23
+ * currently has anything to hand it.
24
+ */
25
+ export function useActivationData() {
26
+ return use(IntegrationActivationDataContext);
27
+ }
28
+ //# sourceMappingURL=integration-activation-data-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration-activation-data-context.js","sourceRoot":"","sources":["../../src/hooks/integration-activation-data-context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAkB,aAAa,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,MAAM,gCAAgC,GAAG,aAAa,CAAU,IAAI,CAAC,CAAC;AAEtE,MAAM,UAAU,iCAAiC,CAAC,EAChD,KAAK,EACL,QAAQ,GAKT;IACC,OAAO,CACL,KAAC,gCAAgC,IAAC,KAAK,EAAE,KAAK,YAC3C,QAAQ,GACwB,CACpC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,GAAG,CAAC,gCAAgC,CAAa,CAAC;AAC3D,CAAC","sourcesContent":["'use client';\n\nimport { type ReactNode, createContext, use } from 'react';\n\n/**\n * The stable call site for reading whatever the host chooses to expose about\n * an integration's activation — the credential half of the client-fetch\n * pattern (`useIntegrationFetch` is the transport half). In production\n * today, no host mounts `IntegrationActivationDataProvider` and nothing is\n * client-visible about an activation beyond\n * `IntegrationComponentProps` — this hook's default is `null`, exactly like\n * today. It exists so partner code has one place to reach for this, ahead of\n * a host-side mechanism (a schema-level `clientReadable` flag on activation\n * fields, mirroring storage's own) that is deliberately out of scope here.\n */\nconst IntegrationActivationDataContext = createContext<unknown>(null);\n\nexport function IntegrationActivationDataProvider({\n value,\n children,\n}: {\n /** Whatever the host/harness chooses to expose. `null` if nothing has. */\n value: unknown;\n children: ReactNode;\n}) {\n return (\n <IntegrationActivationDataContext value={value}>\n {children}\n </IntegrationActivationDataContext>\n );\n}\n\n/**\n * `null` unless something mounted `IntegrationActivationDataProvider` above\n * this widget. In production today, nothing does — this hook exists so\n * partner code has one stable call site to reach for, not because the host\n * currently has anything to hand it.\n */\nexport function useActivationData<T = unknown>(): T | null {\n return use(IntegrationActivationDataContext) as T | null;\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { IntegrationFetch } from '../context/types.js';
3
+ /**
4
+ * Host-mounted (or harness-mounted) provider that supplies the fetch a
5
+ * widget's `useIntegrationFetch()` calls should resolve to — typically an
6
+ * allowlist-enforcing wrapper in the harness, or `harnessFetch`'s production
7
+ * analogue once a host builds one. Outside a provider, `useIntegrationFetch()`
8
+ * falls back to the plain browser default above.
9
+ */
10
+ export declare function IntegrationFetchProvider({ fetch, children, }: {
11
+ fetch: IntegrationFetch;
12
+ children: ReactNode;
13
+ }): import("react").JSX.Element;
14
+ /** The fetch a widget's query functions should use instead of a global. */
15
+ export declare function useIntegrationFetch(): IntegrationFetch;
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, use } from 'react';
4
+ /**
5
+ * The client-side counterpart to `ctx.fetch` — the egress-allowlisted fetch
6
+ * handed to MCP tools and server handlers. There is no server capability a
7
+ * widget can reach for, so this is the seam: a widget's query functions take
8
+ * `useIntegrationFetch()`'s return value as their fetch instead of importing
9
+ * `globalThis.fetch` directly, which is what lets the SAME query function
10
+ * serve `ctx.fetch` on the server and this hook's value on the client.
11
+ *
12
+ * The default for a consumer that never mounts a provider: the browser's own
13
+ * `fetch`, unmodified. This is what a widget's `globalThis.fetch` call does
14
+ * TODAY, in production — the default changes nothing for anyone, it just
15
+ * gives the call a name the lint preset recognizes as mediated.
16
+ */
17
+ const browserFetch = (input, init) => globalThis.fetch(input, init);
18
+ const IntegrationFetchContext = createContext(browserFetch);
19
+ /**
20
+ * Host-mounted (or harness-mounted) provider that supplies the fetch a
21
+ * widget's `useIntegrationFetch()` calls should resolve to — typically an
22
+ * allowlist-enforcing wrapper in the harness, or `harnessFetch`'s production
23
+ * analogue once a host builds one. Outside a provider, `useIntegrationFetch()`
24
+ * falls back to the plain browser default above.
25
+ */
26
+ export function IntegrationFetchProvider({ fetch, children, }) {
27
+ return (_jsx(IntegrationFetchContext, { value: fetch, children: children }));
28
+ }
29
+ /** The fetch a widget's query functions should use instead of a global. */
30
+ export function useIntegrationFetch() {
31
+ return use(IntegrationFetchContext);
32
+ }
33
+ //# sourceMappingURL=integration-fetch-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration-fetch-context.js","sourceRoot":"","sources":["../../src/hooks/integration-fetch-context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAkB,aAAa,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAI3D;;;;;;;;;;;;GAYG;AACH,MAAM,YAAY,GAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CACrD,UAAU,CAAC,KAAK,CAAC,KAAoB,EAAE,IAAI,CAAC,CAAC;AAE/C,MAAM,uBAAuB,GAAG,aAAa,CAAmB,YAAY,CAAC,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,EACvC,KAAK,EACL,QAAQ,GAIT;IACC,OAAO,CACL,KAAC,uBAAuB,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA2B,CAC5E,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,mBAAmB;IACjC,OAAO,GAAG,CAAC,uBAAuB,CAAC,CAAC;AACtC,CAAC","sourcesContent":["'use client';\n\nimport { type ReactNode, createContext, use } from 'react';\n\nimport type { IntegrationFetch } from '../context/types';\n\n/**\n * The client-side counterpart to `ctx.fetch` — the egress-allowlisted fetch\n * handed to MCP tools and server handlers. There is no server capability a\n * widget can reach for, so this is the seam: a widget's query functions take\n * `useIntegrationFetch()`'s return value as their fetch instead of importing\n * `globalThis.fetch` directly, which is what lets the SAME query function\n * serve `ctx.fetch` on the server and this hook's value on the client.\n *\n * The default for a consumer that never mounts a provider: the browser's own\n * `fetch`, unmodified. This is what a widget's `globalThis.fetch` call does\n * TODAY, in production — the default changes nothing for anyone, it just\n * gives the call a name the lint preset recognizes as mediated.\n */\nconst browserFetch: IntegrationFetch = (input, init) =>\n globalThis.fetch(input as RequestInfo, init);\n\nconst IntegrationFetchContext = createContext<IntegrationFetch>(browserFetch);\n\n/**\n * Host-mounted (or harness-mounted) provider that supplies the fetch a\n * widget's `useIntegrationFetch()` calls should resolve to — typically an\n * allowlist-enforcing wrapper in the harness, or `harnessFetch`'s production\n * analogue once a host builds one. Outside a provider, `useIntegrationFetch()`\n * falls back to the plain browser default above.\n */\nexport function IntegrationFetchProvider({\n fetch,\n children,\n}: {\n fetch: IntegrationFetch;\n children: ReactNode;\n}) {\n return (\n <IntegrationFetchContext value={fetch}>{children}</IntegrationFetchContext>\n );\n}\n\n/** The fetch a widget's query functions should use instead of a global. */\nexport function useIntegrationFetch(): IntegrationFetch {\n return use(IntegrationFetchContext);\n}\n"]}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The browser half of the hosted partner OAuth transport.
3
+ *
4
+ * A partner's `oauth` declaration names the provider endpoints and the
5
+ * client-credential secrets; the HOST owns the flow — authorize redirect,
6
+ * `state`, callback, code exchange, and the `onTokens` call — behind three
7
+ * generic routes it serves for every partner integration:
8
+ *
9
+ * GET /api/integrations/[slug]/oauth/connect?accountId=…&returnPath=…
10
+ * GET /api/integrations/[slug]/oauth/callback (the provider redirects here)
11
+ * GET /api/integrations/[slug]/oauth/status?accountId=…
12
+ *
13
+ * These helpers are the only place a partner package should reference those
14
+ * paths, so a route move is one SDK change rather than a hunt through every
15
+ * integration. `OAuthActivationForm`'s `connectPath` default still points at
16
+ * the per-integration `/connect` route first-party packages ship themselves;
17
+ * a partner passes `connectPath: partnerOAuthConnectPath(slug)` explicitly.
18
+ *
19
+ * ```tsx
20
+ * <OAuthActivationForm
21
+ * {...props}
22
+ * config={{ slug, displayName: 'Acme', connectPath: partnerOAuthConnectPath(slug), … }}
23
+ * checkConnection={createPartnerConnectionCheck(slug)}
24
+ * errorParam={partnerOAuthErrorParam(slug)}
25
+ * />
26
+ * ```
27
+ */
28
+ import type { ConnectionCheckAction, OAuthErrorParam } from './use-oauth-connection-status.js';
29
+ export interface PartnerOAuthPathOptions {
30
+ /**
31
+ * Overrides the base path. Exists for harnesses and tests that serve the
32
+ * routes from somewhere other than the host's own origin; production code
33
+ * should leave it alone.
34
+ */
35
+ baseUrl?: string;
36
+ }
37
+ /** The hosted connect route the activation form sends the browser to. */
38
+ export declare function partnerOAuthConnectPath(integrationSlug: string, options?: PartnerOAuthPathOptions): string;
39
+ /** The hosted status route `createPartnerConnectionCheck` reads. */
40
+ export declare function partnerOAuthStatusUrl(integrationSlug: string, options: PartnerOAuthPathOptions & {
41
+ accountId: string;
42
+ }): string;
43
+ /**
44
+ * The error query param the hosted callback appends on failure:
45
+ * `<slug>_oauth_error=<code>`. Codes are the transport's, so the messages
46
+ * live here rather than in every partner package.
47
+ */
48
+ export declare function partnerOAuthErrorParam(integrationSlug: string): OAuthErrorParam;
49
+ /**
50
+ * A `ConnectionCheckAction` backed by the hosted status route. The route
51
+ * answers 200 with the standard envelope for both outcomes — `{ success:
52
+ * true, data: { obtainedAt } }` or `{ success: false, error }` — and a non-OK
53
+ * status only when the request itself is unauthorized, so a query error
54
+ * surfaces as an error rather than as a misleading "not connected".
55
+ */
56
+ export declare function createPartnerConnectionCheck(integrationSlug: string, options?: PartnerOAuthPathOptions): ConnectionCheckAction;
@@ -0,0 +1,63 @@
1
+ const DEFAULT_BASE_URL = '/api/integrations';
2
+ /** The hosted connect route the activation form sends the browser to. */
3
+ export function partnerOAuthConnectPath(integrationSlug, options = {}) {
4
+ var _a;
5
+ const base = (_a = options.baseUrl) !== null && _a !== void 0 ? _a : DEFAULT_BASE_URL;
6
+ return `${base}/${integrationSlug}/oauth/connect`;
7
+ }
8
+ /** The hosted status route `createPartnerConnectionCheck` reads. */
9
+ export function partnerOAuthStatusUrl(integrationSlug, options) {
10
+ var _a;
11
+ const base = (_a = options.baseUrl) !== null && _a !== void 0 ? _a : DEFAULT_BASE_URL;
12
+ const query = new URLSearchParams({ accountId: options.accountId });
13
+ return `${base}/${integrationSlug}/oauth/status?${query}`;
14
+ }
15
+ /**
16
+ * The error query param the hosted callback appends on failure:
17
+ * `<slug>_oauth_error=<code>`. Codes are the transport's, so the messages
18
+ * live here rather than in every partner package.
19
+ */
20
+ export function partnerOAuthErrorParam(integrationSlug) {
21
+ return {
22
+ name: `${integrationSlug}_oauth_error`,
23
+ toMessage: (value) => { var _a; return (_a = PARTNER_OAUTH_ERROR_MESSAGES[value]) !== null && _a !== void 0 ? _a : null; },
24
+ };
25
+ }
26
+ const PARTNER_OAUTH_ERROR_MESSAGES = {
27
+ denied: 'Access was denied at the provider. Try connecting again.',
28
+ expired: 'The connection attempt expired. Try connecting again.',
29
+ invalid: 'The connection attempt could not be verified. Try connecting again.',
30
+ unauthorized: 'You need to be signed in as the person who started the connection.',
31
+ not_configured: 'This integration is not configured for your workspace yet. Ask an administrator to add its OAuth credentials.',
32
+ exchange_failed: 'The provider rejected the connection. Try again, or contact support if it keeps failing.',
33
+ save_failed: 'Connected, but the credentials could not be saved. Try again.',
34
+ };
35
+ /**
36
+ * A `ConnectionCheckAction` backed by the hosted status route. The route
37
+ * answers 200 with the standard envelope for both outcomes — `{ success:
38
+ * true, data: { obtainedAt } }` or `{ success: false, error }` — and a non-OK
39
+ * status only when the request itself is unauthorized, so a query error
40
+ * surfaces as an error rather than as a misleading "not connected".
41
+ */
42
+ export function createPartnerConnectionCheck(integrationSlug, options = {}) {
43
+ return async ({ accountId }) => {
44
+ const url = partnerOAuthStatusUrl(integrationSlug, Object.assign(Object.assign({}, options), { accountId }));
45
+ const response = await globalThis.fetch(url, {
46
+ headers: { accept: 'application/json' },
47
+ });
48
+ if (!response.ok) {
49
+ throw new Error(`Could not read the '${integrationSlug}' connection status (${response.status} ${response.statusText}).`);
50
+ }
51
+ const payload = await response.json();
52
+ if (!isConnectionCheckResult(payload)) {
53
+ throw new Error(`The '${integrationSlug}' status route returned an unexpected payload.`);
54
+ }
55
+ return payload;
56
+ };
57
+ }
58
+ function isConnectionCheckResult(value) {
59
+ return (typeof value === 'object' &&
60
+ value !== null &&
61
+ typeof value.success === 'boolean');
62
+ }
63
+ //# sourceMappingURL=partner-oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"partner-oauth.js","sourceRoot":"","sources":["../../src/hooks/partner-oauth.ts"],"names":[],"mappings":"AAiCA,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAW7C,yEAAyE;AACzE,MAAM,UAAU,uBAAuB,CACrC,eAAuB,EACvB,UAAmC,EAAE;;IAErC,MAAM,IAAI,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,gBAAgB,CAAC;IACjD,OAAO,GAAG,IAAI,IAAI,eAAe,gBAAgB,CAAC;AACpD,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,qBAAqB,CACnC,eAAuB,EACvB,OAAwD;;IAExD,MAAM,IAAI,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,gBAAgB,CAAC;IACjD,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACpE,OAAO,GAAG,IAAI,IAAI,eAAe,iBAAiB,KAAK,EAAE,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,eAAuB;IAEvB,OAAO;QACL,IAAI,EAAE,GAAG,eAAe,cAAc;QACtC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,WAAC,OAAA,MAAA,4BAA4B,CAAC,KAAK,CAAC,mCAAI,IAAI,CAAA,EAAA;KAClE,CAAC;AACJ,CAAC;AAED,MAAM,4BAA4B,GAA2B;IAC3D,MAAM,EAAE,0DAA0D;IAClE,OAAO,EAAE,uDAAuD;IAChE,OAAO,EACL,qEAAqE;IACvE,YAAY,EACV,oEAAoE;IACtE,cAAc,EACZ,+GAA+G;IACjH,eAAe,EACb,0FAA0F;IAC5F,WAAW,EAAE,+DAA+D;CAC7E,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAC1C,eAAuB,EACvB,UAAmC,EAAE;IAErC,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,qBAAqB,CAAC,eAAe,kCAC5C,OAAO,KACV,SAAS,IACT,CAAC;QAEH,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;YAC3C,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;SACxC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,uBAAuB,eAAe,wBAAwB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,CACzG,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAE/C,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,KAAK,CACb,QAAQ,eAAe,gDAAgD,CACxE,CAAC;QACJ,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAC9B,KAAc;IAEd,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAQ,KAA+B,CAAC,OAAO,KAAK,SAAS,CAC9D,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The browser half of the hosted partner OAuth transport.\n *\n * A partner's `oauth` declaration names the provider endpoints and the\n * client-credential secrets; the HOST owns the flow — authorize redirect,\n * `state`, callback, code exchange, and the `onTokens` call — behind three\n * generic routes it serves for every partner integration:\n *\n * GET /api/integrations/[slug]/oauth/connect?accountId=…&returnPath=…\n * GET /api/integrations/[slug]/oauth/callback (the provider redirects here)\n * GET /api/integrations/[slug]/oauth/status?accountId=…\n *\n * These helpers are the only place a partner package should reference those\n * paths, so a route move is one SDK change rather than a hunt through every\n * integration. `OAuthActivationForm`'s `connectPath` default still points at\n * the per-integration `/connect` route first-party packages ship themselves;\n * a partner passes `connectPath: partnerOAuthConnectPath(slug)` explicitly.\n *\n * ```tsx\n * <OAuthActivationForm\n * {...props}\n * config={{ slug, displayName: 'Acme', connectPath: partnerOAuthConnectPath(slug), … }}\n * checkConnection={createPartnerConnectionCheck(slug)}\n * errorParam={partnerOAuthErrorParam(slug)}\n * />\n * ```\n */\nimport type {\n ConnectionCheckAction,\n ConnectionCheckResult,\n OAuthErrorParam,\n} from './use-oauth-connection-status';\n\nconst DEFAULT_BASE_URL = '/api/integrations';\n\nexport interface PartnerOAuthPathOptions {\n /**\n * Overrides the base path. Exists for harnesses and tests that serve the\n * routes from somewhere other than the host's own origin; production code\n * should leave it alone.\n */\n baseUrl?: string;\n}\n\n/** The hosted connect route the activation form sends the browser to. */\nexport function partnerOAuthConnectPath(\n integrationSlug: string,\n options: PartnerOAuthPathOptions = {},\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n return `${base}/${integrationSlug}/oauth/connect`;\n}\n\n/** The hosted status route `createPartnerConnectionCheck` reads. */\nexport function partnerOAuthStatusUrl(\n integrationSlug: string,\n options: PartnerOAuthPathOptions & { accountId: string },\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n const query = new URLSearchParams({ accountId: options.accountId });\n return `${base}/${integrationSlug}/oauth/status?${query}`;\n}\n\n/**\n * The error query param the hosted callback appends on failure:\n * `<slug>_oauth_error=<code>`. Codes are the transport's, so the messages\n * live here rather than in every partner package.\n */\nexport function partnerOAuthErrorParam(\n integrationSlug: string,\n): OAuthErrorParam {\n return {\n name: `${integrationSlug}_oauth_error`,\n toMessage: (value) => PARTNER_OAUTH_ERROR_MESSAGES[value] ?? null,\n };\n}\n\nconst PARTNER_OAUTH_ERROR_MESSAGES: Record<string, string> = {\n denied: 'Access was denied at the provider. Try connecting again.',\n expired: 'The connection attempt expired. Try connecting again.',\n invalid:\n 'The connection attempt could not be verified. Try connecting again.',\n unauthorized:\n 'You need to be signed in as the person who started the connection.',\n not_configured:\n 'This integration is not configured for your workspace yet. Ask an administrator to add its OAuth credentials.',\n exchange_failed:\n 'The provider rejected the connection. Try again, or contact support if it keeps failing.',\n save_failed: 'Connected, but the credentials could not be saved. Try again.',\n};\n\n/**\n * A `ConnectionCheckAction` backed by the hosted status route. The route\n * answers 200 with the standard envelope for both outcomes — `{ success:\n * true, data: { obtainedAt } }` or `{ success: false, error }` — and a non-OK\n * status only when the request itself is unauthorized, so a query error\n * surfaces as an error rather than as a misleading \"not connected\".\n */\nexport function createPartnerConnectionCheck(\n integrationSlug: string,\n options: PartnerOAuthPathOptions = {},\n): ConnectionCheckAction {\n return async ({ accountId }) => {\n const url = partnerOAuthStatusUrl(integrationSlug, {\n ...options,\n accountId,\n });\n\n const response = await globalThis.fetch(url, {\n headers: { accept: 'application/json' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Could not read the '${integrationSlug}' connection status (${response.status} ${response.statusText}).`,\n );\n }\n\n const payload: unknown = await response.json();\n\n if (!isConnectionCheckResult(payload)) {\n throw new Error(\n `The '${integrationSlug}' status route returned an unexpected payload.`,\n );\n }\n\n return payload;\n };\n}\n\nfunction isConnectionCheckResult(\n value: unknown,\n): value is ConnectionCheckResult {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { success?: unknown }).success === 'boolean'\n );\n}\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The fetch-fixture matcher shared by `createMockContext`'s `ctx.fetch` stub
3
+ * and `createMockFetchProvider`'s browser-side stub (extracted from
4
+ * `mock-context.ts`, which used to inline this) — one string-prefix/RegExp
5
+ * matching implementation, so a widget test and a handler test written
6
+ * against the same fixture describe the same vendor behavior once instead of
7
+ * two copies drifting apart.
8
+ */
9
+ export interface MockFetchHandler {
10
+ /** A string matches URLs that start with it; a RegExp is tested against the full URL. */
11
+ match: string | RegExp;
12
+ respond: (request: Request) => Response | Promise<Response>;
13
+ }
14
+ /**
15
+ * Returns the first handler whose `match` matches `url`, or `undefined` when
16
+ * none does — callers fall back to their own default response.
17
+ */
18
+ export declare function matchFetchHandler(url: string, handlers: readonly MockFetchHandler[]): MockFetchHandler | undefined;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The fetch-fixture matcher shared by `createMockContext`'s `ctx.fetch` stub
3
+ * and `createMockFetchProvider`'s browser-side stub (extracted from
4
+ * `mock-context.ts`, which used to inline this) — one string-prefix/RegExp
5
+ * matching implementation, so a widget test and a handler test written
6
+ * against the same fixture describe the same vendor behavior once instead of
7
+ * two copies drifting apart.
8
+ */
9
+ /**
10
+ * Returns the first handler whose `match` matches `url`, or `undefined` when
11
+ * none does — callers fall back to their own default response.
12
+ */
13
+ export function matchFetchHandler(url, handlers) {
14
+ return handlers.find((candidate) => typeof candidate.match === 'string'
15
+ ? url.startsWith(candidate.match)
16
+ : candidate.match.test(url));
17
+ }
18
+ //# sourceMappingURL=fetch-matching.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-matching.js","sourceRoot":"","sources":["../../src/testing/fetch-matching.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,GAAW,EACX,QAAqC;IAErC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CACjC,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ;QACjC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC;QACjC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAC9B,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The fetch-fixture matcher shared by `createMockContext`'s `ctx.fetch` stub\n * and `createMockFetchProvider`'s browser-side stub (extracted from\n * `mock-context.ts`, which used to inline this) — one string-prefix/RegExp\n * matching implementation, so a widget test and a handler test written\n * against the same fixture describe the same vendor behavior once instead of\n * two copies drifting apart.\n */\n\nexport interface MockFetchHandler {\n /** A string matches URLs that start with it; a RegExp is tested against the full URL. */\n match: string | RegExp;\n respond: (request: Request) => Response | Promise<Response>;\n}\n\n/**\n * Returns the first handler whose `match` matches `url`, or `undefined` when\n * none does — callers fall back to their own default response.\n */\nexport function matchFetchHandler(\n url: string,\n handlers: readonly MockFetchHandler[],\n): MockFetchHandler | undefined {\n return handlers.find((candidate) =>\n typeof candidate.match === 'string'\n ? url.startsWith(candidate.match)\n : candidate.match.test(url),\n );\n}\n"]}
@@ -11,6 +11,8 @@
11
11
  * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.
12
12
  */
13
13
  export { createMockContext } from './mock-context.js';
14
- export type { MockContextOptions, MockFetchHandler, MockIntegrationContext, RecordedFetchCall, RecordedLog, } from './mock-context.js';
14
+ export type { MockContextOptions, MockIntegrationContext, RecordedFetchCall, RecordedLog, } from './mock-context.js';
15
+ export type { MockFetchHandler } from './fetch-matching.js';
16
+ export { createMockFetchProvider } from './mock-fetch-provider.js';
15
17
  export { invokeWebhook, invokeSchedule, invokeActivate } from './invoke.js';
16
18
  export type { DefinitionContextOptions, InvokeWebhookOptions, InvokeScheduleOptions, InvokeActivateOptions, WebhookInvocationOutcome, ScheduleInvocationOutcome, ActivateInvocationOutcome, } from './invoke.js';
@@ -11,6 +11,11 @@
11
11
  * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.
12
12
  */
13
13
  export { createMockContext } from './mock-context.js';
14
+ // The browser-side counterpart to `createMockContext`'s `fetchHandlers`: a
15
+ // mounted `IntegrationFetchProvider` for widget tests that call
16
+ // `useIntegrationFetch()`, resolving fixtures against the same
17
+ // `MockFetchHandler` shape and matching semantics.
18
+ export { createMockFetchProvider } from './mock-fetch-provider.js';
14
19
  // Event-surface invocation: run declared webhook/schedule handlers locally,
15
20
  // with the same payload-validation and context-derivation semantics the real
16
21
  // transports will have. Used by partner unit tests and the harness Triggers
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AASnD,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC","sourcesContent":["/**\n * @ekanos/sdk/testing — the partner test harness surface.\n *\n * `createMockContext()` is the day-1 in-memory `IntegrationContext`\n * (proposal §4): partners code and test against `ctx` here before anything\n * lands in a sandbox. Self-contained — no Supabase, no Vault, no network —\n * and enforcement (storage schemas, secret tiers, egress) is the same\n * shared implementation from `@ekanos/sdk/context` that production uses.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { createMockContext } from './mock-context';\nexport type {\n MockContextOptions,\n MockFetchHandler,\n MockIntegrationContext,\n RecordedFetchCall,\n RecordedLog,\n} from './mock-context';\n\n// Event-surface invocation: run declared webhook/schedule handlers locally,\n// with the same payload-validation and context-derivation semantics the real\n// transports will have. Used by partner unit tests and the harness Triggers\n// panel alike. `invokeActivate` does the same for the `onActivate` lifecycle\n// hook, used by partner unit tests and the harness's activation surface.\nexport { invokeWebhook, invokeSchedule, invokeActivate } from './invoke';\nexport type {\n DefinitionContextOptions,\n InvokeWebhookOptions,\n InvokeScheduleOptions,\n InvokeActivateOptions,\n WebhookInvocationOutcome,\n ScheduleInvocationOutcome,\n ActivateInvocationOutcome,\n} from './invoke';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAYnD,2EAA2E;AAC3E,gEAAgE;AAChE,+DAA+D;AAC/D,mDAAmD;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC","sourcesContent":["/**\n * @ekanos/sdk/testing — the partner test harness surface.\n *\n * `createMockContext()` is the day-1 in-memory `IntegrationContext`\n * (proposal §4): partners code and test against `ctx` here before anything\n * lands in a sandbox. Self-contained — no Supabase, no Vault, no network —\n * and enforcement (storage schemas, secret tiers, egress) is the same\n * shared implementation from `@ekanos/sdk/context` that production uses.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { createMockContext } from './mock-context';\nexport type {\n MockContextOptions,\n MockIntegrationContext,\n RecordedFetchCall,\n RecordedLog,\n} from './mock-context';\n\n// Shared by `createMockContext`'s `ctx.fetch` stub and `createMockFetchProvider`\n// below — one fixture shape, one matching implementation, for both surfaces.\nexport type { MockFetchHandler } from './fetch-matching';\n\n// The browser-side counterpart to `createMockContext`'s `fetchHandlers`: a\n// mounted `IntegrationFetchProvider` for widget tests that call\n// `useIntegrationFetch()`, resolving fixtures against the same\n// `MockFetchHandler` shape and matching semantics.\nexport { createMockFetchProvider } from './mock-fetch-provider';\n\n// Event-surface invocation: run declared webhook/schedule handlers locally,\n// with the same payload-validation and context-derivation semantics the real\n// transports will have. Used by partner unit tests and the harness Triggers\n// panel alike. `invokeActivate` does the same for the `onActivate` lifecycle\n// hook, used by partner unit tests and the harness's activation surface.\nexport { invokeWebhook, invokeSchedule, invokeActivate } from './invoke';\nexport type {\n DefinitionContextOptions,\n InvokeWebhookOptions,\n InvokeScheduleOptions,\n InvokeActivateOptions,\n WebhookInvocationOutcome,\n ScheduleInvocationOutcome,\n ActivateInvocationOutcome,\n} from './invoke';\n"]}
@@ -9,14 +9,11 @@
9
9
  */
10
10
  import type { z } from 'zod';
11
11
  import type { IntegrationActor, IntegrationContext, StorageKeySchema, StorageSchemaMap, StorageSchemas } from '../context/types.js';
12
+ import { type MockFetchHandler } from './fetch-matching.js';
12
13
  type ScopeSeed<M> = M extends StorageSchemaMap ? {
13
14
  [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>>;
14
15
  } : Record<never, never>;
15
- export interface MockFetchHandler {
16
- /** A string matches URLs that start with it; a RegExp is tested against the full URL. */
17
- match: string | RegExp;
18
- respond: (request: Request) => Response | Promise<Response>;
19
- }
16
+ export type { MockFetchHandler } from './fetch-matching.js';
20
17
  export interface MockContextOptions<Schemas extends StorageSchemas = StorageSchemas> {
21
18
  accountId?: string;
22
19
  accountSlug?: string | null;
@@ -76,4 +73,3 @@ export interface MockIntegrationContext<Schemas extends StorageSchemas = Storage
76
73
  };
77
74
  }
78
75
  export declare function createMockContext<Schemas extends StorageSchemas = StorageSchemas>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas>;
79
- export {};
@@ -1,6 +1,7 @@
1
1
  import { isEgressAllowed } from '../context/egress.js';
2
2
  import { EgressDeniedError, SecretAccessError } from '../context/errors.js';
3
3
  import { assertStorageKeyDeclared, assertStorageKeyWritable, parseStorageValue, } from '../context/storage-validation.js';
4
+ import { matchFetchHandler } from './fetch-matching.js';
4
5
  function isExpired(row) {
5
6
  return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();
6
7
  }
@@ -160,9 +161,7 @@ export function createMockContext(options) {
160
161
  if (!allowed) {
161
162
  throw new EgressDeniedError(new URL(url).origin);
162
163
  }
163
- const handler = fetchHandlers.find((candidate) => typeof candidate.match === 'string'
164
- ? url.startsWith(candidate.match)
165
- : candidate.match.test(url));
164
+ const handler = matchFetchHandler(url, fetchHandlers);
166
165
  if (handler) {
167
166
  return Promise.resolve(handler.respond(new Request(url, init)));
168
167
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `createMockContext()` — the day-1 in-memory `IntegrationContext` for the\n * partner harness (proposal §4, amended by rulings 1–4). Self-contained:\n * nothing here touches Supabase, Vault, or the network. Enforcement is the\n * SAME shared implementation the production wrapper uses\n * (`@ekanos/sdk/context`): schema validation via `parseStorageValue`, egress\n * via `isEgressAllowed` — a test that passes the mock cannot be a schema or\n * egress surprise later.\n */\nimport type { z } from 'zod';\n\nimport { isEgressAllowed } from '../context/egress';\nimport { EgressDeniedError, SecretAccessError } from '../context/errors';\nimport {\n assertStorageKeyDeclared,\n assertStorageKeyWritable,\n parseStorageValue,\n} from '../context/storage-validation';\nimport type {\n IntegrationActor,\n IntegrationContext,\n IntegrationFetch,\n IntegrationLogger,\n IntegrationSecrets,\n IntegrationStorage,\n ScopedStore,\n StorageEntry,\n StorageKeySchema,\n StorageSchemaMap,\n StorageSchemas,\n StorageWriteOptions,\n} from '../context/types';\n\ntype StorageScope = 'account' | 'user';\n\n// Seeds are typed off the DECLARED schema, resolved out of whichever\n// declaration form the key used — a bare zod schema or a\n// `{ schema, clientReadable }` descriptor.\ntype ScopeSeed<M> = M extends StorageSchemaMap\n ? { [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>> }\n : Record<never, never>;\n\nexport interface MockFetchHandler {\n /** A string matches URLs that start with it; a RegExp is tested against the full URL. */\n match: string | RegExp;\n respond: (request: Request) => Response | Promise<Response>;\n}\n\nexport interface MockContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n accountId?: string;\n accountSlug?: string | null;\n userId?: string | null;\n actor?: IntegrationActor;\n sourceId?: string | null;\n timezone?: string | null;\n integration: { slug: string; productId?: string };\n /**\n * The storage declaration `defineIntegration({ storage })` will carry —\n * the mock enforces the identical schemas via the same shared validator\n * (ruling 1). Omit it and every storage access throws.\n */\n storageSchemas?: Schemas;\n /** Seed rows: key → data, per scope. Validated on construction via the write path. */\n storage?: {\n account?: ScopeSeed<Schemas['account']>;\n user?: ScopeSeed<Schemas['user']>;\n };\n /**\n * Seed secrets, by tier (ruling 2): `account` values are the rows this\n * integration stored — writable via `secrets.set()`; `admin` values stand\n * in for admin-issued source/global-tier credentials — readable, and a\n * `set()` against a name resolvable only there throws `SecretAccessError`.\n */\n secrets?: {\n account?: Record<string, string>;\n admin?: Record<string, string>;\n };\n /** Same allowlist the manifest would declare — enforced by the stub. */\n egress?: readonly string[];\n /** Programmable responses; unmatched allowed calls resolve 200 `{}`. */\n fetchHandlers?: MockFetchHandler[];\n}\n\nexport interface RecordedFetchCall {\n url: string;\n init?: RequestInit;\n denied: boolean;\n}\n\nexport interface RecordedLog {\n level: 'debug' | 'info' | 'warn' | 'error';\n context: Record<string, unknown>;\n message: string;\n}\n\nexport interface MockIntegrationContext<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends IntegrationContext<Schemas> {\n /** Every ctx.fetch call, allowed or denied, in order. */\n readonly fetchCalls: readonly RecordedFetchCall[];\n /** Every log line, per level. */\n readonly logs: readonly RecordedLog[];\n /** Current readable storage state (expired entries excluded), key → data. */\n dumpStorage(): {\n account: Record<string, unknown>;\n user: Record<string, unknown>;\n };\n}\n\ninterface StoredRow {\n data: unknown;\n externalId: string | null;\n expiresAt: string | null;\n updatedAt: string;\n}\n\nfunction isExpired(row: StoredRow): boolean {\n return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();\n}\n\nfunction parseExpiresAt(\n options: StorageWriteOptions | undefined,\n): string | null {\n const expiresAt = options?.expiresAt;\n if (expiresAt === undefined) return null;\n if (Number.isNaN(Date.parse(expiresAt))) {\n throw new TypeError(\n `expiresAt \"${expiresAt}\" is not a parseable timestamp. Pass an ` +\n `ISO-8601 string, e.g. new Date(Date.now() + 60_000).toISOString().`,\n );\n }\n return expiresAt;\n}\n\nfunction createInMemoryStore(args: {\n scope: StorageScope;\n schemas: StorageSchemaMap | undefined;\n rows: Map<string, StoredRow>;\n guard: () => void;\n}): ScopedStore {\n const { scope, schemas, rows, guard } = args;\n\n return {\n async get(key: string): Promise<StorageEntry<unknown> | null> {\n guard();\n assertStorageKeyDeclared({ scope, key, schemas });\n const row = rows.get(key);\n if (!row) return null;\n if (isExpired(row)) {\n rows.delete(key);\n return null;\n }\n const data = parseStorageValue({\n scope,\n key,\n operation: 'read',\n schemas,\n value: row.data,\n });\n return {\n data,\n externalId: row.externalId,\n expiresAt: row.expiresAt,\n updatedAt: row.updatedAt,\n };\n },\n\n async set(\n key: string,\n data: unknown,\n options?: StorageWriteOptions,\n ): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas,\n value: data,\n });\n rows.set(key, {\n data: parsed,\n externalId: options?.externalId ?? null,\n expiresAt: parseExpiresAt(options),\n updatedAt: new Date().toISOString(),\n });\n },\n\n async delete(key: string): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n assertStorageKeyDeclared({ scope, key, schemas });\n rows.delete(key);\n },\n };\n}\n\nfunction dumpScope(rows: Map<string, StoredRow>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, row] of rows) {\n if (!isExpired(row)) out[key] = row.data;\n }\n return out;\n}\n\nexport function createMockContext<\n Schemas extends StorageSchemas = StorageSchemas,\n>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas> {\n const userId = options.userId !== undefined ? options.userId : 'mock-user-id';\n const actor: IntegrationActor =\n options.actor ??\n (userId !== null\n ? { kind: 'user', userId }\n : { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });\n\n const schemas: StorageSchemas = options.storageSchemas ?? {};\n\n const accountRows = new Map<string, StoredRow>();\n const userRows = new Map<string, StoredRow>();\n\n const seedScope = (\n scope: StorageScope,\n rows: Map<string, StoredRow>,\n seed: Record<string, unknown> | undefined,\n ) => {\n for (const [key, value] of Object.entries(seed ?? {})) {\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas: schemas[scope],\n value,\n });\n rows.set(key, {\n data: parsed,\n externalId: null,\n expiresAt: null,\n updatedAt: new Date().toISOString(),\n });\n }\n };\n seedScope('account', accountRows, options.storage?.account);\n seedScope('user', userRows, options.storage?.user);\n\n const guardUserScope = () => {\n if (actor.kind === 'machine') {\n throw new Error(\n 'ctx.storage.user is unavailable for machine actors: there is no ' +\n 'user to scope the row to. Store machine-written state in ' +\n 'ctx.storage.account instead.',\n );\n }\n };\n\n // The stores validate every key and value at runtime via the shared\n // validator; the cast narrows the runtime-checked store to the\n // schema-typed interface the declaration promises.\n const storage = {\n account: createInMemoryStore({\n scope: 'account',\n schemas: schemas.account,\n rows: accountRows,\n guard: () => void 0,\n }),\n user: createInMemoryStore({\n scope: 'user',\n schemas: schemas.user,\n rows: userRows,\n guard: guardUserScope,\n }),\n } as unknown as IntegrationStorage<Schemas>;\n\n const accountSecrets = new Map(\n Object.entries(options.secrets?.account ?? {}),\n );\n const adminSecrets = new Map(Object.entries(options.secrets?.admin ?? {}));\n\n const secrets: IntegrationSecrets = {\n async get(name) {\n return accountSecrets.get(name) ?? adminSecrets.get(name) ?? null;\n },\n async names() {\n return [\n ...new Set([...accountSecrets.keys(), ...adminSecrets.keys()]),\n ].sort();\n },\n async set(name, value) {\n if (!accountSecrets.has(name) && adminSecrets.has(name)) {\n throw new SecretAccessError(name);\n }\n accountSecrets.set(name, value);\n },\n };\n\n const fetchCalls: RecordedFetchCall[] = [];\n const egress = options.egress ?? [];\n const fetchHandlers = options.fetchHandlers ?? [];\n\n // Not an async function: a denied call must throw synchronously, before\n // any I/O (proposal §2.3) — same behavior the production wrapper has.\n const fetch: IntegrationFetch = (input, init) => {\n const url = new URL(input).toString();\n const allowed = isEgressAllowed(url, egress);\n fetchCalls.push(\n init === undefined\n ? { url, denied: !allowed }\n : { url, init, denied: !allowed },\n );\n if (!allowed) {\n throw new EgressDeniedError(new URL(url).origin);\n }\n const handler = fetchHandlers.find((candidate) =>\n typeof candidate.match === 'string'\n ? url.startsWith(candidate.match)\n : candidate.match.test(url),\n );\n if (handler) {\n return Promise.resolve(handler.respond(new Request(url, init)));\n }\n return Promise.resolve(\n new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n }),\n );\n };\n\n const logs: RecordedLog[] = [];\n const record =\n (level: RecordedLog['level']) =>\n (context: Record<string, unknown>, message: string) => {\n logs.push({ level, context, message });\n };\n const logger: IntegrationLogger = {\n debug: record('debug'),\n info: record('info'),\n warn: record('warn'),\n error: record('error'),\n };\n\n return {\n accountId: options.accountId ?? 'mock-account-id',\n accountSlug: options.accountSlug ?? null,\n userId: actor.kind === 'machine' ? null : userId,\n actor,\n sourceId: options.sourceId ?? null,\n integration: {\n slug: options.integration.slug,\n productId: options.integration.productId ?? 'mock-product-id',\n },\n timezone: options.timezone ?? null,\n storage,\n secrets,\n fetch,\n logger,\n fetchCalls,\n logs,\n dumpStorage() {\n return { account: dumpScope(accountRows), user: dumpScope(userRows) };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,+BAA+B,CAAC;AAevC,OAAO,EAAyB,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAqF5E,SAAS,SAAS,CAAC,GAAc;IAC/B,OAAO,GAAG,CAAC,SAAS,KAAK,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;AAC3E,CAAC;AAED,SAAS,cAAc,CACrB,OAAwC;IAExC,MAAM,SAAS,GAAG,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,SAAS,CAAC;IACrC,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,SAAS,CACjB,cAAc,SAAS,0CAA0C;YAC/D,oEAAoE,CACvE,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,mBAAmB,CAAC,IAK5B;IACC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IAE7C,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAW;YACnB,KAAK,EAAE,CAAC;YACR,wBAAwB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;YAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,CAAC,GAAG;gBAAE,OAAO,IAAI,CAAC;YACtB,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;gBACnB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,IAAI,GAAG,iBAAiB,CAAC;gBAC7B,KAAK;gBACL,GAAG;gBACH,SAAS,EAAE,MAAM;gBACjB,OAAO;gBACP,KAAK,EAAE,GAAG,CAAC,IAAI;aAChB,CAAC,CAAC;YACH,OAAO;gBACL,IAAI;gBACJ,UAAU,EAAE,GAAG,CAAC,UAAU;gBAC1B,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,SAAS,EAAE,GAAG,CAAC,SAAS;aACzB,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,GAAG,CACP,GAAW,EACX,IAAa,EACb,OAA6B;;YAE7B,KAAK,EAAE,CAAC;YACR,wBAAwB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,iBAAiB,CAAC;gBAC/B,KAAK;gBACL,GAAG;gBACH,SAAS,EAAE,OAAO;gBAClB,OAAO;gBACP,KAAK,EAAE,IAAI;aACZ,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;gBACZ,IAAI,EAAE,MAAM;gBACZ,UAAU,EAAE,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU,mCAAI,IAAI;gBACvC,SAAS,EAAE,cAAc,CAAC,OAAO,CAAC;gBAClC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,GAAW;YACtB,KAAK,EAAE,CAAC;YACR,wBAAwB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACzC,wBAAwB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,IAA4B;IAC7C,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;IAC3C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,iBAAiB,CAE/B,OAAoC;;IACpC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC;IAC9E,MAAM,KAAK,GACT,MAAA,OAAO,CAAC,KAAK,mCACb,CAAC,MAAM,KAAK,IAAI;QACd,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE;QAC1B,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtE,MAAM,OAAO,GAAmB,MAAA,OAAO,CAAC,cAAc,mCAAI,EAAE,CAAC;IAE7D,MAAM,WAAW,GAAG,IAAI,GAAG,EAAqB,CAAC;IACjD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAqB,CAAC;IAE9C,MAAM,SAAS,GAAG,CAChB,KAAmB,EACnB,IAA4B,EAC5B,IAAyC,EACzC,EAAE;QACF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,aAAJ,IAAI,cAAJ,IAAI,GAAI,EAAE,CAAC,EAAE,CAAC;YACtD,MAAM,MAAM,GAAG,iBAAiB,CAAC;gBAC/B,KAAK;gBACL,GAAG;gBACH,SAAS,EAAE,OAAO;gBAClB,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC;gBACvB,KAAK;aACN,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;gBACZ,IAAI,EAAE,MAAM;gBACZ,UAAU,EAAE,IAAI;gBAChB,SAAS,EAAE,IAAI;gBACf,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC;IACF,SAAS,CAAC,SAAS,EAAE,WAAW,EAAE,MAAA,OAAO,CAAC,OAAO,0CAAE,OAAO,CAAC,CAAC;IAC5D,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAA,OAAO,CAAC,OAAO,0CAAE,IAAI,CAAC,CAAC;IAEnD,MAAM,cAAc,GAAG,GAAG,EAAE;QAC1B,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,kEAAkE;gBAChE,2DAA2D;gBAC3D,8BAA8B,CACjC,CAAC;QACJ,CAAC;IACH,CAAC,CAAC;IAEF,oEAAoE;IACpE,+DAA+D;IAC/D,mDAAmD;IACnD,MAAM,OAAO,GAAG;QACd,OAAO,EAAE,mBAAmB,CAAC;YAC3B,KAAK,EAAE,SAAS;YAChB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC;SACpB,CAAC;QACF,IAAI,EAAE,mBAAmB,CAAC;YACxB,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,OAAO,CAAC,IAAI;YACrB,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,cAAc;SACtB,CAAC;KACuC,CAAC;IAE5C,MAAM,cAAc,GAAG,IAAI,GAAG,CAC5B,MAAM,CAAC,OAAO,CAAC,MAAA,MAAA,OAAO,CAAC,OAAO,0CAAE,OAAO,mCAAI,EAAE,CAAC,CAC/C,CAAC;IACF,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,MAAA,MAAA,OAAO,CAAC,OAAO,0CAAE,KAAK,mCAAI,EAAE,CAAC,CAAC,CAAC;IAE3E,MAAM,OAAO,GAAuB;QAClC,KAAK,CAAC,GAAG,CAAC,IAAI;;YACZ,OAAO,MAAA,MAAA,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,IAAI,CAAC;QACpE,CAAC;QACD,KAAK,CAAC,KAAK;YACT,OAAO;gBACL,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,cAAc,CAAC,IAAI,EAAE,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;aAC/D,CAAC,IAAI,EAAE,CAAC;QACX,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK;YACnB,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxD,MAAM,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC;YACpC,CAAC;YACD,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAClC,CAAC;KACF,CAAC;IAEF,MAAM,UAAU,GAAwB,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAA,OAAO,CAAC,MAAM,mCAAI,EAAE,CAAC;IACpC,MAAM,aAAa,GAAG,MAAA,OAAO,CAAC,aAAa,mCAAI,EAAE,CAAC;IAElD,wEAAwE;IACxE,sEAAsE;IACtE,MAAM,KAAK,GAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAC9C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC7C,UAAU,CAAC,IAAI,CACb,IAAI,KAAK,SAAS;YAChB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE;YAC3B,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,CACpC,CAAC;QACF,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,iBAAiB,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC;QACD,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QACtD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAClE,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CACpB,IAAI,QAAQ,CAAC,IAAI,EAAE;YACjB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CACH,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,IAAI,GAAkB,EAAE,CAAC;IAC/B,MAAM,MAAM,GACV,CAAC,KAA2B,EAAE,EAAE,CAChC,CAAC,OAAgC,EAAE,OAAe,EAAE,EAAE;QACpD,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACzC,CAAC,CAAC;IACJ,MAAM,MAAM,GAAsB;QAChC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC;KACvB,CAAC;IAEF,OAAO;QACL,SAAS,EAAE,MAAA,OAAO,CAAC,SAAS,mCAAI,iBAAiB;QACjD,WAAW,EAAE,MAAA,OAAO,CAAC,WAAW,mCAAI,IAAI;QACxC,MAAM,EAAE,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;QAChD,KAAK;QACL,QAAQ,EAAE,MAAA,OAAO,CAAC,QAAQ,mCAAI,IAAI;QAClC,WAAW,EAAE;YACX,IAAI,EAAE,OAAO,CAAC,WAAW,CAAC,IAAI;YAC9B,SAAS,EAAE,MAAA,OAAO,CAAC,WAAW,CAAC,SAAS,mCAAI,iBAAiB;SAC9D;QACD,QAAQ,EAAE,MAAA,OAAO,CAAC,QAAQ,mCAAI,IAAI;QAClC,OAAO;QACP,OAAO;QACP,KAAK;QACL,MAAM;QACN,UAAU;QACV,IAAI;QACJ,WAAW;YACT,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxE,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * `createMockContext()` — the day-1 in-memory `IntegrationContext` for the\n * partner harness (proposal §4, amended by rulings 1–4). Self-contained:\n * nothing here touches Supabase, Vault, or the network. Enforcement is the\n * SAME shared implementation the production wrapper uses\n * (`@ekanos/sdk/context`): schema validation via `parseStorageValue`, egress\n * via `isEgressAllowed` — a test that passes the mock cannot be a schema or\n * egress surprise later.\n */\nimport type { z } from 'zod';\n\nimport { isEgressAllowed } from '../context/egress';\nimport { EgressDeniedError, SecretAccessError } from '../context/errors';\nimport {\n assertStorageKeyDeclared,\n assertStorageKeyWritable,\n parseStorageValue,\n} from '../context/storage-validation';\nimport type {\n IntegrationActor,\n IntegrationContext,\n IntegrationFetch,\n IntegrationLogger,\n IntegrationSecrets,\n IntegrationStorage,\n ScopedStore,\n StorageEntry,\n StorageKeySchema,\n StorageSchemaMap,\n StorageSchemas,\n StorageWriteOptions,\n} from '../context/types';\nimport { type MockFetchHandler, matchFetchHandler } from './fetch-matching';\n\ntype StorageScope = 'account' | 'user';\n\n// Seeds are typed off the DECLARED schema, resolved out of whichever\n// declaration form the key used — a bare zod schema or a\n// `{ schema, clientReadable }` descriptor.\ntype ScopeSeed<M> = M extends StorageSchemaMap\n ? { [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>> }\n : Record<never, never>;\n\n// Re-exported from here for backward compatibility — `MockFetchHandler`'s\n// canonical home is now `./fetch-matching`, shared with `createMockFetchProvider`.\nexport type { MockFetchHandler } from './fetch-matching';\n\nexport interface MockContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n accountId?: string;\n accountSlug?: string | null;\n userId?: string | null;\n actor?: IntegrationActor;\n sourceId?: string | null;\n timezone?: string | null;\n integration: { slug: string; productId?: string };\n /**\n * The storage declaration `defineIntegration({ storage })` will carry —\n * the mock enforces the identical schemas via the same shared validator\n * (ruling 1). Omit it and every storage access throws.\n */\n storageSchemas?: Schemas;\n /** Seed rows: key → data, per scope. Validated on construction via the write path. */\n storage?: {\n account?: ScopeSeed<Schemas['account']>;\n user?: ScopeSeed<Schemas['user']>;\n };\n /**\n * Seed secrets, by tier (ruling 2): `account` values are the rows this\n * integration stored — writable via `secrets.set()`; `admin` values stand\n * in for admin-issued source/global-tier credentials — readable, and a\n * `set()` against a name resolvable only there throws `SecretAccessError`.\n */\n secrets?: {\n account?: Record<string, string>;\n admin?: Record<string, string>;\n };\n /** Same allowlist the manifest would declare — enforced by the stub. */\n egress?: readonly string[];\n /** Programmable responses; unmatched allowed calls resolve 200 `{}`. */\n fetchHandlers?: MockFetchHandler[];\n}\n\nexport interface RecordedFetchCall {\n url: string;\n init?: RequestInit;\n denied: boolean;\n}\n\nexport interface RecordedLog {\n level: 'debug' | 'info' | 'warn' | 'error';\n context: Record<string, unknown>;\n message: string;\n}\n\nexport interface MockIntegrationContext<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends IntegrationContext<Schemas> {\n /** Every ctx.fetch call, allowed or denied, in order. */\n readonly fetchCalls: readonly RecordedFetchCall[];\n /** Every log line, per level. */\n readonly logs: readonly RecordedLog[];\n /** Current readable storage state (expired entries excluded), key → data. */\n dumpStorage(): {\n account: Record<string, unknown>;\n user: Record<string, unknown>;\n };\n}\n\ninterface StoredRow {\n data: unknown;\n externalId: string | null;\n expiresAt: string | null;\n updatedAt: string;\n}\n\nfunction isExpired(row: StoredRow): boolean {\n return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();\n}\n\nfunction parseExpiresAt(\n options: StorageWriteOptions | undefined,\n): string | null {\n const expiresAt = options?.expiresAt;\n if (expiresAt === undefined) return null;\n if (Number.isNaN(Date.parse(expiresAt))) {\n throw new TypeError(\n `expiresAt \"${expiresAt}\" is not a parseable timestamp. Pass an ` +\n `ISO-8601 string, e.g. new Date(Date.now() + 60_000).toISOString().`,\n );\n }\n return expiresAt;\n}\n\nfunction createInMemoryStore(args: {\n scope: StorageScope;\n schemas: StorageSchemaMap | undefined;\n rows: Map<string, StoredRow>;\n guard: () => void;\n}): ScopedStore {\n const { scope, schemas, rows, guard } = args;\n\n return {\n async get(key: string): Promise<StorageEntry<unknown> | null> {\n guard();\n assertStorageKeyDeclared({ scope, key, schemas });\n const row = rows.get(key);\n if (!row) return null;\n if (isExpired(row)) {\n rows.delete(key);\n return null;\n }\n const data = parseStorageValue({\n scope,\n key,\n operation: 'read',\n schemas,\n value: row.data,\n });\n return {\n data,\n externalId: row.externalId,\n expiresAt: row.expiresAt,\n updatedAt: row.updatedAt,\n };\n },\n\n async set(\n key: string,\n data: unknown,\n options?: StorageWriteOptions,\n ): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas,\n value: data,\n });\n rows.set(key, {\n data: parsed,\n externalId: options?.externalId ?? null,\n expiresAt: parseExpiresAt(options),\n updatedAt: new Date().toISOString(),\n });\n },\n\n async delete(key: string): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n assertStorageKeyDeclared({ scope, key, schemas });\n rows.delete(key);\n },\n };\n}\n\nfunction dumpScope(rows: Map<string, StoredRow>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, row] of rows) {\n if (!isExpired(row)) out[key] = row.data;\n }\n return out;\n}\n\nexport function createMockContext<\n Schemas extends StorageSchemas = StorageSchemas,\n>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas> {\n const userId = options.userId !== undefined ? options.userId : 'mock-user-id';\n const actor: IntegrationActor =\n options.actor ??\n (userId !== null\n ? { kind: 'user', userId }\n : { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });\n\n const schemas: StorageSchemas = options.storageSchemas ?? {};\n\n const accountRows = new Map<string, StoredRow>();\n const userRows = new Map<string, StoredRow>();\n\n const seedScope = (\n scope: StorageScope,\n rows: Map<string, StoredRow>,\n seed: Record<string, unknown> | undefined,\n ) => {\n for (const [key, value] of Object.entries(seed ?? {})) {\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas: schemas[scope],\n value,\n });\n rows.set(key, {\n data: parsed,\n externalId: null,\n expiresAt: null,\n updatedAt: new Date().toISOString(),\n });\n }\n };\n seedScope('account', accountRows, options.storage?.account);\n seedScope('user', userRows, options.storage?.user);\n\n const guardUserScope = () => {\n if (actor.kind === 'machine') {\n throw new Error(\n 'ctx.storage.user is unavailable for machine actors: there is no ' +\n 'user to scope the row to. Store machine-written state in ' +\n 'ctx.storage.account instead.',\n );\n }\n };\n\n // The stores validate every key and value at runtime via the shared\n // validator; the cast narrows the runtime-checked store to the\n // schema-typed interface the declaration promises.\n const storage = {\n account: createInMemoryStore({\n scope: 'account',\n schemas: schemas.account,\n rows: accountRows,\n guard: () => void 0,\n }),\n user: createInMemoryStore({\n scope: 'user',\n schemas: schemas.user,\n rows: userRows,\n guard: guardUserScope,\n }),\n } as unknown as IntegrationStorage<Schemas>;\n\n const accountSecrets = new Map(\n Object.entries(options.secrets?.account ?? {}),\n );\n const adminSecrets = new Map(Object.entries(options.secrets?.admin ?? {}));\n\n const secrets: IntegrationSecrets = {\n async get(name) {\n return accountSecrets.get(name) ?? adminSecrets.get(name) ?? null;\n },\n async names() {\n return [\n ...new Set([...accountSecrets.keys(), ...adminSecrets.keys()]),\n ].sort();\n },\n async set(name, value) {\n if (!accountSecrets.has(name) && adminSecrets.has(name)) {\n throw new SecretAccessError(name);\n }\n accountSecrets.set(name, value);\n },\n };\n\n const fetchCalls: RecordedFetchCall[] = [];\n const egress = options.egress ?? [];\n const fetchHandlers = options.fetchHandlers ?? [];\n\n // Not an async function: a denied call must throw synchronously, before\n // any I/O (proposal §2.3) — same behavior the production wrapper has.\n const fetch: IntegrationFetch = (input, init) => {\n const url = new URL(input).toString();\n const allowed = isEgressAllowed(url, egress);\n fetchCalls.push(\n init === undefined\n ? { url, denied: !allowed }\n : { url, init, denied: !allowed },\n );\n if (!allowed) {\n throw new EgressDeniedError(new URL(url).origin);\n }\n const handler = matchFetchHandler(url, fetchHandlers);\n if (handler) {\n return Promise.resolve(handler.respond(new Request(url, init)));\n }\n return Promise.resolve(\n new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n }),\n );\n };\n\n const logs: RecordedLog[] = [];\n const record =\n (level: RecordedLog['level']) =>\n (context: Record<string, unknown>, message: string) => {\n logs.push({ level, context, message });\n };\n const logger: IntegrationLogger = {\n debug: record('debug'),\n info: record('info'),\n warn: record('warn'),\n error: record('error'),\n };\n\n return {\n accountId: options.accountId ?? 'mock-account-id',\n accountSlug: options.accountSlug ?? null,\n userId: actor.kind === 'machine' ? null : userId,\n actor,\n sourceId: options.sourceId ?? null,\n integration: {\n slug: options.integration.slug,\n productId: options.integration.productId ?? 'mock-product-id',\n },\n timezone: options.timezone ?? null,\n storage,\n secrets,\n fetch,\n logger,\n fetchCalls,\n logs,\n dumpStorage() {\n return { account: dumpScope(accountRows), user: dumpScope(userRows) };\n },\n };\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type MockFetchHandler } from './fetch-matching.js';
3
+ /**
4
+ * Mirrors `createMockContext`'s `fetchHandlers` — same `MockFetchHandler`
5
+ * shape, same string-prefix/RegExp matching, same unmatched-request fallback
6
+ * (a plausible empty 200) — so a widget test and an `onActivate`/handler test
7
+ * written against the same fixture describe the same vendor behavior once.
8
+ * No egress check here: a widget under test isn't exercising the allowlist,
9
+ * it's exercising its own rendering against a canned response.
10
+ *
11
+ * Widgets often call same-origin host routes with a relative path (e.g.
12
+ * `/api/integrations/<slug>/storage`), not just absolute vendor URLs — unlike
13
+ * `ctx.fetch`, which only ever sees the latter. `new URL(input)` throws on a
14
+ * bare relative string, so matching happens against the caller's original
15
+ * `input` verbatim (a relative `match` pattern keeps working), and a
16
+ * relative input is resolved against a dummy origin only to build the
17
+ * `Request` object `respond()` receives.
18
+ */
19
+ export declare function createMockFetchProvider(handlers: MockFetchHandler[]): ({ children, }: {
20
+ children: ReactNode;
21
+ }) => import("react").JSX.Element;
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { IntegrationFetchProvider } from '../hooks/integration-fetch-context.js';
4
+ import { matchFetchHandler } from './fetch-matching.js';
5
+ /**
6
+ * Mirrors `createMockContext`'s `fetchHandlers` — same `MockFetchHandler`
7
+ * shape, same string-prefix/RegExp matching, same unmatched-request fallback
8
+ * (a plausible empty 200) — so a widget test and an `onActivate`/handler test
9
+ * written against the same fixture describe the same vendor behavior once.
10
+ * No egress check here: a widget under test isn't exercising the allowlist,
11
+ * it's exercising its own rendering against a canned response.
12
+ *
13
+ * Widgets often call same-origin host routes with a relative path (e.g.
14
+ * `/api/integrations/<slug>/storage`), not just absolute vendor URLs — unlike
15
+ * `ctx.fetch`, which only ever sees the latter. `new URL(input)` throws on a
16
+ * bare relative string, so matching happens against the caller's original
17
+ * `input` verbatim (a relative `match` pattern keeps working), and a
18
+ * relative input is resolved against a dummy origin only to build the
19
+ * `Request` object `respond()` receives.
20
+ */
21
+ export function createMockFetchProvider(handlers) {
22
+ const fetch = async (input, init) => {
23
+ const url = typeof input === 'string' ? input : input.toString();
24
+ const handler = matchFetchHandler(url, handlers);
25
+ const request = new Request(toRequestUrl(url), init);
26
+ if (handler)
27
+ return handler.respond(request);
28
+ return new Response('{}', {
29
+ status: 200,
30
+ headers: { 'content-type': 'application/json' },
31
+ });
32
+ };
33
+ return function MockIntegrationFetchProvider({ children, }) {
34
+ return (_jsx(IntegrationFetchProvider, { fetch: fetch, children: children }));
35
+ };
36
+ }
37
+ /** `Request` requires an absolute URL; a relative one gets a dummy origin. */
38
+ function toRequestUrl(url) {
39
+ try {
40
+ return new URL(url).toString();
41
+ }
42
+ catch (_a) {
43
+ return new URL(url, 'http://localhost').toString();
44
+ }
45
+ }
46
+ //# sourceMappingURL=mock-fetch-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-fetch-provider.js","sourceRoot":"","sources":["../../src/testing/mock-fetch-provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAKb,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAyB,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE5E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAA4B;IAClE,MAAM,KAAK,GAAqB,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QACpD,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjE,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAErD,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAE7C,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;YACxB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO,SAAS,4BAA4B,CAAC,EAC3C,QAAQ,GAGT;QACC,OAAO,CACL,KAAC,wBAAwB,IAAC,KAAK,EAAE,KAAK,YACnC,QAAQ,GACgB,CAC5B,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;IACjC,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,IAAI,GAAG,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC,QAAQ,EAAE,CAAC;IACrD,CAAC;AACH,CAAC","sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\n\nimport type { IntegrationFetch } from '../context/types';\nimport { IntegrationFetchProvider } from '../hooks/integration-fetch-context';\nimport { type MockFetchHandler, matchFetchHandler } from './fetch-matching';\n\n/**\n * Mirrors `createMockContext`'s `fetchHandlers` — same `MockFetchHandler`\n * shape, same string-prefix/RegExp matching, same unmatched-request fallback\n * (a plausible empty 200) — so a widget test and an `onActivate`/handler test\n * written against the same fixture describe the same vendor behavior once.\n * No egress check here: a widget under test isn't exercising the allowlist,\n * it's exercising its own rendering against a canned response.\n *\n * Widgets often call same-origin host routes with a relative path (e.g.\n * `/api/integrations/<slug>/storage`), not just absolute vendor URLs — unlike\n * `ctx.fetch`, which only ever sees the latter. `new URL(input)` throws on a\n * bare relative string, so matching happens against the caller's original\n * `input` verbatim (a relative `match` pattern keeps working), and a\n * relative input is resolved against a dummy origin only to build the\n * `Request` object `respond()` receives.\n */\nexport function createMockFetchProvider(handlers: MockFetchHandler[]) {\n const fetch: IntegrationFetch = async (input, init) => {\n const url = typeof input === 'string' ? input : input.toString();\n const handler = matchFetchHandler(url, handlers);\n const request = new Request(toRequestUrl(url), init);\n\n if (handler) return handler.respond(request);\n\n return new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n });\n };\n\n return function MockIntegrationFetchProvider({\n children,\n }: {\n children: ReactNode;\n }) {\n return (\n <IntegrationFetchProvider fetch={fetch}>\n {children}\n </IntegrationFetchProvider>\n );\n };\n}\n\n/** `Request` requires an absolute URL; a relative one gets a dummy origin. */\nfunction toRequestUrl(url: string): string {\n try {\n return new URL(url).toString();\n } catch {\n return new URL(url, 'http://localhost').toString();\n }\n}\n"]}
package/eslint.preset.mjs CHANGED
@@ -62,6 +62,25 @@ const CTX_FETCH_REMEDIATION =
62
62
  'bypass. Widening `egress` is a reviewed security-posture change, not a ' +
63
63
  'config tweak.';
64
64
 
65
+ /**
66
+ * `fetch` is the one escape that legitimately has TWO capability-mediated
67
+ * replacements, depending on where the code runs — unlike every other entry
68
+ * in this preset, which has exactly one. Naming only `ctx.fetch` in a
69
+ * widget/client file sends a partner chasing a capability that does not
70
+ * exist on that side of the boundary, so this message names both.
71
+ */
72
+ const CLIENT_FETCH_REMEDIATION =
73
+ 'A global fetch reaches the network outside the capability layer, which is ' +
74
+ 'an egress bypass. In server/handler code, use `ctx.fetch` from the ' +
75
+ "capability context — it enforces your integration's `egress` allowlist " +
76
+ '(`defineIntegration({ egress: [...] })`), re-checks every redirect hop, ' +
77
+ 'and pins the vetted addresses; that enforcement is server-side only. In ' +
78
+ 'client/widget code, use `useIntegrationFetch()` from `@ekanos/sdk/hooks` ' +
79
+ 'instead — the dev harness gates it against the same `egress` list, but a ' +
80
+ 'browser cannot pin addresses or re-vet redirects, so treat that check as ' +
81
+ 'advisory, not a security boundary. Widening `egress` is a reviewed ' +
82
+ 'security-posture change either way.';
83
+
65
84
  const CTX_SECRETS_REMEDIATION =
66
85
  'Use `ctx.secrets.get(name)` from the capability context instead, and ' +
67
86
  "declare the credential in your integration's activation schema. The host " +
@@ -190,9 +209,14 @@ const EGRESS_GLOBALS = ['fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource'];
190
209
  /** Objects a `globalThis.fetch`-style member access can hide behind. */
191
210
  const GLOBAL_OBJECTS = 'globalThis|window|self|global';
192
211
 
193
- /** Members of those objects that are the very escapes banned as bare globals. */
194
- const GLOBAL_ESCAPE_MEMBERS =
195
- 'fetch|XMLHttpRequest|WebSocket|EventSource|process|eval|require|Function';
212
+ /**
213
+ * Members of those objects that are the very escapes banned as bare globals,
214
+ * split so `fetch` — the one escape with a client-side remediation — can
215
+ * carry its own message. See `CLIENT_FETCH_REMEDIATION`.
216
+ */
217
+ const GLOBAL_ESCAPE_FETCH_MEMBER = 'fetch';
218
+ const GLOBAL_ESCAPE_OTHER_MEMBERS =
219
+ 'XMLHttpRequest|WebSocket|EventSource|process|eval|require|Function';
196
220
 
197
221
  /**
198
222
  * `@kit/*` is checked with a PATTERN rather than a path so every subpath is
@@ -250,7 +274,12 @@ export const capabilityRules = {
250
274
  'error',
251
275
  ...EGRESS_GLOBALS.map((name) => ({
252
276
  name,
253
- message: `\`${name}\` reaches the network outside the capability layer. ${CTX_FETCH_REMEDIATION}`,
277
+ // `fetch` has a client-side remediation the other egress globals don't
278
+ // (`useIntegrationFetch()`), so it gets its own message.
279
+ message:
280
+ name === 'fetch'
281
+ ? CLIENT_FETCH_REMEDIATION
282
+ : `\`${name}\` reaches the network outside the capability layer. ${CTX_FETCH_REMEDIATION}`,
254
283
  })),
255
284
  {
256
285
  name: 'process',
@@ -283,8 +312,18 @@ export const capabilityRules = {
283
312
  {
284
313
  // Closes the hole in no-restricted-globals: that rule resolves
285
314
  // identifiers, so `globalThis.fetch` (a member access, not a global
286
- // reference) is invisible to it.
287
- selector: `MemberExpression[object.name=/^(${GLOBAL_OBJECTS})$/][property.name=/^(${GLOBAL_ESCAPE_MEMBERS})$/]`,
315
+ // reference) is invisible to it. Split out from the selector below so
316
+ // it can carry the client-side remediation.
317
+ selector: `MemberExpression[object.name=/^(${GLOBAL_OBJECTS})$/][property.name=/^(${GLOBAL_ESCAPE_FETCH_MEMBER})$/]`,
318
+ message:
319
+ 'Reaching `fetch` through the global object is the same bypass as ' +
320
+ 'calling it directly, and it is how a capability control gets routed ' +
321
+ 'around without tripping an import rule. ' +
322
+ CLIENT_FETCH_REMEDIATION,
323
+ },
324
+ {
325
+ // Same hole, for the escapes that have no client-side twin.
326
+ selector: `MemberExpression[object.name=/^(${GLOBAL_OBJECTS})$/][property.name=/^(${GLOBAL_ESCAPE_OTHER_MEMBERS})$/]`,
288
327
  message:
289
328
  'Reaching an escape through the global object is the same bypass as ' +
290
329
  'naming it directly, and it is how a capability control gets routed ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/sdk",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "The official SDK for building Ekanos integrations.",
6
6
  "license": "MIT",
@@ -49,8 +49,8 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@ekanos/integration-schema": "0.1.5",
53
- "@ekanos/ui": "0.1.5",
52
+ "@ekanos/integration-schema": "0.2.0",
53
+ "@ekanos/ui": "0.2.0",
54
54
  "@supabase/supabase-js": "2.87.1",
55
55
  "server-only": "^0.0.1"
56
56
  },
@@ -69,8 +69,10 @@
69
69
  "@kit/tsconfig": "0.1.0",
70
70
  "@tanstack/react-query": "5.102.8",
71
71
  "@types/react": "19.2.18",
72
+ "@types/react-dom": "19.2.5",
72
73
  "next": "16.3.3",
73
74
  "react": "19.2.8",
75
+ "react-dom": "19.2.8",
74
76
  "react-hook-form": "^7.87.0",
75
77
  "typescript": "^5.9.3",
76
78
  "vitest": "4.1.10",