@ekanos/sdk 0.1.6 → 0.3.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.
Files changed (35) hide show
  1. package/api-report.md +16 -4
  2. package/dist/errors/public-action-error.d.ts +31 -0
  3. package/dist/errors/public-action-error.js +35 -0
  4. package/dist/errors/public-action-error.js.map +1 -0
  5. package/dist/hooks/index.d.ts +4 -0
  6. package/dist/hooks/index.js +13 -0
  7. package/dist/hooks/index.js.map +1 -1
  8. package/dist/hooks/integration-activation-data-context.d.ts +13 -0
  9. package/dist/hooks/integration-activation-data-context.js +28 -0
  10. package/dist/hooks/integration-activation-data-context.js.map +1 -0
  11. package/dist/hooks/integration-fetch-context.d.ts +15 -0
  12. package/dist/hooks/integration-fetch-context.js +33 -0
  13. package/dist/hooks/integration-fetch-context.js.map +1 -0
  14. package/dist/hooks/use-integration-action.d.ts +105 -0
  15. package/dist/hooks/use-integration-action.js +134 -0
  16. package/dist/hooks/use-integration-action.js.map +1 -0
  17. package/dist/integration/index.d.ts +1 -0
  18. package/dist/integration/index.js +4 -0
  19. package/dist/integration/index.js.map +1 -1
  20. package/dist/testing/fetch-matching.d.ts +18 -0
  21. package/dist/testing/fetch-matching.js +18 -0
  22. package/dist/testing/fetch-matching.js.map +1 -0
  23. package/dist/testing/index.d.ts +3 -1
  24. package/dist/testing/index.js +5 -0
  25. package/dist/testing/index.js.map +1 -1
  26. package/dist/testing/mock-context.d.ts +2 -6
  27. package/dist/testing/mock-context.js +2 -3
  28. package/dist/testing/mock-context.js.map +1 -1
  29. package/dist/testing/mock-fetch-provider.d.ts +21 -0
  30. package/dist/testing/mock-fetch-provider.js +46 -0
  31. package/dist/testing/mock-fetch-provider.js.map +1 -0
  32. package/dist/types/integration.d.ts +16 -4
  33. package/dist/types/integration.js.map +1 -1
  34. package/eslint.preset.mjs +45 -6
  35. 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: **155** across 7 entrypoints.
12
+ Total exported names: **167** 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` (33 exports)
80
+ ## `@ekanos/sdk/hooks` (43 exports)
81
81
 
82
82
  | Export | Kind |
83
83
  | --- | --- |
@@ -89,12 +89,18 @@ Total exported names: **155** across 7 entrypoints.
89
89
  | `DeactivateIntegrationAction` | type alias |
90
90
  | `DeactivateIntegrationInput` | interface |
91
91
  | `FetchIntegrationStorageOptions` | interface |
92
+ | `IntegrationActionError` | class |
93
+ | `IntegrationActionErrorCode` | type alias |
94
+ | `IntegrationActionErrorDetails` | interface |
92
95
  | `IntegrationActionResult` | type alias |
93
96
  | `IntegrationActivationActions` | interface |
97
+ | `IntegrationActivationDataProvider` | function |
94
98
  | `IntegrationActivationProvider` | function |
99
+ | `IntegrationFetchProvider` | function |
95
100
  | `OAuthErrorParam` | interface |
96
101
  | `PartnerOAuthPathOptions` | interface |
97
102
  | `PublishEventInput` | interface |
103
+ | `UseIntegrationActionOptions` | interface |
98
104
  | `UseOAuthConnectionStatusOptions` | interface |
99
105
  | `WidgetEvent` | interface |
100
106
  | `WidgetEventFilter` | interface |
@@ -102,13 +108,17 @@ Total exported names: **155** across 7 entrypoints.
102
108
  | `WidgetEventSubscriber` | type alias |
103
109
  | `createPartnerConnectionCheck` | function |
104
110
  | `fetchIntegrationStorage` | function |
111
+ | `integrationActionUrl` | function |
105
112
  | `integrationStorageUrl` | function |
106
113
  | `partnerOAuthConnectPath` | function |
107
114
  | `partnerOAuthErrorParam` | function |
108
115
  | `partnerOAuthStatusUrl` | function |
109
116
  | `useActivateIntegration` | function |
117
+ | `useActivationData` | function |
110
118
  | `useAskAssistant` | function |
111
119
  | `useDeactivateIntegration` | function |
120
+ | `useIntegrationAction` | function |
121
+ | `useIntegrationFetch` | function |
112
122
  | `useOAuthConnectionStatus` | function |
113
123
  | `useOAuthWidgetGate` | function |
114
124
  | `useRegisterAskAssistant` | function |
@@ -158,7 +168,7 @@ Total exported names: **155** across 7 entrypoints.
158
168
  | `requireContext` | function |
159
169
  | `resolveStorageKeyDeclaration` | function |
160
170
 
161
- ## `@ekanos/sdk/testing` (16 exports)
171
+ ## `@ekanos/sdk/testing` (17 exports)
162
172
 
163
173
  | Export | Kind |
164
174
  | --- | --- |
@@ -175,11 +185,12 @@ Total exported names: **155** across 7 entrypoints.
175
185
  | `ScheduleInvocationOutcome` | interface |
176
186
  | `WebhookInvocationOutcome` | interface |
177
187
  | `createMockContext` | function |
188
+ | `createMockFetchProvider` | function |
178
189
  | `invokeActivate` | function |
179
190
  | `invokeSchedule` | function |
180
191
  | `invokeWebhook` | function |
181
192
 
182
- ## `@ekanos/sdk/integration` (26 exports)
193
+ ## `@ekanos/sdk/integration` (27 exports)
183
194
 
184
195
  | Export | Kind |
185
196
  | --- | --- |
@@ -200,6 +211,7 @@ Total exported names: **155** across 7 entrypoints.
200
211
  | `PartnerToolParameters` | type alias |
201
212
  | `PartnerWebhookDeclaration` | interface |
202
213
  | `PartnerWidgetDeclaration` | type alias |
214
+ | `PublicActionError` | class |
203
215
  | `ScheduleInvocation` | interface |
204
216
  | `ScheduleResult` | interface |
205
217
  | `ToolClassificationProposal` | type alias |
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The ONE escape hatch from the actions host route's default opaque-failure
3
+ * discipline (the widget-actions opaque-failure ruling). Every throw
4
+ * from an action's `handler` is, by default, logged in full server-side
5
+ * (class + length only — never the message content, since a partner
6
+ * exception can embed provider response bodies, PII, or credentials) and
7
+ * collapses to a generic "Action failed." response. A handler that wants
8
+ * its OWN message shown to the end user verbatim (e.g. "That record already
9
+ * exists") throws this instead — the route recognizes it by `error.name ===
10
+ * 'PublicActionError'` and surfaces `message` as-is, still under the same
11
+ * `handler_error` code and status.
12
+ *
13
+ * RECOGNITION IS BY NAME, NOT `instanceof`. `@ekanos/sdk` is a published
14
+ * package: a partner's own bundle resolves its own copy of this module
15
+ * (through the partner's registry package, e.g. `@companydotcom/
16
+ * integration-*`), which is NOT the same module instance the host imports
17
+ * from its workspace dependency — a classic dual-package hazard. `error
18
+ * instanceof PublicActionError` in the host would compare against the
19
+ * HOST's class identity and silently fail for every real partner throw.
20
+ * The constructor below sets `this.name` durably for exactly this reason;
21
+ * do not rely on `instanceof` anywhere this error crosses the host/partner
22
+ * boundary.
23
+ *
24
+ * Dependency-pure and isomorphic (no `@kit/*`, no `server-only`) so it is
25
+ * importable from both a partner's own handler code and the host route that
26
+ * catches it — the same discipline every other authoring type in this
27
+ * module follows.
28
+ */
29
+ export declare class PublicActionError extends Error {
30
+ constructor(message: string);
31
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The ONE escape hatch from the actions host route's default opaque-failure
3
+ * discipline (the widget-actions opaque-failure ruling). Every throw
4
+ * from an action's `handler` is, by default, logged in full server-side
5
+ * (class + length only — never the message content, since a partner
6
+ * exception can embed provider response bodies, PII, or credentials) and
7
+ * collapses to a generic "Action failed." response. A handler that wants
8
+ * its OWN message shown to the end user verbatim (e.g. "That record already
9
+ * exists") throws this instead — the route recognizes it by `error.name ===
10
+ * 'PublicActionError'` and surfaces `message` as-is, still under the same
11
+ * `handler_error` code and status.
12
+ *
13
+ * RECOGNITION IS BY NAME, NOT `instanceof`. `@ekanos/sdk` is a published
14
+ * package: a partner's own bundle resolves its own copy of this module
15
+ * (through the partner's registry package, e.g. `@companydotcom/
16
+ * integration-*`), which is NOT the same module instance the host imports
17
+ * from its workspace dependency — a classic dual-package hazard. `error
18
+ * instanceof PublicActionError` in the host would compare against the
19
+ * HOST's class identity and silently fail for every real partner throw.
20
+ * The constructor below sets `this.name` durably for exactly this reason;
21
+ * do not rely on `instanceof` anywhere this error crosses the host/partner
22
+ * boundary.
23
+ *
24
+ * Dependency-pure and isomorphic (no `@kit/*`, no `server-only`) so it is
25
+ * importable from both a partner's own handler code and the host route that
26
+ * catches it — the same discipline every other authoring type in this
27
+ * module follows.
28
+ */
29
+ export class PublicActionError extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = 'PublicActionError';
33
+ }
34
+ }
35
+ //# sourceMappingURL=public-action-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-action-error.js","sourceRoot":"","sources":["../../src/errors/public-action-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF","sourcesContent":["/**\n * The ONE escape hatch from the actions host route's default opaque-failure\n * discipline (the widget-actions opaque-failure ruling). Every throw\n * from an action's `handler` is, by default, logged in full server-side\n * (class + length only — never the message content, since a partner\n * exception can embed provider response bodies, PII, or credentials) and\n * collapses to a generic \"Action failed.\" response. A handler that wants\n * its OWN message shown to the end user verbatim (e.g. \"That record already\n * exists\") throws this instead — the route recognizes it by `error.name ===\n * 'PublicActionError'` and surfaces `message` as-is, still under the same\n * `handler_error` code and status.\n *\n * RECOGNITION IS BY NAME, NOT `instanceof`. `@ekanos/sdk` is a published\n * package: a partner's own bundle resolves its own copy of this module\n * (through the partner's registry package, e.g. `@companydotcom/\n * integration-*`), which is NOT the same module instance the host imports\n * from its workspace dependency — a classic dual-package hazard. `error\n * instanceof PublicActionError` in the host would compare against the\n * HOST's class identity and silently fail for every real partner throw.\n * The constructor below sets `this.name` durably for exactly this reason;\n * do not rely on `instanceof` anywhere this error crosses the host/partner\n * boundary.\n *\n * Dependency-pure and isomorphic (no `@kit/*`, no `server-only`) so it is\n * importable from both a partner's own handler code and the host route that\n * catches it — the same discipline every other authoring type in this\n * module follows.\n */\nexport class PublicActionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PublicActionError';\n }\n}\n"]}
@@ -24,7 +24,11 @@ export { useOAuthConnectionStatus, useOAuthWidgetGate, } from './use-oauth-conne
24
24
  export type { ConnectionCheckAction, ConnectionCheckResult, OAuthErrorParam, UseOAuthConnectionStatusOptions, } from './use-oauth-connection-status.js';
