@ekanos/sdk 0.3.0 → 0.5.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/README.md +171 -75
- package/api-report.md +8 -3
- package/dist/components/base-activation-dialog.d.ts +15 -5
- package/dist/components/base-activation-dialog.js +31 -5
- package/dist/components/base-activation-dialog.js.map +1 -1
- package/dist/components/base-activation-form.d.ts +20 -5
- package/dist/components/base-activation-form.js +12 -5
- package/dist/components/base-activation-form.js.map +1 -1
- package/dist/components/base-marketplace-tile.js +12 -10
- package/dist/components/base-marketplace-tile.js.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/oauth-activation-form.d.ts +23 -16
- package/dist/components/oauth-activation-form.js +34 -20
- package/dist/components/oauth-activation-form.js.map +1 -1
- package/dist/components/widgets/ai-prompt-chip.js +2 -2
- package/dist/components/widgets/ai-prompt-chip.js.map +1 -1
- package/dist/components/widgets/use-relative-time.d.ts +7 -0
- package/dist/components/widgets/use-relative-time.js +55 -0
- package/dist/components/widgets/use-relative-time.js.map +1 -0
- package/dist/components/widgets/widget-ask-bar.js +1 -1
- package/dist/components/widgets/widget-ask-bar.js.map +1 -1
- package/dist/components/widgets/widget-context.d.ts +21 -0
- package/dist/components/widgets/widget-context.js.map +1 -1
- package/dist/components/widgets/widget-preview-provider.d.ts +1 -1
- package/dist/components/widgets/widget-preview-provider.js +1 -1
- package/dist/components/widgets/widget-preview-provider.js.map +1 -1
- package/dist/components/widgets/widget-state-components.d.ts +4 -1
- package/dist/components/widgets/widget-state-components.js +6 -4
- package/dist/components/widgets/widget-state-components.js.map +1 -1
- package/dist/components/widgets/widget.d.ts +47 -2
- package/dist/components/widgets/widget.js +111 -23
- package/dist/components/widgets/widget.js.map +1 -1
- package/dist/context/egress.js.map +1 -1
- package/dist/context/storage-validation.d.ts +24 -4
- package/dist/context/storage-validation.js +5 -2
- package/dist/context/storage-validation.js.map +1 -1
- package/dist/hooks/ask-assistant-context.js.map +1 -1
- package/dist/hooks/fetch-integration-storage.js.map +1 -1
- package/dist/hooks/partner-oauth.js +3 -0
- package/dist/hooks/partner-oauth.js.map +1 -1
- package/dist/hooks/use-integration-action.js.map +1 -1
- package/dist/hooks/use-oauth-connection-status.d.ts +78 -78
- package/dist/hooks/use-oauth-connection-status.js +17 -10
- package/dist/hooks/use-oauth-connection-status.js.map +1 -1
- package/dist/hooks/widget-event-context.js.map +1 -1
- package/dist/integration/event-surfaces.js.map +1 -1
- package/dist/integration/index.d.ts +2 -2
- package/dist/integration/index.js +1 -1
- package/dist/integration/index.js.map +1 -1
- package/dist/testing/invoke.js +9 -5
- package/dist/testing/invoke.js.map +1 -1
- package/dist/testing/mock-context.js +43 -8
- package/dist/testing/mock-context.js.map +1 -1
- package/dist/testing/mock-fetch-provider.js.map +1 -1
- package/dist/types/integration.d.ts +27 -21
- package/dist/types/integration.js.map +1 -1
- package/dist/types/widget-ask-context.d.ts +9 -0
- package/dist/types/widget-ask-context.js.map +1 -1
- package/dist/types/workspace-target.d.ts +3 -2
- package/dist/types/workspace-target.js.map +1 -1
- package/eslint.preset.mjs +9 -8
- package/package.json +37 -41
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask-assistant-context.js","sourceRoot":"","sources":["../../src/hooks/ask-assistant-context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAEL,aAAa,EACb,GAAG,EACH,WAAW,EACX,eAAe,EACf,OAAO,EACP,MAAM,GACP,MAAM,OAAO,CAAC;AAaf,MAAM,mBAAmB,GAAG,aAAa,CACvC,IAAI,CACL,CAAC;AAMF;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,QAAQ,EAA6B;IAC1E,MAAM,UAAU,GAAG,MAAM,CAA6B,IAAI,CAAC,CAAC;IAE5D,MAAM,GAAG,GAAG,WAAW,CAAsB,CAAC,GAAG,EAAE,EAAE;;QACnD,MAAA,UAAU,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"ask-assistant-context.js","sourceRoot":"","sources":["../../src/hooks/ask-assistant-context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAEL,aAAa,EACb,GAAG,EACH,WAAW,EACX,eAAe,EACf,OAAO,EACP,MAAM,GACP,MAAM,OAAO,CAAC;AAaf,MAAM,mBAAmB,GAAG,aAAa,CACvC,IAAI,CACL,CAAC;AAMF;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,QAAQ,EAA6B;IAC1E,MAAM,UAAU,GAAG,MAAM,CAA6B,IAAI,CAAC,CAAC;IAE5D,MAAM,GAAG,GAAG,WAAW,CAAsB,CAAC,GAAG,EAAE,EAAE;;QACnD,MAAA,UAAU,CAAC,OAAO,+CAAlB,UAAU,EAAW,GAAG,CAAC,CAAC;IAC5B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,eAAe,GAAG,WAAW,CAAC,CAAC,OAAmC,EAAE,EAAE;QAC1E,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;IAC/B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,eAAe,EAAE,CAAC,EAChC,CAAC,GAAG,EAAE,eAAe,CAAC,CACvB,CAAC;IAEF,OAAO,KAAC,mBAAmB,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAuB,CAAC;AAC7E,CAAC;AAED,MAAM,QAAQ,GAAwB,GAAG,EAAE,GAAE,CAAC,CAAC;AAE/C;;;;GAIG;AACH,MAAM,UAAU,eAAe;;IAC7B,MAAM,OAAO,GAAG,GAAG,CAAC,mBAAmB,CAAC,CAAC;IACzC,aAAO,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG,mCAAI,QAAQ,CAAC;AAClC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAA4B;IAClE,MAAM,OAAO,GAAG,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAEzC,eAAe,CAAC,GAAG,EAAE;QACnB,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;QACjC,OAAO,GAAG,EAAE;YACV,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AACzB,CAAC","sourcesContent":["'use client';\n\nimport {\n type ReactNode,\n createContext,\n use,\n useCallback,\n useLayoutEffect,\n useMemo,\n useRef,\n} from 'react';\n\nimport type { WidgetAskContext } from '../types/widget-ask-context';\n\ntype AskAssistantHandler = (ctx: WidgetAskContext) => void;\n\ninterface AskAssistantContextValue {\n /** Stable dispatcher widgets call. Routes to the registered handler at invocation time. */\n ask: AskAssistantHandler;\n /** Internal — used by the dock side of the bridge to install its handler. */\n registerHandler: (handler: AskAssistantHandler | null) => void;\n}\n\nconst AskAssistantContext = createContext<AskAssistantContextValue | null>(\n null,\n);\n\ninterface AskAssistantProviderProps {\n children: ReactNode;\n}\n\n/**\n * Hosts the bridge between widget ask-bars and the AI assistant dock.\n *\n * Widgets call `useAskAssistant()` to get a stable dispatcher. The dock\n * registers its actual handler via `useRegisterAskAssistant()` once it\n * has access to its chat hook. Until a handler is registered (e.g. on\n * routes without a dock), calls are no-ops.\n */\nexport function AskAssistantProvider({ children }: AskAssistantProviderProps) {\n const handlerRef = useRef<AskAssistantHandler | null>(null);\n\n const ask = useCallback<AskAssistantHandler>((ctx) => {\n handlerRef.current?.(ctx);\n }, []);\n\n const registerHandler = useCallback((handler: AskAssistantHandler | null) => {\n handlerRef.current = handler;\n }, []);\n\n const value = useMemo<AskAssistantContextValue>(\n () => ({ ask, registerHandler }),\n [ask, registerHandler],\n );\n\n return <AskAssistantContext value={value}>{children}</AskAssistantContext>;\n}\n\nconst NOOP_ASK: AskAssistantHandler = () => {};\n\n/**\n * Returns a stable dispatcher widgets can call to send a question about\n * themselves to the assistant. Outside an `AskAssistantProvider` (e.g.\n * widget preview pages), this is a no-op.\n */\nexport function useAskAssistant(): AskAssistantHandler {\n const context = use(AskAssistantContext);\n return context?.ask ?? NOOP_ASK;\n}\n\n/**\n * Installs the dock-side handler that the widget dispatcher will call.\n * Re-registers whenever `handler` changes; clears on unmount.\n *\n * Uses useLayoutEffect so the handler is wired up before the browser\n * paints the dock — otherwise a chip click that fires during the first\n * paint frame (e.g. user opens a drawer fast) finds a null handler and\n * silently no-ops.\n */\nexport function useRegisterAskAssistant(handler: AskAssistantHandler): void {\n const context = use(AskAssistantContext);\n\n useLayoutEffect(() => {\n if (!context) return;\n context.registerHandler(handler);\n return () => {\n context.registerHandler(null);\n };\n }, [context, handler]);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-integration-storage.js","sourceRoot":"","sources":["../../src/hooks/fetch-integration-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,oDAAoD;AACpD,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAgBhC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,eAAuB,EACvB,OAEC;;IAED,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC;QAChC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;KAC7B,CAAC,CAAC;IAEH,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"fetch-integration-storage.js","sourceRoot":"","sources":["../../src/hooks/fetch-integration-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,oDAAoD;AACpD,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAgBhC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,eAAuB,EACvB,OAEC;;IAED,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC;QAChC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;KAC7B,CAAC,CAAC;IAEH,MAAM,IAAI,SAAG,OAAO,CAAC,OAAO,mCAAI,mBAAmB,CAAC;IAEpD,OAAO,GAAG,IAAI,IAAI,eAAe,YAAY,KAAK,EAAE,CAAC;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,eAAuB,EACvB,OAAuC;;IAEvC,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,oCAAoC,CAChF,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,oBAAoB,OAAO,CAAC,IAAI,CAAC,MAAM,SAAS;YACzF,gCAAgC,oBAAoB,+BAA+B,CACtF,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,qBAAqB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAE5D,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;QAC3C,MAAM,QAAE,OAAO,CAAC,MAAM,mCAAI,IAAI;QAC9B,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;KACxC,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,mBAAmB,eAAe,cAAc,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAE/C,IACE,OAAO,KAAK,IAAI;QAChB,OAAO,OAAO,KAAK,QAAQ;QAC3B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EACtB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,QAAQ,eAAe,gDAAgD,CACxE,CAAC;IACJ,CAAC;IAED,OAAO,OAAkC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * The browser half of the generic storage bridge.\n *\n * `ctx.storage` is a server capability and widgets are client components, so\n * a saved setting has to round-trip through HTTP. The host serves that\n * round-trip once, generically, at\n * `GET /api/integrations/[slug]/storage?accountId=…&keys=a,b`; this is the\n * client that calls it, so an integration never hand-writes the URL, the\n * query string, or the not-ok handling.\n *\n * Only keys whose declaration opted in with `clientReadable: true` come back.\n * A key that did not opt in, was never declared, or simply has no stored row\n * is ABSENT from the result — all three are indistinguishable by design, so\n * callers must treat a missing key as \"no value\", never as an error.\n *\n * Values are returned raw and unvalidated: the integration owns the shape of\n * its own storage, so it validates with the same zod schemas it declared:\n *\n * ```ts\n * const raw = await fetchIntegrationStorage('acme-weather', {\n * accountId,\n * keys: ['settings/location'],\n * });\n *\n * const parsed = LocationSchema.safeParse(raw['settings/location']);\n * const location = parsed.success ? parsed.data : null;\n * ```\n */\n\n/** Mirrors the host route's own per-request cap. */\nconst MAX_KEYS_PER_REQUEST = 20;\n\nexport interface FetchIntegrationStorageOptions {\n /** The account whose storage to read. Must be one the caller belongs to. */\n accountId: string;\n /** Declared, `clientReadable` account-scope keys, e.g. `['settings/units']`. */\n keys: readonly string[];\n signal?: AbortSignal;\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 storage-route URL for one integration. Exported because a\n * harness or a test double needs to recognize the exact URL this client will\n * request.\n */\nexport function integrationStorageUrl(\n integrationSlug: string,\n options: Pick<FetchIntegrationStorageOptions, 'accountId' | 'keys'> & {\n baseUrl?: string;\n },\n): string {\n const query = new URLSearchParams({\n accountId: options.accountId,\n keys: options.keys.join(','),\n });\n\n const base = options.baseUrl ?? '/api/integrations';\n\n return `${base}/${integrationSlug}/storage?${query}`;\n}\n\n/**\n * Reads declared, client-readable account-scope storage for one integration.\n *\n * Throws when the request itself fails — a 403 (not signed in, not a member,\n * not activated, or no such integration, all deliberately indistinguishable)\n * or a transport error. It does NOT throw for a key that came back absent.\n */\nexport async function fetchIntegrationStorage(\n integrationSlug: string,\n options: FetchIntegrationStorageOptions,\n): Promise<Record<string, unknown>> {\n if (options.keys.length === 0) {\n throw new Error(\n `fetchIntegrationStorage('${integrationSlug}') needs at least one storage key.`,\n );\n }\n\n if (options.keys.length > MAX_KEYS_PER_REQUEST) {\n throw new Error(\n `fetchIntegrationStorage('${integrationSlug}') was asked for ${options.keys.length} keys; ` +\n `the host route reads at most ${MAX_KEYS_PER_REQUEST} per request. Split the read.`,\n );\n }\n\n const url = integrationStorageUrl(integrationSlug, options);\n\n const response = await globalThis.fetch(url, {\n signal: options.signal ?? null,\n headers: { accept: 'application/json' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Could not read '${integrationSlug}' storage (${response.status} ${response.statusText}).`,\n );\n }\n\n const payload: unknown = await response.json();\n\n if (\n payload === null ||\n typeof payload !== 'object' ||\n Array.isArray(payload)\n ) {\n throw new Error(\n `The '${integrationSlug}' storage route returned a non-object payload.`,\n );\n }\n\n return payload as Record<string, unknown>;\n}\n"]}
|
|
@@ -31,6 +31,9 @@ const PARTNER_OAUTH_ERROR_MESSAGES = {
|
|
|
31
31
|
not_configured: 'This integration is not configured for your workspace yet. Ask an administrator to add its OAuth credentials.',
|
|
32
32
|
exchange_failed: 'The provider rejected the connection. Try again, or contact support if it keeps failing.',
|
|
33
33
|
save_failed: 'Connected, but the credentials could not be saved. Try again.',
|
|
34
|
+
wrong_workspace: 'You signed in to a different account or workspace than this one is set up for. Switch to that account at the provider and try again.',
|
|
35
|
+
app_not_set_up: 'The provider refused this integration’s app settings, so trying again won’t help. Ask an administrator to check the app’s scopes and credentials.',
|
|
36
|
+
organization_access: 'Connect with personal access — this integration only uses your own account. Try again and choose personal access.',
|
|
34
37
|
};
|
|
35
38
|
/**
|
|
36
39
|
* A `ConnectionCheckAction` backed by the hosted status route. The route
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"partner-oauth.js","sourceRoot":"","sources":["../../src/hooks/partner-oauth.ts"],"names":[],"mappings":"AAiCA,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAW7C,yEAAyE;AACzE,MAAM,UAAU,uBAAuB,CACrC,eAAuB,EACvB,
|
|
1
|
+
{"version":3,"file":"partner-oauth.js","sourceRoot":"","sources":["../../src/hooks/partner-oauth.ts"],"names":[],"mappings":"AAiCA,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAW7C,yEAAyE;AACzE,MAAM,UAAU,uBAAuB,CACrC,eAAuB,EACvB,OAAO,GAA4B,EAAE;;IAErC,MAAM,IAAI,SAAG,OAAO,CAAC,OAAO,mCAAI,gBAAgB,CAAC;IACjD,OAAO,GAAG,IAAI,IAAI,eAAe,gBAAgB,CAAC;AACpD,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,qBAAqB,CACnC,eAAuB,EACvB,OAAwD;;IAExD,MAAM,IAAI,SAAG,OAAO,CAAC,OAAO,mCAAI,gBAAgB,CAAC;IACjD,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACpE,OAAO,GAAG,IAAI,IAAI,eAAe,iBAAiB,KAAK,EAAE,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,eAAuB;IAEvB,OAAO;QACL,IAAI,EAAE,GAAG,eAAe,cAAc;QACtC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,wBAAC,4BAA4B,CAAC,KAAK,CAAC,mCAAI,IAAI,GAAA;KAClE,CAAC;AACJ,CAAC;AAED,MAAM,4BAA4B,GAA2B;IAC3D,MAAM,EAAE,0DAA0D;IAClE,OAAO,EAAE,uDAAuD;IAChE,OAAO,EACL,qEAAqE;IACvE,YAAY,EACV,oEAAoE;IACtE,cAAc,EACZ,+GAA+G;IACjH,eAAe,EACb,0FAA0F;IAC5F,WAAW,EAAE,+DAA+D;IAC5E,eAAe,EACb,sIAAsI;IACxI,cAAc,EACZ,mJAAmJ;IACrJ,mBAAmB,EACjB,mHAAmH;CACtH,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAC1C,eAAuB,EACvB,OAAO,GAA4B,EAAE;IAErC,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,qBAAqB,CAAC,eAAe,kCAC5C,OAAO,KACV,SAAS,IACT,CAAC;QAEH,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;YAC3C,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;SACxC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,uBAAuB,eAAe,wBAAwB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,CACzG,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAE/C,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,KAAK,CACb,QAAQ,eAAe,gDAAgD,CACxE,CAAC;QACJ,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAC9B,KAAc;IAEd,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAQ,KAA+B,CAAC,OAAO,KAAK,SAAS,CAC9D,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The browser half of the hosted partner OAuth transport.\n *\n * A partner's `oauth` declaration names the provider endpoints and the\n * client-credential secrets; the HOST owns the flow — authorize redirect,\n * `state`, callback, code exchange, and the `onTokens` call — behind three\n * generic routes it serves for every partner integration:\n *\n * GET /api/integrations/[slug]/oauth/connect?accountId=…&returnPath=…\n * GET /api/integrations/[slug]/oauth/callback (the provider redirects here)\n * GET /api/integrations/[slug]/oauth/status?accountId=…\n *\n * These helpers are the only place a partner package should reference those\n * paths, so a route move is one SDK change rather than a hunt through every\n * integration. `OAuthActivationForm`'s `connectPath` default still points at\n * the per-integration `/connect` route first-party packages ship themselves;\n * a partner passes `connectPath: partnerOAuthConnectPath(slug)` explicitly.\n *\n * ```tsx\n * <OAuthActivationForm\n * {...props}\n * config={{ slug, displayName: 'Acme', connectPath: partnerOAuthConnectPath(slug), … }}\n * checkConnection={createPartnerConnectionCheck(slug)}\n * errorParam={partnerOAuthErrorParam(slug)}\n * />\n * ```\n */\nimport type {\n ConnectionCheckAction,\n ConnectionCheckResult,\n OAuthErrorParam,\n} from './use-oauth-connection-status';\n\nconst DEFAULT_BASE_URL = '/api/integrations';\n\nexport interface PartnerOAuthPathOptions {\n /**\n * Overrides the base path. Exists for harnesses and tests that serve the\n * routes from somewhere other than the host's own origin; production code\n * should leave it alone.\n */\n baseUrl?: string;\n}\n\n/** The hosted connect route the activation form sends the browser to. */\nexport function partnerOAuthConnectPath(\n integrationSlug: string,\n options: PartnerOAuthPathOptions = {},\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n return `${base}/${integrationSlug}/oauth/connect`;\n}\n\n/** The hosted status route `createPartnerConnectionCheck` reads. */\nexport function partnerOAuthStatusUrl(\n integrationSlug: string,\n options: PartnerOAuthPathOptions & { accountId: string },\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n const query = new URLSearchParams({ accountId: options.accountId });\n return `${base}/${integrationSlug}/oauth/status?${query}`;\n}\n\n/**\n * The error query param the hosted callback appends on failure:\n * `<slug>_oauth_error=<code>`. Codes are the transport's, so the messages\n * live here rather than in every partner package.\n */\nexport function partnerOAuthErrorParam(\n integrationSlug: string,\n): OAuthErrorParam {\n return {\n name: `${integrationSlug}_oauth_error`,\n toMessage: (value) => PARTNER_OAUTH_ERROR_MESSAGES[value] ?? null,\n };\n}\n\nconst PARTNER_OAUTH_ERROR_MESSAGES: Record<string, string> = {\n denied: 'Access was denied at the provider. Try connecting again.',\n expired: 'The connection attempt expired. Try connecting again.',\n invalid:\n 'The connection attempt could not be verified. Try connecting again.',\n unauthorized:\n 'You need to be signed in as the person who started the connection.',\n not_configured:\n 'This integration is not configured for your workspace yet. Ask an administrator to add its OAuth credentials.',\n exchange_failed:\n 'The provider rejected the connection. Try again, or contact support if it keeps failing.',\n save_failed: 'Connected, but the credentials could not be saved. Try again.',\n wrong_workspace:\n 'You signed in to a different account or workspace than this one is set up for. Switch to that account at the provider and try again.',\n app_not_set_up:\n 'The provider refused this integration’s app settings, so trying again won’t help. Ask an administrator to check the app’s scopes and credentials.',\n organization_access:\n 'Connect with personal access — this integration only uses your own account. Try again and choose personal access.',\n};\n\n/**\n * A `ConnectionCheckAction` backed by the hosted status route. The route\n * answers 200 with the standard envelope for both outcomes — `{ success:\n * true, data: { obtainedAt } }` or `{ success: false, error }` — and a non-OK\n * status only when the request itself is unauthorized, so a query error\n * surfaces as an error rather than as a misleading \"not connected\".\n */\nexport function createPartnerConnectionCheck(\n integrationSlug: string,\n options: PartnerOAuthPathOptions = {},\n): ConnectionCheckAction {\n return async ({ accountId }) => {\n const url = partnerOAuthStatusUrl(integrationSlug, {\n ...options,\n accountId,\n });\n\n const response = await globalThis.fetch(url, {\n headers: { accept: 'application/json' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Could not read the '${integrationSlug}' connection status (${response.status} ${response.statusText}).`,\n );\n }\n\n const payload: unknown = await response.json();\n\n if (!isConnectionCheckResult(payload)) {\n throw new Error(\n `The '${integrationSlug}' status route returned an unexpected payload.`,\n );\n }\n\n return payload;\n };\n}\n\nfunction isConnectionCheckResult(\n value: unknown,\n): value is ConnectionCheckResult {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { success?: unknown }).success === 'boolean'\n );\n}\n"]}
|
|
@@ -1 +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"]}
|
|
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 */\nimport { type UseMutationResult, useMutation } 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"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Result shape of an integration's check-connection server action
|
|
3
|
-
* (the standard
|
|
3
|
+
* (the standard server-action envelope).
|
|
4
4
|
*/
|
|
5
5
|
export interface ConnectionCheckResult {
|
|
6
6
|
success: boolean;
|
|
@@ -40,26 +40,6 @@ export interface UseOAuthConnectionStatusOptions {
|
|
|
40
40
|
* connected without a manual refresh.
|
|
41
41
|
*/
|
|
42
42
|
export declare function useOAuthConnectionStatus(accountId: string, options: UseOAuthConnectionStatusOptions): {
|
|
43
|
-
oauthJustCompleted: boolean;
|
|
44
|
-
oauthError: string | null;
|
|
45
|
-
data: {
|
|
46
|
-
isConnected: boolean;
|
|
47
|
-
error: string | undefined;
|
|
48
|
-
obtainedAt?: undefined;
|
|
49
|
-
} | {
|
|
50
|
-
isConnected: boolean;
|
|
51
|
-
obtainedAt: string | null | undefined;
|
|
52
|
-
error?: undefined;
|
|
53
|
-
};
|
|
54
|
-
error: Error;
|
|
55
|
-
isError: true;
|
|
56
|
-
isPending: false;
|
|
57
|
-
isLoading: false;
|
|
58
|
-
isLoadingError: false;
|
|
59
|
-
isRefetchError: true;
|
|
60
|
-
isSuccess: false;
|
|
61
|
-
isPlaceholderData: false;
|
|
62
|
-
status: "error";
|
|
63
43
|
dataUpdatedAt: number;
|
|
64
44
|
errorUpdatedAt: number;
|
|
65
45
|
failureCount: number;
|
|
@@ -78,32 +58,24 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
78
58
|
error: string | undefined;
|
|
79
59
|
obtainedAt?: undefined;
|
|
80
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
81
62
|
isConnected: boolean;
|
|
82
63
|
obtainedAt: string | null | undefined;
|
|
83
|
-
error?: undefined;
|
|
84
64
|
}, Error>>;
|
|
85
65
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
data: {
|
|
90
|
-
isConnected: boolean;
|
|
91
|
-
error: string | undefined;
|
|
92
|
-
obtainedAt?: undefined;
|
|
93
|
-
} | {
|
|
94
|
-
isConnected: boolean;
|
|
95
|
-
obtainedAt: string | null | undefined;
|
|
96
|
-
error?: undefined;
|
|
97
|
-
};
|
|
98
|
-
error: null;
|
|
99
|
-
isError: false;
|
|
66
|
+
data: undefined;
|
|
67
|
+
error: Error;
|
|
68
|
+
isError: true;
|
|
100
69
|
isPending: false;
|
|
101
70
|
isLoading: false;
|
|
102
|
-
isLoadingError:
|
|
71
|
+
isLoadingError: true;
|
|
103
72
|
isRefetchError: false;
|
|
104
|
-
isSuccess:
|
|
73
|
+
isSuccess: false;
|
|
105
74
|
isPlaceholderData: false;
|
|
106
|
-
status:
|
|
75
|
+
status: 'error';
|
|
76
|
+
oauthJustCompleted: boolean;
|
|
77
|
+
oauthError: string | null;
|
|
78
|
+
} | {
|
|
107
79
|
dataUpdatedAt: number;
|
|
108
80
|
errorUpdatedAt: number;
|
|
109
81
|
failureCount: number;
|
|
@@ -122,24 +94,24 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
122
94
|
error: string | undefined;
|
|
123
95
|
obtainedAt?: undefined;
|
|
124
96
|
} | {
|
|
97
|
+
error?: undefined;
|
|
125
98
|
isConnected: boolean;
|
|
126
99
|
obtainedAt: string | null | undefined;
|
|
127
|
-
error?: undefined;
|
|
128
100
|
}, Error>>;
|
|
129
101
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
130
|
-
} | {
|
|
131
|
-
oauthJustCompleted: boolean;
|
|
132
|
-
oauthError: string | null;
|
|
133
102
|
data: undefined;
|
|
134
|
-
error:
|
|
135
|
-
isError:
|
|
136
|
-
isPending:
|
|
137
|
-
isLoading:
|
|
138
|
-
isLoadingError:
|
|
103
|
+
error: null;
|
|
104
|
+
isError: false;
|
|
105
|
+
isPending: true;
|
|
106
|
+
isLoading: true;
|
|
107
|
+
isLoadingError: false;
|
|
139
108
|
isRefetchError: false;
|
|
140
109
|
isSuccess: false;
|
|
141
110
|
isPlaceholderData: false;
|
|
142
|
-
status:
|
|
111
|
+
status: 'pending';
|
|
112
|
+
oauthJustCompleted: boolean;
|
|
113
|
+
oauthError: string | null;
|
|
114
|
+
} | {
|
|
143
115
|
dataUpdatedAt: number;
|
|
144
116
|
errorUpdatedAt: number;
|
|
145
117
|
failureCount: number;
|
|
@@ -148,6 +120,7 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
148
120
|
isFetched: boolean;
|
|
149
121
|
isFetchedAfterMount: boolean;
|
|
150
122
|
isFetching: boolean;
|
|
123
|
+
isLoading: boolean;
|
|
151
124
|
isInitialLoading: boolean;
|
|
152
125
|
isPaused: boolean;
|
|
153
126
|
isRefetching: boolean;
|
|
@@ -158,24 +131,23 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
158
131
|
error: string | undefined;
|
|
159
132
|
obtainedAt?: undefined;
|
|
160
133
|
} | {
|
|
134
|
+
error?: undefined;
|
|
161
135
|
isConnected: boolean;
|
|
162
136
|
obtainedAt: string | null | undefined;
|
|
163
|
-
error?: undefined;
|
|
164
137
|
}, Error>>;
|
|
165
138
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
166
|
-
} | {
|
|
167
|
-
oauthJustCompleted: boolean;
|
|
168
|
-
oauthError: string | null;
|
|
169
139
|
data: undefined;
|
|
170
140
|
error: null;
|
|
171
141
|
isError: false;
|
|
172
142
|
isPending: true;
|
|
173
|
-
isLoading: true;
|
|
174
143
|
isLoadingError: false;
|
|
175
144
|
isRefetchError: false;
|
|
176
145
|
isSuccess: false;
|
|
177
146
|
isPlaceholderData: false;
|
|
178
|
-
status:
|
|
147
|
+
status: 'pending';
|
|
148
|
+
oauthJustCompleted: boolean;
|
|
149
|
+
oauthError: string | null;
|
|
150
|
+
} | {
|
|
179
151
|
dataUpdatedAt: number;
|
|
180
152
|
errorUpdatedAt: number;
|
|
181
153
|
failureCount: number;
|
|
@@ -194,23 +166,32 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
194
166
|
error: string | undefined;
|
|
195
167
|
obtainedAt?: undefined;
|
|
196
168
|
} | {
|
|
169
|
+
error?: undefined;
|
|
197
170
|
isConnected: boolean;
|
|
198
171
|
obtainedAt: string | null | undefined;
|
|
199
|
-
error?: undefined;
|
|
200
172
|
}, Error>>;
|
|
201
173
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
174
|
+
data: {
|
|
175
|
+
isConnected: boolean;
|
|
176
|
+
error: string | undefined;
|
|
177
|
+
obtainedAt?: undefined;
|
|
178
|
+
} | {
|
|
179
|
+
error?: undefined;
|
|
180
|
+
isConnected: boolean;
|
|
181
|
+
obtainedAt: string | null | undefined;
|
|
182
|
+
};
|
|
207
183
|
isError: false;
|
|
208
|
-
|
|
184
|
+
error: null;
|
|
185
|
+
isPending: false;
|
|
186
|
+
isLoading: false;
|
|
209
187
|
isLoadingError: false;
|
|
210
188
|
isRefetchError: false;
|
|
211
|
-
isSuccess:
|
|
212
|
-
isPlaceholderData:
|
|
213
|
-
status:
|
|
189
|
+
isSuccess: true;
|
|
190
|
+
isPlaceholderData: true;
|
|
191
|
+
status: 'success';
|
|
192
|
+
oauthJustCompleted: boolean;
|
|
193
|
+
oauthError: string | null;
|
|
194
|
+
} | {
|
|
214
195
|
dataUpdatedAt: number;
|
|
215
196
|
errorUpdatedAt: number;
|
|
216
197
|
failureCount: number;
|
|
@@ -219,7 +200,6 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
219
200
|
isFetched: boolean;
|
|
220
201
|
isFetchedAfterMount: boolean;
|
|
221
202
|
isFetching: boolean;
|
|
222
|
-
isLoading: boolean;
|
|
223
203
|
isInitialLoading: boolean;
|
|
224
204
|
isPaused: boolean;
|
|
225
205
|
isRefetching: boolean;
|
|
@@ -230,32 +210,32 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
230
210
|
error: string | undefined;
|
|
231
211
|
obtainedAt?: undefined;
|
|
232
212
|
} | {
|
|
213
|
+
error?: undefined;
|
|
233
214
|
isConnected: boolean;
|
|
234
215
|
obtainedAt: string | null | undefined;
|
|
235
|
-
error?: undefined;
|
|
236
216
|
}, Error>>;
|
|
237
217
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
238
|
-
} | {
|
|
239
|
-
oauthJustCompleted: boolean;
|
|
240
|
-
oauthError: string | null;
|
|
241
218
|
data: {
|
|
242
219
|
isConnected: boolean;
|
|
243
220
|
error: string | undefined;
|
|
244
221
|
obtainedAt?: undefined;
|
|
245
222
|
} | {
|
|
223
|
+
error?: undefined;
|
|
246
224
|
isConnected: boolean;
|
|
247
225
|
obtainedAt: string | null | undefined;
|
|
248
|
-
error?: undefined;
|
|
249
226
|
};
|
|
250
|
-
|
|
251
|
-
|
|
227
|
+
error: Error;
|
|
228
|
+
isError: true;
|
|
252
229
|
isPending: false;
|
|
253
230
|
isLoading: false;
|
|
254
231
|
isLoadingError: false;
|
|
255
|
-
isRefetchError:
|
|
256
|
-
isSuccess:
|
|
257
|
-
isPlaceholderData:
|
|
258
|
-
status:
|
|
232
|
+
isRefetchError: true;
|
|
233
|
+
isSuccess: false;
|
|
234
|
+
isPlaceholderData: false;
|
|
235
|
+
status: 'error';
|
|
236
|
+
oauthJustCompleted: boolean;
|
|
237
|
+
oauthError: string | null;
|
|
238
|
+
} | {
|
|
259
239
|
dataUpdatedAt: number;
|
|
260
240
|
errorUpdatedAt: number;
|
|
261
241
|
failureCount: number;
|
|
@@ -274,11 +254,31 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
274
254
|
error: string | undefined;
|
|
275
255
|
obtainedAt?: undefined;
|
|
276
256
|
} | {
|
|
257
|
+
error?: undefined;
|
|
277
258
|
isConnected: boolean;
|
|
278
259
|
obtainedAt: string | null | undefined;
|
|
279
|
-
error?: undefined;
|
|
280
260
|
}, Error>>;
|
|
281
261
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
262
|
+
data: {
|
|
263
|
+
isConnected: boolean;
|
|
264
|
+
error: string | undefined;
|
|
265
|
+
obtainedAt?: undefined;
|
|
266
|
+
} | {
|
|
267
|
+
error?: undefined;
|
|
268
|
+
isConnected: boolean;
|
|
269
|
+
obtainedAt: string | null | undefined;
|
|
270
|
+
};
|
|
271
|
+
error: null;
|
|
272
|
+
isError: false;
|
|
273
|
+
isPending: false;
|
|
274
|
+
isLoading: false;
|
|
275
|
+
isLoadingError: false;
|
|
276
|
+
isRefetchError: false;
|
|
277
|
+
isSuccess: true;
|
|
278
|
+
isPlaceholderData: false;
|
|
279
|
+
status: 'success';
|
|
280
|
+
oauthJustCompleted: boolean;
|
|
281
|
+
oauthError: string | null;
|
|
282
282
|
};
|
|
283
283
|
/**
|
|
284
284
|
* Resolves the gating booleans every OAuth-backed dashboard widget needs:
|
|
@@ -42,14 +42,18 @@ export function useOAuthConnectionStatus(accountId, options) {
|
|
|
42
42
|
refetchOnWindowFocus: false,
|
|
43
43
|
});
|
|
44
44
|
const errorParamName = errorParam === null || errorParam === void 0 ? void 0 : errorParam.name;
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
45
|
+
const returnedWithError = oauthError !== null;
|
|
46
|
+
// Justified useEffect: after the OAuth redirect lands back on this page —
|
|
47
|
+
// with `?oauth_success=true` OR with the integration's error param — we
|
|
48
|
+
// must (1) strip the transient query params from the URL, so a reload does
|
|
49
|
+
// not re-announce a stale success or re-show a stale error, and (2) on
|
|
50
|
+
// success invalidate the cached connection status so widgets flip to
|
|
51
|
+
// connected without a manual refresh. Both are post-render side effects
|
|
52
|
+
// on external systems (history API + query cache), not derivable during
|
|
53
|
+
// render. The error itself stays in state for this render, so the alert
|
|
54
|
+
// still shows once.
|
|
51
55
|
useEffect(() => {
|
|
52
|
-
if (!oauthJustCompleted)
|
|
56
|
+
if (!oauthJustCompleted && !returnedWithError)
|
|
53
57
|
return;
|
|
54
58
|
const url = new URL(window.location.href);
|
|
55
59
|
url.searchParams.delete('oauth_success');
|
|
@@ -57,11 +61,14 @@ export function useOAuthConnectionStatus(accountId, options) {
|
|
|
57
61
|
url.searchParams.delete(errorParamName);
|
|
58
62
|
}
|
|
59
63
|
window.history.replaceState({}, '', url.pathname + url.search);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
if (oauthJustCompleted) {
|
|
65
|
+
queryClient.invalidateQueries({
|
|
66
|
+
queryKey: ['integration-data', accountId, connectionKey],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
63
69
|
}, [
|
|
64
70
|
oauthJustCompleted,
|
|
71
|
+
returnedWithError,
|
|
65
72
|
accountId,
|
|
66
73
|
connectionKey,
|
|
67
74
|
errorParamName,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-oauth-connection-status.js","sourceRoot":"","sources":["../../src/hooks/use-oauth-connection-status.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAuCjE;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CACtC,SAAiB,EACjB,OAAwC;IAExC,MAAM,EACJ,aAAa,EACb,eAAe,EACf,OAAO,GAAG,IAAI,EACd,UAAU,GACX,GAAG,OAAO,CAAC;IACZ,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,yEAAyE;IACzE,0EAA0E;IAC1E,MAAM,CAAC,kBAAkB,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE;QACzC,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO,KAAK,CAAC;QAChD,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC3D,OAAO,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,MAAM,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAgB,GAAG,EAAE;QAChD,IAAI,CAAC,UAAU,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO,IAAI,CAAC;QAC9D,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAC3D,UAAU,CAAC,IAAI,CAChB,CAAC;QACF,OAAO,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,QAAQ,CAAC;QACrB,QAAQ,EAAE,CAAC,kBAAkB,EAAE,SAAS,EAAE,aAAa,CAAC;QACxD,OAAO,EAAE,KAAK,IAAI,EAAE;;YAClB,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;YAEpD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;YACrD,CAAC;YAED,OAAO;gBACL,WAAW,EAAE,IAAI;gBACjB,UAAU,EAAE,MAAA,MAAM,CAAC,IAAI,0CAAE,UAAU;aACpC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,SAAS;QAC/B,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI;QACxB,oBAAoB,EAAE,KAAK;KAC5B,CAAC,CAAC;IAEH,MAAM,cAAc,GAAG,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAE,IAAI,CAAC;IAExC,
|
|
1
|
+
{"version":3,"file":"use-oauth-connection-status.js","sourceRoot":"","sources":["../../src/hooks/use-oauth-connection-status.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nimport { useQuery, useQueryClient } from '@tanstack/react-query';\n\n/**\n * Result shape of an integration's check-connection server action\n * (the standard server-action envelope).\n */\nexport interface ConnectionCheckResult {\n success: boolean;\n data?: { obtainedAt?: string | null } | null;\n error?: string;\n}\n\nexport type ConnectionCheckAction = (input: {\n accountId: string;\n}) => Promise<ConnectionCheckResult>;\n\n/**\n * Optional OAuth failure query param an integration's connect route may\n * append on redirect (e.g. dropbox_error=credentials_not_configured).\n */\nexport interface OAuthErrorParam {\n name: string;\n /** Map the raw param value to a user-facing message; return null to ignore. */\n toMessage: (value: string) => string | null;\n}\n\nexport interface UseOAuthConnectionStatusOptions {\n /**\n * Cache key segment, by convention `<slug>-connection`. The full query key\n * is ['integration-data', accountId, connectionKey] so all widgets and the\n * activation form share one deduped request per dashboard load.\n */\n connectionKey: string;\n checkConnection: ConnectionCheckAction;\n /** Gate the query (e.g. on integration-active state). Defaults to true. */\n enabled?: boolean;\n errorParam?: OAuthErrorParam;\n}\n\n/**\n * Shared OAuth connection-status hook. Detects the `oauth_success=true`\n * return from an integration's connect route, strips the transient params\n * from the URL, and invalidates the cached status so dependent UI flips to\n * connected without a manual refresh.\n */\nexport function useOAuthConnectionStatus(\n accountId: string,\n options: UseOAuthConnectionStatusOptions,\n) {\n const {\n connectionKey,\n checkConnection,\n enabled = true,\n errorParam,\n } = options;\n const queryClient = useQueryClient();\n\n // Read the OAuth return params synchronously on mount (not in an effect)\n // so the first render already knows whether we just came back from OAuth.\n const [oauthJustCompleted] = useState(() => {\n if (typeof window === 'undefined') return false;\n const params = new URLSearchParams(window.location.search);\n return params.get('oauth_success') === 'true';\n });\n\n const [oauthError] = useState<string | null>(() => {\n if (!errorParam || typeof window === 'undefined') return null;\n const value = new URLSearchParams(window.location.search).get(\n errorParam.name,\n );\n return value ? errorParam.toMessage(value) : null;\n });\n\n const query = useQuery({\n queryKey: ['integration-data', accountId, connectionKey],\n queryFn: async () => {\n const result = await checkConnection({ accountId });\n\n if (!result.success) {\n return { isConnected: false, error: result.error };\n }\n\n return {\n isConnected: true,\n obtainedAt: result.data?.obtainedAt,\n };\n },\n enabled: enabled && !!accountId,\n staleTime: 5 * 60 * 1000,\n refetchOnWindowFocus: false,\n });\n\n const errorParamName = errorParam?.name;\n\n const returnedWithError = oauthError !== null;\n\n // Justified useEffect: after the OAuth redirect lands back on this page —\n // with `?oauth_success=true` OR with the integration's error param — we\n // must (1) strip the transient query params from the URL, so a reload does\n // not re-announce a stale success or re-show a stale error, and (2) on\n // success invalidate the cached connection status so widgets flip to\n // connected without a manual refresh. Both are post-render side effects\n // on external systems (history API + query cache), not derivable during\n // render. The error itself stays in state for this render, so the alert\n // still shows once.\n useEffect(() => {\n if (!oauthJustCompleted && !returnedWithError) return;\n\n const url = new URL(window.location.href);\n url.searchParams.delete('oauth_success');\n\n if (errorParamName) {\n url.searchParams.delete(errorParamName);\n }\n\n window.history.replaceState({}, '', url.pathname + url.search);\n\n if (oauthJustCompleted) {\n queryClient.invalidateQueries({\n queryKey: ['integration-data', accountId, connectionKey],\n });\n }\n }, [\n oauthJustCompleted,\n returnedWithError,\n accountId,\n connectionKey,\n errorParamName,\n queryClient,\n ]);\n\n return { ...query, oauthJustCompleted, oauthError };\n}\n\n/**\n * Resolves the gating booleans every OAuth-backed dashboard widget needs:\n *\n * - `enabled` — the integration is active and an account id is present.\n * - `isConnected` — a live OAuth connection exists (access token).\n * - `dataEnabled` — safe to fire the widget's data query (active + connected).\n * - `connectionPending` — still resolving the connection status.\n * - `connectionError` — the connection check itself failed (network/5xx).\n * Distinct from `notConnected`: a transient failure should surface as an\n * error, not as a misleading \"connect X\" prompt.\n * - `notConnected` — activated, resolved without error, but no OAuth\n * connection. The widget should render its \"connect X\" inactive state.\n *\n * The underlying connection check shares one query key across all widgets,\n * so react-query dedupes it to a single request per dashboard load.\n */\nexport function useOAuthWidgetGate(\n accountId: string | undefined,\n integrationActive: boolean,\n options: Pick<\n UseOAuthConnectionStatusOptions,\n 'connectionKey' | 'checkConnection'\n >,\n) {\n const enabled = integrationActive && !!accountId;\n const query = useOAuthConnectionStatus(accountId ?? '', {\n ...options,\n enabled,\n });\n\n const connectionPending = enabled && query.isPending;\n const connectionError = enabled && query.isError;\n const isConnected = query.data?.isConnected === true;\n const dataEnabled = enabled && isConnected;\n // Only treat as \"not connected\" when the check resolved successfully with\n // no token — a query error is an error state, not a disconnected account.\n const notConnected =\n enabled && !connectionPending && !connectionError && !isConnected;\n\n return {\n enabled,\n isConnected,\n connectionPending,\n connectionError,\n dataEnabled,\n notConnected,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"widget-event-context.js","sourceRoot":"","sources":["../../src/hooks/widget-event-context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAa,aAAa,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAa3E,MAAM,qBAAqB,GAAG,aAAa,CACzC,IAAI,CACL,CAAC;AAMF;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAE,QAAQ,EAA4B;IACxE,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,GAAG,EAAyB,CAAC,CAAC;IAChE,yEAAyE;IACzE,2EAA2E;IAC3E,yEAAyE;IACzE,6EAA6E;IAC7E,8EAA8E;IAC9E,4EAA4E;IAC5E,MAAM,kBAAkB,GAAG,MAAM,CAAC,IAAI,GAAG,EAAuB,CAAC,CAAC;IAElE,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,QAA+B,EAAE,EAAE;QAChE,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAErC,2EAA2E;QAC3E,qDAAqD;QACrD,KAAK,MAAM,KAAK,IAAI,kBAAkB,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YACxD,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,GAAG,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;QAED,OAAO,GAAG,EAAE;YACV,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,KAAwB,EAAE,EAAE;;QACvD,MAAM,KAAK,GAAgB;YACzB,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;YACvB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,IAAI,EAAE,QAAQ;YACd,UAAU,
|
|
1
|
+
{"version":3,"file":"widget-event-context.js","sourceRoot":"","sources":["../../src/hooks/widget-event-context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAa,aAAa,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAa3E,MAAM,qBAAqB,GAAG,aAAa,CACzC,IAAI,CACL,CAAC;AAMF;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAE,QAAQ,EAA4B;IACxE,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,GAAG,EAAyB,CAAC,CAAC;IAChE,yEAAyE;IACzE,2EAA2E;IAC3E,yEAAyE;IACzE,6EAA6E;IAC7E,8EAA8E;IAC9E,4EAA4E;IAC5E,MAAM,kBAAkB,GAAG,MAAM,CAAC,IAAI,GAAG,EAAuB,CAAC,CAAC;IAElE,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,QAA+B,EAAE,EAAE;QAChE,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAErC,2EAA2E;QAC3E,qDAAqD;QACrD,KAAK,MAAM,KAAK,IAAI,kBAAkB,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YACxD,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,GAAG,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;QAED,OAAO,GAAG,EAAE;YACV,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,KAAwB,EAAE,EAAE;;QACvD,MAAM,KAAK,GAAgB;YACzB,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;YACvB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,IAAI,EAAE,QAAQ;YACd,UAAU,QAAE,KAAK,CAAC,UAAU,mCAAI,KAAK;SACtC,CAAC;QAEF,wEAAwE;QACxE,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;YACrB,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;QAED,KAAK,MAAM,UAAU,IAAI,cAAc,CAAC,OAAO,EAAE,CAAC;YAChD,IAAI,CAAC;gBACH,UAAU,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,GAAG,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,CACL,KAAC,qBAAqB,IAAC,KAAK,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,YACjD,QAAQ,GACa,CACzB,CAAC;AACJ,CAAC;AAED,MAAM,QAAQ,GAA+B;IAC3C,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC;IACjB,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAE,CAAC;CAC1B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,OAAO,GAAG,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAE3C,OAAO,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,QAAQ,CAAC;AAC7B,CAAC","sourcesContent":["'use client';\n\nimport { ReactNode, createContext, use, useCallback, useRef } from 'react';\n\nimport type {\n PublishEventInput,\n WidgetEvent,\n WidgetEventSubscriber,\n} from './widget-event-types';\n\ninterface WidgetEventBusContextValue {\n publish: (input: PublishEventInput) => void;\n subscribe: (callback: WidgetEventSubscriber) => () => void;\n}\n\nconst WidgetEventBusContext = createContext<WidgetEventBusContextValue | null>(\n null,\n);\n\ninterface WidgetEventProviderProps {\n children: ReactNode;\n}\n\n/**\n * Provides the widget event bus to the dashboard tree.\n * Wrap around DashboardGrid so all widgets can publish and subscribe.\n */\nexport function WidgetEventProvider({ children }: WidgetEventProviderProps) {\n const subscribersRef = useRef(new Set<WidgetEventSubscriber>());\n // Latest REPLAYABLE event per subtype, replayed to late subscribers so a\n // widget that mounts AFTER a producer published (e.g. Deal Alerts mounting\n // after My Pipeline already loaded) still receives the most recent event\n // instead of hanging forever waiting for one that already fired. Only sticky\n // state events opt in via `replayable`; transient commands are never retained\n // here so a late mount / remount can't re-trigger them against stale state.\n const latestBySubtypeRef = useRef(new Map<string, WidgetEvent>());\n\n const subscribe = useCallback((callback: WidgetEventSubscriber) => {\n subscribersRef.current.add(callback);\n\n // Replay the latest retained event for each subtype to the new subscriber.\n // The subscriber's own filter decides what it keeps.\n for (const event of latestBySubtypeRef.current.values()) {\n try {\n callback(event);\n } catch (err) {\n console.error('[WidgetEventBus] Replay subscriber error:', err);\n }\n }\n\n return () => {\n subscribersRef.current.delete(callback);\n };\n }, []);\n\n const publish = useCallback((input: PublishEventInput) => {\n const event: WidgetEvent = {\n id: crypto.randomUUID(),\n subtype: input.subtype,\n sourceWidget: input.sourceWidget,\n sourceProduct: input.sourceProduct,\n payload: input.payload,\n timestamp: Date.now(),\n tier: 'client',\n replayable: input.replayable ?? false,\n };\n\n // Only sticky state events are retained for replay to late subscribers.\n if (event.replayable) {\n latestBySubtypeRef.current.set(event.subtype, event);\n }\n\n for (const subscriber of subscribersRef.current) {\n try {\n subscriber(event);\n } catch (err) {\n console.error('[WidgetEventBus] Subscriber error:', err);\n }\n }\n }, []);\n\n return (\n <WidgetEventBusContext value={{ publish, subscribe }}>\n {children}\n </WidgetEventBusContext>\n );\n}\n\nconst NOOP_BUS: WidgetEventBusContextValue = {\n publish: () => {},\n subscribe: () => () => {},\n};\n\n/**\n * Access the raw event bus for publishing and subscribing.\n * Returns a no-op bus when used outside a WidgetEventProvider\n * (e.g., admin widget preview panel).\n * Prefer `useWidgetEvents` for subscribing with filters.\n */\nexport function useWidgetEventBus() {\n const context = use(WidgetEventBusContext);\n\n return context ?? NOOP_BUS;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-surfaces.js","sourceRoot":"","sources":["../../src/integration/event-surfaces.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAE7C,MAAM,UAAU,wBAAwB,CACtC,UAAiC;;IAEjC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,QAAQ,
|
|
1
|
+
{"version":3,"file":"event-surfaces.js","sourceRoot":"","sources":["../../src/integration/event-surfaces.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAE7C,MAAM,UAAU,wBAAwB,CACtC,UAAiC;;IAEjC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,QAAQ,UAAI,UAAU,CAAC,SAAS,mCAAI,EAAE,EAAE,CAAC;QAClD,IAAI,CAAC;YACH,mBAAmB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,QAAQ,CAAC,IAAI,CACX,cAAc,QAAQ,CAAC,EAAE,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACzF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,MAAM,SAAG,UAAU,CAAC,MAAM,mCAAI,EAAE,CAAC;QACvC,MAAM,SAAS,GAAG;YAChB,CAAC,kBAAkB,EAAE,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,gBAAgB,CAAC;YAChE,CAAC,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;SACxC,CAAC;QAEX,KAAK,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,SAAS,EAAE,CAAC;YACrC,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,IAAI,CAAC;gBACH,OAAO,GAAG,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACzC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CACX,kBAAkB,KAAK,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACrF,CAAC;gBACF,SAAS;YACX,CAAC;YACD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;gBACnC,QAAQ,CAAC,IAAI,CACX,kBAAkB,KAAK,gBAAgB,MAAM,qBAAqB;oBAChE,iCAAiC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ;oBAC/D,8DAA8D;oBAC9D,mBAAmB,MAAM,wCAAwC;oBACjE,+DAA+D;oBAC/D,wBAAwB,CAC3B,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,uCAAuC,UAAU,CAAC,IAAI,MAAM;YAC1D,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CACnD,CAAC;IACJ,CAAC;AACH,CAAC","sourcesContent":["/**\n * SDK-layer validation of the three event surfaces (webhooks, schedules,\n * OAuth) — the cross-field rules the dependency-pure schema package cannot\n * own, layered on top of the canonical parse the same way\n * `parsePartnerIntegration` layers host workspace rules:\n *\n * - every `schedules[].cron` must be valid 5-field cron syntax\n * (`parseCronExpression`);\n * - the OAuth endpoints' origins must be covered by the definition's own\n * `egress` list, because the code exchange runs through the integration's\n * declared egress — an origin the reviewer never saw is an origin the\n * exchange must not reach.\n */\nimport type { IntegrationDefinition } from '@ekanos/integration-schema';\n\nimport { isEgressAllowed } from '../context/egress';\nimport { parseCronExpression } from './cron';\n\nexport function assertEventSurfacesValid(\n definition: IntegrationDefinition,\n): void {\n const problems: string[] = [];\n\n for (const schedule of definition.schedules ?? []) {\n try {\n parseCronExpression(schedule.cron);\n } catch (error) {\n problems.push(\n `schedules[\"${schedule.id}\"]: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n if (definition.oauth) {\n const egress = definition.egress ?? [];\n const endpoints = [\n ['authorizationUrl', definition.oauth.provider.authorizationUrl],\n ['tokenUrl', definition.oauth.provider.tokenUrl],\n ] as const;\n\n for (const [field, url] of endpoints) {\n let covered = false;\n try {\n covered = isEgressAllowed(url, egress);\n } catch (error) {\n problems.push(\n `oauth.provider.${field}: ${error instanceof Error ? error.message : String(error)}`,\n );\n continue;\n }\n if (!covered) {\n const origin = new URL(url).origin;\n problems.push(\n `oauth.provider.${field}: its origin ${origin} is not covered by ` +\n `this definition's egress list ${JSON.stringify(egress)}. The ` +\n `OAuth code exchange runs through the integration's declared ` +\n `egress, so add \"${origin}\" (or a covering \"*.\" wildcard entry) ` +\n `to \\`egress\\` — widening egress is a security-posture change ` +\n `reviewed at promotion.`,\n );\n }\n }\n }\n\n if (problems.length > 0) {\n throw new Error(\n `Invalid integration definition for \"${definition.slug}\":\\n` +\n problems.map((line) => ` - ${line}`).join('\\n'),\n );\n }\n}\n"]}
|
|
@@ -17,5 +17,5 @@
|
|
|
17
17
|
export { defineIntegration } from './define-integration.js';
|
|
18
18
|
export { parseCronExpression } from './cron.js';
|
|
19
19
|
export { PublicActionError } from '../errors/public-action-error.js';
|
|
20
|
-
export { IntegrationDefinitionSchema, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
|
|
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';
|
|
20
|
+
export { DEFAULT_SIGNATURE_TOLERANCE_SECONDS, IntegrationDefinitionSchema, PARTNER_STORAGE_LIMITS, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
|
|
21
|
+
export type { IntegrationDefinition, IntegrationComponentDeclarations, IntegrationProposals, IntegrationCapabilityDeclaration, IntegrationPermissionDeclaration, PartnerWidgetDeclaration, PartnerToolModule, PartnerToolParameters, PartnerActionDeclaration, ToolClassificationProposal, DefinitionCollisionInput, FirstPartyInventory, PartnerWebhookDeclaration, WebhookSignatureDeclaration, WebhookSignatureScheme, WebhookEvent, WebhookResult, PartnerScheduleDeclaration, ScheduleInvocation, ScheduleResult, PartnerOAuthDeclaration, OAuthProviderDeclaration, OAuthTokens, OnActivateHandler, } from '@ekanos/integration-schema';
|
|
@@ -23,5 +23,5 @@ export { parseCronExpression } from './cron.js';
|
|
|
23
23
|
// discipline — see the class's own doc comment
|
|
24
24
|
// (the widget-actions opaque-failure ruling).
|
|
25
25
|
export { PublicActionError } from '../errors/public-action-error.js';
|
|
26
|
-
export { IntegrationDefinitionSchema, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
|
|
26
|
+
export { DEFAULT_SIGNATURE_TOLERANCE_SECONDS, IntegrationDefinitionSchema, PARTNER_STORAGE_LIMITS, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
|
|
27
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,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"]}
|
|
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,mCAAmC,EACnC,2BAA2B,EAC3B,sBAAsB,EACtB,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 DEFAULT_SIGNATURE_TOLERANCE_SECONDS,\n IntegrationDefinitionSchema,\n PARTNER_STORAGE_LIMITS,\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 // Widget-invoked server handlers (`actions`): the declaration type a\n // partner needs to export a handler shared between an action and a tool.\n PartnerActionDeclaration,\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 WebhookSignatureScheme,\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"]}
|