@ekanos/sdk 0.2.0 → 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.
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: **160** 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: **160** across 7 entrypoints.
77
77
  | `WidgetState` | interface |
78
78
  | `currentReturnPath` | function |
79
79
 
80
- ## `@ekanos/sdk/hooks` (37 exports)
80
+ ## `@ekanos/sdk/hooks` (43 exports)
81
81
 
82
82
  | Export | Kind |
83
83
  | --- | --- |
@@ -89,6 +89,9 @@ Total exported names: **160** 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 |
94
97
  | `IntegrationActivationDataProvider` | function |
@@ -97,6 +100,7 @@ Total exported names: **160** across 7 entrypoints.
97
100
  | `OAuthErrorParam` | interface |
98
101
  | `PartnerOAuthPathOptions` | interface |
99
102
  | `PublishEventInput` | interface |
103
+ | `UseIntegrationActionOptions` | interface |
100
104
  | `UseOAuthConnectionStatusOptions` | interface |
101
105
  | `WidgetEvent` | interface |
102
106
  | `WidgetEventFilter` | interface |
@@ -104,6 +108,7 @@ Total exported names: **160** across 7 entrypoints.
104
108
  | `WidgetEventSubscriber` | type alias |
105
109
  | `createPartnerConnectionCheck` | function |
106
110
  | `fetchIntegrationStorage` | function |
111
+ | `integrationActionUrl` | function |
107
112
  | `integrationStorageUrl` | function |
108
113
  | `partnerOAuthConnectPath` | function |
109
114
  | `partnerOAuthErrorParam` | function |
@@ -112,6 +117,7 @@ Total exported names: **160** across 7 entrypoints.
112
117
  | `useActivationData` | function |
113
118
  | `useAskAssistant` | function |
114
119
  | `useDeactivateIntegration` | function |
120
+ | `useIntegrationAction` | function |
115
121
  | `useIntegrationFetch` | function |
116
122
  | `useOAuthConnectionStatus` | function |
117
123
  | `useOAuthWidgetGate` | function |
@@ -184,7 +190,7 @@ Total exported names: **160** across 7 entrypoints.
184
190
  | `invokeSchedule` | function |
185
191
  | `invokeWebhook` | function |
186
192
 
187
- ## `@ekanos/sdk/integration` (26 exports)
193
+ ## `@ekanos/sdk/integration` (27 exports)
188
194
 
189
195
  | Export | Kind |
190
196
  | --- | --- |
@@ -205,6 +211,7 @@ Total exported names: **160** across 7 entrypoints.
205
211
  | `PartnerToolParameters` | type alias |
206
212
  | `PartnerWebhookDeclaration` | interface |
207
213
  | `PartnerWidgetDeclaration` | type alias |
214
+ | `PublicActionError` | class |
208
215
  | `ScheduleInvocation` | interface |
209
216
  | `ScheduleResult` | interface |