25
25
  export { createPartnerConnectionCheck, partnerOAuthConnectPath, partnerOAuthErrorParam, partnerOAuthStatusUrl, } from './partner-oauth.js';
26
26
  export type { PartnerOAuthPathOptions } from './partner-oauth.js';
27
+ export { useIntegrationAction, integrationActionUrl, IntegrationActionError, } from './use-integration-action.js';
28
+ export type { UseIntegrationActionOptions, IntegrationActionErrorCode, IntegrationActionErrorDetails, } from './use-integration-action.js';
27
29
  export { WidgetEventProvider, useWidgetEventBus } from './widget-event-context.js';
28
30
  export { useWidgetEvents } from './use-widget-events.js';
29
31
  export type { PublishEventInput, WidgetEvent, WidgetEventFilter, WidgetEventSubscriber, } from './widget-event-types.js';
30
32
  export { AskAssistantProvider, useAskAssistant, useRegisterAskAssistant, } from './ask-assistant-context.js';
33
+ export { IntegrationFetchProvider, useIntegrationFetch, } from './integration-fetch-context.js';
34
+ export { IntegrationActivationDataProvider, useActivationData, } from './integration-activation-data-context.js';
@@ -29,9 +29,22 @@ export { useOAuthConnectionStatus, useOAuthWidgetGate, } from './use-oauth-conne
29
29
  // passes `connectPath: partnerOAuthConnectPath(slug)` to OAuthActivationForm
30
30
  // and `createPartnerConnectionCheck(slug)` as its `checkConnection`.
31
31
  export { createPartnerConnectionCheck, partnerOAuthConnectPath, partnerOAuthErrorParam, partnerOAuthStatusUrl, } from './partner-oauth.js';
32
+ // Widget actions (host route:
33
+ // POST /api/integrations/[slug]/actions/[actionName]). Invokes a partner's
34
+ // declared `actions[name]` handler directly from a widget — deterministic,
35
+ // no assistant round trip. `TInput`/`TOutput` are supplied by the caller;
36
+ // see the module doc comment for the typing discipline.
37
+ export { useIntegrationAction, integrationActionUrl, IntegrationActionError, } from './use-integration-action.js';
32
38
  // Widget Event Bus
33
39
  export { WidgetEventProvider, useWidgetEventBus } from './widget-event-context.js';
34
40
  export { useWidgetEvents } from './use-widget-events.js';
35
41
  // Widget Ask-Bar Bridge
36
42
  export { AskAssistantProvider, useAskAssistant, useRegisterAskAssistant, } from './ask-assistant-context.js';
