@ekanos/sdk 0.1.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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/components/base-activation-dialog.d.ts +41 -0
- package/dist/components/base-activation-dialog.js +41 -0
- package/dist/components/base-activation-dialog.js.map +1 -0
- package/dist/components/base-activation-form.d.ts +36 -0
- package/dist/components/base-activation-form.js +21 -0
- package/dist/components/base-activation-form.js.map +1 -0
- package/dist/components/base-marketplace-tile.d.ts +34 -0
- package/dist/components/base-marketplace-tile.js +40 -0
- package/dist/components/base-marketplace-tile.js.map +1 -0
- package/dist/components/current-return-path.d.ts +19 -0
- package/dist/components/current-return-path.js +26 -0
- package/dist/components/current-return-path.js.map +1 -0
- package/dist/components/index.d.ts +40 -0
- package/dist/components/index.js +38 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/oauth-activation-form.d.ts +50 -0
- package/dist/components/oauth-activation-form.js +119 -0
- package/dist/components/oauth-activation-form.js.map +1 -0
- package/dist/components/widgets/ai-prompt-chip.d.ts +31 -0
- package/dist/components/widgets/ai-prompt-chip.js +38 -0
- package/dist/components/widgets/ai-prompt-chip.js.map +1 -0
- package/dist/components/widgets/ask-icon.d.ts +3 -0
- package/dist/components/widgets/ask-icon.js +5 -0
- package/dist/components/widgets/ask-icon.js.map +1 -0
- package/dist/components/widgets/use-animated-height.d.ts +12 -0
- package/dist/components/widgets/use-animated-height.js +39 -0
- package/dist/components/widgets/use-animated-height.js.map +1 -0
- package/dist/components/widgets/widget-ask-bar.d.ts +18 -0
- package/dist/components/widgets/widget-ask-bar.js +62 -0
- package/dist/components/widgets/widget-ask-bar.js.map +1 -0
- package/dist/components/widgets/widget-context.d.ts +38 -0
- package/dist/components/widgets/widget-context.js +4 -0
- package/dist/components/widgets/widget-context.js.map +1 -0
- package/dist/components/widgets/widget-state-components.d.ts +5 -0
- package/dist/components/widgets/widget-state-components.js +19 -0
- package/dist/components/widgets/widget-state-components.js.map +1 -0
- package/dist/components/widgets/widget.d.ts +91 -0
- package/dist/components/widgets/widget.js +205 -0
- package/dist/components/widgets/widget.js.map +1 -0
- package/dist/context/egress.d.ts +30 -0
- package/dist/context/egress.js +81 -0
- package/dist/context/egress.js.map +1 -0
- package/dist/context/errors.d.ts +57 -0
- package/dist/context/errors.js +78 -0
- package/dist/context/errors.js.map +1 -0
- package/dist/context/index.d.ts +19 -0
- package/dist/context/index.js +20 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/require-context.d.ts +26 -0
- package/dist/context/require-context.js +36 -0
- package/dist/context/require-context.js.map +1 -0
- package/dist/context/storage-validation.d.ts +71 -0
- package/dist/context/storage-validation.js +132 -0
- package/dist/context/storage-validation.js.map +1 -0
- package/dist/context/types.d.ts +11 -0
- package/dist/context/types.js +2 -0
- package/dist/context/types.js.map +1 -0
- package/dist/hooks/activation-actions-context.d.ts +80 -0
- package/dist/hooks/activation-actions-context.js +31 -0
- package/dist/hooks/activation-actions-context.js.map +1 -0
- package/dist/hooks/ask-assistant-context.d.ts +32 -0
- package/dist/hooks/ask-assistant-context.js +56 -0
- package/dist/hooks/ask-assistant-context.js.map +1 -0
- package/dist/hooks/fetch-integration-storage.d.ts +48 -0
- package/dist/hooks/fetch-integration-storage.js +68 -0
- package/dist/hooks/fetch-integration-storage.js.map +1 -0
- package/dist/hooks/index.d.ts +28 -0
- package/dist/hooks/index.js +32 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-activate-integration.d.ts +15 -0
- package/dist/hooks/use-activate-integration.js +61 -0
- package/dist/hooks/use-activate-integration.js.map +1 -0
- package/dist/hooks/use-oauth-connection-status.d.ts +360 -0
- package/dist/hooks/use-oauth-connection-status.js +108 -0
- package/dist/hooks/use-oauth-connection-status.js.map +1 -0
- package/dist/hooks/use-widget-events.d.ts +20 -0
- package/dist/hooks/use-widget-events.js +51 -0
- package/dist/hooks/use-widget-events.js.map +1 -0
- package/dist/hooks/widget-event-context.d.ts +22 -0
- package/dist/hooks/widget-event-context.js +75 -0
- package/dist/hooks/widget-event-context.js.map +1 -0
- package/dist/hooks/widget-event-types.d.ts +55 -0
- package/dist/hooks/widget-event-types.js +9 -0
- package/dist/hooks/widget-event-types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/cron.d.ts +29 -0
- package/dist/integration/cron.js +102 -0
- package/dist/integration/cron.js.map +1 -0
- package/dist/integration/define-integration.d.ts +28 -0
- package/dist/integration/define-integration.js +41 -0
- package/dist/integration/define-integration.js.map +1 -0
- package/dist/integration/event-surfaces.d.ts +15 -0
- package/dist/integration/event-surfaces.js +45 -0
- package/dist/integration/event-surfaces.js.map +1 -0
- package/dist/integration/index.d.ts +20 -0
- package/dist/integration/index.js +23 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/types.d.ts +7 -0
- package/dist/integration/types.js +2 -0
- package/dist/integration/types.js.map +1 -0
- package/dist/mcp/guards.d.ts +40 -0
- package/dist/mcp/guards.js +99 -0
- package/dist/mcp/guards.js.map +1 -0
- package/dist/mcp/index.d.ts +22 -0
- package/dist/mcp/index.js +22 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/ownership.d.ts +74 -0
- package/dist/mcp/ownership.js +83 -0
- package/dist/mcp/ownership.js.map +1 -0
- package/dist/mcp/types.d.ts +145 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/invoke.d.ts +77 -0
- package/dist/testing/invoke.js +83 -0
- package/dist/testing/invoke.js.map +1 -0
- package/dist/testing/mock-context.d.ts +79 -0
- package/dist/testing/mock-context.js +206 -0
- package/dist/testing/mock-context.js.map +1 -0
- package/dist/types/icon.d.ts +12 -0
- package/dist/types/icon.js +2 -0
- package/dist/types/icon.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/integration.d.ts +258 -0
- package/dist/types/integration.js +2 -0
- package/dist/types/integration.js.map +1 -0
- package/dist/types/widget-ask-context.d.ts +36 -0
- package/dist/types/widget-ask-context.js +2 -0
- package/dist/types/widget-ask-context.js.map +1 -0
- package/dist/types/workspace-target.d.ts +33 -0
- package/dist/types/workspace-target.js +2 -0
- package/dist/types/workspace-target.js.map +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WidgetEvent, WidgetEventFilter } from './widget-event-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Subscribe to widget events with optional filtering.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```tsx
|
|
7
|
+
* const { latestEvent } = useWidgetEvents({
|
|
8
|
+
* subtypes: ['deal_selected', 'deal_deselected'],
|
|
9
|
+
* });
|
|
10
|
+
*
|
|
11
|
+
* if (latestEvent?.subtype === 'deal_selected') {
|
|
12
|
+
* const { contactId } = latestEvent.payload as { contactId: string };
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function useWidgetEvents(filter?: WidgetEventFilter): {
|
|
17
|
+
latestEvent: WidgetEvent | null;
|
|
18
|
+
events: WidgetEvent[];
|
|
19
|
+
clear: () => void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useWidgetEventBus } from './widget-event-context.js';
|
|
4
|
+
const INITIAL_STATE = { latestEvent: null, events: [] };
|
|
5
|
+
const MAX_EVENT_HISTORY = 50;
|
|
6
|
+
/**
|
|
7
|
+
* Subscribe to widget events with optional filtering.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const { latestEvent } = useWidgetEvents({
|
|
12
|
+
* subtypes: ['deal_selected', 'deal_deselected'],
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* if (latestEvent?.subtype === 'deal_selected') {
|
|
16
|
+
* const { contactId } = latestEvent.payload as { contactId: string };
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useWidgetEvents(filter) {
|
|
21
|
+
const { subscribe } = useWidgetEventBus();
|
|
22
|
+
const [state, setState] = useState(INITIAL_STATE);
|
|
23
|
+
// Stabilize filter reference to avoid re-subscribing on every render
|
|
24
|
+
const filterRef = useRef(filter);
|
|
25
|
+
// Effect is required: synchronizing a ref from render props for use in a stable callback
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
filterRef.current = filter;
|
|
28
|
+
});
|
|
29
|
+
const handleEvent = useCallback((event) => {
|
|
30
|
+
const f = filterRef.current;
|
|
31
|
+
if ((f === null || f === void 0 ? void 0 : f.subtypes) && !f.subtypes.includes(event.subtype))
|
|
32
|
+
return;
|
|
33
|
+
if ((f === null || f === void 0 ? void 0 : f.sourceProducts) && !f.sourceProducts.includes(event.sourceProduct))
|
|
34
|
+
return;
|
|
35
|
+
if ((f === null || f === void 0 ? void 0 : f.sourceWidgets) && !f.sourceWidgets.includes(event.sourceWidget))
|
|
36
|
+
return;
|
|
37
|
+
setState((prev) => ({
|
|
38
|
+
latestEvent: event,
|
|
39
|
+
events: [event, ...prev.events].slice(0, MAX_EVENT_HISTORY),
|
|
40
|
+
}));
|
|
41
|
+
}, []);
|
|
42
|
+
// Effect is required: subscribing to an external event source (the bus) on mount
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
return subscribe(handleEvent);
|
|
45
|
+
}, [subscribe, handleEvent]);
|
|
46
|
+
const clear = useCallback(() => {
|
|
47
|
+
setState(INITIAL_STATE);
|
|
48
|
+
}, []);
|
|
49
|
+
return { latestEvent: state.latestEvent, events: state.events, clear };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=use-widget-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-widget-events.js","sourceRoot":"","sources":["../../src/hooks/use-widget-events.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAQ3D,MAAM,aAAa,GAAqB,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAC1E,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,MAA0B;IAKxD,MAAM,EAAE,SAAS,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAC1C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAmB,aAAa,CAAC,CAAC;IAEpE,qEAAqE;IACrE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAEjC,yFAAyF;IACzF,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,WAAW,CAAC,CAAC,KAAkB,EAAE,EAAE;QACrD,MAAM,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC;QAE5B,IAAI,CAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,QAAQ,KAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,OAAO;QAC/D,IAAI,CAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,cAAc,KAAI,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC;YACtE,OAAO;QACT,IAAI,CAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,aAAa,KAAI,CAAC,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC;YACnE,OAAO;QAET,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAClB,WAAW,EAAE,KAAK;YAClB,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,iBAAiB,CAAC;SAC5D,CAAC,CAAC,CAAC;IACN,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,iFAAiF;IACjF,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,SAAS,CAAC,WAAW,CAAC,CAAC;IAChC,CAAC,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC;IAE7B,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,QAAQ,CAAC,aAAa,CAAC,CAAC;IAC1B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;AACzE,CAAC","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useWidgetEventBus } from './widget-event-context';\nimport type { WidgetEvent, WidgetEventFilter } from './widget-event-types';\n\ninterface WidgetEventState {\n latestEvent: WidgetEvent | null;\n events: WidgetEvent[];\n}\n\nconst INITIAL_STATE: WidgetEventState = { latestEvent: null, events: [] };\nconst MAX_EVENT_HISTORY = 50;\n\n/**\n * Subscribe to widget events with optional filtering.\n *\n * @example\n * ```tsx\n * const { latestEvent } = useWidgetEvents({\n * subtypes: ['deal_selected', 'deal_deselected'],\n * });\n *\n * if (latestEvent?.subtype === 'deal_selected') {\n * const { contactId } = latestEvent.payload as { contactId: string };\n * }\n * ```\n */\nexport function useWidgetEvents(filter?: WidgetEventFilter): {\n latestEvent: WidgetEvent | null;\n events: WidgetEvent[];\n clear: () => void;\n} {\n const { subscribe } = useWidgetEventBus();\n const [state, setState] = useState<WidgetEventState>(INITIAL_STATE);\n\n // Stabilize filter reference to avoid re-subscribing on every render\n const filterRef = useRef(filter);\n\n // Effect is required: synchronizing a ref from render props for use in a stable callback\n useEffect(() => {\n filterRef.current = filter;\n });\n\n const handleEvent = useCallback((event: WidgetEvent) => {\n const f = filterRef.current;\n\n if (f?.subtypes && !f.subtypes.includes(event.subtype)) return;\n if (f?.sourceProducts && !f.sourceProducts.includes(event.sourceProduct))\n return;\n if (f?.sourceWidgets && !f.sourceWidgets.includes(event.sourceWidget))\n return;\n\n setState((prev) => ({\n latestEvent: event,\n events: [event, ...prev.events].slice(0, MAX_EVENT_HISTORY),\n }));\n }, []);\n\n // Effect is required: subscribing to an external event source (the bus) on mount\n useEffect(() => {\n return subscribe(handleEvent);\n }, [subscribe, handleEvent]);\n\n const clear = useCallback(() => {\n setState(INITIAL_STATE);\n }, []);\n\n return { latestEvent: state.latestEvent, events: state.events, clear };\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import type { PublishEventInput, WidgetEventSubscriber } from './widget-event-types.js';
|
|
3
|
+
interface WidgetEventBusContextValue {
|
|
4
|
+
publish: (input: PublishEventInput) => void;
|
|
5
|
+
subscribe: (callback: WidgetEventSubscriber) => () => void;
|
|
6
|
+
}
|
|
7
|
+
interface WidgetEventProviderProps {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Provides the widget event bus to the dashboard tree.
|
|
12
|
+
* Wrap around DashboardGrid so all widgets can publish and subscribe.
|
|
13
|
+
*/
|
|
14
|
+
export declare function WidgetEventProvider({ children }: WidgetEventProviderProps): import("react").JSX.Element;
|
|
15
|
+
/**
|
|
16
|
+
* Access the raw event bus for publishing and subscribing.
|
|
17
|
+
* Returns a no-op bus when used outside a WidgetEventProvider
|
|
18
|
+
* (e.g., admin widget preview panel).
|
|
19
|
+
* Prefer `useWidgetEvents` for subscribing with filters.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useWidgetEventBus(): WidgetEventBusContextValue;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, use, useCallback, useRef } from 'react';
|
|
4
|
+
const WidgetEventBusContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* Provides the widget event bus to the dashboard tree.
|
|
7
|
+
* Wrap around DashboardGrid so all widgets can publish and subscribe.
|
|
8
|
+
*/
|
|
9
|
+
export function WidgetEventProvider({ children }) {
|
|
10
|
+
const subscribersRef = useRef(new Set());
|
|
11
|
+
// Latest REPLAYABLE event per subtype, replayed to late subscribers so a
|
|
12
|
+
// widget that mounts AFTER a producer published (e.g. Deal Alerts mounting
|
|
13
|
+
// after My Pipeline already loaded) still receives the most recent event
|
|
14
|
+
// instead of hanging forever waiting for one that already fired. Only sticky
|
|
15
|
+
// state events opt in via `replayable`; transient commands are never retained
|
|
16
|
+
// here so a late mount / remount can't re-trigger them against stale state.
|
|
17
|
+
const latestBySubtypeRef = useRef(new Map());
|
|
18
|
+
const subscribe = useCallback((callback) => {
|
|
19
|
+
subscribersRef.current.add(callback);
|
|
20
|
+
// Replay the latest retained event for each subtype to the new subscriber.
|
|
21
|
+
// The subscriber's own filter decides what it keeps.
|
|
22
|
+
for (const event of latestBySubtypeRef.current.values()) {
|
|
23
|
+
try {
|
|
24
|
+
callback(event);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.error('[WidgetEventBus] Replay subscriber error:', err);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return () => {
|
|
31
|
+
subscribersRef.current.delete(callback);
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
const publish = useCallback((input) => {
|
|
35
|
+
var _a;
|
|
36
|
+
const event = {
|
|
37
|
+
id: crypto.randomUUID(),
|
|
38
|
+
subtype: input.subtype,
|
|
39
|
+
sourceWidget: input.sourceWidget,
|
|
40
|
+
sourceProduct: input.sourceProduct,
|
|
41
|
+
payload: input.payload,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
tier: 'client',
|
|
44
|
+
replayable: (_a = input.replayable) !== null && _a !== void 0 ? _a : false,
|
|
45
|
+
};
|
|
46
|
+
// Only sticky state events are retained for replay to late subscribers.
|
|
47
|
+
if (event.replayable) {
|
|
48
|
+
latestBySubtypeRef.current.set(event.subtype, event);
|
|
49
|
+
}
|
|
50
|
+
for (const subscriber of subscribersRef.current) {
|
|
51
|
+
try {
|
|
52
|
+
subscriber(event);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error('[WidgetEventBus] Subscriber error:', err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, []);
|
|
59
|
+
return (_jsx(WidgetEventBusContext, { value: { publish, subscribe }, children: children }));
|
|
60
|
+
}
|
|
61
|
+
const NOOP_BUS = {
|
|
62
|
+
publish: () => { },
|
|
63
|
+
subscribe: () => () => { },
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Access the raw event bus for publishing and subscribing.
|
|
67
|
+
* Returns a no-op bus when used outside a WidgetEventProvider
|
|
68
|
+
* (e.g., admin widget preview panel).
|
|
69
|
+
* Prefer `useWidgetEvents` for subscribing with filters.
|
|
70
|
+
*/
|
|
71
|
+
export function useWidgetEventBus() {
|
|
72
|
+
const context = use(WidgetEventBusContext);
|
|
73
|
+
return context !== null && context !== void 0 ? context : NOOP_BUS;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=widget-event-context.js.map
|
|
@@ -0,0 +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,EAAE,MAAA,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"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget Event Bus — Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Generic event system for cross-widget communication.
|
|
5
|
+
* Events flow through a client-side pub/sub bus (Tier 1)
|
|
6
|
+
* and optionally through Supabase Realtime (Tier 2, future).
|
|
7
|
+
*/
|
|
8
|
+
/** A widget event flowing through the bus */
|
|
9
|
+
export interface WidgetEvent {
|
|
10
|
+
/** Unique event ID */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Event name, e.g. 'deal_selected', 'document_uploaded' */
|
|
13
|
+
subtype: string;
|
|
14
|
+
/** Widget ID that published this event */
|
|
15
|
+
sourceWidget: string;
|
|
16
|
+
/** Product slug that owns the source widget */
|
|
17
|
+
sourceProduct: string;
|
|
18
|
+
/** Event-specific data */
|
|
19
|
+
payload: Record<string, unknown>;
|
|
20
|
+
/** Unix timestamp in milliseconds */
|
|
21
|
+
timestamp: number;
|
|
22
|
+
/** Which tier produced this event */
|
|
23
|
+
tier: 'client' | 'database';
|
|
24
|
+
/**
|
|
25
|
+
* When true, the bus retains this event as the latest for its subtype and
|
|
26
|
+
* replays it to subscribers that mount afterward (sticky state). Leave false
|
|
27
|
+
* for transient commands (e.g. open-dialog, preview) so they are never
|
|
28
|
+
* re-delivered on a late mount / remount and re-executed against stale state.
|
|
29
|
+
*/
|
|
30
|
+
replayable: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Input for publishing an event (id, timestamp, and tier are auto-generated) */
|
|
33
|
+
export interface PublishEventInput {
|
|
34
|
+
subtype: string;
|
|
35
|
+
sourceWidget: string;
|
|
36
|
+
sourceProduct: string;
|
|
37
|
+
payload: Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Mark this event as sticky so late subscribers receive the most recent one.
|
|
40
|
+
* Only set for events that represent durable state (e.g. a loaded list),
|
|
41
|
+
* never for one-shot commands. Defaults to false.
|
|
42
|
+
*/
|
|
43
|
+
replayable?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** Filter options for subscribing to events */
|
|
46
|
+
export interface WidgetEventFilter {
|
|
47
|
+
/** Only receive events with these subtypes */
|
|
48
|
+
subtypes?: string[];
|
|
49
|
+
/** Only receive events from these product slugs */
|
|
50
|
+
sourceProducts?: string[];
|
|
51
|
+
/** Only receive events from these widget IDs */
|
|
52
|
+
sourceWidgets?: string[];
|
|
53
|
+
}
|
|
54
|
+
/** Subscriber callback */
|
|
55
|
+
export type WidgetEventSubscriber = (event: WidgetEvent) => void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget Event Bus — Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Generic event system for cross-widget communication.
|
|
5
|
+
* Events flow through a client-side pub/sub bus (Tier 1)
|
|
6
|
+
* and optionally through Supabase Realtime (Tier 2, future).
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=widget-event-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-event-types.js","sourceRoot":"","sources":["../../src/hooks/widget-event-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * Widget Event Bus — Type Definitions\n *\n * Generic event system for cross-widget communication.\n * Events flow through a client-side pub/sub bus (Tier 1)\n * and optionally through Supabase Realtime (Tier 2, future).\n */\n\n/** A widget event flowing through the bus */\nexport interface WidgetEvent {\n /** Unique event ID */\n id: string;\n /** Event name, e.g. 'deal_selected', 'document_uploaded' */\n subtype: string;\n /** Widget ID that published this event */\n sourceWidget: string;\n /** Product slug that owns the source widget */\n sourceProduct: string;\n /** Event-specific data */\n payload: Record<string, unknown>;\n /** Unix timestamp in milliseconds */\n timestamp: number;\n /** Which tier produced this event */\n tier: 'client' | 'database';\n /**\n * When true, the bus retains this event as the latest for its subtype and\n * replays it to subscribers that mount afterward (sticky state). Leave false\n * for transient commands (e.g. open-dialog, preview) so they are never\n * re-delivered on a late mount / remount and re-executed against stale state.\n */\n replayable: boolean;\n}\n\n/** Input for publishing an event (id, timestamp, and tier are auto-generated) */\nexport interface PublishEventInput {\n subtype: string;\n sourceWidget: string;\n sourceProduct: string;\n payload: Record<string, unknown>;\n /**\n * Mark this event as sticky so late subscribers receive the most recent one.\n * Only set for events that represent durable state (e.g. a loaded list),\n * never for one-shot commands. Defaults to false.\n */\n replayable?: boolean;\n}\n\n/** Filter options for subscribing to events */\nexport interface WidgetEventFilter {\n /** Only receive events with these subtypes */\n subtypes?: string[];\n /** Only receive events from these product slugs */\n sourceProducts?: string[];\n /** Only receive events from these widget IDs */\n sourceWidgets?: string[];\n}\n\n/** Subscriber callback */\nexport type WidgetEventSubscriber = (event: WidgetEvent) => void;\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ekanos/sdk — the integration contract.
|
|
3
|
+
*
|
|
4
|
+
* Types only. This entrypoint is importable from any module, client or
|
|
5
|
+
* server, and must stay that way: every export below is a `type` re-export,
|
|
6
|
+
* so nothing here can pull runtime code into a client bundle. Runtime lives
|
|
7
|
+
* in `@ekanos/sdk/components`, `@ekanos/sdk/hooks`, and `@ekanos/sdk/mcp`.
|
|
8
|
+
*
|
|
9
|
+
* The SDK OWNS these definitions (`./types` — nothing imported from `@kit/*`);
|
|
10
|
+
* `@kit/integrations-core` keeps copies of some for now, and the parity guard
|
|
11
|
+
* (`packages/integrations/core/src/core/__tests__/sdk-parity.test-d.ts` —
|
|
12
|
+
* core-side since the inversion, because the dependency arrow is core → SDK)
|
|
13
|
+
* fails core's typecheck on any drift.
|
|
14
|
+
*
|
|
15
|
+
* Surface discipline: additions require a second consumer and a line in
|
|
16
|
+
* docs/devex/sdk-export-map.md. Keep the whole surface readable in one pass.
|
|
17
|
+
*/
|
|
18
|
+
export type { IntegrationMetadata, IntegrationCapability, IntegrationPermission, IntegrationConfig, IntegrationComponentProps, ActivationFormProps, ActivationResult, MarketplaceTileProps, WidgetConfig, WorkspaceTargetDefinition, WidgetAskContext, WidgetSnapshot, WidgetSuggestedPrompt, HealthCheckResult, HealthStatus, AccountProductData, AccountProductDataType, } from './types/index.js';
|
|
19
|
+
export type { IntegrationContext, IntegrationActor, IntegrationStorage, ScopedStore, IntegrationSecrets, IntegrationFetch, IntegrationLogger, StorageEntry, StorageWriteOptions, StorageSchemas, StorageSchemaMap, StorageScopeSchemas, StorageKeyDeclaration, StorageKeyDeclarationInput, StorageKeySchema, } from './context/types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ekanos/sdk — the integration contract.
|
|
3
|
+
*
|
|
4
|
+
* Types only. This entrypoint is importable from any module, client or
|
|
5
|
+
* server, and must stay that way: every export below is a `type` re-export,
|
|
6
|
+
* so nothing here can pull runtime code into a client bundle. Runtime lives
|
|
7
|
+
* in `@ekanos/sdk/components`, `@ekanos/sdk/hooks`, and `@ekanos/sdk/mcp`.
|
|
8
|
+
*
|
|
9
|
+
* The SDK OWNS these definitions (`./types` — nothing imported from `@kit/*`);
|
|
10
|
+
* `@kit/integrations-core` keeps copies of some for now, and the parity guard
|
|
11
|
+
* (`packages/integrations/core/src/core/__tests__/sdk-parity.test-d.ts` —
|
|
12
|
+
* core-side since the inversion, because the dependency arrow is core → SDK)
|
|
13
|
+
* fails core's typecheck on any drift.
|
|
14
|
+
*
|
|
15
|
+
* Surface discipline: additions require a second consumer and a line in
|
|
16
|
+
* docs/devex/sdk-export-map.md. Keep the whole surface readable in one pass.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG","sourcesContent":["/**\n * @ekanos/sdk — the integration contract.\n *\n * Types only. This entrypoint is importable from any module, client or\n * server, and must stay that way: every export below is a `type` re-export,\n * so nothing here can pull runtime code into a client bundle. Runtime lives\n * in `@ekanos/sdk/components`, `@ekanos/sdk/hooks`, and `@ekanos/sdk/mcp`.\n *\n * The SDK OWNS these definitions (`./types` — nothing imported from `@kit/*`);\n * `@kit/integrations-core` keeps copies of some for now, and the parity guard\n * (`packages/integrations/core/src/core/__tests__/sdk-parity.test-d.ts` —\n * core-side since the inversion, because the dependency arrow is core → SDK)\n * fails core's typecheck on any drift.\n *\n * Surface discipline: additions require a second consumer and a line in\n * docs/devex/sdk-export-map.md. Keep the whole surface readable in one pass.\n */\n\nexport type {\n // The integration definition\n IntegrationMetadata,\n IntegrationCapability,\n IntegrationPermission,\n IntegrationConfig,\n\n // Component contracts\n IntegrationComponentProps,\n ActivationFormProps,\n ActivationResult,\n MarketplaceTileProps,\n WidgetConfig,\n WorkspaceTargetDefinition,\n\n // AI ask-footer contract\n WidgetAskContext,\n WidgetSnapshot,\n WidgetSuggestedPrompt,\n\n // Health contract (required to implement checkHealth)\n HealthCheckResult,\n HealthStatus,\n\n // Account-scoped data seam\n AccountProductData,\n AccountProductDataType,\n} from './types';\n\nexport type {\n // The capability context (docs/devex/capability-context-proposal.md) —\n // handed to MCP tool run() and server handlers; the mock lives in\n // @ekanos/sdk/testing, the shared runtime values in @ekanos/sdk/context.\n IntegrationContext,\n IntegrationActor,\n IntegrationStorage,\n ScopedStore,\n IntegrationSecrets,\n IntegrationFetch,\n IntegrationLogger,\n StorageEntry,\n StorageWriteOptions,\n\n // Storage schema declaration (ruling 1) — consumed by defineIntegration()\n // and createMockContext()\n StorageSchemas,\n StorageSchemaMap,\n StorageScopeSchemas,\n\n // Per-key declaration form carrying exposure flags. `clientReadable`\n // (default false) is what opts a key in to the generic browser-readable\n // storage route — see StorageKeyDeclaration for the full rationale.\n StorageKeyDeclaration,\n StorageKeyDeclarationInput,\n StorageKeySchema,\n} from './context/types';\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, dependency-free 5-field cron validator.
|
|
3
|
+
*
|
|
4
|
+
* Lives at the SDK layer on purpose: the dependency-pure schema package
|
|
5
|
+
* checks only that `schedules[].cron` is a non-empty string, and
|
|
6
|
+
* `defineIntegration()` layers full syntax validation on top — the same
|
|
7
|
+
* split the workspace-target host rules use. The HOST scheduler binds to
|
|
8
|
+
* the same 5-field grammar later, so an expression this parser accepts is
|
|
9
|
+
* an expression the platform will run.
|
|
10
|
+
*
|
|
11
|
+
* Grammar (numeric, standard cron):
|
|
12
|
+
* field := element ("," element)*
|
|
13
|
+
* element := "*" | value | range — each optionally "/" step
|
|
14
|
+
* range := value "-" value
|
|
15
|
+
* Fields and bounds: minute 0-59, hour 0-23, day-of-month 1-31, month 1-12,
|
|
16
|
+
* day-of-week 0-7 (both 0 and 7 mean Sunday). No names (JAN/MON), no
|
|
17
|
+
* @-macros, no seconds field — declare what a plain cron daemon runs.
|
|
18
|
+
*/
|
|
19
|
+
export interface CronField {
|
|
20
|
+
name: string;
|
|
21
|
+
min: number;
|
|
22
|
+
max: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Validates a 5-field cron expression, throwing a `TypeError` whose message
|
|
26
|
+
* is a remediation instruction. Returns the expression's fields (trimmed)
|
|
27
|
+
* so callers can display them.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseCronExpression(expression: string): string[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, dependency-free 5-field cron validator.
|
|
3
|
+
*
|
|
4
|
+
* Lives at the SDK layer on purpose: the dependency-pure schema package
|
|
5
|
+
* checks only that `schedules[].cron` is a non-empty string, and
|
|
6
|
+
* `defineIntegration()` layers full syntax validation on top — the same
|
|
7
|
+
* split the workspace-target host rules use. The HOST scheduler binds to
|
|
8
|
+
* the same 5-field grammar later, so an expression this parser accepts is
|
|
9
|
+
* an expression the platform will run.
|
|
10
|
+
*
|
|
11
|
+
* Grammar (numeric, standard cron):
|
|
12
|
+
* field := element ("," element)*
|
|
13
|
+
* element := "*" | value | range — each optionally "/" step
|
|
14
|
+
* range := value "-" value
|
|
15
|
+
* Fields and bounds: minute 0-59, hour 0-23, day-of-month 1-31, month 1-12,
|
|
16
|
+
* day-of-week 0-7 (both 0 and 7 mean Sunday). No names (JAN/MON), no
|
|
17
|
+
* @-macros, no seconds field — declare what a plain cron daemon runs.
|
|
18
|
+
*/
|
|
19
|
+
const CRON_FIELDS = [
|
|
20
|
+
{ name: 'minute', min: 0, max: 59 },
|
|
21
|
+
{ name: 'hour', min: 0, max: 23 },
|
|
22
|
+
{ name: 'day-of-month', min: 1, max: 31 },
|
|
23
|
+
{ name: 'month', min: 1, max: 12 },
|
|
24
|
+
{ name: 'day-of-week', min: 0, max: 7 },
|
|
25
|
+
];
|
|
26
|
+
function fail(expression, detail) {
|
|
27
|
+
throw new TypeError(`Cron expression "${expression}" is invalid: ${detail}. Expected 5 ` +
|
|
28
|
+
`space-separated fields (minute hour day-of-month month day-of-week), ` +
|
|
29
|
+
`numeric values only, with "*", lists ("1,15"), ranges ("1-5"), and ` +
|
|
30
|
+
`steps ("*/15", "0-30/5") — e.g. "0 6 * * *" for 06:00 daily.`);
|
|
31
|
+
}
|
|
32
|
+
function parseValue(expression, field, raw) {
|
|
33
|
+
if (!/^\d+$/.test(raw)) {
|
|
34
|
+
fail(expression, `"${raw}" in the ${field.name} field is not a number (names and macros are not supported)`);
|
|
35
|
+
}
|
|
36
|
+
const value = Number(raw);
|
|
37
|
+
if (value < field.min || value > field.max) {
|
|
38
|
+
fail(expression, `${value} is out of range for the ${field.name} field (${field.min}-${field.max})`);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function parseElement(expression, field, element) {
|
|
43
|
+
if (element === '') {
|
|
44
|
+
fail(expression, `the ${field.name} field has an empty list element`);
|
|
45
|
+
}
|
|
46
|
+
const [base, step, ...extra] = element.split('/');
|
|
47
|
+
if (extra.length > 0 || step === '') {
|
|
48
|
+
fail(expression, `"${element}" in the ${field.name} field has a malformed step`);
|
|
49
|
+
}
|
|
50
|
+
if (step !== undefined) {
|
|
51
|
+
if (!/^\d+$/.test(step) || Number(step) === 0) {
|
|
52
|
+
fail(expression, `step "/${step}" in the ${field.name} field must be a positive number`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (base === '*')
|
|
56
|
+
return;
|
|
57
|
+
if (base === undefined || base === '') {
|
|
58
|
+
fail(expression, `the ${field.name} field is missing a value`);
|
|
59
|
+
}
|
|
60
|
+
const rangeParts = base.split('-');
|
|
61
|
+
if (rangeParts.length > 2) {
|
|
62
|
+
fail(expression, `"${base}" in the ${field.name} field is not a value or a range`);
|
|
63
|
+
}
|
|
64
|
+
if (rangeParts.length === 2) {
|
|
65
|
+
const [fromRaw, toRaw] = rangeParts;
|
|
66
|
+
const from = parseValue(expression, field, fromRaw);
|
|
67
|
+
const to = parseValue(expression, field, toRaw);
|
|
68
|
+
if (from > to) {
|
|
69
|
+
fail(expression, `range "${base}" in the ${field.name} field runs backwards (${from} > ${to})`);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const value = parseValue(expression, field, base);
|
|
74
|
+
if (step !== undefined) {
|
|
75
|
+
// "5/15"-style stepped single values are a non-portable vixie-cron
|
|
76
|
+
// extension; require an explicit range or "*" so the declaration reads
|
|
77
|
+
// the same everywhere.
|
|
78
|
+
fail(expression, `"${element}" in the ${field.name} field applies a step to the single value ${value} — use "*/${step}" or a range like "${field.min}-${field.max}/${step}"`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validates a 5-field cron expression, throwing a `TypeError` whose message
|
|
83
|
+
* is a remediation instruction. Returns the expression's fields (trimmed)
|
|
84
|
+
* so callers can display them.
|
|
85
|
+
*/
|
|
86
|
+
export function parseCronExpression(expression) {
|
|
87
|
+
const fields = expression.trim().split(/\s+/);
|
|
88
|
+
if (fields.length !== 5) {
|
|
89
|
+
fail(expression, `it has ${fields.length} field${fields.length === 1 ? '' : 's'}, not 5` +
|
|
90
|
+
(fields.length === 6
|
|
91
|
+
? ' (a seconds field is not supported — drop it)'
|
|
92
|
+
: ''));
|
|
93
|
+
}
|
|
94
|
+
fields.forEach((field, index) => {
|
|
95
|
+
const spec = CRON_FIELDS[index];
|
|
96
|
+
for (const element of field.split(',')) {
|
|
97
|
+
parseElement(expression, spec, element);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return fields;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=cron.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron.js","sourceRoot":"","sources":["../../src/integration/cron.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A tiny, dependency-free 5-field cron validator.\n *\n * Lives at the SDK layer on purpose: the dependency-pure schema package\n * checks only that `schedules[].cron` is a non-empty string, and\n * `defineIntegration()` layers full syntax validation on top — the same\n * split the workspace-target host rules use. The HOST scheduler binds to\n * the same 5-field grammar later, so an expression this parser accepts is\n * an expression the platform will run.\n *\n * Grammar (numeric, standard cron):\n * field := element (\",\" element)*\n * element := \"*\" | value | range — each optionally \"/\" step\n * range := value \"-\" value\n * Fields and bounds: minute 0-59, hour 0-23, day-of-month 1-31, month 1-12,\n * day-of-week 0-7 (both 0 and 7 mean Sunday). No names (JAN/MON), no\n * @-macros, no seconds field — declare what a plain cron daemon runs.\n */\n\nexport interface CronField {\n name: string;\n min: number;\n max: number;\n}\n\nconst CRON_FIELDS: readonly CronField[] = [\n { name: 'minute', min: 0, max: 59 },\n { name: 'hour', min: 0, max: 23 },\n { name: 'day-of-month', min: 1, max: 31 },\n { name: 'month', min: 1, max: 12 },\n { name: 'day-of-week', min: 0, max: 7 },\n];\n\nfunction fail(expression: string, detail: string): never {\n throw new TypeError(\n `Cron expression \"${expression}\" is invalid: ${detail}. Expected 5 ` +\n `space-separated fields (minute hour day-of-month month day-of-week), ` +\n `numeric values only, with \"*\", lists (\"1,15\"), ranges (\"1-5\"), and ` +\n `steps (\"*/15\", \"0-30/5\") — e.g. \"0 6 * * *\" for 06:00 daily.`,\n );\n}\n\nfunction parseValue(expression: string, field: CronField, raw: string): number {\n if (!/^\\d+$/.test(raw)) {\n fail(\n expression,\n `\"${raw}\" in the ${field.name} field is not a number (names and macros are not supported)`,\n );\n }\n const value = Number(raw);\n if (value < field.min || value > field.max) {\n fail(\n expression,\n `${value} is out of range for the ${field.name} field (${field.min}-${field.max})`,\n );\n }\n return value;\n}\n\nfunction parseElement(\n expression: string,\n field: CronField,\n element: string,\n): void {\n if (element === '') {\n fail(expression, `the ${field.name} field has an empty list element`);\n }\n\n const [base, step, ...extra] = element.split('/');\n if (extra.length > 0 || step === '') {\n fail(\n expression,\n `\"${element}\" in the ${field.name} field has a malformed step`,\n );\n }\n if (step !== undefined) {\n if (!/^\\d+$/.test(step) || Number(step) === 0) {\n fail(\n expression,\n `step \"/${step}\" in the ${field.name} field must be a positive number`,\n );\n }\n }\n\n if (base === '*') return;\n\n if (base === undefined || base === '') {\n fail(expression, `the ${field.name} field is missing a value`);\n }\n\n const rangeParts = base.split('-');\n if (rangeParts.length > 2) {\n fail(\n expression,\n `\"${base}\" in the ${field.name} field is not a value or a range`,\n );\n }\n\n if (rangeParts.length === 2) {\n const [fromRaw, toRaw] = rangeParts as [string, string];\n const from = parseValue(expression, field, fromRaw);\n const to = parseValue(expression, field, toRaw);\n if (from > to) {\n fail(\n expression,\n `range \"${base}\" in the ${field.name} field runs backwards (${from} > ${to})`,\n );\n }\n return;\n }\n\n const value = parseValue(expression, field, base);\n if (step !== undefined) {\n // \"5/15\"-style stepped single values are a non-portable vixie-cron\n // extension; require an explicit range or \"*\" so the declaration reads\n // the same everywhere.\n fail(\n expression,\n `\"${element}\" in the ${field.name} field applies a step to the single value ${value} — use \"*/${step}\" or a range like \"${field.min}-${field.max}/${step}\"`,\n );\n }\n}\n\n/**\n * Validates a 5-field cron expression, throwing a `TypeError` whose message\n * is a remediation instruction. Returns the expression's fields (trimmed)\n * so callers can display them.\n */\nexport function parseCronExpression(expression: string): string[] {\n const fields = expression.trim().split(/\\s+/);\n\n if (fields.length !== 5) {\n fail(\n expression,\n `it has ${fields.length} field${fields.length === 1 ? '' : 's'}, not 5` +\n (fields.length === 6\n ? ' (a seconds field is not supported — drop it)'\n : ''),\n );\n }\n\n fields.forEach((field, index) => {\n const spec = CRON_FIELDS[index]!;\n for (const element of field.split(',')) {\n parseElement(expression, spec, element);\n }\n });\n\n return fields;\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineIntegration()` — the v1 partner authoring contract
|
|
3
|
+
* (sdk-export-map.md, adversarial review outcome 3: partners never extend
|
|
4
|
+
* `BaseIntegration`; they hand the host a validated, frozen definition and
|
|
5
|
+
* the host adapts it internally).
|
|
6
|
+
*
|
|
7
|
+
* The validation logic and the schema itself live in the dependency-pure
|
|
8
|
+
* `@ekanos/integration-schema` (F3/F9): `defineIntegration()` validates at
|
|
9
|
+
* authoring time and `registerPartnerIntegration()` re-validates against the
|
|
10
|
+
* SAME schema at the host trust boundary, so there is one contract with no
|
|
11
|
+
* hand-written twin. This function adds only the generic authoring type;
|
|
12
|
+
* `IntegrationDefinitionSchema` is re-exported for the promote gate and
|
|
13
|
+
* dev-mcp validators.
|
|
14
|
+
*/
|
|
15
|
+
import { type IntegrationDefinition, type StorageSchemas } from '@ekanos/integration-schema';
|
|
16
|
+
/**
|
|
17
|
+
* Validates a partner integration definition and returns it deep-frozen.
|
|
18
|
+
*
|
|
19
|
+
* `parseIntegrationDefinition` performs the validation AND the deep-freeze
|
|
20
|
+
* (F4: the canonical parser freezes so both this and
|
|
21
|
+
* `registerPartnerIntegration()` return immutable output). Throws with a
|
|
22
|
+
* remediation-instruction message on invalid input. The declared `Schemas`
|
|
23
|
+
* generic threads through to `ctx.storage` typing in each tool's `run`
|
|
24
|
+
* handler and is preserved by `registerPartnerIntegration()`.
|
|
25
|
+
* Cross-DEFINITION collisions (two packages, one slug) are
|
|
26
|
+
* `validateIntegrationDefinitions()`'s job.
|
|
27
|
+
*/
|
|
28
|
+
export declare function defineIntegration<Schemas extends StorageSchemas = StorageSchemas>(definition: IntegrationDefinition<Schemas>): IntegrationDefinition<Schemas>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineIntegration()` — the v1 partner authoring contract
|
|
3
|
+
* (sdk-export-map.md, adversarial review outcome 3: partners never extend
|
|
4
|
+
* `BaseIntegration`; they hand the host a validated, frozen definition and
|
|
5
|
+
* the host adapts it internally).
|
|
6
|
+
*
|
|
7
|
+
* The validation logic and the schema itself live in the dependency-pure
|
|
8
|
+
* `@ekanos/integration-schema` (F3/F9): `defineIntegration()` validates at
|
|
9
|
+
* authoring time and `registerPartnerIntegration()` re-validates against the
|
|
10
|
+
* SAME schema at the host trust boundary, so there is one contract with no
|
|
11
|
+
* hand-written twin. This function adds only the generic authoring type;
|
|
12
|
+
* `IntegrationDefinitionSchema` is re-exported for the promote gate and
|
|
13
|
+
* dev-mcp validators.
|
|
14
|
+
*/
|
|
15
|
+
import { parseIntegrationDefinition, } from '@ekanos/integration-schema';
|
|
16
|
+
import { assertEventSurfacesValid } from './event-surfaces.js';
|
|
17
|
+
/**
|
|
18
|
+
* Validates a partner integration definition and returns it deep-frozen.
|
|
19
|
+
*
|
|
20
|
+
* `parseIntegrationDefinition` performs the validation AND the deep-freeze
|
|
21
|
+
* (F4: the canonical parser freezes so both this and
|
|
22
|
+
* `registerPartnerIntegration()` return immutable output). Throws with a
|
|
23
|
+
* remediation-instruction message on invalid input. The declared `Schemas`
|
|
24
|
+
* generic threads through to `ctx.storage` typing in each tool's `run`
|
|
25
|
+
* handler and is preserved by `registerPartnerIntegration()`.
|
|
26
|
+
* Cross-DEFINITION collisions (two packages, one slug) are
|
|
27
|
+
* `validateIntegrationDefinitions()`'s job.
|
|
28
|
+
*/
|
|
29
|
+
export function defineIntegration(definition) {
|
|
30
|
+
// parseIntegrationDefinition returns the loose (default-generic) shape; the
|
|
31
|
+
// storage/tool generic cannot be recovered from a runtime parse, but the
|
|
32
|
+
// parsed value IS the same definition, so re-narrowing to the caller's
|
|
33
|
+
// declared Schemas is sound.
|
|
34
|
+
const parsed = parseIntegrationDefinition(definition);
|
|
35
|
+
// SDK-layer event-surface rules the zod-only schema package cannot own:
|
|
36
|
+
// 5-field cron syntax on every schedule, and OAuth endpoint origins covered
|
|
37
|
+
// by the definition's own egress list.
|
|
38
|
+
assertEventSurfacesValid(parsed);
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=define-integration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-integration.js","sourceRoot":"","sources":["../../src/integration/define-integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAGL,0BAA0B,GAC3B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAE/B,UAA0C;IAC1C,4EAA4E;IAC5E,yEAAyE;IACzE,uEAAuE;IACvE,6BAA6B;IAC7B,MAAM,MAAM,GAAG,0BAA0B,CAAC,UAAU,CAAC,CAAC;IAEtD,wEAAwE;IACxE,4EAA4E;IAC5E,uCAAuC;IACvC,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAEjC,OAAO,MAAmD,CAAC;AAC7D,CAAC","sourcesContent":["/**\n * `defineIntegration()` — the v1 partner authoring contract\n * (sdk-export-map.md, adversarial review outcome 3: partners never extend\n * `BaseIntegration`; they hand the host a validated, frozen definition and\n * the host adapts it internally).\n *\n * The validation logic and the schema itself live in the dependency-pure\n * `@ekanos/integration-schema` (F3/F9): `defineIntegration()` validates at\n * authoring time and `registerPartnerIntegration()` re-validates against the\n * SAME schema at the host trust boundary, so there is one contract with no\n * hand-written twin. This function adds only the generic authoring type;\n * `IntegrationDefinitionSchema` is re-exported for the promote gate and\n * dev-mcp validators.\n */\nimport {\n type IntegrationDefinition,\n type StorageSchemas,\n parseIntegrationDefinition,\n} from '@ekanos/integration-schema';\n\nimport { assertEventSurfacesValid } from './event-surfaces';\n\n/**\n * Validates a partner integration definition and returns it deep-frozen.\n *\n * `parseIntegrationDefinition` performs the validation AND the deep-freeze\n * (F4: the canonical parser freezes so both this and\n * `registerPartnerIntegration()` return immutable output). Throws with a\n * remediation-instruction message on invalid input. The declared `Schemas`\n * generic threads through to `ctx.storage` typing in each tool's `run`\n * handler and is preserved by `registerPartnerIntegration()`.\n * Cross-DEFINITION collisions (two packages, one slug) are\n * `validateIntegrationDefinitions()`'s job.\n */\nexport function defineIntegration<\n Schemas extends StorageSchemas = StorageSchemas,\n>(definition: IntegrationDefinition<Schemas>): IntegrationDefinition<Schemas> {\n // parseIntegrationDefinition returns the loose (default-generic) shape; the\n // storage/tool generic cannot be recovered from a runtime parse, but the\n // parsed value IS the same definition, so re-narrowing to the caller's\n // declared Schemas is sound.\n const parsed = parseIntegrationDefinition(definition);\n\n // SDK-layer event-surface rules the zod-only schema package cannot own:\n // 5-field cron syntax on every schedule, and OAuth endpoint origins covered\n // by the definition's own egress list.\n assertEventSurfacesValid(parsed);\n\n return parsed as unknown as IntegrationDefinition<Schemas>;\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK-layer validation of the three event surfaces (webhooks, schedules,
|
|
3
|
+
* OAuth) — the cross-field rules the dependency-pure schema package cannot
|
|
4
|
+
* own, layered on top of the canonical parse the same way
|
|
5
|
+
* `parsePartnerIntegration` layers host workspace rules:
|
|
6
|
+
*
|
|
7
|
+
* - every `schedules[].cron` must be valid 5-field cron syntax
|
|
8
|
+
* (`parseCronExpression`);
|
|
9
|
+
* - the OAuth endpoints' origins must be covered by the definition's own
|
|
10
|
+
* `egress` list, because the code exchange runs through the integration's
|
|
11
|
+
* declared egress — an origin the reviewer never saw is an origin the
|
|
12
|
+
* exchange must not reach.
|
|
13
|
+
*/
|
|
14
|
+
import type { IntegrationDefinition } from '@ekanos/integration-schema';
|
|
15
|
+
export declare function assertEventSurfacesValid(definition: IntegrationDefinition): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isEgressAllowed } from '../context/egress.js';
|
|
2
|
+
import { parseCronExpression } from './cron.js';
|
|
3
|
+
export function assertEventSurfacesValid(definition) {
|
|
4
|
+
var _a, _b;
|
|
5
|
+
const problems = [];
|
|
6
|
+
for (const schedule of (_a = definition.schedules) !== null && _a !== void 0 ? _a : []) {
|
|
7
|
+
try {
|
|
8
|
+
parseCronExpression(schedule.cron);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
problems.push(`schedules["${schedule.id}"]: ${error instanceof Error ? error.message : String(error)}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (definition.oauth) {
|
|
15
|
+
const egress = (_b = definition.egress) !== null && _b !== void 0 ? _b : [];
|
|
16
|
+
const endpoints = [
|
|
17
|
+
['authorizationUrl', definition.oauth.provider.authorizationUrl],
|
|
18
|
+
['tokenUrl', definition.oauth.provider.tokenUrl],
|
|
19
|
+
];
|
|
20
|
+
for (const [field, url] of endpoints) {
|
|
21
|
+
let covered = false;
|
|
22
|
+
try {
|
|
23
|
+
covered = isEgressAllowed(url, egress);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
problems.push(`oauth.provider.${field}: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!covered) {
|
|
30
|
+
const origin = new URL(url).origin;
|
|
31
|
+
problems.push(`oauth.provider.${field}: its origin ${origin} is not covered by ` +
|
|
32
|
+
`this definition's egress list ${JSON.stringify(egress)}. The ` +
|
|
33
|
+
`OAuth code exchange runs through the integration's declared ` +
|
|
34
|
+
`egress, so add "${origin}" (or a covering "*." wildcard entry) ` +
|
|
35
|
+
`to \`egress\` — widening egress is a security-posture change ` +
|
|
36
|
+
`reviewed at promotion.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (problems.length > 0) {
|
|
41
|
+
throw new Error(`Invalid integration definition for "${definition.slug}":\n` +
|
|
42
|
+
problems.map((line) => ` - ${line}`).join('\n'));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=event-surfaces.js.map
|
|
@@ -0,0 +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,IAAI,MAAA,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,GAAG,MAAA,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"]}
|