@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 +13 -3
- package/dist/components/widgets/widget-preview-provider.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +12 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/integration-activation-data-context.d.ts +13 -0
- package/dist/hooks/integration-activation-data-context.js +28 -0
- package/dist/hooks/integration-activation-data-context.js.map +1 -0
- package/dist/hooks/integration-fetch-context.d.ts +15 -0
- package/dist/hooks/integration-fetch-context.js +33 -0
- package/dist/hooks/integration-fetch-context.js.map +1 -0
- package/dist/hooks/partner-oauth.d.ts +56 -0
- package/dist/hooks/partner-oauth.js +63 -0
- package/dist/hooks/partner-oauth.js.map +1 -0
- package/dist/testing/fetch-matching.d.ts +18 -0
- package/dist/testing/fetch-matching.js +18 -0
- package/dist/testing/fetch-matching.js.map +1 -0
- package/dist/testing/index.d.ts +3 -1
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock-context.d.ts +2 -6
- package/dist/testing/mock-context.js +2 -3
- package/dist/testing/mock-context.js.map +1 -1
- package/dist/testing/mock-fetch-provider.d.ts +21 -0
- package/dist/testing/mock-fetch-provider.js +46 -0
- package/dist/testing/mock-fetch-provider.js.map +1 -0
- package/eslint.preset.mjs +45 -6
- package/package.json +5 -3
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: **
|
|
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` (
|
|
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` (
|
|
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,
|
|
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"]}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/hooks/index.js
CHANGED
|
@@ -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
|
package/dist/hooks/index.js.map
CHANGED
|
@@ -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"]}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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,
|
|
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';
|
package/dist/testing/index.js
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
|
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":"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;AAqGvC,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,aAAa,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAC/C,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ;YACjC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC;YACjC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAC9B,CAAC;QACF,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';\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":"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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';\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
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
53
|
-
"@ekanos/ui": "0.
|
|
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",
|