43
+ // Client-side fetch (host routes: none — this mediates a widget's own
44
+ // vendor calls, mirroring `ctx.fetch` on the server). Default outside a
45
+ // provider is a plain `globalThis.fetch` passthrough.
46
+ export { IntegrationFetchProvider, useIntegrationFetch, } from './integration-fetch-context.js';
47
+ // Activation data (the credential half of the client-fetch pattern).
48
+ // Default outside a provider is `null` — no host mounts this yet.
49
+ export { IntegrationActivationDataProvider, useActivationData, } from './integration-activation-data-context.js';
37
50
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,4CAA4C;AAC5C,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAU7E,kDAAkD;AAClD,mEAAmE;AACnE,qEAAqE;AACrE,OAAO,EACL,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AAGrC,0BAA0B;AAC1B,OAAO,EACL,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,+BAA+B,CAAC;AAQvC,+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,8BAA8B;AAC9B,2EAA2E;AAC3E,2EAA2E;AAC3E,0EAA0E;AAC1E,wDAAwD;AACxD,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAOlC,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 actions (host route:\n// POST /api/integrations/[slug]/actions/[actionName]). Invokes a partner's\n// declared `actions[name]` handler directly from a widget — deterministic,\n// no assistant round trip. `TInput`/`TOutput` are supplied by the caller;\n// see the module doc comment for the typing discipline.\nexport {\n useIntegrationAction,\n integrationActionUrl,\n IntegrationActionError,\n} from './use-integration-action';\nexport type {\n UseIntegrationActionOptions,\n IntegrationActionErrorCode,\n IntegrationActionErrorDetails,\n} from './use-integration-action';\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,105 @@
1
+ /**
2
+ * The browser half of the widget-actions transport.
3
+ *
4
+ * A partner's `actions` declaration on `IntegrationDefinition` is a
5
+ * record of named, deterministic server handlers —
6
+ * `(ctx, input) => Promise<unknown>` — invoked by a widget's own UI (a Save
7
+ * button), never synthesized by the AI assistant. The host serves one
8
+ * generic route per action:
9
+ *
10
+ * POST /api/integrations/[slug]/actions/[actionName]
11
+ *
12
+ * and this hook is the only place a partner package should build that
13
+ * request, so a route move is one SDK change rather than a hunt through
14
+ * every widget — the same discipline `partnerOAuthConnectPath`/
15
+ * `fetchIntegrationStorage` already apply to their own routes.
16
+ *
17
+ * ```tsx
18
+ * const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(
19
+ * 'acme-crm',
20
+ * 'create-record',
21
+ * { accountId },
22
+ * );
23
+ *
24
+ * <Button onClick={() => createRecord.mutate(formValues)}>Save</Button>
25
+ * ```
26
+ *
27
+ * ── Typing is a convention, not a cross-package mechanism ───────────────────
28
+ *
29
+ * `@ekanos/sdk` is a separately published package — it cannot see a specific
30
+ * partner's `z.ZodTypeAny` instance, since that object lives in the
31
+ * partner's own server bundle and never ships to the browser, let alone into
32
+ * the SDK's own compiled types. `TInput`/`TOutput` are generic parameters
33
+ * the CALLER supplies by hand at the call site — typically by also exporting
34
+ * `type CreateRecordInput = z.infer<typeof createRecordSchema>` from the same
35
+ * package that declares the action and importing it into the widget. This
36
+ * hook treats the wire payload as `unknown` at runtime — it never parses it
37
+ * against the partner's own zod schema, only against the success/error
38
+ * envelope shape — so drift between the declared action and the widget's
39
+ * type import is a partner-package refactor problem, not a network-boundary
40
+ * one. Same discipline `fetchIntegrationStorage` already documents for
41
+ * storage ("Values are returned raw and unvalidated").
42
+ *
43
+ * ── Errors are typed, never raw fetch/transport exceptions ──────────────────
44
+ *
45
+ * A rejected mutation's `error` is always an `IntegrationActionError` —
46
+ * never a bare `Error`, a `Response`, or an unwrapped parse/network
47
+ * exception. `status` and `code` are always present; `issues` (the partner's
48
+ * own zod issues) is present only for the route's 400 `invalid_input`
49
+ * response, so a form can map failures back onto fields without
50
+ * string-matching a message.
51
+ */
52
+ import { type UseMutationResult } from '@tanstack/react-query';
53
+ export interface UseIntegrationActionOptions {
54
+ /** The account the action runs against. Must be one the caller belongs to. */
55
+ accountId: string;
56
+ /**
57
+ * Overrides the base path. Exists for harnesses and tests that serve the
58
+ * route from somewhere other than the host's own origin; production code
59
+ * should leave it alone.
60
+ */
61
+ baseUrl?: string;
62
+ }
63
+ /**
64
+ * Builds the host actions-route URL for one integration/action pair.
65
+ * Exported because a harness or a test double needs to recognize the exact
66
+ * URL this hook will request.
67
+ */
68
+ export declare function integrationActionUrl(integrationSlug: string, actionName: string, options?: {
69
+ baseUrl?: string;
70
+ }): string;
71
+ /**
72
+ * Route-defined failure codes the host actions route answers with, plus two
73
+ * transport-level codes this hook assigns itself when the response never
74
+ * reaches a valid envelope: `'network_error'` (the request itself failed —
75
+ * offline, DNS, aborted) and `'invalid_response'` (a response came back but
76
+ * its body isn't the envelope shape this transport expects).
77
+ */
78
+ export type IntegrationActionErrorCode = 'action_not_found' | 'rate_limited' | 'invalid_input' | 'handler_error' | 'network_error' | 'invalid_response' | (string & {});
79
+ export interface IntegrationActionErrorDetails {
80
+ /** HTTP status; `0` means the request never reached the server. */
81
+ status: number;
82
+ code: IntegrationActionErrorCode;
83
+ /** The partner's own zod issues — present only for a 400 `invalid_input` response. */
84
+ issues?: unknown[];
85
+ }
86
+ /**
87
+ * The typed failure a rejected `useIntegrationAction` mutation always throws.
88
+ * `status`/`code` let a caller branch without string-matching `message`;
89
+ * `issues` lets a form map the partner's own validation failures back onto
90
+ * fields.
91
+ */
92
+ export declare class IntegrationActionError extends Error {
93
+ readonly status: number;
94
+ readonly code: IntegrationActionErrorCode;
95
+ readonly issues?: unknown[];
96
+ constructor(message: string, details: IntegrationActionErrorDetails);
97
+ }
98
+ /**
99
+ * Invokes a declared partner action from a widget. `mutate`/`mutateAsync`
100
+ * take the action's `TInput` directly — the caller's own already-validated
101
+ * values, no assistant and no natural-language round trip in between. A
102
+ * resolved mutation's `data` is `TOutput`; a rejected one's `error` is always
103
+ * an `IntegrationActionError` (see the module doc comment above).
104
+ */
105
+ export declare function useIntegrationAction<TInput = unknown, TOutput = unknown>(integrationSlug: string, actionName: string, options: UseIntegrationActionOptions): UseMutationResult<TOutput, IntegrationActionError, TInput>;
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+ /**
3
+ * The browser half of the widget-actions transport.
4
+ *
5
+ * A partner's `actions` declaration on `IntegrationDefinition` is a
6
+ * record of named, deterministic server handlers —
7
+ * `(ctx, input) => Promise<unknown>` — invoked by a widget's own UI (a Save
8
+ * button), never synthesized by the AI assistant. The host serves one
9
+ * generic route per action:
10
+ *
11
+ * POST /api/integrations/[slug]/actions/[actionName]
12
+ *
13
+ * and this hook is the only place a partner package should build that
14
+ * request, so a route move is one SDK change rather than a hunt through
15
+ * every widget — the same discipline `partnerOAuthConnectPath`/
16
+ * `fetchIntegrationStorage` already apply to their own routes.
17
+ *
18
+ * ```tsx
19
+ * const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(
20
+ * 'acme-crm',
21
+ * 'create-record',
22
+ * { accountId },
23
+ * );
24
+ *
25
+ * <Button onClick={() => createRecord.mutate(formValues)}>Save</Button>
26
+ * ```
27
+ *
28
+ * ── Typing is a convention, not a cross-package mechanism ───────────────────
29
+ *
30
+ * `@ekanos/sdk` is a separately published package — it cannot see a specific
31
+ * partner's `z.ZodTypeAny` instance, since that object lives in the
32
+ * partner's own server bundle and never ships to the browser, let alone into
33
+ * the SDK's own compiled types. `TInput`/`TOutput` are generic parameters
34
+ * the CALLER supplies by hand at the call site — typically by also exporting
35
+ * `type CreateRecordInput = z.infer<typeof createRecordSchema>` from the same
36
+ * package that declares the action and importing it into the widget. This
37
+ * hook treats the wire payload as `unknown` at runtime — it never parses it
38
+ * against the partner's own zod schema, only against the success/error
39
+ * envelope shape — so drift between the declared action and the widget's
40
+ * type import is a partner-package refactor problem, not a network-boundary
41
+ * one. Same discipline `fetchIntegrationStorage` already documents for
42
+ * storage ("Values are returned raw and unvalidated").
43
+ *
44
+ * ── Errors are typed, never raw fetch/transport exceptions ──────────────────
45
+ *
46
+ * A rejected mutation's `error` is always an `IntegrationActionError` —
47
+ * never a bare `Error`, a `Response`, or an unwrapped parse/network
48
+ * exception. `status` and `code` are always present; `issues` (the partner's
49
+ * own zod issues) is present only for the route's 400 `invalid_input`
50
+ * response, so a form can map failures back onto fields without
51
+ * string-matching a message.
52
+ */
53
+ import { useMutation } from '@tanstack/react-query';
54
+ const DEFAULT_BASE_URL = '/api/integrations';
55
+ /**
56
+ * Builds the host actions-route URL for one integration/action pair.
57
+ * Exported because a harness or a test double needs to recognize the exact
58
+ * URL this hook will request.
59
+ */
60
+ export function integrationActionUrl(integrationSlug, actionName, options = {}) {
61
+ var _a;
62
+ const base = (_a = options.baseUrl) !== null && _a !== void 0 ? _a : DEFAULT_BASE_URL;
63
+ return `${base}/${integrationSlug}/actions/${actionName}`;
64
+ }
65
+ /**
66
+ * The typed failure a rejected `useIntegrationAction` mutation always throws.
67
+ * `status`/`code` let a caller branch without string-matching `message`;
68
+ * `issues` lets a form map the partner's own validation failures back onto
69
+ * fields.
70
+ */
71
+ export class IntegrationActionError extends Error {
72
+ constructor(message, details) {
73
+ super(message);
74
+ this.name = 'IntegrationActionError';
75
+ this.status = details.status;
76
+ this.code = details.code;
77
+ this.issues = details.issues;
78
+ }
79
+ }
80
+ function isSuccessEnvelope(value) {
81
+ return (typeof value === 'object' &&
82
+ value !== null &&
83
+ value.success === true &&
84
+ 'data' in value);
85
+ }
86
+ function isErrorEnvelope(value) {
87
+ return (typeof value === 'object' &&
88
+ value !== null &&
89
+ value.success === false &&
90
+ typeof value.error === 'string');
91
+ }
92
+ async function runIntegrationAction(integrationSlug, actionName, input, options) {
93
+ var _a;
94
+ const url = integrationActionUrl(integrationSlug, actionName, options);
95
+ let response;
96
+ try {
97
+ response = await globalThis.fetch(url, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'content-type': 'application/json',
101
+ accept: 'application/json',
102
+ },
103
+ body: JSON.stringify({ accountId: options.accountId, input }),
104
+ });
105
+ }
106
+ catch (error) {
107
+ throw new IntegrationActionError(`Could not reach '${integrationSlug}/${actionName}' (${error instanceof Error ? error.message : 'network error'}).`, { status: 0, code: 'network_error' });
108
+ }
109
+ const payload = await response.json().catch(() => null);
110
+ if (response.ok && isSuccessEnvelope(payload)) {
111
+ return payload.data;
112
+ }
113
+ if (isErrorEnvelope(payload)) {
114
+ throw new IntegrationActionError(payload.error, {
115
+ status: response.status,
116
+ code: (_a = payload.code) !== null && _a !== void 0 ? _a : 'handler_error',
117
+ issues: payload.issues,
118
+ });
119
+ }
120
+ throw new IntegrationActionError(`Could not run '${integrationSlug}/${actionName}' (${response.status} ${response.statusText}).`, { status: response.status, code: 'invalid_response' });
121
+ }
122
+ /**
123
+ * Invokes a declared partner action from a widget. `mutate`/`mutateAsync`
124
+ * take the action's `TInput` directly — the caller's own already-validated
125
+ * values, no assistant and no natural-language round trip in between. A
126
+ * resolved mutation's `data` is `TOutput`; a rejected one's `error` is always
127
+ * an `IntegrationActionError` (see the module doc comment above).
128
+ */
129
+ export function useIntegrationAction(integrationSlug, actionName, options) {
130
+ return useMutation({
131
+ mutationFn: (input) => runIntegrationAction(integrationSlug, actionName, input, options),
132
+ });
133
+ }
134
+ //# sourceMappingURL=use-integration-action.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-integration-action.js","sourceRoot":"","sources":["../../src/hooks/use-integration-action.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\n/**\n * The browser half of the widget-actions transport.\n *\n * A partner's `actions` declaration on `IntegrationDefinition` is a\n * record of named, deterministic server handlers —\n * `(ctx, input) => Promise<unknown>` — invoked by a widget's own UI (a Save\n * button), never synthesized by the AI assistant. The host serves one\n * generic route per action:\n *\n * POST /api/integrations/[slug]/actions/[actionName]\n *\n * and this hook is the only place a partner package should build that\n * request, so a route move is one SDK change rather than a hunt through\n * every widget — the same discipline `partnerOAuthConnectPath`/\n * `fetchIntegrationStorage` already apply to their own routes.\n *\n * ```tsx\n * const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(\n * 'acme-crm',\n * 'create-record',\n * { accountId },\n * );\n *\n * <Button onClick={() => createRecord.mutate(formValues)}>Save</Button>\n * ```\n *\n * ── Typing is a convention, not a cross-package mechanism ───────────────────\n *\n * `@ekanos/sdk` is a separately published package — it cannot see a specific\n * partner's `z.ZodTypeAny` instance, since that object lives in the\n * partner's own server bundle and never ships to the browser, let alone into\n * the SDK's own compiled types. `TInput`/`TOutput` are generic parameters\n * the CALLER supplies by hand at the call site — typically by also exporting\n * `type CreateRecordInput = z.infer<typeof createRecordSchema>` from the same\n * package that declares the action and importing it into the widget. This\n * hook treats the wire payload as `unknown` at runtime — it never parses it\n * against the partner's own zod schema, only against the success/error\n * envelope shape — so drift between the declared action and the widget's\n * type import is a partner-package refactor problem, not a network-boundary\n * one. Same discipline `fetchIntegrationStorage` already documents for\n * storage (\"Values are returned raw and unvalidated\").\n *\n * ── Errors are typed, never raw fetch/transport exceptions ──────────────────\n *\n * A rejected mutation's `error` is always an `IntegrationActionError` —\n * never a bare `Error`, a `Response`, or an unwrapped parse/network\n * exception. `status` and `code` are always present; `issues` (the partner's\n * own zod issues) is present only for the route's 400 `invalid_input`\n * response, so a form can map failures back onto fields without\n * string-matching a message.\n */\n\nimport { useMutation, type UseMutationResult } from '@tanstack/react-query';\n\nconst DEFAULT_BASE_URL = '/api/integrations';\n\nexport interface UseIntegrationActionOptions {\n /** The account the action runs against. Must be one the caller belongs to. */\n accountId: string;\n /**\n * Overrides the base path. Exists for harnesses and tests that serve the\n * route from somewhere other than the host's own origin; production code\n * should leave it alone.\n */\n baseUrl?: string;\n}\n\n/**\n * Builds the host actions-route URL for one integration/action pair.\n * Exported because a harness or a test double needs to recognize the exact\n * URL this hook will request.\n */\nexport function integrationActionUrl(\n integrationSlug: string,\n actionName: string,\n options: { baseUrl?: string } = {},\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n return `${base}/${integrationSlug}/actions/${actionName}`;\n}\n\n/**\n * Route-defined failure codes the host actions route answers with, plus two\n * transport-level codes this hook assigns itself when the response never\n * reaches a valid envelope: `'network_error'` (the request itself failed —\n * offline, DNS, aborted) and `'invalid_response'` (a response came back but\n * its body isn't the envelope shape this transport expects).\n */\nexport type IntegrationActionErrorCode =\n | 'action_not_found'\n | 'rate_limited'\n | 'invalid_input'\n | 'handler_error'\n | 'network_error'\n | 'invalid_response'\n | (string & {});\n\nexport interface IntegrationActionErrorDetails {\n /** HTTP status; `0` means the request never reached the server. */\n status: number;\n code: IntegrationActionErrorCode;\n /** The partner's own zod issues — present only for a 400 `invalid_input` response. */\n issues?: unknown[];\n}\n\n/**\n * The typed failure a rejected `useIntegrationAction` mutation always throws.\n * `status`/`code` let a caller branch without string-matching `message`;\n * `issues` lets a form map the partner's own validation failures back onto\n * fields.\n */\nexport class IntegrationActionError extends Error {\n readonly status: number;\n readonly code: IntegrationActionErrorCode;\n readonly issues?: unknown[];\n\n constructor(message: string, details: IntegrationActionErrorDetails) {\n super(message);\n this.name = 'IntegrationActionError';\n this.status = details.status;\n this.code = details.code;\n this.issues = details.issues;\n }\n}\n\ninterface ActionSuccessEnvelope {\n success: true;\n data: unknown;\n}\n\ninterface ActionErrorEnvelope {\n success: false;\n error: string;\n code?: string;\n issues?: unknown[];\n}\n\nfunction isSuccessEnvelope(value: unknown): value is ActionSuccessEnvelope {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { success?: unknown }).success === true &&\n 'data' in value\n );\n}\n\nfunction isErrorEnvelope(value: unknown): value is ActionErrorEnvelope {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { success?: unknown }).success === false &&\n typeof (value as { error?: unknown }).error === 'string'\n );\n}\n\nasync function runIntegrationAction<TInput, TOutput>(\n integrationSlug: string,\n actionName: string,\n input: TInput,\n options: UseIntegrationActionOptions,\n): Promise<TOutput> {\n const url = integrationActionUrl(integrationSlug, actionName, options);\n\n let response: Response;\n try {\n response = await globalThis.fetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n body: JSON.stringify({ accountId: options.accountId, input }),\n });\n } catch (error) {\n throw new IntegrationActionError(\n `Could not reach '${integrationSlug}/${actionName}' (${\n error instanceof Error ? error.message : 'network error'\n }).`,\n { status: 0, code: 'network_error' },\n );\n }\n\n const payload: unknown = await response.json().catch(() => null);\n\n if (response.ok && isSuccessEnvelope(payload)) {\n return payload.data as TOutput;\n }\n\n if (isErrorEnvelope(payload)) {\n throw new IntegrationActionError(payload.error, {\n status: response.status,\n code: payload.code ?? 'handler_error',\n issues: payload.issues,\n });\n }\n\n throw new IntegrationActionError(\n `Could not run '${integrationSlug}/${actionName}' (${response.status} ${response.statusText}).`,\n { status: response.status, code: 'invalid_response' },\n );\n}\n\n/**\n * Invokes a declared partner action from a widget. `mutate`/`mutateAsync`\n * take the action's `TInput` directly — the caller's own already-validated\n * values, no assistant and no natural-language round trip in between. A\n * resolved mutation's `data` is `TOutput`; a rejected one's `error` is always\n * an `IntegrationActionError` (see the module doc comment above).\n */\nexport function useIntegrationAction<TInput = unknown, TOutput = unknown>(\n integrationSlug: string,\n actionName: string,\n options: UseIntegrationActionOptions,\n): UseMutationResult<TOutput, IntegrationActionError, TInput> {\n return useMutation({\n mutationFn: (input: TInput) =>\n runIntegrationAction<TInput, TOutput>(\n integrationSlug,\n actionName,\n input,\n options,\n ),\n });\n}\n"]}
@@ -16,5 +16,6 @@
16
16
  */
