@ekanos/sdk 0.1.6 → 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 +8 -3
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +7 -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/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 -4
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: **155** 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,7 +91,9 @@ Total exported names: **155** 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 |
|
|
96
98
|
| `PartnerOAuthPathOptions` | interface |
|
|
97
99
|
| `PublishEventInput` | interface |
|
|
@@ -107,8 +109,10 @@ Total exported names: **155** across 7 entrypoints.
|
|
|
107
109
|
| `partnerOAuthErrorParam` | function |
|
|
108
110
|
| `partnerOAuthStatusUrl` | function |
|
|
109
111
|
| `useActivateIntegration` | function |
|
|
112
|
+
| `useActivationData` | function |
|
|
110
113
|
| `useAskAssistant` | function |
|
|
111
114
|
| `useDeactivateIntegration` | function |
|
|
115
|
+
| `useIntegrationFetch` | function |
|
|
112
116
|
| `useOAuthConnectionStatus` | function |
|
|
113
117
|
| `useOAuthWidgetGate` | function |
|
|
114
118
|
| `useRegisterAskAssistant` | function |
|
|
@@ -158,7 +162,7 @@ Total exported names: **155** across 7 entrypoints.
|
|
|
158
162
|
| `requireContext` | function |
|
|
159
163
|
| `resolveStorageKeyDeclaration` | function |
|
|
160
164
|
|
|
161
|
-
## `@ekanos/sdk/testing` (
|
|
165
|
+
## `@ekanos/sdk/testing` (17 exports)
|
|
162
166
|
|
|
163
167
|
| Export | Kind |
|
|
164
168
|
| --- | --- |
|
|
@@ -175,6 +179,7 @@ Total exported names: **155** across 7 entrypoints.
|
|
|
175
179
|
| `ScheduleInvocationOutcome` | interface |
|
|
176
180
|
| `WebhookInvocationOutcome` | interface |
|
|
177
181
|
| `createMockContext` | function |
|
|
182
|
+
| `createMockFetchProvider` | function |
|
|
178
183
|
| `invokeActivate` | function |
|
|
179
184
|
| `invokeSchedule` | function |
|
|
180
185
|
| `invokeWebhook` | function |
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -28,3 +28,5 @@ export { WidgetEventProvider, useWidgetEventBus } from './widget-event-context.j
|
|
|
28
28
|
export { useWidgetEvents } from './use-widget-events.js';
|
|
29
29
|
export type { PublishEventInput, WidgetEvent, WidgetEventFilter, WidgetEventSubscriber, } from './widget-event-types.js';
|
|
30
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
|
@@ -34,4 +34,11 @@ export { WidgetEventProvider, useWidgetEventBus } from './widget-event-context.j
|
|
|
34
34
|
export { useWidgetEvents } from './use-widget-events.js';
|
|
35
35
|
// Widget Ask-Bar Bridge
|
|
36
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';
|
|
37
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,+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","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"]}
|
|
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,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":"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","sourcesContent":["/**\n * `createMockContext()` — the day-1 in-memory `IntegrationContext` for the\n * partner harness (proposal §4, amended by rulings 1–4). Self-contained:\n * nothing here touches Supabase, Vault, or the network. Enforcement is the\n * SAME shared implementation the production wrapper uses\n * (`@ekanos/sdk/context`): schema validation via `parseStorageValue`, egress\n * via `isEgressAllowed` — a test that passes the mock cannot be a schema or\n * egress surprise later.\n */\nimport type { z } from 'zod';\n\nimport { isEgressAllowed } from '../context/egress';\nimport { EgressDeniedError, SecretAccessError } from '../context/errors';\nimport {\n assertStorageKeyDeclared,\n assertStorageKeyWritable,\n parseStorageValue,\n} from '../context/storage-validation';\nimport type {\n IntegrationActor,\n IntegrationContext,\n IntegrationFetch,\n IntegrationLogger,\n IntegrationSecrets,\n IntegrationStorage,\n ScopedStore,\n StorageEntry,\n StorageKeySchema,\n StorageSchemaMap,\n StorageSchemas,\n StorageWriteOptions,\n} from '../context/types';\n\ntype StorageScope = 'account' | 'user';\n\n// Seeds are typed off the DECLARED schema, resolved out of whichever\n// declaration form the key used — a bare zod schema or a\n// `{ schema, clientReadable }` descriptor.\ntype ScopeSeed<M> = M extends StorageSchemaMap\n ? { [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>> }\n : Record<never, never>;\n\nexport interface MockFetchHandler {\n /** A string matches URLs that start with it; a RegExp is tested against the full URL. */\n match: string | RegExp;\n respond: (request: Request) => Response | Promise<Response>;\n}\n\nexport interface MockContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n accountId?: string;\n accountSlug?: string | null;\n userId?: string | null;\n actor?: IntegrationActor;\n sourceId?: string | null;\n timezone?: string | null;\n integration: { slug: string; productId?: string };\n /**\n * The storage declaration `defineIntegration({ storage })` will carry —\n * the mock enforces the identical schemas via the same shared validator\n * (ruling 1). Omit it and every storage access throws.\n */\n storageSchemas?: Schemas;\n /** Seed rows: key → data, per scope. Validated on construction via the write path. */\n storage?: {\n account?: ScopeSeed<Schemas['account']>;\n user?: ScopeSeed<Schemas['user']>;\n };\n /**\n * Seed secrets, by tier (ruling 2): `account` values are the rows this\n * integration stored — writable via `secrets.set()`; `admin` values stand\n * in for admin-issued source/global-tier credentials — readable, and a\n * `set()` against a name resolvable only there throws `SecretAccessError`.\n */\n secrets?: {\n account?: Record<string, string>;\n admin?: Record<string, string>;\n };\n /** Same allowlist the manifest would declare — enforced by the stub. */\n egress?: readonly string[];\n /** Programmable responses; unmatched allowed calls resolve 200 `{}`. */\n fetchHandlers?: MockFetchHandler[];\n}\n\nexport interface RecordedFetchCall {\n url: string;\n init?: RequestInit;\n denied: boolean;\n}\n\nexport interface RecordedLog {\n level: 'debug' | 'info' | 'warn' | 'error';\n context: Record<string, unknown>;\n message: string;\n}\n\nexport interface MockIntegrationContext<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends IntegrationContext<Schemas> {\n /** Every ctx.fetch call, allowed or denied, in order. */\n readonly fetchCalls: readonly RecordedFetchCall[];\n /** Every log line, per level. */\n readonly logs: readonly RecordedLog[];\n /** Current readable storage state (expired entries excluded), key → data. */\n dumpStorage(): {\n account: Record<string, unknown>;\n user: Record<string, unknown>;\n };\n}\n\ninterface StoredRow {\n data: unknown;\n externalId: string | null;\n expiresAt: string | null;\n updatedAt: string;\n}\n\nfunction isExpired(row: StoredRow): boolean {\n return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();\n}\n\nfunction parseExpiresAt(\n options: StorageWriteOptions | undefined,\n): string | null {\n const expiresAt = options?.expiresAt;\n if (expiresAt === undefined) return null;\n if (Number.isNaN(Date.parse(expiresAt))) {\n throw new TypeError(\n `expiresAt \"${expiresAt}\" is not a parseable timestamp. Pass an ` +\n `ISO-8601 string, e.g. new Date(Date.now() + 60_000).toISOString().`,\n );\n }\n return expiresAt;\n}\n\nfunction createInMemoryStore(args: {\n scope: StorageScope;\n schemas: StorageSchemaMap | undefined;\n rows: Map<string, StoredRow>;\n guard: () => void;\n}): ScopedStore {\n const { scope, schemas, rows, guard } = args;\n\n return {\n async get(key: string): Promise<StorageEntry<unknown> | null> {\n guard();\n assertStorageKeyDeclared({ scope, key, schemas });\n const row = rows.get(key);\n if (!row) return null;\n if (isExpired(row)) {\n rows.delete(key);\n return null;\n }\n const data = parseStorageValue({\n scope,\n key,\n operation: 'read',\n schemas,\n value: row.data,\n });\n return {\n data,\n externalId: row.externalId,\n expiresAt: row.expiresAt,\n updatedAt: row.updatedAt,\n };\n },\n\n async set(\n key: string,\n data: unknown,\n options?: StorageWriteOptions,\n ): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas,\n value: data,\n });\n rows.set(key, {\n data: parsed,\n externalId: options?.externalId ?? null,\n expiresAt: parseExpiresAt(options),\n updatedAt: new Date().toISOString(),\n });\n },\n\n async delete(key: string): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n assertStorageKeyDeclared({ scope, key, schemas });\n rows.delete(key);\n },\n };\n}\n\nfunction dumpScope(rows: Map<string, StoredRow>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, row] of rows) {\n if (!isExpired(row)) out[key] = row.data;\n }\n return out;\n}\n\nexport function createMockContext<\n Schemas extends StorageSchemas = StorageSchemas,\n>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas> {\n const userId = options.userId !== undefined ? options.userId : 'mock-user-id';\n const actor: IntegrationActor =\n options.actor ??\n (userId !== null\n ? { kind: 'user', userId }\n : { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });\n\n const schemas: StorageSchemas = options.storageSchemas ?? {};\n\n const accountRows = new Map<string, StoredRow>();\n const userRows = new Map<string, StoredRow>();\n\n const seedScope = (\n scope: StorageScope,\n rows: Map<string, StoredRow>,\n seed: Record<string, unknown> | undefined,\n ) => {\n for (const [key, value] of Object.entries(seed ?? {})) {\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas: schemas[scope],\n value,\n });\n rows.set(key, {\n data: parsed,\n externalId: null,\n expiresAt: null,\n updatedAt: new Date().toISOString(),\n });\n }\n };\n seedScope('account', accountRows, options.storage?.account);\n seedScope('user', userRows, options.storage?.user);\n\n const guardUserScope = () => {\n if (actor.kind === 'machine') {\n throw new Error(\n 'ctx.storage.user is unavailable for machine actors: there is no ' +\n 'user to scope the row to. Store machine-written state in ' +\n 'ctx.storage.account instead.',\n );\n }\n };\n\n // The stores validate every key and value at runtime via the shared\n // validator; the cast narrows the runtime-checked store to the\n // schema-typed interface the declaration promises.\n const storage = {\n account: createInMemoryStore({\n scope: 'account',\n schemas: schemas.account,\n rows: accountRows,\n guard: () => void 0,\n }),\n user: createInMemoryStore({\n scope: 'user',\n schemas: schemas.user,\n rows: userRows,\n guard: guardUserScope,\n }),\n } as unknown as IntegrationStorage<Schemas>;\n\n const accountSecrets = new Map(\n Object.entries(options.secrets?.account ?? {}),\n );\n const adminSecrets = new Map(Object.entries(options.secrets?.admin ?? {}));\n\n const secrets: IntegrationSecrets = {\n async get(name) {\n return accountSecrets.get(name) ?? adminSecrets.get(name) ?? null;\n },\n async names() {\n return [\n ...new Set([...accountSecrets.keys(), ...adminSecrets.keys()]),\n ].sort();\n },\n async set(name, value) {\n if (!accountSecrets.has(name) && adminSecrets.has(name)) {\n throw new SecretAccessError(name);\n }\n accountSecrets.set(name, value);\n },\n };\n\n const fetchCalls: RecordedFetchCall[] = [];\n const egress = options.egress ?? [];\n const fetchHandlers = options.fetchHandlers ?? [];\n\n // Not an async function: a denied call must throw synchronously, before\n // any I/O (proposal §2.3) — same behavior the production wrapper has.\n const fetch: IntegrationFetch = (input, init) => {\n const url = new URL(input).toString();\n const allowed = isEgressAllowed(url, egress);\n fetchCalls.push(\n init === undefined\n ? { url, denied: !allowed }\n : { url, init, denied: !allowed },\n );\n if (!allowed) {\n throw new EgressDeniedError(new URL(url).origin);\n }\n const handler = fetchHandlers.find((candidate) =>\n typeof candidate.match === 'string'\n ? url.startsWith(candidate.match)\n : candidate.match.test(url),\n );\n if (handler) {\n return Promise.resolve(handler.respond(new Request(url, init)));\n }\n return Promise.resolve(\n new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n }),\n );\n };\n\n const logs: RecordedLog[] = [];\n const record =\n (level: RecordedLog['level']) =>\n (context: Record<string, unknown>, message: string) => {\n logs.push({ level, context, message });\n };\n const logger: IntegrationLogger = {\n debug: record('debug'),\n info: record('info'),\n warn: record('warn'),\n error: record('error'),\n };\n\n return {\n accountId: options.accountId ?? 'mock-account-id',\n accountSlug: options.accountSlug ?? null,\n userId: actor.kind === 'machine' ? null : userId,\n actor,\n sourceId: options.sourceId ?? null,\n integration: {\n slug: options.integration.slug,\n productId: options.integration.productId ?? 'mock-product-id',\n },\n timezone: options.timezone ?? null,\n storage,\n secrets,\n fetch,\n logger,\n fetchCalls,\n logs,\n dumpStorage() {\n return { account: dumpScope(accountRows), user: dumpScope(userRows) };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"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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,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekanos/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"private": false,
|
|
3
|
+
"version": "0.2.0",
|
|
5
4
|
"type": "module",
|
|
6
5
|
"description": "The official SDK for building Ekanos integrations.",
|
|
7
6
|
"license": "MIT",
|
|
@@ -50,8 +49,8 @@
|
|
|
50
49
|
"access": "public"
|
|
51
50
|
},
|
|
52
51
|
"dependencies": {
|
|
53
|
-
"@ekanos/integration-schema": "0.
|
|
54
|
-
"@ekanos/ui": "0.
|
|
52
|
+
"@ekanos/integration-schema": "0.2.0",
|
|
53
|
+
"@ekanos/ui": "0.2.0",
|
|
55
54
|
"@supabase/supabase-js": "2.87.1",
|
|
56
55
|
"server-only": "^0.0.1"
|
|
57
56
|
},
|
|
@@ -70,8 +69,10 @@
|
|
|
70
69
|
"@kit/tsconfig": "0.1.0",
|
|
71
70
|
"@tanstack/react-query": "5.102.8",
|
|
72
71
|
"@types/react": "19.2.18",
|
|
72
|
+
"@types/react-dom": "19.2.5",
|
|
73
73
|
"next": "16.3.3",
|
|
74
74
|
"react": "19.2.8",
|
|
75
|
+
"react-dom": "19.2.8",
|
|
75
76
|
"react-hook-form": "^7.87.0",
|
|
76
77
|
"typescript": "^5.9.3",
|
|
77
78
|
"vitest": "4.1.10",
|