210
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,6 +24,8 @@ 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';
@@ -29,6 +29,12 @@ 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';
@@ -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;AAEjC,sEAAsE;AACtE,wEAAwE;AACxE,sDAAsD;AACtD,OAAO,EACL,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AAErC,qEAAqE;AACrE,kEAAkE;AAClE,OAAO,EACL,iCAAiC,EACjC,iBAAiB,GAClB,MAAM,uCAAuC,CAAC","sourcesContent":["/**\n * @ekanos/sdk/hooks — client-side hooks.\n *\n * Import from client components only. The SDK OWNS these implementations;\n * `@kit/integrations-core/hooks` re-exports them for first-party code, so\n * there is one source of truth with two consumption paths\n * so there is one source of truth with two consumption paths.\n *\n * Server seam: nothing here imports a server module. The activation hooks\n * reach the host's `'use server'` actions ONLY through the\n * `IntegrationActivationProvider` injection seam\n * (activation-actions-context.tsx) — that is what keeps this entrypoint free\n * of `server-only` in a partner's module graph.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\n// Activation (host-injected server actions)\nexport {\n useActivateIntegration,\n useDeactivateIntegration,\n} from './use-activate-integration';\nexport { IntegrationActivationProvider } from './activation-actions-context';\nexport type {\n ActivateIntegrationAction,\n ActivateIntegrationInput,\n DeactivateIntegrationAction,\n DeactivateIntegrationInput,\n IntegrationActionResult,\n IntegrationActivationActions,\n} from './activation-actions-context';\n\n// Generic client-side storage bridge (host route:\n// GET /api/integrations/[slug]/storage). Serves only keys declared\n// `clientReadable: true`; a key that did not opt in reads as absent.\nexport {\n fetchIntegrationStorage,\n integrationStorageUrl,\n} from './fetch-integration-storage';\nexport type { FetchIntegrationStorageOptions } from './fetch-integration-storage';\n\n// OAuth connection status\nexport {\n useOAuthConnectionStatus,\n useOAuthWidgetGate,\n} from './use-oauth-connection-status';\nexport type {\n ConnectionCheckAction,\n ConnectionCheckResult,\n OAuthErrorParam,\n UseOAuthConnectionStatusOptions,\n} from './use-oauth-connection-status';\n\n// Hosted partner OAuth transport (host routes:\n// GET /api/integrations/[slug]/oauth/{connect,callback,status}). A partner\n// passes `connectPath: partnerOAuthConnectPath(slug)` to OAuthActivationForm\n// and `createPartnerConnectionCheck(slug)` as its `checkConnection`.\nexport {\n createPartnerConnectionCheck,\n partnerOAuthConnectPath,\n partnerOAuthErrorParam,\n partnerOAuthStatusUrl,\n} from './partner-oauth';\nexport type { PartnerOAuthPathOptions } from './partner-oauth';\n\n// Widget Event Bus\nexport { WidgetEventProvider, useWidgetEventBus } from './widget-event-context';\nexport { useWidgetEvents } from './use-widget-events';\nexport type {\n PublishEventInput,\n WidgetEvent,\n WidgetEventFilter,\n WidgetEventSubscriber,\n} from './widget-event-types';\n\n// Widget Ask-Bar Bridge\nexport {\n AskAssistantProvider,\n useAskAssistant,\n useRegisterAskAssistant,\n} from './ask-assistant-context';\n\n// Client-side fetch (host routes: none — this mediates a widget's own\n// vendor calls, mirroring `ctx.fetch` on the server). Default outside a\n// provider is a plain `globalThis.fetch` passthrough.\nexport {\n IntegrationFetchProvider,\n useIntegrationFetch,\n} from './integration-fetch-context';\n\n// Activation data (the credential half of the client-fetch pattern).\n// Default outside a provider is `null` — no host mounts this yet.\nexport {\n IntegrationActivationDataProvider,\n useActivationData,\n} from './integration-activation-data-context';\n"]}
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,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":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,EAAE,WAAW,EAA0B,MAAM,uBAAuB,CAAC;AAE5E,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAa7C;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,eAAuB,EACvB,UAAkB,EAClB,UAAgC,EAAE;;IAElC,MAAM,IAAI,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,gBAAgB,CAAC;IACjD,OAAO,GAAG,IAAI,IAAI,eAAe,YAAY,UAAU,EAAE,CAAC;AAC5D,CAAC;AA0BD;;;;;GAKG;AACH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAK/C,YAAY,OAAe,EAAE,OAAsC;QACjE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC/B,CAAC;CACF;AAcD,SAAS,iBAAiB,CAAC,KAAc;IACvC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAA+B,CAAC,OAAO,KAAK,IAAI;QACjD,MAAM,IAAI,KAAK,CAChB,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAA+B,CAAC,OAAO,KAAK,KAAK;QAClD,OAAQ,KAA6B,CAAC,KAAK,KAAK,QAAQ,CACzD,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,oBAAoB,CACjC,eAAuB,EACvB,UAAkB,EAClB,KAAa,EACb,OAAoC;;IAEpC,MAAM,GAAG,GAAG,oBAAoB,CAAC,eAAe,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IAEvE,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;YACrC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,MAAM,EAAE,kBAAkB;aAC3B;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC;SAC9D,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,sBAAsB,CAC9B,oBAAoB,eAAe,IAAI,UAAU,MAC/C,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAC3C,IAAI,EACJ,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CACrC,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAEjE,IAAI,QAAQ,CAAC,EAAE,IAAI,iBAAiB,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9C,OAAO,OAAO,CAAC,IAAe,CAAC;IACjC,CAAC;IAED,IAAI,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,sBAAsB,CAAC,OAAO,CAAC,KAAK,EAAE;YAC9C,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,IAAI,EAAE,MAAA,OAAO,CAAC,IAAI,mCAAI,eAAe;YACrC,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,sBAAsB,CAC9B,kBAAkB,eAAe,IAAI,UAAU,MAAM,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,EAC/F,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,kBAAkB,EAAE,CACtD,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,eAAuB,EACvB,UAAkB,EAClB,OAAoC;IAEpC,OAAO,WAAW,CAAC;QACjB,UAAU,EAAE,CAAC,KAAa,EAAE,EAAE,CAC5B,oBAAoB,CAClB,eAAe,EACf,UAAU,EACV,KAAK,EACL,OAAO,CACR;KACJ,CAAC,CAAC;AACL,CAAC","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"]}
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "The official SDK for building Ekanos integrations.",
6
6
  "license": "MIT",
@@ -49,8 +49,8 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@ekanos/integration-schema": "0.2.0",
53
- "@ekanos/ui": "0.2.0",
52
+ "@ekanos/integration-schema": "0.3.0",
53
+ "@ekanos/ui": "0.3.0",
54
54
  "@supabase/supabase-js": "2.87.1",
55
55
  "server-only": "^0.0.1"
56
56
  },