17
17
  export { defineIntegration } from './define-integration.js';
18
18
  export { parseCronExpression } from './cron.js';
19
+ export { PublicActionError } from '../errors/public-action-error.js';
19
20
  export { IntegrationDefinitionSchema, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
20
21
  export type { IntegrationDefinition, IntegrationComponentDeclarations, IntegrationProposals, IntegrationCapabilityDeclaration, IntegrationPermissionDeclaration, PartnerWidgetDeclaration, PartnerToolModule, PartnerToolParameters, ToolClassificationProposal, DefinitionCollisionInput, FirstPartyInventory, PartnerWebhookDeclaration, WebhookSignatureDeclaration, WebhookEvent, WebhookResult, PartnerScheduleDeclaration, ScheduleInvocation, ScheduleResult, PartnerOAuthDeclaration, OAuthProviderDeclaration, OAuthTokens, OnActivateHandler, } from '@ekanos/integration-schema';
@@ -19,5 +19,9 @@ export { defineIntegration } from './define-integration.js';
19
19
  // `defineIntegration()`'s schedule validation, exported so validators and
20
20
  // the harness can check/describe an expression the same way the SDK does.
21
21
  export { parseCronExpression } from './cron.js';
22
+ // The one escape hatch from the actions host route's opaque-failure
23
+ // discipline — see the class's own doc comment
24
+ // (the widget-actions opaque-failure ruling).
25
+ export { PublicActionError } from '../errors/public-action-error.js';
22
26
  export { IntegrationDefinitionSchema, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
23
27
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/integration/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,mEAAmE;AACnE,0EAA0E;AAC1E,0EAA0E;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAE7C,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,4BAA4B,CAAC","sourcesContent":["/**\n * @ekanos/sdk/integration — the partner authoring contract.\n *\n * `defineIntegration()` is THE way a partner declares an integration\n * Partners never extend a base class.\n * The host adapts the returned definition internally via\n * `registerPartnerIntegration` in `@kit/integrations-core`, which re-parses\n * against the SAME canonical schema (`@ekanos/integration-schema`) at the\n * trust boundary — one contract, no structural twin (F3/F9).\n *\n * Dependency-pure and isomorphic: zod plus the schema package, no `@kit/*`,\n * no `server-only`.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { defineIntegration } from './define-integration';\n\n// Event-surface validation values: the 5-field cron parser backing\n// `defineIntegration()`'s schedule validation, exported so validators and\n// the harness can check/describe an expression the same way the SDK does.\nexport { parseCronExpression } from './cron';\n\nexport {\n IntegrationDefinitionSchema,\n validateIntegrationDefinitions,\n} from '@ekanos/integration-schema';\n\nexport type {\n IntegrationDefinition,\n IntegrationComponentDeclarations,\n IntegrationProposals,\n IntegrationCapabilityDeclaration,\n IntegrationPermissionDeclaration,\n PartnerWidgetDeclaration,\n PartnerToolModule,\n PartnerToolParameters,\n ToolClassificationProposal,\n DefinitionCollisionInput,\n FirstPartyInventory,\n // Event surfaces (webhooks, schedules, OAuth): declared in the definition,\n // executed locally by the harness/testing helpers today; the host's real\n // transports (public ingress, scheduler, hosted OAuth callback) bind to\n // these exact declarations later with no partner code change.\n PartnerWebhookDeclaration,\n WebhookSignatureDeclaration,\n WebhookEvent,\n WebhookResult,\n PartnerScheduleDeclaration,\n ScheduleInvocation,\n ScheduleResult,\n PartnerOAuthDeclaration,\n OAuthProviderDeclaration,\n OAuthTokens,\n // Activation lifecycle hook: cache seeding / eager validation at connect\n // time. Runs after first persist and after every activationData update;\n // v1 errors are non-fatal (logged as a warning, activation stays active).\n OnActivateHandler,\n} from '@ekanos/integration-schema';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/integration/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,mEAAmE;AACnE,0EAA0E;AAC1E,0EAA0E;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAE7C,oEAAoE;AACpE,+CAA+C;AAC/C,8CAA8C;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,4BAA4B,CAAC","sourcesContent":["/**\n * @ekanos/sdk/integration — the partner authoring contract.\n *\n * `defineIntegration()` is THE way a partner declares an integration\n * Partners never extend a base class.\n * The host adapts the returned definition internally via\n * `registerPartnerIntegration` in `@kit/integrations-core`, which re-parses\n * against the SAME canonical schema (`@ekanos/integration-schema`) at the\n * trust boundary — one contract, no structural twin (F3/F9).\n *\n * Dependency-pure and isomorphic: zod plus the schema package, no `@kit/*`,\n * no `server-only`.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { defineIntegration } from './define-integration';\n\n// Event-surface validation values: the 5-field cron parser backing\n// `defineIntegration()`'s schedule validation, exported so validators and\n// the harness can check/describe an expression the same way the SDK does.\nexport { parseCronExpression } from './cron';\n\n// The one escape hatch from the actions host route's opaque-failure\n// discipline — see the class's own doc comment\n// (the widget-actions opaque-failure ruling).\nexport { PublicActionError } from '../errors/public-action-error';\n\nexport {\n IntegrationDefinitionSchema,\n validateIntegrationDefinitions,\n} from '@ekanos/integration-schema';\n\nexport type {\n IntegrationDefinition,\n IntegrationComponentDeclarations,\n IntegrationProposals,\n IntegrationCapabilityDeclaration,\n IntegrationPermissionDeclaration,\n PartnerWidgetDeclaration,\n PartnerToolModule,\n PartnerToolParameters,\n ToolClassificationProposal,\n DefinitionCollisionInput,\n FirstPartyInventory,\n // Event surfaces (webhooks, schedules, OAuth): declared in the definition,\n // executed locally by the harness/testing helpers today; the host's real\n // transports (public ingress, scheduler, hosted OAuth callback) bind to\n // these exact declarations later with no partner code change.\n PartnerWebhookDeclaration,\n WebhookSignatureDeclaration,\n WebhookEvent,\n WebhookResult,\n PartnerScheduleDeclaration,\n ScheduleInvocation,\n ScheduleResult,\n PartnerOAuthDeclaration,\n OAuthProviderDeclaration,\n OAuthTokens,\n // Activation lifecycle hook: cache seeding / eager validation at connect\n // time. Runs after first persist and after every activationData update;\n // v1 errors are non-fatal (logged as a warning, activation stays active).\n OnActivateHandler,\n} from '@ekanos/integration-schema';\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The fetch-fixture matcher shared by `createMockContext`'s `ctx.fetch` stub
3
+ * and `createMockFetchProvider`'s browser-side stub (extracted from
4
+ * `mock-context.ts`, which used to inline this) — one string-prefix/RegExp
5
+ * matching implementation, so a widget test and a handler test written
6
+ * against the same fixture describe the same vendor behavior once instead of
7
+ * two copies drifting apart.
8
+ */
9
+ export interface MockFetchHandler {
10
+ /** A string matches URLs that start with it; a RegExp is tested against the full URL. */
11
+ match: string | RegExp;
12
+ respond: (request: Request) => Response | Promise<Response>;
13
+ }
14
+ /**
15
+ * Returns the first handler whose `match` matches `url`, or `undefined` when
16
+ * none does — callers fall back to their own default response.
17
+ */
18
+ export declare function matchFetchHandler(url: string, handlers: readonly MockFetchHandler[]): MockFetchHandler | undefined;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The fetch-fixture matcher shared by `createMockContext`'s `ctx.fetch` stub
3
+ * and `createMockFetchProvider`'s browser-side stub (extracted from
4
+ * `mock-context.ts`, which used to inline this) — one string-prefix/RegExp
5
+ * matching implementation, so a widget test and a handler test written
6
+ * against the same fixture describe the same vendor behavior once instead of
7
+ * two copies drifting apart.
8
+ */
9
+ /**
10
+ * Returns the first handler whose `match` matches `url`, or `undefined` when
11
+ * none does — callers fall back to their own default response.
12
+ */
13
+ export function matchFetchHandler(url, handlers) {
14
+ return handlers.find((candidate) => typeof candidate.match === 'string'
15
+ ? url.startsWith(candidate.match)
16
+ : candidate.match.test(url));
17
+ }
18
+ //# sourceMappingURL=fetch-matching.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-matching.js","sourceRoot":"","sources":["../../src/testing/fetch-matching.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,GAAW,EACX,QAAqC;IAErC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CACjC,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ;QACjC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC;QACjC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAC9B,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The fetch-fixture matcher shared by `createMockContext`'s `ctx.fetch` stub\n * and `createMockFetchProvider`'s browser-side stub (extracted from\n * `mock-context.ts`, which used to inline this) — one string-prefix/RegExp\n * matching implementation, so a widget test and a handler test written\n * against the same fixture describe the same vendor behavior once instead of\n * two copies drifting apart.\n */\n\nexport interface MockFetchHandler {\n /** A string matches URLs that start with it; a RegExp is tested against the full URL. */\n match: string | RegExp;\n respond: (request: Request) => Response | Promise<Response>;\n}\n\n/**\n * Returns the first handler whose `match` matches `url`, or `undefined` when\n * none does — callers fall back to their own default response.\n */\nexport function matchFetchHandler(\n url: string,\n handlers: readonly MockFetchHandler[],\n): MockFetchHandler | undefined {\n return handlers.find((candidate) =>\n typeof candidate.match === 'string'\n ? url.startsWith(candidate.match)\n : candidate.match.test(url),\n );\n}\n"]}
@@ -11,6 +11,8 @@
11
11
  * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.
12
12
  */
13
13
  export { createMockContext } from './mock-context.js';
14
- export type { MockContextOptions, MockFetchHandler, MockIntegrationContext, RecordedFetchCall, RecordedLog, } from './mock-context.js';
14
+ export type { MockContextOptions, MockIntegrationContext, RecordedFetchCall, RecordedLog, } from './mock-context.js';
15
+ export type { MockFetchHandler } from './fetch-matching.js';
16
+ export { createMockFetchProvider } from './mock-fetch-provider.js';
15
17
  export { invokeWebhook, invokeSchedule, invokeActivate } from './invoke.js';
16
18
  export type { DefinitionContextOptions, InvokeWebhookOptions, InvokeScheduleOptions, InvokeActivateOptions, WebhookInvocationOutcome, ScheduleInvocationOutcome, ActivateInvocationOutcome, } from './invoke.js';
@@ -11,6 +11,11 @@
11
11
  * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.
12
12
  */
13
13
  export { createMockContext } from './mock-context.js';
14
+ // The browser-side counterpart to `createMockContext`'s `fetchHandlers`: a
15
+ // mounted `IntegrationFetchProvider` for widget tests that call
16
+ // `useIntegrationFetch()`, resolving fixtures against the same
17
+ // `MockFetchHandler` shape and matching semantics.
18
+ export { createMockFetchProvider } from './mock-fetch-provider.js';
14
19
  // Event-surface invocation: run declared webhook/schedule handlers locally,
15
20
  // with the same payload-validation and context-derivation semantics the real
16
21
  // transports will have. Used by partner unit tests and the harness Triggers
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AASnD,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC","sourcesContent":["/**\n * @ekanos/sdk/testing — the partner test harness surface.\n *\n * `createMockContext()` is the day-1 in-memory `IntegrationContext`\n * (proposal §4): partners code and test against `ctx` here before anything\n * lands in a sandbox. Self-contained — no Supabase, no Vault, no network —\n * and enforcement (storage schemas, secret tiers, egress) is the same\n * shared implementation from `@ekanos/sdk/context` that production uses.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { createMockContext } from './mock-context';\nexport type {\n MockContextOptions,\n MockFetchHandler,\n MockIntegrationContext,\n RecordedFetchCall,\n RecordedLog,\n} from './mock-context';\n\n// Event-surface invocation: run declared webhook/schedule handlers locally,\n// with the same payload-validation and context-derivation semantics the real\n// transports will have. Used by partner unit tests and the harness Triggers\n// panel alike. `invokeActivate` does the same for the `onActivate` lifecycle\n// hook, used by partner unit tests and the harness's activation surface.\nexport { invokeWebhook, invokeSchedule, invokeActivate } from './invoke';\nexport type {\n DefinitionContextOptions,\n InvokeWebhookOptions,\n InvokeScheduleOptions,\n InvokeActivateOptions,\n WebhookInvocationOutcome,\n ScheduleInvocationOutcome,\n ActivateInvocationOutcome,\n} from './invoke';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAYnD,2EAA2E;AAC3E,gEAAgE;AAChE,+DAA+D;AAC/D,mDAAmD;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC","sourcesContent":["/**\n * @ekanos/sdk/testing — the partner test harness surface.\n *\n * `createMockContext()` is the day-1 in-memory `IntegrationContext`\n * (proposal §4): partners code and test against `ctx` here before anything\n * lands in a sandbox. Self-contained — no Supabase, no Vault, no network —\n * and enforcement (storage schemas, secret tiers, egress) is the same\n * shared implementation from `@ekanos/sdk/context` that production uses.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { createMockContext } from './mock-context';\nexport type {\n MockContextOptions,\n MockIntegrationContext,\n RecordedFetchCall,\n RecordedLog,\n} from './mock-context';\n\n// Shared by `createMockContext`'s `ctx.fetch` stub and `createMockFetchProvider`\n// below — one fixture shape, one matching implementation, for both surfaces.\nexport type { MockFetchHandler } from './fetch-matching';\n\n// The browser-side counterpart to `createMockContext`'s `fetchHandlers`: a\n// mounted `IntegrationFetchProvider` for widget tests that call\n// `useIntegrationFetch()`, resolving fixtures against the same\n// `MockFetchHandler` shape and matching semantics.\nexport { createMockFetchProvider } from './mock-fetch-provider';\n\n// Event-surface invocation: run declared webhook/schedule handlers locally,\n// with the same payload-validation and context-derivation semantics the real\n// transports will have. Used by partner unit tests and the harness Triggers\n// panel alike. `invokeActivate` does the same for the `onActivate` lifecycle\n// hook, used by partner unit tests and the harness's activation surface.\nexport { invokeWebhook, invokeSchedule, invokeActivate } from './invoke';\nexport type {\n DefinitionContextOptions,\n InvokeWebhookOptions,\n InvokeScheduleOptions,\n InvokeActivateOptions,\n WebhookInvocationOutcome,\n ScheduleInvocationOutcome,\n ActivateInvocationOutcome,\n} from './invoke';\n"]}
@@ -9,14 +9,11 @@
9
9
  */
10
10
  import type { z } from 'zod';
11
11
  import type { IntegrationActor, IntegrationContext, StorageKeySchema, StorageSchemaMap, StorageSchemas } from '../context/types.js';
12
+ import { type MockFetchHandler } from './fetch-matching.js';
12
13
  type ScopeSeed<M> = M extends StorageSchemaMap ? {
13
14
  [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>>;
14
15
  } : Record<never, never>;
15
- export interface MockFetchHandler {
16
- /** A string matches URLs that start with it; a RegExp is tested against the full URL. */
17
- match: string | RegExp;
18
- respond: (request: Request) => Response | Promise<Response>;
19
- }
16
+ export type { MockFetchHandler } from './fetch-matching.js';
20
17
  export interface MockContextOptions<Schemas extends StorageSchemas = StorageSchemas> {
21
18
  accountId?: string;
22
19
  accountSlug?: string | null;
@@ -76,4 +73,3 @@ export interface MockIntegrationContext<Schemas extends StorageSchemas = Storage
76
73
  };
77
74
  }
78
75
  export declare function createMockContext<Schemas extends StorageSchemas = StorageSchemas>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas>;
79
- export {};
@@ -1,6 +1,7 @@
1
1
  import { isEgressAllowed } from '../context/egress.js';
2
2
  import { EgressDeniedError, SecretAccessError } from '../context/errors.js';
3
3
  import { assertStorageKeyDeclared, assertStorageKeyWritable, parseStorageValue, } from '../context/storage-validation.js';
4
+ import { matchFetchHandler } from './fetch-matching.js';
4
5
  function isExpired(row) {
5
6
  return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();
6
7
  }
@@ -160,9 +161,7 @@ export function createMockContext(options) {
160
161
  if (!allowed) {
161
162
  throw new EgressDeniedError(new URL(url).origin);
162
163
  }
163
- const handler = fetchHandlers.find((candidate) => typeof candidate.match === 'string'
164
- ? url.startsWith(candidate.match)
165
- : candidate.match.test(url));
164
+ const handler = matchFetchHandler(url, fetchHandlers);
166
165
  if (handler) {
167
166
  return Promise.resolve(handler.respond(new Request(url, init)));
168
167
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"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"]}
@@ -94,10 +94,9 @@ export interface IntegrationMetadata {
94
94
  * This is deliberately distinct from "has no CUSTOM activation form": an
95
95
  * integration that omits `getActivationForm()` still gets the generic one, and
96
96
  * six integrations depend on that. Use this flag only when submitting ANY
97
- * activation form would be a dead end (e.g. Workflows, which is turned on by
98
- * the `source_configs.enable_workflows` feature flag and whose `activate()`
99
- * throws 'not implemented' — showing a form there hands the user a button that
100
- * cannot work). Undefined/true = normal self-serve activation.
97
+ * activation form would be a dead end — where `activate()` cannot succeed, so
98
+ * showing a form hands the user a button that cannot work.
99
+ * Undefined/true = normal self-serve activation.
101
100
  */
102
101
  selfServeActivation?: boolean;
103
102
  /**
@@ -147,6 +146,19 @@ export interface IntegrationMetadata {
147
146
  * fails the build on either violation.
148
147
  */
149
148
  kind?: 'integration' | 'feature';
149
+ /**
150
+ * Product slugs that MUST be attached to the same Source for this product to
151
+ * function — a hard dependency, not a suggestion.
152
+ *
153
+ * The host REFUSES to attach this product to a Source missing any of them,
154
+ * and refuses a detach that would strand a dependent. Admin surfaces render
155
+ * the dependency in both directions ("Requires X" / "Enables Y").
156
+ *
157
+ * Host CI enforces: every slug is registered, nothing requires itself, no
158
+ * cycles, and a `kind: 'feature'` may require an `'integration'` but never
159
+ * the reverse.
160
+ */
161
+ requires?: string[];
150
162
  }
151
163
  export interface WidgetConfig {
152
164
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"integration.js","sourceRoot":"","sources":["../../src/types/integration.ts"],"names":[],"mappings":"","sourcesContent":["import type { ComponentType } from 'react';\n\nimport type { IconRenderer } from './icon';\n\n// Capability shown on the integration detail page (numbered grid)\nexport interface IntegrationCapability {\n label: string;\n description: string;\n icon?: IconRenderer;\n}\n\n// Permission scope shown on the integration detail page\nexport interface IntegrationPermission {\n label: string;\n detail: string;\n type: 'read' | 'write';\n}\n\n// ---- Integration Health Monitoring (issue #474) -----------------------------\n\n// Higher = healthier. Used internally for \"least healthy wins\" merging.\nexport type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';\n\n/**\n * Result returned by `BaseIntegration.healthCheck(accountId)`. Routed through\n * `classifyHealth()` (server-side) before writing to\n * `integration_health_status`.\n */\nexport interface HealthCheckResult {\n status: HealthStatus;\n errorCode?: string;\n errorMessage?: string;\n /** Rolling 24h error rate from `account_product_usage_events`, 0..100. */\n errorRatePct?: number;\n /** Sample size used to compute errorRatePct; if below threshold, treated as unknown. */\n sampleSize?: number;\n /** Whether the most recent observed call succeeded. */\n lastObservationSucceeded?: boolean;\n /** Token/credential expiry, if known. Drives token-expiry warnings. */\n tokenExpiresAt?: string;\n /** Free-form per-integration data carried to the dashboard. */\n metadata?: Record<string, unknown>;\n}\n\n// Activation result from integration\nexport interface ActivationResult {\n success: boolean;\n configData?: Record<string, unknown>;\n externalId?: string;\n errorMessage?: string;\n metadata?: Record<string, unknown>;\n}\n\n// Base props that all integration components need\ninterface BaseIntegrationComponentProps {\n accountId: string;\n sourceId?: string;\n integrationConfig?: Record<string, unknown>;\n}\n\n// Discriminated union for product reference - only one allowed\ntype ProductReference =\n | { productId: string; productSlug?: never }\n | { productSlug: string; productId?: never };\n\n// New base type using discriminated union\nexport type IntegrationComponentProps = BaseIntegrationComponentProps &\n ProductReference;\n\n// Marketplace tile props\nexport type MarketplaceTileProps = IntegrationComponentProps & {\n onActivate?: () => void;\n isActivated?: boolean;\n /** Navigation URL for the integration detail page */\n href?: string;\n};\n\n// Activation form props\nexport type ActivationFormProps = IntegrationComponentProps & {\n onSuccess?: (result: ActivationResult) => void;\n onCancel?: () => void;\n isLoading?: boolean;\n /** Render inline instead of in a dialog */\n inline?: boolean;\n /**\n * The TEAM account context the user is activating within — separate from\n * `accountId`, which for `scope: 'user'` integrations is collapsed to the\n * personal account for activation storage/token purposes. Only consumed by\n * workspace materialization (`materializeWorkspaceOnActivation`), never for\n * activation/token storage itself (plan 009 Part A2). Optional: undefined\n * for team-scoped integrations (their `accountId` already IS the team) and\n * for any caller that hasn't threaded it yet — the server action falls back\n * to `accountId` when absent.\n */\n teamAccountId?: string;\n};\n\n// Integration metadata\nexport interface IntegrationMetadata {\n productId: string;\n productSlug: string;\n integrationName: string;\n description: string;\n category: string;\n version: string;\n icon?: string;\n /** Bundled logo URL — overrides product.logo_path on the marketplace */\n logoPath?: string;\n /** When 'user', activation data is stored under the user's personal account (user.id) instead of the team account */\n scope?: 'user' | 'team';\n /**\n * Set FALSE to assert this integration has NO self-serve activation surface —\n * the marketplace detail page then renders no activation form at all.\n *\n * This is deliberately distinct from \"has no CUSTOM activation form\": an\n * integration that omits `getActivationForm()` still gets the generic one, and\n * six integrations depend on that. Use this flag only when submitting ANY\n * activation form would be a dead end (e.g. Workflows, which is turned on by\n * the `source_configs.enable_workflows` feature flag and whose `activate()`\n * throws 'not implemented' — showing a form there hands the user a button that\n * cannot work). Undefined/true = normal self-serve activation.\n */\n selfServeActivation?: boolean;\n /**\n * How this integration's MCP tools resolve credentials — the integration's\n * SELF-DECLARATION of machine-executability, consumed dynamically by the\n * workflow engine's fail-closed tool allowlist (isGatewayExposableTool).\n *\n * 'account' — credentials/identity resolve from team- or source-level\n * config via the ToolContext (`getAccountId(ctx)` + `ctx.supabase`).\n * Safe for durable machine-principal execution (workflows): eligible\n * tools (read + public/internal sensitivity) become workflow-callable.\n * 'user' — credentials or identity are personal (ctx.user.id, personal\n * OAuth, session reads like getSupabaseServerClient()/requireUser()).\n * NEVER machine-executable: a workflow would silently ride one\n * employee's personal grant.\n * 'source' — platform-level credentials (diagnostics-class integrations).\n * Not exposed to account workflows.\n *\n * UNDECLARED = fail closed (treated as not machine-executable). Declaring\n * 'account' is a security assertion: the integration's MCP run() handlers\n * must use ctx-based resolution ONLY — no session clients, no personal\n * grants. A guard test in @kit/integrations-workflows enforces the obvious\n * violations (session-client reads in mcp/ code of 'account' declarers).\n */\n credentialModel?: 'account' | 'user' | 'source';\n /**\n * What KIND of thing this integration is — the platform's presentation and\n * governance discriminant. Undefined means `'integration'`, which is what\n * every third-party connector is; the field only ever needs setting to opt\n * INTO feature treatment.\n *\n * 'integration' — connects Fusion to a third party. Has credentials, an\n * activation flow, and a marketplace presence. Its widgets live on\n * workspace dashboards and it declares its workspaces via\n * `getWorkspaceTargets()`.\n * 'feature' — a Fusion-native capability with a bespoke experience: no\n * third-party credentials, its own route tree under\n * `/home/[account]/…`, and its own sidebar section declared via\n * `getSidebarNav()` (optionally rendered by its own\n * `getSidebarComponent()`).\n *\n * The two are mutually exclusive on the nav question. A `'feature'` MUST\n * declare `getSidebarNav()` and MUST NOT declare `getWorkspaceTargets()` —\n * its widgets are placed onto whatever workspace an admin chooses in the\n * composer (`/admin/sources/[id]/layout`) rather than onto a workspace\n * named after itself. A host-side test\n * fails the build on either violation.\n */\n kind?: 'integration' | 'feature';\n}\n\n// Widget configuration\nexport interface WidgetConfig {\n id: string;\n name: string;\n component: ComponentType<IntegrationComponentProps>;\n gridSize?: { cols: number; rows: number } | { cols: number; rows: number }[];\n gridPosition?: { col: number; row: number };\n widgetState: 'active' | 'inactive' | 'disabled';\n // React-grid-layout format\n layouts?: {\n lg?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n md?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n sm?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n };\n // Integration metadata for widgets\n integrationMetadata?: IntegrationMetadata;\n category?: { id: string; name: string; slug: string; icon: string | null };\n // Database product ID (UUID) from widget_config table\n productId?: string;\n isCollapsible?: boolean;\n // For collapsed state persistence\n collapsed?: boolean;\n widgetConfigId?: string;\n // Resolved workspace placement (explicit widget_config.workspace_id\n // override, else the product's inheriting attachment). Populated by the\n // dashboard widgets loader; null = no workspace (unscoped dashboard).\n workspaceId?: string | null;\n isPinnable?: boolean;\n isPinned?: boolean;\n // When true, the dashboard renders the \"Ask about this\" AI footer bar for\n // this widget. The widget may still pass `askContext` to the wrapper for a\n // rich snapshot; otherwise a minimal default is used.\n aiFooterEnabled?: boolean;\n // Integration health for this widget's product. `unhealthy` downgrades the\n // widget to `disabled`; `degraded` surfaces a stale-data footer.\n health?: {\n status: HealthStatus;\n lastSuccessAt?: string | null;\n errorMessage?: string | null;\n };\n}\n\n// Database types for integration tables.\n//\n// PARTNER-VISIBLE SUBSET: these row types deliberately OMIT `vault_secret_id`.\n// A vault id is a host handle to a decryptable secret; the partner surface\n// never exposes one — `ctx.secrets` fences it off. Host code keeps the full\n// row because it needs the handle; a parity test pins the SDK type as the\n// host's row minus exactly the host-only fields, so a new column cannot drift\n// in here unnoticed.\nexport interface IntegrationConfig {\n id: string;\n account_id: string | null;\n product_id: string;\n config_type: 'api_credentials' | 'activation_data' | 'settings';\n config_data: Record<string, unknown>;\n status: 'active' | 'inactive' | 'pending' | 'error' | 'suspended';\n is_active: boolean;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n\n/**\n * The PARTNER-VISIBLE `data_type` values on `account_product_data` — a strict\n * SUBSET of `@kit/integrations-core`'s union.\n *\n * `'secret'` is deliberately absent. Secret rows are host-managed (one row per\n * secret name, value in Vault, `vault_secret_id` on the row), written only by\n * the capability context's `persistSecret` and read only through\n * `ctx.secrets`. Naming the kind here would advertise a row shape partner code\n * must never address: `ctx.storage` rejects it, and core's generic accessors\n * reject it too (`config-utils.ts`, `assertNotHostOnlyDataType`) precisely\n * because a write there would land the plaintext in the `data` column.\n *\n * When you add an integration-specific kind, add it in BOTH places (core owns\n * the full union) and keep this one partner-appropriate. Prefer `dataSubtype`\n * for orthogonal slicing within a kind (e.g. `data_type='cache'`,\n * `data_subtype='profile'`); reserve a new top-level kind for data that has a\n * distinct lifecycle or is queried independently.\n */\nexport type AccountProductDataType =\n | 'activation'\n | 'settings'\n | 'metrics_summary'\n | 'sync_state'\n | 'cache'\n // Integration-specific kinds — extend here when needed.\n | 'cash_forecast' // quickbooks — user-supplied manual cash-forecast estimates\n | 'tax_config'; // quickbooks — per-user federal/state tax rate config\n\nexport interface AccountProductData {\n id: string;\n account_id: string;\n product_id: string;\n data_type: AccountProductDataType;\n data_subtype?: string;\n data: Record<string, unknown>;\n expires_at?: string;\n integration_config_id?: string;\n external_id?: string;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n"]}
1
+ {"version":3,"file":"integration.js","sourceRoot":"","sources":["../../src/types/integration.ts"],"names":[],"mappings":"","sourcesContent":["import type { ComponentType } from 'react';\n\nimport type { IconRenderer } from './icon';\n\n// Capability shown on the integration detail page (numbered grid)\nexport interface IntegrationCapability {\n label: string;\n description: string;\n icon?: IconRenderer;\n}\n\n// Permission scope shown on the integration detail page\nexport interface IntegrationPermission {\n label: string;\n detail: string;\n type: 'read' | 'write';\n}\n\n// ---- Integration Health Monitoring (issue #474) -----------------------------\n\n// Higher = healthier. Used internally for \"least healthy wins\" merging.\nexport type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';\n\n/**\n * Result returned by `BaseIntegration.healthCheck(accountId)`. Routed through\n * `classifyHealth()` (server-side) before writing to\n * `integration_health_status`.\n */\nexport interface HealthCheckResult {\n status: HealthStatus;\n errorCode?: string;\n errorMessage?: string;\n /** Rolling 24h error rate from `account_product_usage_events`, 0..100. */\n errorRatePct?: number;\n /** Sample size used to compute errorRatePct; if below threshold, treated as unknown. */\n sampleSize?: number;\n /** Whether the most recent observed call succeeded. */\n lastObservationSucceeded?: boolean;\n /** Token/credential expiry, if known. Drives token-expiry warnings. */\n tokenExpiresAt?: string;\n /** Free-form per-integration data carried to the dashboard. */\n metadata?: Record<string, unknown>;\n}\n\n// Activation result from integration\nexport interface ActivationResult {\n success: boolean;\n configData?: Record<string, unknown>;\n externalId?: string;\n errorMessage?: string;\n metadata?: Record<string, unknown>;\n}\n\n// Base props that all integration components need\ninterface BaseIntegrationComponentProps {\n accountId: string;\n sourceId?: string;\n integrationConfig?: Record<string, unknown>;\n}\n\n// Discriminated union for product reference - only one allowed\ntype ProductReference =\n | { productId: string; productSlug?: never }\n | { productSlug: string; productId?: never };\n\n// New base type using discriminated union\nexport type IntegrationComponentProps = BaseIntegrationComponentProps &\n ProductReference;\n\n// Marketplace tile props\nexport type MarketplaceTileProps = IntegrationComponentProps & {\n onActivate?: () => void;\n isActivated?: boolean;\n /** Navigation URL for the integration detail page */\n href?: string;\n};\n\n// Activation form props\nexport type ActivationFormProps = IntegrationComponentProps & {\n onSuccess?: (result: ActivationResult) => void;\n onCancel?: () => void;\n isLoading?: boolean;\n /** Render inline instead of in a dialog */\n inline?: boolean;\n /**\n * The TEAM account context the user is activating within — separate from\n * `accountId`, which for `scope: 'user'` integrations is collapsed to the\n * personal account for activation storage/token purposes. Only consumed by\n * workspace materialization (`materializeWorkspaceOnActivation`), never for\n * activation/token storage itself (plan 009 Part A2). Optional: undefined\n * for team-scoped integrations (their `accountId` already IS the team) and\n * for any caller that hasn't threaded it yet — the server action falls back\n * to `accountId` when absent.\n */\n teamAccountId?: string;\n};\n\n// Integration metadata\nexport interface IntegrationMetadata {\n productId: string;\n productSlug: string;\n integrationName: string;\n description: string;\n category: string;\n version: string;\n icon?: string;\n /** Bundled logo URL — overrides product.logo_path on the marketplace */\n logoPath?: string;\n /** When 'user', activation data is stored under the user's personal account (user.id) instead of the team account */\n scope?: 'user' | 'team';\n /**\n * Set FALSE to assert this integration has NO self-serve activation surface —\n * the marketplace detail page then renders no activation form at all.\n *\n * This is deliberately distinct from \"has no CUSTOM activation form\": an\n * integration that omits `getActivationForm()` still gets the generic one, and\n * six integrations depend on that. Use this flag only when submitting ANY\n * activation form would be a dead end — where `activate()` cannot succeed, so\n * showing a form hands the user a button that cannot work.\n * Undefined/true = normal self-serve activation.\n */\n selfServeActivation?: boolean;\n /**\n * How this integration's MCP tools resolve credentials — the integration's\n * SELF-DECLARATION of machine-executability, consumed dynamically by the\n * workflow engine's fail-closed tool allowlist (isGatewayExposableTool).\n *\n * 'account' — credentials/identity resolve from team- or source-level\n * config via the ToolContext (`getAccountId(ctx)` + `ctx.supabase`).\n * Safe for durable machine-principal execution (workflows): eligible\n * tools (read + public/internal sensitivity) become workflow-callable.\n * 'user' — credentials or identity are personal (ctx.user.id, personal\n * OAuth, session reads like getSupabaseServerClient()/requireUser()).\n * NEVER machine-executable: a workflow would silently ride one\n * employee's personal grant.\n * 'source' — platform-level credentials (diagnostics-class integrations).\n * Not exposed to account workflows.\n *\n * UNDECLARED = fail closed (treated as not machine-executable). Declaring\n * 'account' is a security assertion: the integration's MCP run() handlers\n * must use ctx-based resolution ONLY — no session clients, no personal\n * grants. A guard test in @kit/integrations-workflows enforces the obvious\n * violations (session-client reads in mcp/ code of 'account' declarers).\n */\n credentialModel?: 'account' | 'user' | 'source';\n /**\n * What KIND of thing this integration is — the platform's presentation and\n * governance discriminant. Undefined means `'integration'`, which is what\n * every third-party connector is; the field only ever needs setting to opt\n * INTO feature treatment.\n *\n * 'integration' — connects Fusion to a third party. Has credentials, an\n * activation flow, and a marketplace presence. Its widgets live on\n * workspace dashboards and it declares its workspaces via\n * `getWorkspaceTargets()`.\n * 'feature' — a Fusion-native capability with a bespoke experience: no\n * third-party credentials, its own route tree under\n * `/home/[account]/…`, and its own sidebar section declared via\n * `getSidebarNav()` (optionally rendered by its own\n * `getSidebarComponent()`).\n *\n * The two are mutually exclusive on the nav question. A `'feature'` MUST\n * declare `getSidebarNav()` and MUST NOT declare `getWorkspaceTargets()` —\n * its widgets are placed onto whatever workspace an admin chooses in the\n * composer (`/admin/sources/[id]/layout`) rather than onto a workspace\n * named after itself. A host-side test\n * fails the build on either violation.\n */\n kind?: 'integration' | 'feature';\n /**\n * Product slugs that MUST be attached to the same Source for this product to\n * function — a hard dependency, not a suggestion.\n *\n * The host REFUSES to attach this product to a Source missing any of them,\n * and refuses a detach that would strand a dependent. Admin surfaces render\n * the dependency in both directions (\"Requires X\" / \"Enables Y\").\n *\n * Host CI enforces: every slug is registered, nothing requires itself, no\n * cycles, and a `kind: 'feature'` may require an `'integration'` but never\n * the reverse.\n */\n requires?: string[];\n}\n\n// Widget configuration\nexport interface WidgetConfig {\n id: string;\n name: string;\n component: ComponentType<IntegrationComponentProps>;\n gridSize?: { cols: number; rows: number } | { cols: number; rows: number }[];\n gridPosition?: { col: number; row: number };\n widgetState: 'active' | 'inactive' | 'disabled';\n // React-grid-layout format\n layouts?: {\n lg?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n md?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n sm?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n };\n // Integration metadata for widgets\n integrationMetadata?: IntegrationMetadata;\n category?: { id: string; name: string; slug: string; icon: string | null };\n // Database product ID (UUID) from widget_config table\n productId?: string;\n isCollapsible?: boolean;\n // For collapsed state persistence\n collapsed?: boolean;\n widgetConfigId?: string;\n // Resolved workspace placement (explicit widget_config.workspace_id\n // override, else the product's inheriting attachment). Populated by the\n // dashboard widgets loader; null = no workspace (unscoped dashboard).\n workspaceId?: string | null;\n isPinnable?: boolean;\n isPinned?: boolean;\n // When true, the dashboard renders the \"Ask about this\" AI footer bar for\n // this widget. The widget may still pass `askContext` to the wrapper for a\n // rich snapshot; otherwise a minimal default is used.\n aiFooterEnabled?: boolean;\n // Integration health for this widget's product. `unhealthy` downgrades the\n // widget to `disabled`; `degraded` surfaces a stale-data footer.\n health?: {\n status: HealthStatus;\n lastSuccessAt?: string | null;\n errorMessage?: string | null;\n };\n}\n\n// Database types for integration tables.\n//\n// PARTNER-VISIBLE SUBSET: these row types deliberately OMIT `vault_secret_id`.\n// A vault id is a host handle to a decryptable secret; the partner surface\n// never exposes one — `ctx.secrets` fences it off. Host code keeps the full\n// row because it needs the handle; a parity test pins the SDK type as the\n// host's row minus exactly the host-only fields, so a new column cannot drift\n// in here unnoticed.\nexport interface IntegrationConfig {\n id: string;\n account_id: string | null;\n product_id: string;\n config_type: 'api_credentials' | 'activation_data' | 'settings';\n config_data: Record<string, unknown>;\n status: 'active' | 'inactive' | 'pending' | 'error' | 'suspended';\n is_active: boolean;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n\n/**\n * The PARTNER-VISIBLE `data_type` values on `account_product_data` — a strict\n * SUBSET of `@kit/integrations-core`'s union.\n *\n * `'secret'` is deliberately absent. Secret rows are host-managed (one row per\n * secret name, value in Vault, `vault_secret_id` on the row), written only by\n * the capability context's `persistSecret` and read only through\n * `ctx.secrets`. Naming the kind here would advertise a row shape partner code\n * must never address: `ctx.storage` rejects it, and core's generic accessors\n * reject it too (`config-utils.ts`, `assertNotHostOnlyDataType`) precisely\n * because a write there would land the plaintext in the `data` column.\n *\n * When you add an integration-specific kind, add it in BOTH places (core owns\n * the full union) and keep this one partner-appropriate. Prefer `dataSubtype`\n * for orthogonal slicing within a kind (e.g. `data_type='cache'`,\n * `data_subtype='profile'`); reserve a new top-level kind for data that has a\n * distinct lifecycle or is queried independently.\n */\nexport type AccountProductDataType =\n | 'activation'\n | 'settings'\n | 'metrics_summary'\n | 'sync_state'\n | 'cache'\n // Integration-specific kinds — extend here when needed.\n | 'cash_forecast' // quickbooks — user-supplied manual cash-forecast estimates\n | 'tax_config'; // quickbooks — per-user federal/state tax rate config\n\nexport interface AccountProductData {\n id: string;\n account_id: string;\n product_id: string;\n data_type: AccountProductDataType;\n data_subtype?: string;\n data: Record<string, unknown>;\n expires_at?: string;\n integration_config_id?: string;\n external_id?: string;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n"]}
package/eslint.preset.mjs CHANGED
@@ -62,6 +62,25 @@ const CTX_FETCH_REMEDIATION =
62
62
  'bypass. Widening `egress` is a reviewed security-posture change, not a ' +
63
63
  'config tweak.';
64
64
 
65
+ /**
66
+ * `fetch` is the one escape that legitimately has TWO capability-mediated
67
+ * replacements, depending on where the code runs — unlike every other entry
68
+ * in this preset, which has exactly one. Naming only `ctx.fetch` in a
69
+ * widget/client file sends a partner chasing a capability that does not
70
+ * exist on that side of the boundary, so this message names both.
71
+ */
72
+ const CLIENT_FETCH_REMEDIATION =
73
+ 'A global fetch reaches the network outside the capability layer, which is ' +
74
+ 'an egress bypass. In server/handler code, use `ctx.fetch` from the ' +
75
+ "capability context — it enforces your integration's `egress` allowlist " +
76
+ '(`defineIntegration({ egress: [...] })`), re-checks every redirect hop, ' +
77
+ 'and pins the vetted addresses; that enforcement is server-side only. In ' +
78
+ 'client/widget code, use `useIntegrationFetch()` from `@ekanos/sdk/hooks` ' +
79
+ 'instead — the dev harness gates it against the same `egress` list, but a ' +
80
+ 'browser cannot pin addresses or re-vet redirects, so treat that check as ' +
81
+ 'advisory, not a security boundary. Widening `egress` is a reviewed ' +
82
+ 'security-posture change either way.';
83
+
65
84
  const CTX_SECRETS_REMEDIATION =
66
85
  'Use `ctx.secrets.get(name)` from the capability context instead, and ' +
67
86
  "declare the credential in your integration's activation schema. The host " +
@@ -190,9 +209,14 @@ const EGRESS_GLOBALS = ['fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource'];
190
209
  /** Objects a `globalThis.fetch`-style member access can hide behind. */
191
210
  const GLOBAL_OBJECTS = 'globalThis|window|self|global';
192
211
 
193
- /** Members of those objects that are the very escapes banned as bare globals. */
194
- const GLOBAL_ESCAPE_MEMBERS =
195
- 'fetch|XMLHttpRequest|WebSocket|EventSource|process|eval|require|Function';
212
+ /**
213
+ * Members of those objects that are the very escapes banned as bare globals,
214
+ * split so `fetch` — the one escape with a client-side remediation — can
215
+ * carry its own message. See `CLIENT_FETCH_REMEDIATION`.
216
+ */
217
+ const GLOBAL_ESCAPE_FETCH_MEMBER = 'fetch';
218
+ const GLOBAL_ESCAPE_OTHER_MEMBERS =
219
+ 'XMLHttpRequest|WebSocket|EventSource|process|eval|require|Function';
196
220
 
197
221
  /**
198
222
  * `@kit/*` is checked with a PATTERN rather than a path so every subpath is
@@ -250,7 +274,12 @@ export const capabilityRules = {
250
274
  'error',
251
275
  ...EGRESS_GLOBALS.map((name) => ({
252
276
  name,
253
- message: `\`${name}\` reaches the network outside the capability layer. ${CTX_FETCH_REMEDIATION}`,
277
+ // `fetch` has a client-side remediation the other egress globals don't
278
+ // (`useIntegrationFetch()`), so it gets its own message.
279
+ message:
280
+ name === 'fetch'
281
+ ? CLIENT_FETCH_REMEDIATION
282
+ : `\`${name}\` reaches the network outside the capability layer. ${CTX_FETCH_REMEDIATION}`,
254
283
  })),
255
284
  {
256
285
  name: 'process',
@@ -283,8 +312,18 @@ export const capabilityRules = {
283
312
  {
284
313
  // Closes the hole in no-restricted-globals: that rule resolves
285
314
  // identifiers, so `globalThis.fetch` (a member access, not a global
286
- // reference) is invisible to it.
287
- selector: `MemberExpression[object.name=/^(${GLOBAL_OBJECTS})$/][property.name=/^(${GLOBAL_ESCAPE_MEMBERS})$/]`,
315
+ // reference) is invisible to it. Split out from the selector below so
316
+ // it can carry the client-side remediation.
317
+ selector: `MemberExpression[object.name=/^(${GLOBAL_OBJECTS})$/][property.name=/^(${GLOBAL_ESCAPE_FETCH_MEMBER})$/]`,
318
+ message:
319
+ 'Reaching `fetch` through the global object is the same bypass as ' +
320
+ 'calling it directly, and it is how a capability control gets routed ' +
321
+ 'around without tripping an import rule. ' +
322
+ CLIENT_FETCH_REMEDIATION,
323
+ },
324
+ {
325
+ // Same hole, for the escapes that have no client-side twin.
326
+ selector: `MemberExpression[object.name=/^(${GLOBAL_OBJECTS})$/][property.name=/^(${GLOBAL_ESCAPE_OTHER_MEMBERS})$/]`,
288
327
  message:
289
328
  'Reaching an escape through the global object is the same bypass as ' +
290
329
  'naming it directly, and it is how a capability control gets routed ' +
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/sdk",
3
- "version": "0.1.6",
4
- "private": false,
3
+ "version": "0.3.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.1.6",
54
- "@ekanos/ui": "0.1.6",
52
+ "@ekanos/integration-schema": "0.3.0",
53
+ "@ekanos/ui": "0.3.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",