@ekanos/sdk 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +95 -13
  2. package/api-report.md +51 -6
  3. package/dist/components/index.d.ts +3 -3
  4. package/dist/components/index.js +2 -2
  5. package/dist/components/index.js.map +1 -1
  6. package/dist/components/widgets/widget-context.d.ts +17 -0
  7. package/dist/components/widgets/widget-context.js.map +1 -1
  8. package/dist/components/widgets/widget-preview-provider.d.ts +4 -2
  9. package/dist/components/widgets/widget-preview-provider.js +3 -1
  10. package/dist/components/widgets/widget-preview-provider.js.map +1 -1
  11. package/dist/components/widgets/widget-state-components.d.ts +59 -3
  12. package/dist/components/widgets/widget-state-components.js +65 -7
  13. package/dist/components/widgets/widget-state-components.js.map +1 -1
  14. package/dist/components/widgets/widget.d.ts +124 -21
  15. package/dist/components/widgets/widget.js +159 -37
  16. package/dist/components/widgets/widget.js.map +1 -1
  17. package/dist/hooks/index.d.ts +2 -2
  18. package/dist/hooks/index.js +1 -1
  19. package/dist/hooks/index.js.map +1 -1
  20. package/dist/hooks/use-oauth-connection-status.d.ts +17 -1
  21. package/dist/hooks/use-oauth-connection-status.js +12 -17
  22. package/dist/hooks/use-oauth-connection-status.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/integration/actor-role.d.ts +14 -0
  26. package/dist/integration/actor-role.js +21 -0
  27. package/dist/integration/actor-role.js.map +1 -0
  28. package/dist/integration/index.d.ts +3 -1
  29. package/dist/integration/index.js +4 -0
  30. package/dist/integration/index.js.map +1 -1
  31. package/dist/integration/types.d.ts +1 -1
  32. package/dist/integration/types.js.map +1 -1
  33. package/dist/integration/untrusted-text.d.ts +24 -0
  34. package/dist/integration/untrusted-text.js +36 -0
  35. package/dist/integration/untrusted-text.js.map +1 -0
  36. package/dist/layout/index.d.ts +8 -0
  37. package/dist/layout/index.js +9 -0
  38. package/dist/layout/index.js.map +1 -0
  39. package/dist/layout/widget-layout.d.ts +84 -0
  40. package/dist/layout/widget-layout.js +138 -0
  41. package/dist/layout/widget-layout.js.map +1 -0
  42. package/dist/testing/index.d.ts +4 -0
  43. package/dist/testing/index.js +5 -0
  44. package/dist/testing/index.js.map +1 -1
  45. package/dist/testing/mock-context.js +3 -1
  46. package/dist/testing/mock-context.js.map +1 -1
  47. package/dist/testing/verify-signature.d.ts +26 -0
  48. package/dist/testing/verify-signature.js +45 -0
  49. package/dist/testing/verify-signature.js.map +1 -0
  50. package/dist/types/index.d.ts +1 -0
  51. package/dist/types/index.js.map +1 -1
  52. package/dist/types/integration.d.ts +5 -0
  53. package/dist/types/integration.js.map +1 -1
  54. package/dist/types/widget-verdict.d.ts +6 -0
  55. package/dist/types/widget-verdict.js +2 -0
  56. package/dist/types/widget-verdict.js.map +1 -0
  57. package/eslint.design-preset.mjs +226 -0
  58. package/eslint.design-rules.mjs +437 -0
  59. package/package.json +15 -5
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-layout.js","sourceRoot":"","sources":["../../src/layout/widget-layout.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH,gEAAgE;AAChE,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAC7B,0EAA0E;AAC1E,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAmB,EACnB,OAAO,GAAmC,EAAE;;;IAE5C,IAAI,MAAM,CAAC,IAAI;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IAEpC,MAAM,CAAC,SAAG,MAAA,MAAA,MAAM,CAAC,OAAO,0CAAE,EAAE,0CAAE,CAAC,mCAAI,kBAAkB,CAAC;IACtD,IAAI,CAAC,GAAG,kBAAkB;QAAE,OAAO,MAAM,CAAC;IAE1C,MAAM,gBAAgB,SAAG,OAAO,CAAC,gBAAgB,mCAAI,kBAAkB,CAAC;IACxE,OAAO,gBAAgB,IAAI,CAAC,IAAI,mBAAmB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;AAC3E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAmB;IAC5C,OAAO,iBAAiB,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC;AAOD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAqB;IAErB,MAAM,MAAM,GAAwB,EAAE,CAAC;IAEvC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAE3B,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACxC,CAAC;aAAM,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YACtC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAI,OAAqB;IAIpD,MAAM,IAAI,GAAQ,EAAE,CAAC;IACrB,MAAM,KAAK,GAAQ,EAAE,CAAC;IAEtB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM;YAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;;YAC9C,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CACxC,KAAmB;IAEnB,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAC1B,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,EAAE,CAAC;YACjC,IAAI,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1C,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAE,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC;gBAC1D,+DAA+D;gBAC/D,MAAM,GAAG,CAAC,CAAC;YACb,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,CAAC,CAAC;YACb,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAmB;IAEnB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE5C,KAAK,MAAM,KAAK,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1C,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACtD,SAAS;QACX,CAAC;QAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACrD,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YAC5B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;YACjE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;YACxB,IAAI,MAAM;gBAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAAC,KAA6B;IAC1D,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,OAAO,MAAM,KAAK,CAAC,CAAC;AACtB,CAAC","sourcesContent":["/**\n * The one widget layout module (the design system's widget rules): size resolution,\n * column spans, row grouping and the balanced two-column split, shared by the\n * Ekanos dashboard, the pinned board's editor and `@ekanos/harness`. Pure\n * functions, no React, so a server loader can use them too.\n */\n\n/**\n * `compact` is a figure tile laid out 1 → 2 → 4 across its row; `half` and\n * `full` are the two card widths. Height follows content.\n */\nexport type WidgetSize = 'compact' | 'half' | 'full';\n\n/** The fields of a widget declaration that decide its size. */\nexport interface SizedWidget {\n size?: WidgetSize;\n layouts?: { lg?: { w: number } };\n}\n\n/** A declared `layouts.lg.w` over this is a full-width card. */\nconst HALF_WIDTH_COLUMNS = 6;\n/** A declared `layouts.lg.w` at or under this reads as a compact tile. */\nconst COMPACT_MAX_COLUMNS = 3;\n\n/**\n * Whether a declared `w <= 3` resolves to `compact`. On since the\n * `widget-compact-sizes` decision (`/admin/design-system`, 2026-10-08) chose\n * option B: the five widgets that are figures become tiles, and the two that\n * are not (`google-drive-connection-setup`, `discourse-chat`) now declare an\n * explicit `size: 'half'` so this rule never reaches them.\n */\nexport const COMPACT_FROM_WIDTH = true;\n\n/**\n * A widget's size: its explicit `size`, else derived from the\n * react-grid-layout `layouts.lg.w` it declares (`w > 6` is full; `w <= 3` is\n * compact while `COMPACT_FROM_WIDTH` is on; everything else is half). `h`\n * means nothing.\n */\nexport function resolveWidgetSize(\n widget: SizedWidget,\n options: { compactFromWidth?: boolean } = {},\n): WidgetSize {\n if (widget.size) return widget.size;\n\n const w = widget.layouts?.lg?.w ?? HALF_WIDTH_COLUMNS;\n if (w > HALF_WIDTH_COLUMNS) return 'full';\n\n const compactFromWidth = options.compactFromWidth ?? COMPACT_FROM_WIDTH;\n return compactFromWidth && w <= COMPACT_MAX_COLUMNS ? 'compact' : 'half';\n}\n\n/**\n * Columns a widget takes in a two-column grid. A compact tile sits in a\n * half-width cell wherever a grid has no tile row of its own.\n */\nexport function getColSpan(widget: SizedWidget): 1 | 2 {\n return resolveWidgetSize(widget) === 'full' ? 2 : 1;\n}\n\nexport type WidgetRowGroup<T> =\n | { type: 'full'; widget: T }\n | { type: 'half'; widgets: T[] }\n | { type: 'compact'; widgets: T[] };\n\n/**\n * Rows in widget order: each full-width widget gets its own row, and each\n * run of contiguous half-width (or compact) widgets shares one.\n */\nexport function buildRowGroups<T extends SizedWidget>(\n widgets: readonly T[],\n): WidgetRowGroup<T>[] {\n const groups: WidgetRowGroup<T>[] = [];\n\n for (const widget of widgets) {\n const size = resolveWidgetSize(widget);\n const last = groups.at(-1);\n\n if (size === 'full') {\n groups.push({ type: 'full', widget });\n } else if (last && last.type === size) {\n last.widgets.push(widget);\n } else {\n groups.push({ type: size, widgets: [widget] });\n }\n }\n\n return groups;\n}\n\n/**\n * Splits a run of half-width widgets into two balanced columns, alternating\n * into whichever column is shorter (left wins ties).\n */\nexport function splitBalanced<T>(widgets: readonly T[]): {\n left: T[];\n right: T[];\n} {\n const left: T[] = [];\n const right: T[] = [];\n\n for (const widget of widgets) {\n if (left.length <= right.length) left.push(widget);\n else right.push(widget);\n }\n\n return { left, right };\n}\n\n/**\n * Keeps a full-width widget from landing in the right-hand column of a\n * two-column grid. When one would start a row's second slot, it trades\n * places with the half-width widget before it, unless it is last, where the\n * gap it leaves is harmless (and swapping would undo a \"move to the end\").\n */\nexport function snapFullWidthToRowBoundary<T extends SizedWidget>(\n items: readonly T[],\n): T[] {\n const result = [...items];\n let column = 0;\n\n for (let i = 0; i < result.length; i++) {\n if (getColSpan(result[i]!) === 2) {\n if (column === 1 && i < result.length - 1) {\n [result[i - 1], result[i]] = [result[i]!, result[i - 1]!];\n // The half-width widget it traded with now opens the next row.\n column = 1;\n } else {\n column = 0;\n }\n } else {\n column = column === 0 ? 1 : 0;\n }\n }\n\n return result;\n}\n\n/**\n * Each widget's id mapped to the ids that share its row on the dashboard,\n * itself included. Built from the same {@link buildRowGroups} and\n * {@link splitBalanced} the live grid renders with, so an editor that\n * highlights a row shows the row members will see: the i-th widget of a\n * run's left column beside the i-th of its right.\n */\nexport function pairRows<T extends SizedWidget & { id: string }>(\n items: readonly T[],\n): Map<string, Set<string>> {\n const rows = new Map<string, Set<string>>();\n\n for (const group of buildRowGroups(items)) {\n if (group.type === 'full') {\n rows.set(group.widget.id, new Set([group.widget.id]));\n continue;\n }\n\n const { left, right } = splitBalanced(group.widgets);\n left.forEach((first, index) => {\n const second = right[index];\n const row = new Set(second ? [first.id, second.id] : [first.id]);\n rows.set(first.id, row);\n if (second) rows.set(second.id, row);\n });\n }\n\n return rows;\n}\n\n/** Whether the last row of a two-column grid has an empty right-hand slot. */\nexport function hasTrailingGap(items: readonly SizedWidget[]): boolean {\n let column = 0;\n\n for (const item of items) {\n column = getColSpan(item) === 2 ? 0 : column === 0 ? 1 : 0;\n }\n\n return column === 1;\n}\n"]}
@@ -16,3 +16,7 @@ export type { MockFetchHandler } from './fetch-matching.js';
16
16
  export { createMockFetchProvider } from './mock-fetch-provider.js';
17
17
  export { invokeWebhook, invokeSchedule, invokeActivate } from './invoke.js';
18
18
  export type { DefinitionContextOptions, InvokeWebhookOptions, InvokeScheduleOptions, InvokeActivateOptions, WebhookInvocationOutcome, ScheduleInvocationOutcome, ActivateInvocationOutcome, } from './invoke.js';
19
+ export { verifyDeclaredWebhookSignature } from './verify-signature.js';
20
+ export type { VerifyDeclaredWebhookSignatureOptions } from './verify-signature.js';
21
+ export { verifyWebhookSignature } from '@ekanos/integration-schema';
22
+ export type { SignatureVerification, VerifyWebhookSignatureArgs, } from '@ekanos/integration-schema';
@@ -22,4 +22,9 @@ export { createMockFetchProvider } from './mock-fetch-provider.js';
22
22
  // panel alike. `invokeActivate` does the same for the `onActivate` lifecycle
23
23
  // hook, used by partner unit tests and the harness's activation surface.
24
24
  export { invokeWebhook, invokeSchedule, invokeActivate } from './invoke.js';
25
+ // The host's webhook-signature verifier, run against one of the definition's
26
+ // declarations: sign a payload the vendor's way in a test and prove the
27
+ // declaration accepts it BEFORE a real delivery is rejected on the host.
28
+ export { verifyDeclaredWebhookSignature } from './verify-signature.js';
29
+ export { verifyWebhookSignature } from '@ekanos/integration-schema';
25
30
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAYnD,2EAA2E;AAC3E,gEAAgE;AAChE,+DAA+D;AAC/D,mDAAmD;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC","sourcesContent":["/**\n * @ekanos/sdk/testing — the partner test harness surface.\n *\n * `createMockContext()` is the day-1 in-memory `IntegrationContext`\n * (proposal §4): partners code and test against `ctx` here before anything\n * lands in a sandbox. Self-contained — no Supabase, no Vault, no network —\n * and enforcement (storage schemas, secret tiers, egress) is the same\n * shared implementation from `@ekanos/sdk/context` that production uses.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { createMockContext } from './mock-context';\nexport type {\n MockContextOptions,\n MockIntegrationContext,\n RecordedFetchCall,\n RecordedLog,\n} from './mock-context';\n\n// Shared by `createMockContext`'s `ctx.fetch` stub and `createMockFetchProvider`\n// below — one fixture shape, one matching implementation, for both surfaces.\nexport type { MockFetchHandler } from './fetch-matching';\n\n// The browser-side counterpart to `createMockContext`'s `fetchHandlers`: a\n// mounted `IntegrationFetchProvider` for widget tests that call\n// `useIntegrationFetch()`, resolving fixtures against the same\n// `MockFetchHandler` shape and matching semantics.\nexport { createMockFetchProvider } from './mock-fetch-provider';\n\n// Event-surface invocation: run declared webhook/schedule handlers locally,\n// with the same payload-validation and context-derivation semantics the real\n// transports will have. Used by partner unit tests and the harness Triggers\n// panel alike. `invokeActivate` does the same for the `onActivate` lifecycle\n// hook, used by partner unit tests and the harness's activation surface.\nexport { invokeWebhook, invokeSchedule, invokeActivate } from './invoke';\nexport type {\n DefinitionContextOptions,\n InvokeWebhookOptions,\n InvokeScheduleOptions,\n InvokeActivateOptions,\n WebhookInvocationOutcome,\n ScheduleInvocationOutcome,\n ActivateInvocationOutcome,\n} from './invoke';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAYnD,2EAA2E;AAC3E,gEAAgE;AAChE,+DAA+D;AAC/D,mDAAmD;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAWzE,6EAA6E;AAC7E,wEAAwE;AACxE,yEAAyE;AACzE,OAAO,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC","sourcesContent":["/**\n * @ekanos/sdk/testing — the partner test harness surface.\n *\n * `createMockContext()` is the day-1 in-memory `IntegrationContext`\n * (proposal §4): partners code and test against `ctx` here before anything\n * lands in a sandbox. Self-contained — no Supabase, no Vault, no network —\n * and enforcement (storage schemas, secret tiers, egress) is the same\n * shared implementation from `@ekanos/sdk/context` that production uses.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { createMockContext } from './mock-context';\nexport type {\n MockContextOptions,\n MockIntegrationContext,\n RecordedFetchCall,\n RecordedLog,\n} from './mock-context';\n\n// Shared by `createMockContext`'s `ctx.fetch` stub and `createMockFetchProvider`\n// below — one fixture shape, one matching implementation, for both surfaces.\nexport type { MockFetchHandler } from './fetch-matching';\n\n// The browser-side counterpart to `createMockContext`'s `fetchHandlers`: a\n// mounted `IntegrationFetchProvider` for widget tests that call\n// `useIntegrationFetch()`, resolving fixtures against the same\n// `MockFetchHandler` shape and matching semantics.\nexport { createMockFetchProvider } from './mock-fetch-provider';\n\n// Event-surface invocation: run declared webhook/schedule handlers locally,\n// with the same payload-validation and context-derivation semantics the real\n// transports will have. Used by partner unit tests and the harness Triggers\n// panel alike. `invokeActivate` does the same for the `onActivate` lifecycle\n// hook, used by partner unit tests and the harness's activation surface.\nexport { invokeWebhook, invokeSchedule, invokeActivate } from './invoke';\nexport type {\n DefinitionContextOptions,\n InvokeWebhookOptions,\n InvokeScheduleOptions,\n InvokeActivateOptions,\n WebhookInvocationOutcome,\n ScheduleInvocationOutcome,\n ActivateInvocationOutcome,\n} from './invoke';\n\n// The host's webhook-signature verifier, run against one of the definition's\n// declarations: sign a payload the vendor's way in a test and prove the\n// declaration accepts it BEFORE a real delivery is rejected on the host.\nexport { verifyDeclaredWebhookSignature } from './verify-signature';\nexport type { VerifyDeclaredWebhookSignatureOptions } from './verify-signature';\nexport { verifyWebhookSignature } from '@ekanos/integration-schema';\nexport type {\n SignatureVerification,\n VerifyWebhookSignatureArgs,\n} from '@ekanos/integration-schema';\n"]}
@@ -100,7 +100,9 @@ export function createMockContext(options) {
100
100
  var _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
101
101
  const userId = options.userId !== undefined ? options.userId : 'mock-user-id';
102
102
  const actor = (_e = options.actor) !== null && _e !== void 0 ? _e : (userId !== null
103
- ? { kind: 'user', userId }
103
+ ? // The host always resolves a role; the mock's default user is the
104
+ // account owner. Pass `actor` with a lower role to test a gate.
105
+ { kind: 'user', userId, role: { name: 'owner', hierarchyLevel: 1 } }
104
106
  : { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });
105
107
  const schemas = (_f = options.storageSchemas) !== null && _f !== void 0 ? _f : {};
106
108
  const accountRows = new Map();
@@ -1 +1 @@
1
- {"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"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","sourcesContent":["import { PARTNER_STORAGE_LIMITS } from '@ekanos/integration-schema';\n/**\n * `createMockContext()` — the day-1 in-memory `IntegrationContext` for the\n * partner harness (proposal §4, amended by rulings 1–4). Self-contained:\n * nothing here touches Supabase, Vault, or the network. Enforcement is the\n * SAME shared implementation the production wrapper uses\n * (`@ekanos/sdk/context`): schema validation via `parseStorageValue`, egress\n * via `isEgressAllowed` — a test that passes the mock cannot be a schema or\n * egress surprise later.\n */\nimport type { z } from 'zod';\n\nimport { isEgressAllowed } from '../context/egress';\nimport {\n EgressDeniedError,\n SecretAccessError,\n StorageValidationError,\n} from '../context/errors';\nimport {\n assertStorageKeyDeclared,\n assertStorageKeyWritable,\n parseStorageValue,\n} from '../context/storage-validation';\nimport type {\n IntegrationActor,\n IntegrationContext,\n IntegrationFetch,\n IntegrationLogger,\n IntegrationSecrets,\n IntegrationStorage,\n ScopedStore,\n StorageEntry,\n StorageKeySchema,\n StorageSchemaMap,\n StorageSchemas,\n StorageWriteOptions,\n} from '../context/types';\nimport { type MockFetchHandler, matchFetchHandler } from './fetch-matching';\n\ntype StorageScope = 'account' | 'user';\n\n// Seeds are typed off the DECLARED schema, resolved out of whichever\n// declaration form the key used — a bare zod schema or a\n// `{ schema, clientReadable }` descriptor.\ntype ScopeSeed<M> = M extends StorageSchemaMap\n ? { [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>> }\n : Record<never, never>;\n\n// Re-exported from here for backward compatibility — `MockFetchHandler`'s\n// canonical home is now `./fetch-matching`, shared with `createMockFetchProvider`.\nexport type { MockFetchHandler } from './fetch-matching';\n\nexport interface MockContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n accountId?: string;\n accountSlug?: string | null;\n userId?: string | null;\n actor?: IntegrationActor;\n sourceId?: string | null;\n timezone?: string | null;\n integration: { slug: string; productId?: string };\n /**\n * The storage declaration `defineIntegration({ storage })` will carry —\n * the mock enforces the identical schemas via the same shared validator\n * (ruling 1). Omit it and every storage access throws.\n */\n storageSchemas?: Schemas;\n /** Seed rows: key → data, per scope. Validated on construction via the write path. */\n storage?: {\n account?: ScopeSeed<Schemas['account']>;\n user?: ScopeSeed<Schemas['user']>;\n };\n /**\n * Seed secrets, by tier (ruling 2): `account` values are the rows this\n * integration stored — writable via `secrets.set()`; `admin` values stand\n * in for admin-issued source/global-tier credentials — readable, and a\n * `set()` against a name resolvable only there throws `SecretAccessError`.\n */\n secrets?: {\n account?: Record<string, string>;\n admin?: Record<string, string>;\n };\n /** Same allowlist the manifest would declare — enforced by the stub. */\n egress?: readonly string[];\n /** Programmable responses; unmatched allowed calls resolve 200 `{}`. */\n fetchHandlers?: MockFetchHandler[];\n}\n\nexport interface RecordedFetchCall {\n url: string;\n init?: RequestInit;\n denied: boolean;\n}\n\nexport interface RecordedLog {\n level: 'debug' | 'info' | 'warn' | 'error';\n context: Record<string, unknown>;\n message: string;\n}\n\nexport interface MockIntegrationContext<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends IntegrationContext<Schemas> {\n /** Every ctx.fetch call, allowed or denied, in order. */\n readonly fetchCalls: readonly RecordedFetchCall[];\n /** Every log line, per level. */\n readonly logs: readonly RecordedLog[];\n /** Current readable storage state (expired entries excluded), key → data. */\n dumpStorage(): {\n account: Record<string, unknown>;\n user: Record<string, unknown>;\n };\n}\n\ninterface StoredRow {\n data: unknown;\n externalId: string | null;\n expiresAt: string | null;\n updatedAt: string;\n}\n\nfunction isExpired(row: StoredRow): boolean {\n return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();\n}\n\nfunction parseExpiresAt(\n options: StorageWriteOptions | undefined,\n): string | null {\n const expiresAt = options?.expiresAt;\n if (expiresAt === undefined) return null;\n if (Number.isNaN(Date.parse(expiresAt))) {\n throw new TypeError(\n `expiresAt \"${expiresAt}\" is not a parseable timestamp. Pass an ` +\n `ISO-8601 string, e.g. new Date(Date.now() + 60_000).toISOString().`,\n );\n }\n return expiresAt;\n}\n\nfunction createInMemoryStore(args: {\n scope: StorageScope;\n schemas: StorageSchemaMap | undefined;\n rows: Map<string, StoredRow>;\n guard: () => void;\n}): ScopedStore {\n const { scope, schemas, rows, guard } = args;\n\n return {\n async get(key: string): Promise<StorageEntry<unknown> | null> {\n guard();\n assertStorageKeyDeclared({ scope, key, schemas });\n const row = rows.get(key);\n if (!row) return null;\n if (isExpired(row)) {\n rows.delete(key);\n return null;\n }\n const data = parseStorageValue({\n scope,\n key,\n operation: 'read',\n schemas,\n value: row.data,\n });\n return {\n data,\n externalId: row.externalId,\n expiresAt: row.expiresAt,\n updatedAt: row.updatedAt,\n };\n },\n\n async set(\n key: string,\n data: unknown,\n options?: StorageWriteOptions,\n ): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas,\n value: data,\n });\n // The host's per-value cap (finding E), enforced here with the host's\n // own wording so `ekanos test` fails where production would.\n const serializedBytes = utf8Bytes(JSON.stringify(parsed));\n if (serializedBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {\n throw new StorageValidationError({\n scope,\n key,\n hint:\n `The value written to ${scope} storage key \"${key}\" serializes ` +\n `to ${serializedBytes} bytes, above the ` +\n `${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte per-value ` +\n `limit. Store less per key: trim cached payloads, or split state ` +\n `across purpose-named keys.`,\n });\n }\n rows.set(key, {\n data: parsed,\n externalId: options?.externalId ?? null,\n expiresAt: parseExpiresAt(options),\n updatedAt: new Date().toISOString(),\n });\n },\n\n async delete(key: string): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n assertStorageKeyDeclared({ scope, key, schemas });\n rows.delete(key);\n },\n };\n}\n\n/** Byte length of a string as UTF-8, without Buffer (this module runs in the browser-side harness too). */\nfunction utf8Bytes(value: string): number {\n return new TextEncoder().encode(value).length;\n}\n\nfunction dumpScope(rows: Map<string, StoredRow>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, row] of rows) {\n if (!isExpired(row)) out[key] = row.data;\n }\n return out;\n}\n\nexport function createMockContext<\n Schemas extends StorageSchemas = StorageSchemas,\n>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas> {\n const userId = options.userId !== undefined ? options.userId : 'mock-user-id';\n const actor: IntegrationActor =\n options.actor ??\n (userId !== null\n ? { kind: 'user', userId }\n : { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });\n\n const schemas: StorageSchemas = options.storageSchemas ?? {};\n\n const accountRows = new Map<string, StoredRow>();\n const userRows = new Map<string, StoredRow>();\n\n const seedScope = (\n scope: StorageScope,\n rows: Map<string, StoredRow>,\n seed: Record<string, unknown> | undefined,\n ) => {\n for (const [key, value] of Object.entries(seed ?? {})) {\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas: schemas[scope],\n value,\n });\n rows.set(key, {\n data: parsed,\n externalId: null,\n expiresAt: null,\n updatedAt: new Date().toISOString(),\n });\n }\n };\n seedScope('account', accountRows, options.storage?.account);\n seedScope('user', userRows, options.storage?.user);\n\n const guardUserScope = () => {\n if (actor.kind === 'machine') {\n throw new Error(\n 'ctx.storage.user is unavailable for machine actors: there is no ' +\n 'user to scope the row to. Store machine-written state in ' +\n 'ctx.storage.account instead.',\n );\n }\n };\n\n // The stores validate every key and value at runtime via the shared\n // validator; the cast narrows the runtime-checked store to the\n // schema-typed interface the declaration promises.\n const storage = {\n account: createInMemoryStore({\n scope: 'account',\n schemas: schemas.account,\n rows: accountRows,\n guard: () => void 0,\n }),\n user: createInMemoryStore({\n scope: 'user',\n schemas: schemas.user,\n rows: userRows,\n guard: guardUserScope,\n }),\n } as unknown as IntegrationStorage<Schemas>;\n\n const accountSecrets = new Map(\n Object.entries(options.secrets?.account ?? {}),\n );\n const adminSecrets = new Map(Object.entries(options.secrets?.admin ?? {}));\n\n const secrets: IntegrationSecrets = {\n async get(name) {\n return accountSecrets.get(name) ?? adminSecrets.get(name) ?? null;\n },\n async names() {\n return [\n ...new Set([...accountSecrets.keys(), ...adminSecrets.keys()]),\n ].sort();\n },\n async set(name, value) {\n if (!accountSecrets.has(name) && adminSecrets.has(name)) {\n throw new SecretAccessError(name);\n }\n // The host's caps (finding E): one Vault record per value, one row +\n // record per distinct name. Same wording as the host.\n const valueBytes = utf8Bytes(value);\n if (valueBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {\n throw new TypeError(\n `secrets.set(\"${name}\", …): the value is ${valueBytes} bytes, ` +\n `above the ${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte ` +\n `per-secret limit. Secrets hold credentials, not payloads — store ` +\n `bulk data under a ctx.storage key instead.`,\n );\n }\n if (\n !accountSecrets.has(name) &&\n accountSecrets.size >= PARTNER_STORAGE_LIMITS.maxSecretNames\n ) {\n throw new Error(\n `secrets.set(\"${name}\", …): this integration already stores ` +\n `${PARTNER_STORAGE_LIMITS.maxSecretNames} distinct secret names, ` +\n `the per-integration limit. Reuse or rotate an existing name.`,\n );\n }\n accountSecrets.set(name, value);\n },\n };\n\n const fetchCalls: RecordedFetchCall[] = [];\n const egress = options.egress ?? [];\n const fetchHandlers = options.fetchHandlers ?? [];\n\n // Not an async function: a denied call must throw synchronously, before\n // any I/O (proposal §2.3) — same behavior the production wrapper has.\n const fetch: IntegrationFetch = (input, init) => {\n const url = new URL(input).toString();\n const allowed = isEgressAllowed(url, egress);\n fetchCalls.push(\n init === undefined\n ? { url, denied: !allowed }\n : { url, init, denied: !allowed },\n );\n if (!allowed) {\n throw new EgressDeniedError(new URL(url).origin);\n }\n const handler = matchFetchHandler(url, fetchHandlers);\n if (handler) {\n return Promise.resolve(handler.respond(new Request(url, init)));\n }\n return Promise.resolve(\n new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n }),\n );\n };\n\n const logs: RecordedLog[] = [];\n const record =\n (level: RecordedLog['level']) =>\n (context: Record<string, unknown>, message: string) => {\n logs.push({ level, context, message });\n };\n const logger: IntegrationLogger = {\n debug: record('debug'),\n info: record('info'),\n warn: record('warn'),\n error: record('error'),\n };\n\n return {\n accountId: options.accountId ?? 'mock-account-id',\n accountSlug: options.accountSlug ?? null,\n userId: actor.kind === 'machine' ? null : userId,\n actor,\n sourceId: options.sourceId ?? null,\n integration: {\n slug: options.integration.slug,\n productId: options.integration.productId ?? 'mock-product-id',\n },\n timezone: options.timezone ?? null,\n storage,\n secrets,\n fetch,\n logger,\n fetchCalls,\n logs,\n dumpStorage() {\n return { account: dumpScope(accountRows), user: dumpScope(userRows) };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"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","sourcesContent":["import { PARTNER_STORAGE_LIMITS } from '@ekanos/integration-schema';\n/**\n * `createMockContext()` — the day-1 in-memory `IntegrationContext` for the\n * partner harness (proposal §4, amended by rulings 1–4). Self-contained:\n * nothing here touches Supabase, Vault, or the network. Enforcement is the\n * SAME shared implementation the production wrapper uses\n * (`@ekanos/sdk/context`): schema validation via `parseStorageValue`, egress\n * via `isEgressAllowed` — a test that passes the mock cannot be a schema or\n * egress surprise later.\n */\nimport type { z } from 'zod';\n\nimport { isEgressAllowed } from '../context/egress';\nimport {\n EgressDeniedError,\n SecretAccessError,\n StorageValidationError,\n} from '../context/errors';\nimport {\n assertStorageKeyDeclared,\n assertStorageKeyWritable,\n parseStorageValue,\n} from '../context/storage-validation';\nimport type {\n IntegrationActor,\n IntegrationContext,\n IntegrationFetch,\n IntegrationLogger,\n IntegrationSecrets,\n IntegrationStorage,\n ScopedStore,\n StorageEntry,\n StorageKeySchema,\n StorageSchemaMap,\n StorageSchemas,\n StorageWriteOptions,\n} from '../context/types';\nimport { type MockFetchHandler, matchFetchHandler } from './fetch-matching';\n\ntype StorageScope = 'account' | 'user';\n\n// Seeds are typed off the DECLARED schema, resolved out of whichever\n// declaration form the key used — a bare zod schema or a\n// `{ schema, clientReadable }` descriptor.\ntype ScopeSeed<M> = M extends StorageSchemaMap\n ? { [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>> }\n : Record<never, never>;\n\n// Re-exported from here for backward compatibility — `MockFetchHandler`'s\n// canonical home is now `./fetch-matching`, shared with `createMockFetchProvider`.\nexport type { MockFetchHandler } from './fetch-matching';\n\nexport interface MockContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n accountId?: string;\n accountSlug?: string | null;\n userId?: string | null;\n actor?: IntegrationActor;\n sourceId?: string | null;\n timezone?: string | null;\n integration: { slug: string; productId?: string };\n /**\n * The storage declaration `defineIntegration({ storage })` will carry —\n * the mock enforces the identical schemas via the same shared validator\n * (ruling 1). Omit it and every storage access throws.\n */\n storageSchemas?: Schemas;\n /** Seed rows: key → data, per scope. Validated on construction via the write path. */\n storage?: {\n account?: ScopeSeed<Schemas['account']>;\n user?: ScopeSeed<Schemas['user']>;\n };\n /**\n * Seed secrets, by tier (ruling 2): `account` values are the rows this\n * integration stored — writable via `secrets.set()`; `admin` values stand\n * in for admin-issued source/global-tier credentials — readable, and a\n * `set()` against a name resolvable only there throws `SecretAccessError`.\n */\n secrets?: {\n account?: Record<string, string>;\n admin?: Record<string, string>;\n };\n /** Same allowlist the manifest would declare — enforced by the stub. */\n egress?: readonly string[];\n /** Programmable responses; unmatched allowed calls resolve 200 `{}`. */\n fetchHandlers?: MockFetchHandler[];\n}\n\nexport interface RecordedFetchCall {\n url: string;\n init?: RequestInit;\n denied: boolean;\n}\n\nexport interface RecordedLog {\n level: 'debug' | 'info' | 'warn' | 'error';\n context: Record<string, unknown>;\n message: string;\n}\n\nexport interface MockIntegrationContext<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends IntegrationContext<Schemas> {\n /** Every ctx.fetch call, allowed or denied, in order. */\n readonly fetchCalls: readonly RecordedFetchCall[];\n /** Every log line, per level. */\n readonly logs: readonly RecordedLog[];\n /** Current readable storage state (expired entries excluded), key → data. */\n dumpStorage(): {\n account: Record<string, unknown>;\n user: Record<string, unknown>;\n };\n}\n\ninterface StoredRow {\n data: unknown;\n externalId: string | null;\n expiresAt: string | null;\n updatedAt: string;\n}\n\nfunction isExpired(row: StoredRow): boolean {\n return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();\n}\n\nfunction parseExpiresAt(\n options: StorageWriteOptions | undefined,\n): string | null {\n const expiresAt = options?.expiresAt;\n if (expiresAt === undefined) return null;\n if (Number.isNaN(Date.parse(expiresAt))) {\n throw new TypeError(\n `expiresAt \"${expiresAt}\" is not a parseable timestamp. Pass an ` +\n `ISO-8601 string, e.g. new Date(Date.now() + 60_000).toISOString().`,\n );\n }\n return expiresAt;\n}\n\nfunction createInMemoryStore(args: {\n scope: StorageScope;\n schemas: StorageSchemaMap | undefined;\n rows: Map<string, StoredRow>;\n guard: () => void;\n}): ScopedStore {\n const { scope, schemas, rows, guard } = args;\n\n return {\n async get(key: string): Promise<StorageEntry<unknown> | null> {\n guard();\n assertStorageKeyDeclared({ scope, key, schemas });\n const row = rows.get(key);\n if (!row) return null;\n if (isExpired(row)) {\n rows.delete(key);\n return null;\n }\n const data = parseStorageValue({\n scope,\n key,\n operation: 'read',\n schemas,\n value: row.data,\n });\n return {\n data,\n externalId: row.externalId,\n expiresAt: row.expiresAt,\n updatedAt: row.updatedAt,\n };\n },\n\n async set(\n key: string,\n data: unknown,\n options?: StorageWriteOptions,\n ): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas,\n value: data,\n });\n // The host's per-value cap (finding E), enforced here with the host's\n // own wording so `ekanos test` fails where production would.\n const serializedBytes = utf8Bytes(JSON.stringify(parsed));\n if (serializedBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {\n throw new StorageValidationError({\n scope,\n key,\n hint:\n `The value written to ${scope} storage key \"${key}\" serializes ` +\n `to ${serializedBytes} bytes, above the ` +\n `${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte per-value ` +\n `limit. Store less per key: trim cached payloads, or split state ` +\n `across purpose-named keys.`,\n });\n }\n rows.set(key, {\n data: parsed,\n externalId: options?.externalId ?? null,\n expiresAt: parseExpiresAt(options),\n updatedAt: new Date().toISOString(),\n });\n },\n\n async delete(key: string): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n assertStorageKeyDeclared({ scope, key, schemas });\n rows.delete(key);\n },\n };\n}\n\n/** Byte length of a string as UTF-8, without Buffer (this module runs in the browser-side harness too). */\nfunction utf8Bytes(value: string): number {\n return new TextEncoder().encode(value).length;\n}\n\nfunction dumpScope(rows: Map<string, StoredRow>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, row] of rows) {\n if (!isExpired(row)) out[key] = row.data;\n }\n return out;\n}\n\nexport function createMockContext<\n Schemas extends StorageSchemas = StorageSchemas,\n>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas> {\n const userId = options.userId !== undefined ? options.userId : 'mock-user-id';\n const actor: IntegrationActor =\n options.actor ??\n (userId !== null\n ? // The host always resolves a role; the mock's default user is the\n // account owner. Pass `actor` with a lower role to test a gate.\n { kind: 'user', userId, role: { name: 'owner', hierarchyLevel: 1 } }\n : { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });\n\n const schemas: StorageSchemas = options.storageSchemas ?? {};\n\n const accountRows = new Map<string, StoredRow>();\n const userRows = new Map<string, StoredRow>();\n\n const seedScope = (\n scope: StorageScope,\n rows: Map<string, StoredRow>,\n seed: Record<string, unknown> | undefined,\n ) => {\n for (const [key, value] of Object.entries(seed ?? {})) {\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas: schemas[scope],\n value,\n });\n rows.set(key, {\n data: parsed,\n externalId: null,\n expiresAt: null,\n updatedAt: new Date().toISOString(),\n });\n }\n };\n seedScope('account', accountRows, options.storage?.account);\n seedScope('user', userRows, options.storage?.user);\n\n const guardUserScope = () => {\n if (actor.kind === 'machine') {\n throw new Error(\n 'ctx.storage.user is unavailable for machine actors: there is no ' +\n 'user to scope the row to. Store machine-written state in ' +\n 'ctx.storage.account instead.',\n );\n }\n };\n\n // The stores validate every key and value at runtime via the shared\n // validator; the cast narrows the runtime-checked store to the\n // schema-typed interface the declaration promises.\n const storage = {\n account: createInMemoryStore({\n scope: 'account',\n schemas: schemas.account,\n rows: accountRows,\n guard: () => void 0,\n }),\n user: createInMemoryStore({\n scope: 'user',\n schemas: schemas.user,\n rows: userRows,\n guard: guardUserScope,\n }),\n } as unknown as IntegrationStorage<Schemas>;\n\n const accountSecrets = new Map(\n Object.entries(options.secrets?.account ?? {}),\n );\n const adminSecrets = new Map(Object.entries(options.secrets?.admin ?? {}));\n\n const secrets: IntegrationSecrets = {\n async get(name) {\n return accountSecrets.get(name) ?? adminSecrets.get(name) ?? null;\n },\n async names() {\n return [\n ...new Set([...accountSecrets.keys(), ...adminSecrets.keys()]),\n ].sort();\n },\n async set(name, value) {\n if (!accountSecrets.has(name) && adminSecrets.has(name)) {\n throw new SecretAccessError(name);\n }\n // The host's caps (finding E): one Vault record per value, one row +\n // record per distinct name. Same wording as the host.\n const valueBytes = utf8Bytes(value);\n if (valueBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {\n throw new TypeError(\n `secrets.set(\"${name}\", …): the value is ${valueBytes} bytes, ` +\n `above the ${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte ` +\n `per-secret limit. Secrets hold credentials, not payloads — store ` +\n `bulk data under a ctx.storage key instead.`,\n );\n }\n if (\n !accountSecrets.has(name) &&\n accountSecrets.size >= PARTNER_STORAGE_LIMITS.maxSecretNames\n ) {\n throw new Error(\n `secrets.set(\"${name}\", …): this integration already stores ` +\n `${PARTNER_STORAGE_LIMITS.maxSecretNames} distinct secret names, ` +\n `the per-integration limit. Reuse or rotate an existing name.`,\n );\n }\n accountSecrets.set(name, value);\n },\n };\n\n const fetchCalls: RecordedFetchCall[] = [];\n const egress = options.egress ?? [];\n const fetchHandlers = options.fetchHandlers ?? [];\n\n // Not an async function: a denied call must throw synchronously, before\n // any I/O (proposal §2.3) — same behavior the production wrapper has.\n const fetch: IntegrationFetch = (input, init) => {\n const url = new URL(input).toString();\n const allowed = isEgressAllowed(url, egress);\n fetchCalls.push(\n init === undefined\n ? { url, denied: !allowed }\n : { url, init, denied: !allowed },\n );\n if (!allowed) {\n throw new EgressDeniedError(new URL(url).origin);\n }\n const handler = matchFetchHandler(url, fetchHandlers);\n if (handler) {\n return Promise.resolve(handler.respond(new Request(url, init)));\n }\n return Promise.resolve(\n new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n }),\n );\n };\n\n const logs: RecordedLog[] = [];\n const record =\n (level: RecordedLog['level']) =>\n (context: Record<string, unknown>, message: string) => {\n logs.push({ level, context, message });\n };\n const logger: IntegrationLogger = {\n debug: record('debug'),\n info: record('info'),\n warn: record('warn'),\n error: record('error'),\n };\n\n return {\n accountId: options.accountId ?? 'mock-account-id',\n accountSlug: options.accountSlug ?? null,\n userId: actor.kind === 'machine' ? null : userId,\n actor,\n sourceId: options.sourceId ?? null,\n integration: {\n slug: options.integration.slug,\n productId: options.integration.productId ?? 'mock-product-id',\n },\n timezone: options.timezone ?? null,\n storage,\n secrets,\n fetch,\n logger,\n fetchCalls,\n logs,\n dumpStorage() {\n return { account: dumpScope(accountRows), user: dumpScope(userRows) };\n },\n };\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import { type IntegrationDefinition, type SignatureVerification, type StorageSchemas } from '@ekanos/integration-schema';
2
+ export interface VerifyDeclaredWebhookSignatureOptions {
3
+ /** The delivery's headers, as the vendor would send them. Names are matched case-insensitively. */
4
+ headers: Record<string, string>;
5
+ /** The exact body bytes the vendor signed, before any parsing. */
6
+ rawBody: string;
7
+ /** The signing secret the account would hold under the declaration's `secretName`. */
8
+ secret: string;
9
+ /** Injectable clock for freshness checks. Epoch milliseconds. */
10
+ now?: number;
11
+ }
12
+ /**
13
+ * Run the host's signature check against one of a definition's webhook
14
+ * declarations, exactly as the ingress route will: the declared header (and
15
+ * timestamp header, when declared) are read from `headers`, the `scheme` is
16
+ * the declaration's own, and the verifier is the same function the host runs.
17
+ *
18
+ * Sign the payload in your test the way the VENDOR documents it, then assert
19
+ * `{ ok: true }` here. That proves the declaration — header name, keyed
20
+ * pairs, signed-payload template, tolerance — matches the vendor before a
21
+ * single real delivery is rejected on the host, where `ekanos status` cannot
22
+ * see it. Throws for an undeclared webhook id and for a declaration with
23
+ * `signature: 'none'`: neither is something to verify, and a silent `ok`
24
+ * there would be a test that proves nothing.
25
+ */
26
+ export declare function verifyDeclaredWebhookSignature<Schemas extends StorageSchemas = StorageSchemas>(definition: IntegrationDefinition<Schemas>, webhookId: string, options: VerifyDeclaredWebhookSignatureOptions): Promise<SignatureVerification>;
@@ -0,0 +1,45 @@
1
+ import { verifyWebhookSignature, } from '@ekanos/integration-schema';
2
+ function headerValue(headers, name) {
3
+ const wanted = name.toLowerCase();
4
+ for (const [key, value] of Object.entries(headers)) {
5
+ if (key.toLowerCase() === wanted)
6
+ return value;
7
+ }
8
+ return null;
9
+ }
10
+ /**
11
+ * Run the host's signature check against one of a definition's webhook
12
+ * declarations, exactly as the ingress route will: the declared header (and
13
+ * timestamp header, when declared) are read from `headers`, the `scheme` is
14
+ * the declaration's own, and the verifier is the same function the host runs.
15
+ *
16
+ * Sign the payload in your test the way the VENDOR documents it, then assert
17
+ * `{ ok: true }` here. That proves the declaration — header name, keyed
18
+ * pairs, signed-payload template, tolerance — matches the vendor before a
19
+ * single real delivery is rejected on the host, where `ekanos status` cannot
20
+ * see it. Throws for an undeclared webhook id and for a declaration with
21
+ * `signature: 'none'`: neither is something to verify, and a silent `ok`
22
+ * there would be a test that proves nothing.
23
+ */
24
+ export async function verifyDeclaredWebhookSignature(definition, webhookId, options) {
25
+ var _a;
26
+ const webhook = ((_a = definition.webhooks) !== null && _a !== void 0 ? _a : []).find((w) => w.id === webhookId);
27
+ if (!webhook) {
28
+ throw new Error(`verifyDeclaredWebhookSignature: "${definition.slug}" declares no webhook "${webhookId}".`);
29
+ }
30
+ if (webhook.signature === 'none') {
31
+ throw new Error(`verifyDeclaredWebhookSignature: webhook "${webhookId}" declares signature: 'none'; there is nothing to verify.`);
32
+ }
33
+ const { header, scheme } = webhook.signature;
34
+ return verifyWebhookSignature({
35
+ rawBody: options.rawBody,
36
+ presented: headerValue(options.headers, header),
37
+ secret: options.secret,
38
+ scheme,
39
+ timestampHeaderValue: (scheme === null || scheme === void 0 ? void 0 : scheme.timestampHeader)
40
+ ? headerValue(options.headers, scheme.timestampHeader)
41
+ : null,
42
+ now: options.now,
43
+ });
44
+ }
45
+ //# sourceMappingURL=verify-signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-signature.js","sourceRoot":"","sources":["../../src/testing/verify-signature.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AAapC,SAAS,WAAW,CAClB,OAA+B,EAC/B,IAAY;IAEZ,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,MAAM;YAAE,OAAO,KAAK,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAGlD,UAA0C,EAC1C,SAAiB,EACjB,OAA8C;;IAE9C,MAAM,OAAO,GAAG,OAAC,UAAU,CAAC,QAAQ,mCAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC;IAC5E,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,oCAAoC,UAAU,CAAC,IAAI,0BAA0B,SAAS,IAAI,CAC3F,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,SAAS,KAAK,MAAM,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CACb,4CAA4C,SAAS,2DAA2D,CACjH,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IAC7C,OAAO,sBAAsB,CAAC;QAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,SAAS,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC;QAC/C,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM;QACN,oBAAoB,EAAE,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,eAAe;YAC3C,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,eAAe,CAAC;YACtD,CAAC,CAAC,IAAI;QACR,GAAG,EAAE,OAAO,CAAC,GAAG;KACjB,CAAC,CAAC;AACL,CAAC","sourcesContent":["import {\n type IntegrationDefinition,\n type SignatureVerification,\n type StorageSchemas,\n verifyWebhookSignature,\n} from '@ekanos/integration-schema';\n\nexport interface VerifyDeclaredWebhookSignatureOptions {\n /** The delivery's headers, as the vendor would send them. Names are matched case-insensitively. */\n headers: Record<string, string>;\n /** The exact body bytes the vendor signed, before any parsing. */\n rawBody: string;\n /** The signing secret the account would hold under the declaration's `secretName`. */\n secret: string;\n /** Injectable clock for freshness checks. Epoch milliseconds. */\n now?: number;\n}\n\nfunction headerValue(\n headers: Record<string, string>,\n name: string,\n): string | null {\n const wanted = name.toLowerCase();\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === wanted) return value;\n }\n return null;\n}\n\n/**\n * Run the host's signature check against one of a definition's webhook\n * declarations, exactly as the ingress route will: the declared header (and\n * timestamp header, when declared) are read from `headers`, the `scheme` is\n * the declaration's own, and the verifier is the same function the host runs.\n *\n * Sign the payload in your test the way the VENDOR documents it, then assert\n * `{ ok: true }` here. That proves the declaration — header name, keyed\n * pairs, signed-payload template, tolerance — matches the vendor before a\n * single real delivery is rejected on the host, where `ekanos status` cannot\n * see it. Throws for an undeclared webhook id and for a declaration with\n * `signature: 'none'`: neither is something to verify, and a silent `ok`\n * there would be a test that proves nothing.\n */\nexport async function verifyDeclaredWebhookSignature<\n Schemas extends StorageSchemas = StorageSchemas,\n>(\n definition: IntegrationDefinition<Schemas>,\n webhookId: string,\n options: VerifyDeclaredWebhookSignatureOptions,\n): Promise<SignatureVerification> {\n const webhook = (definition.webhooks ?? []).find((w) => w.id === webhookId);\n if (!webhook) {\n throw new Error(\n `verifyDeclaredWebhookSignature: \"${definition.slug}\" declares no webhook \"${webhookId}\".`,\n );\n }\n if (webhook.signature === 'none') {\n throw new Error(\n `verifyDeclaredWebhookSignature: webhook \"${webhookId}\" declares signature: 'none'; there is nothing to verify.`,\n );\n }\n const { header, scheme } = webhook.signature;\n return verifyWebhookSignature({\n rawBody: options.rawBody,\n presented: headerValue(options.headers, header),\n secret: options.secret,\n scheme,\n timestampHeaderValue: scheme?.timestampHeader\n ? headerValue(options.headers, scheme.timestampHeader)\n : null,\n now: options.now,\n });\n}\n"]}
@@ -7,3 +7,4 @@ export type { IconRenderer } from './icon.js';
7
7
  export type { AccountProductData, AccountProductDataType, ActivationFormProps, ActivationResult, HealthCheckResult, HealthStatus, IntegrationCapability, IntegrationComponentProps, IntegrationConfig, IntegrationMetadata, IntegrationPermission, MarketplaceTileProps, WidgetConfig, } from './integration.js';
8
8
  export type { WidgetAskContext, WidgetSnapshot, WidgetSuggestedPrompt, } from './widget-ask-context.js';
9
9
  export type { WorkspaceTargetDefinition } from './workspace-target.js';
10
+ export type { AttentionSignal, WidgetVerdict, WidgetVerdictTone, } from './widget-verdict.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG","sourcesContent":["/**\n * SDK-owned type definitions: the canonical partner-facing shapes live here,\n * import nothing from `@kit/*`, and are held mutually assignable with the\n * host's copies by a parity test. The full export list is api-report.md.\n */\n\nexport type { IconRenderer } from './icon';\n\nexport type {\n AccountProductData,\n AccountProductDataType,\n ActivationFormProps,\n ActivationResult,\n HealthCheckResult,\n HealthStatus,\n IntegrationCapability,\n IntegrationComponentProps,\n IntegrationConfig,\n IntegrationMetadata,\n IntegrationPermission,\n MarketplaceTileProps,\n WidgetConfig,\n} from './integration';\n\nexport type {\n WidgetAskContext,\n WidgetSnapshot,\n WidgetSuggestedPrompt,\n} from './widget-ask-context';\n\nexport type { WorkspaceTargetDefinition } from './workspace-target';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG","sourcesContent":["/**\n * SDK-owned type definitions: the canonical partner-facing shapes live here,\n * import nothing from `@kit/*`, and are held mutually assignable with the\n * host's copies by a parity test. The full export list is api-report.md.\n */\n\nexport type { IconRenderer } from './icon';\n\nexport type {\n AccountProductData,\n AccountProductDataType,\n ActivationFormProps,\n ActivationResult,\n HealthCheckResult,\n HealthStatus,\n IntegrationCapability,\n IntegrationComponentProps,\n IntegrationConfig,\n IntegrationMetadata,\n IntegrationPermission,\n MarketplaceTileProps,\n WidgetConfig,\n} from './integration';\n\nexport type {\n WidgetAskContext,\n WidgetSnapshot,\n WidgetSuggestedPrompt,\n} from './widget-ask-context';\n\nexport type { WorkspaceTargetDefinition } from './workspace-target';\n\nexport type {\n AttentionSignal,\n WidgetVerdict,\n WidgetVerdictTone,\n} from './widget-verdict';\n"]}
@@ -182,6 +182,11 @@ export interface WidgetConfig {
182
182
  row: number;
183
183
  };
184
184
  widgetState: 'active' | 'inactive' | 'disabled';
185
+ /**
186
+ * `compact` (a figure tile, 1 → 2 → 4 across its row), `half` or `full`
187
+ * (the design system's widget rules). Unset, it is derived from `layouts.lg.w`.
188
+ */
189
+ size?: 'compact' | 'half' | 'full';
185
190
  layouts?: {
186
191
  lg?: {
187
192
  x: number;
@@ -1 +1 @@
1
- {"version":3,"file":"integration.js","sourceRoot":"","sources":["../../src/types/integration.ts"],"names":[],"mappings":"","sourcesContent":["import type { ComponentType } from 'react';\n\nimport type { IconRenderer } from './icon';\n\n// Capability shown on the integration detail page (numbered grid)\nexport interface IntegrationCapability {\n label: string;\n description: string;\n icon?: IconRenderer;\n}\n\n// Permission scope shown on the integration detail page\nexport interface IntegrationPermission {\n label: string;\n detail: string;\n type: 'read' | 'write';\n}\n\n// ---- Integration Health Monitoring (issue #474) -----------------------------\n\n// Higher = healthier. Used internally for \"least healthy wins\" merging.\nexport type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';\n\n/**\n * Result returned by `BaseIntegration.healthCheck(accountId)`. Routed through\n * `classifyHealth()` (server-side) before writing to\n * `integration_health_status`.\n */\nexport interface HealthCheckResult {\n status: HealthStatus;\n errorCode?: string;\n errorMessage?: string;\n /** Rolling 24h error rate from `account_product_usage_events`, 0..100. */\n errorRatePct?: number;\n /** Sample size used to compute errorRatePct; if below threshold, treated as unknown. */\n sampleSize?: number;\n /** Whether the most recent observed call succeeded. */\n lastObservationSucceeded?: boolean;\n /** Token/credential expiry, if known. Drives token-expiry warnings. */\n tokenExpiresAt?: string;\n /** Free-form per-integration data carried to the dashboard. */\n metadata?: Record<string, unknown>;\n}\n\n// Activation result from integration\nexport interface ActivationResult {\n success: boolean;\n configData?: Record<string, unknown>;\n externalId?: string;\n errorMessage?: string;\n metadata?: Record<string, unknown>;\n}\n\n// Base props that all integration components need\ninterface BaseIntegrationComponentProps {\n accountId: string;\n sourceId?: string;\n integrationConfig?: Record<string, unknown>;\n}\n\n// Discriminated union for product reference - only one allowed\ntype ProductReference =\n | { productId: string; productSlug?: never }\n | { productSlug: string; productId?: never };\n\n// New base type using discriminated union\nexport type IntegrationComponentProps = BaseIntegrationComponentProps &\n ProductReference;\n\n// Marketplace tile props\nexport type MarketplaceTileProps = IntegrationComponentProps & {\n onActivate?: () => void;\n isActivated?: boolean;\n /** Navigation URL for the integration detail page */\n href?: string;\n};\n\n// Activation form props\nexport type ActivationFormProps = IntegrationComponentProps & {\n onSuccess?: (result: ActivationResult) => void;\n onCancel?: () => void;\n isLoading?: boolean;\n /** Render inline instead of in a dialog */\n inline?: boolean;\n /**\n * The integration is already connected and the person chose to connect it\n * again — after a token expired, a password changed, or a health check\n * failed. An OAuth form sends them through sign-in even when a token is\n * stored; a credential form simply accepts new values. Activating again\n * resets the integration's health to unknown until the next check.\n */\n reconnect?: boolean;\n /**\n * The TEAM account context the user is activating within — separate from\n * `accountId`, which for `scope: 'user'` integrations is collapsed to the\n * personal account for activation storage/token purposes. Only consumed by\n * workspace materialization (`materializeWorkspaceOnActivation`), never for\n * activation/token storage itself (plan 009 Part A2). Optional: undefined\n * for team-scoped integrations (their `accountId` already IS the team) and\n * for any caller that hasn't threaded it yet — the server action falls back\n * to `accountId` when absent.\n */\n teamAccountId?: string;\n};\n\n// Integration metadata\nexport interface IntegrationMetadata {\n productId: string;\n productSlug: string;\n integrationName: string;\n description: string;\n category: string;\n version: string;\n icon?: string;\n /** Bundled logo URL — overrides product.logo_path on the marketplace */\n logoPath?: string;\n /** When 'user', activation data is stored under the user's personal account (user.id) instead of the team account */\n scope?: 'user' | 'team';\n /**\n * Set FALSE to assert this integration has NO self-serve activation surface —\n * the marketplace detail page then renders no activation form at all.\n *\n * This is deliberately distinct from \"has no CUSTOM activation form\": an\n * integration that omits `getActivationForm()` still gets the generic one, and\n * six integrations depend on that. Use this flag only when submitting ANY\n * activation form would be a dead end — where `activate()` cannot succeed, so\n * showing a form hands the user a button that cannot work.\n * Undefined/true = normal self-serve activation.\n */\n selfServeActivation?: boolean;\n /**\n * How this integration's MCP tools resolve credentials — the integration's\n * SELF-DECLARATION of machine-executability, consumed dynamically by the\n * workflow engine's fail-closed tool allowlist (isGatewayExposableTool).\n *\n * 'account' — credentials/identity resolve from team- or source-level\n * config via the ToolContext (`getAccountId(ctx)` + `ctx.supabase`).\n * Safe for durable machine-principal execution (workflows): eligible\n * tools (read + public/internal sensitivity) become workflow-callable.\n * 'user' — credentials or identity are personal (ctx.user.id, personal\n * OAuth, session reads like getSupabaseServerClient()/requireUser()).\n * NEVER machine-executable: a workflow would silently ride one\n * employee's personal grant.\n * 'source' — platform-level credentials (diagnostics-class integrations).\n * Not exposed to account workflows.\n *\n * UNDECLARED = fail closed (treated as not machine-executable). Declaring\n * 'account' is a security assertion: the integration's MCP run() handlers\n * must use ctx-based resolution ONLY — no session clients, no personal\n * grants. A guard test in @kit/integrations-workflows enforces the obvious\n * violations (session-client reads in mcp/ code of 'account' declarers).\n */\n credentialModel?: 'account' | 'user' | 'source';\n /**\n * Which KIND of integration this is. **Required** — nothing is inferred from\n * omission. An Integration is any package extending `BaseIntegration`; its\n * kind says who turns it on and whether it needs credentials:\n *\n * 'feature' — an Ekanos-native capability. Enabled for a Source by a\n * super-admin; no third-party credentials, no activation flow, no\n * marketplace tile. Always declares its own top-level sidebar Section\n * via `getSidebarNav()` and NEVER declares `getWorkspaceTargets()` —\n * its widgets have no Workspace of their own, so an admin places them\n * on one in the composer (`/admin/sources/[id]/layout`).\n * 'integration' — a connection to an outside system. Made available to a\n * Source by a super-admin, then connected by a team or user (usually\n * OAuth/SSO); has credentials, an activation flow and a marketplace\n * tile. Declares a Workspace via `getWorkspaceTargets()`, a top-level\n * Section via `getSidebarNav()`, or both (Centrex does both).\n *\n * The asymmetry is one-directional and enforced by the host's CI:\n * a Feature MUST declare a Section and MUST NOT declare a Workspace; a\n * connector MAY declare either or both.\n */\n kind: 'feature' | 'integration';\n /**\n * Product slugs that MUST be attached to the same Source for this product to\n * function — a hard dependency, not a suggestion.\n *\n * The host REFUSES to attach this product to a Source missing any of them,\n * and refuses a detach that would strand a dependent. Admin surfaces render\n * the dependency in both directions (\"Requires X\" / \"Enables Y\").\n *\n * Host CI enforces: every slug is registered, nothing requires itself, no\n * cycles, and a `kind: 'feature'` may require a `'integration'` but never\n * the reverse.\n */\n requires?: string[];\n}\n\n// Widget configuration\nexport interface WidgetConfig {\n id: string;\n name: string;\n component: ComponentType<IntegrationComponentProps>;\n gridSize?: { cols: number; rows: number } | { cols: number; rows: number }[];\n gridPosition?: { col: number; row: number };\n widgetState: 'active' | 'inactive' | 'disabled';\n // React-grid-layout format\n layouts?: {\n lg?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n md?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n sm?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n };\n // Integration metadata for widgets\n integrationMetadata?: IntegrationMetadata;\n category?: { id: string; name: string; slug: string; icon: string | null };\n // Database product ID (UUID) from widget_config table\n productId?: string;\n isCollapsible?: boolean;\n // For collapsed state persistence\n collapsed?: boolean;\n widgetConfigId?: string;\n // Resolved workspace placement (explicit widget_config.workspace_id\n // override, else the product's inheriting attachment). Populated by the\n // dashboard widgets loader; null = no workspace (unscoped dashboard).\n workspaceId?: string | null;\n isPinnable?: boolean;\n isPinned?: boolean;\n // When true, the dashboard renders the \"Ask about this\" AI footer bar for\n // this widget. The widget may still pass `askContext` to the wrapper for a\n // rich snapshot; otherwise a minimal default is used.\n aiFooterEnabled?: boolean;\n // Integration health for this widget's product. `unhealthy` downgrades the\n // widget to `disabled`; `degraded` surfaces a stale-data footer.\n health?: {\n status: HealthStatus;\n lastSuccessAt?: string | null;\n errorMessage?: string | null;\n };\n}\n\n// Database types for integration tables.\n//\n// PARTNER-VISIBLE SUBSET: these row types deliberately OMIT `vault_secret_id`.\n// A vault id is a host handle to a decryptable secret; the partner surface\n// never exposes one — `ctx.secrets` fences it off. Host code keeps the full\n// row because it needs the handle; a parity test pins the SDK type as the\n// host's row minus exactly the host-only fields, so a new column cannot drift\n// in here unnoticed.\nexport interface IntegrationConfig {\n id: string;\n account_id: string | null;\n product_id: string;\n config_type: 'api_credentials' | 'activation_data' | 'settings';\n config_data: Record<string, unknown>;\n status: 'active' | 'inactive' | 'pending' | 'error' | 'suspended';\n is_active: boolean;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n\n/**\n * The PARTNER-VISIBLE `data_type` values on `account_product_data` — a strict\n * SUBSET of `@kit/integrations-core`'s union.\n *\n * `'secret'` is deliberately absent. Secret rows are host-managed (one row per\n * secret name, value in Vault, `vault_secret_id` on the row), written only by\n * the capability context's `persistSecret` and read only through\n * `ctx.secrets`. Naming the kind here would advertise a row shape partner code\n * must never address: `ctx.storage` rejects it, and core's generic accessors\n * reject it too (`config-utils.ts`, `assertNotHostOnlyDataType`) precisely\n * because a write there would land the plaintext in the `data` column.\n *\n * When you add an integration-specific kind, add it in BOTH places (core owns\n * the full union) and keep this one partner-appropriate. Prefer `dataSubtype`\n * for orthogonal slicing within a kind (e.g. `data_type='cache'`,\n * `data_subtype='profile'`); reserve a new top-level kind for data that has a\n * distinct lifecycle or is queried independently.\n */\nexport type AccountProductDataType =\n | 'activation'\n | 'settings'\n | 'metrics_summary'\n | 'sync_state'\n | 'cache'\n // Integration-specific kinds — extend here when needed.\n | 'cash_forecast' // quickbooks — user-supplied manual cash-forecast estimates\n | 'tax_config'; // quickbooks — per-user federal/state tax rate config\n\nexport interface AccountProductData {\n id: string;\n account_id: string;\n product_id: string;\n data_type: AccountProductDataType;\n data_subtype?: string;\n data: Record<string, unknown>;\n expires_at?: string;\n integration_config_id?: string;\n external_id?: string;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n"]}
1
+ {"version":3,"file":"integration.js","sourceRoot":"","sources":["../../src/types/integration.ts"],"names":[],"mappings":"","sourcesContent":["import type { ComponentType } from 'react';\n\nimport type { IconRenderer } from './icon';\n\n// Capability shown on the integration detail page (numbered grid)\nexport interface IntegrationCapability {\n label: string;\n description: string;\n icon?: IconRenderer;\n}\n\n// Permission scope shown on the integration detail page\nexport interface IntegrationPermission {\n label: string;\n detail: string;\n type: 'read' | 'write';\n}\n\n// ---- Integration Health Monitoring (issue #474) -----------------------------\n\n// Higher = healthier. Used internally for \"least healthy wins\" merging.\nexport type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';\n\n/**\n * Result returned by `BaseIntegration.healthCheck(accountId)`. Routed through\n * `classifyHealth()` (server-side) before writing to\n * `integration_health_status`.\n */\nexport interface HealthCheckResult {\n status: HealthStatus;\n errorCode?: string;\n errorMessage?: string;\n /** Rolling 24h error rate from `account_product_usage_events`, 0..100. */\n errorRatePct?: number;\n /** Sample size used to compute errorRatePct; if below threshold, treated as unknown. */\n sampleSize?: number;\n /** Whether the most recent observed call succeeded. */\n lastObservationSucceeded?: boolean;\n /** Token/credential expiry, if known. Drives token-expiry warnings. */\n tokenExpiresAt?: string;\n /** Free-form per-integration data carried to the dashboard. */\n metadata?: Record<string, unknown>;\n}\n\n// Activation result from integration\nexport interface ActivationResult {\n success: boolean;\n configData?: Record<string, unknown>;\n externalId?: string;\n errorMessage?: string;\n metadata?: Record<string, unknown>;\n}\n\n// Base props that all integration components need\ninterface BaseIntegrationComponentProps {\n accountId: string;\n sourceId?: string;\n integrationConfig?: Record<string, unknown>;\n}\n\n// Discriminated union for product reference - only one allowed\ntype ProductReference =\n | { productId: string; productSlug?: never }\n | { productSlug: string; productId?: never };\n\n// New base type using discriminated union\nexport type IntegrationComponentProps = BaseIntegrationComponentProps &\n ProductReference;\n\n// Marketplace tile props\nexport type MarketplaceTileProps = IntegrationComponentProps & {\n onActivate?: () => void;\n isActivated?: boolean;\n /** Navigation URL for the integration detail page */\n href?: string;\n};\n\n// Activation form props\nexport type ActivationFormProps = IntegrationComponentProps & {\n onSuccess?: (result: ActivationResult) => void;\n onCancel?: () => void;\n isLoading?: boolean;\n /** Render inline instead of in a dialog */\n inline?: boolean;\n /**\n * The integration is already connected and the person chose to connect it\n * again — after a token expired, a password changed, or a health check\n * failed. An OAuth form sends them through sign-in even when a token is\n * stored; a credential form simply accepts new values. Activating again\n * resets the integration's health to unknown until the next check.\n */\n reconnect?: boolean;\n /**\n * The TEAM account context the user is activating within — separate from\n * `accountId`, which for `scope: 'user'` integrations is collapsed to the\n * personal account for activation storage/token purposes. Only consumed by\n * workspace materialization (`materializeWorkspaceOnActivation`), never for\n * activation/token storage itself (plan 009 Part A2). Optional: undefined\n * for team-scoped integrations (their `accountId` already IS the team) and\n * for any caller that hasn't threaded it yet — the server action falls back\n * to `accountId` when absent.\n */\n teamAccountId?: string;\n};\n\n// Integration metadata\nexport interface IntegrationMetadata {\n productId: string;\n productSlug: string;\n integrationName: string;\n description: string;\n category: string;\n version: string;\n icon?: string;\n /** Bundled logo URL — overrides product.logo_path on the marketplace */\n logoPath?: string;\n /** When 'user', activation data is stored under the user's personal account (user.id) instead of the team account */\n scope?: 'user' | 'team';\n /**\n * Set FALSE to assert this integration has NO self-serve activation surface —\n * the marketplace detail page then renders no activation form at all.\n *\n * This is deliberately distinct from \"has no CUSTOM activation form\": an\n * integration that omits `getActivationForm()` still gets the generic one, and\n * six integrations depend on that. Use this flag only when submitting ANY\n * activation form would be a dead end — where `activate()` cannot succeed, so\n * showing a form hands the user a button that cannot work.\n * Undefined/true = normal self-serve activation.\n */\n selfServeActivation?: boolean;\n /**\n * How this integration's MCP tools resolve credentials — the integration's\n * SELF-DECLARATION of machine-executability, consumed dynamically by the\n * workflow engine's fail-closed tool allowlist (isGatewayExposableTool).\n *\n * 'account' — credentials/identity resolve from team- or source-level\n * config via the ToolContext (`getAccountId(ctx)` + `ctx.supabase`).\n * Safe for durable machine-principal execution (workflows): eligible\n * tools (read + public/internal sensitivity) become workflow-callable.\n * 'user' — credentials or identity are personal (ctx.user.id, personal\n * OAuth, session reads like getSupabaseServerClient()/requireUser()).\n * NEVER machine-executable: a workflow would silently ride one\n * employee's personal grant.\n * 'source' — platform-level credentials (diagnostics-class integrations).\n * Not exposed to account workflows.\n *\n * UNDECLARED = fail closed (treated as not machine-executable). Declaring\n * 'account' is a security assertion: the integration's MCP run() handlers\n * must use ctx-based resolution ONLY — no session clients, no personal\n * grants. A guard test in @kit/integrations-workflows enforces the obvious\n * violations (session-client reads in mcp/ code of 'account' declarers).\n */\n credentialModel?: 'account' | 'user' | 'source';\n /**\n * Which KIND of integration this is. **Required** — nothing is inferred from\n * omission. An Integration is any package extending `BaseIntegration`; its\n * kind says who turns it on and whether it needs credentials:\n *\n * 'feature' — an Ekanos-native capability. Enabled for a Source by a\n * super-admin; no third-party credentials, no activation flow, no\n * marketplace tile. Always declares its own top-level sidebar Section\n * via `getSidebarNav()` and NEVER declares `getWorkspaceTargets()` —\n * its widgets have no Workspace of their own, so an admin places them\n * on one in the composer (`/admin/sources/[id]/layout`).\n * 'integration' — a connection to an outside system. Made available to a\n * Source by a super-admin, then connected by a team or user (usually\n * OAuth/SSO); has credentials, an activation flow and a marketplace\n * tile. Declares a Workspace via `getWorkspaceTargets()`, a top-level\n * Section via `getSidebarNav()`, or both (Centrex does both).\n *\n * The asymmetry is one-directional and enforced by the host's CI:\n * a Feature MUST declare a Section and MUST NOT declare a Workspace; a\n * connector MAY declare either or both.\n */\n kind: 'feature' | 'integration';\n /**\n * Product slugs that MUST be attached to the same Source for this product to\n * function — a hard dependency, not a suggestion.\n *\n * The host REFUSES to attach this product to a Source missing any of them,\n * and refuses a detach that would strand a dependent. Admin surfaces render\n * the dependency in both directions (\"Requires X\" / \"Enables Y\").\n *\n * Host CI enforces: every slug is registered, nothing requires itself, no\n * cycles, and a `kind: 'feature'` may require a `'integration'` but never\n * the reverse.\n */\n requires?: string[];\n}\n\n// Widget configuration\nexport interface WidgetConfig {\n id: string;\n name: string;\n component: ComponentType<IntegrationComponentProps>;\n gridSize?: { cols: number; rows: number } | { cols: number; rows: number }[];\n gridPosition?: { col: number; row: number };\n widgetState: 'active' | 'inactive' | 'disabled';\n /**\n * `compact` (a figure tile, 1 → 2 → 4 across its row), `half` or `full`\n * (the design system's widget rules). Unset, it is derived from `layouts.lg.w`.\n */\n size?: 'compact' | 'half' | 'full';\n // React-grid-layout format\n layouts?: {\n lg?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n md?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n sm?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n };\n // Integration metadata for widgets\n integrationMetadata?: IntegrationMetadata;\n category?: { id: string; name: string; slug: string; icon: string | null };\n // Database product ID (UUID) from widget_config table\n productId?: string;\n isCollapsible?: boolean;\n // For collapsed state persistence\n collapsed?: boolean;\n widgetConfigId?: string;\n // Resolved workspace placement (explicit widget_config.workspace_id\n // override, else the product's inheriting attachment). Populated by the\n // dashboard widgets loader; null = no workspace (unscoped dashboard).\n workspaceId?: string | null;\n isPinnable?: boolean;\n isPinned?: boolean;\n // When true, the dashboard renders the \"Ask about this\" AI footer bar for\n // this widget. The widget may still pass `askContext` to the wrapper for a\n // rich snapshot; otherwise a minimal default is used.\n aiFooterEnabled?: boolean;\n // Integration health for this widget's product. `unhealthy` downgrades the\n // widget to `disabled`; `degraded` surfaces a stale-data footer.\n health?: {\n status: HealthStatus;\n lastSuccessAt?: string | null;\n errorMessage?: string | null;\n };\n}\n\n// Database types for integration tables.\n//\n// PARTNER-VISIBLE SUBSET: these row types deliberately OMIT `vault_secret_id`.\n// A vault id is a host handle to a decryptable secret; the partner surface\n// never exposes one — `ctx.secrets` fences it off. Host code keeps the full\n// row because it needs the handle; a parity test pins the SDK type as the\n// host's row minus exactly the host-only fields, so a new column cannot drift\n// in here unnoticed.\nexport interface IntegrationConfig {\n id: string;\n account_id: string | null;\n product_id: string;\n config_type: 'api_credentials' | 'activation_data' | 'settings';\n config_data: Record<string, unknown>;\n status: 'active' | 'inactive' | 'pending' | 'error' | 'suspended';\n is_active: boolean;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n\n/**\n * The PARTNER-VISIBLE `data_type` values on `account_product_data` — a strict\n * SUBSET of `@kit/integrations-core`'s union.\n *\n * `'secret'` is deliberately absent. Secret rows are host-managed (one row per\n * secret name, value in Vault, `vault_secret_id` on the row), written only by\n * the capability context's `persistSecret` and read only through\n * `ctx.secrets`. Naming the kind here would advertise a row shape partner code\n * must never address: `ctx.storage` rejects it, and core's generic accessors\n * reject it too (`config-utils.ts`, `assertNotHostOnlyDataType`) precisely\n * because a write there would land the plaintext in the `data` column.\n *\n * When you add an integration-specific kind, add it in BOTH places (core owns\n * the full union) and keep this one partner-appropriate. Prefer `dataSubtype`\n * for orthogonal slicing within a kind (e.g. `data_type='cache'`,\n * `data_subtype='profile'`); reserve a new top-level kind for data that has a\n * distinct lifecycle or is queried independently.\n */\nexport type AccountProductDataType =\n | 'activation'\n | 'settings'\n | 'metrics_summary'\n | 'sync_state'\n | 'cache'\n // Integration-specific kinds — extend here when needed.\n | 'cash_forecast' // quickbooks — user-supplied manual cash-forecast estimates\n | 'tax_config'; // quickbooks — per-user federal/state tax rate config\n\nexport interface AccountProductData {\n id: string;\n account_id: string;\n product_id: string;\n data_type: AccountProductDataType;\n data_subtype?: string;\n data: Record<string, unknown>;\n expires_at?: string;\n integration_config_id?: string;\n external_id?: string;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A widget's verdict (the design system's widget rules). The canonical shapes live
3
+ * in `@ekanos/integration-schema`, beside the `attention` handler that returns
4
+ * them; the SDK re-exports them for widget code.
5
+ */
6
+ export type { AttentionSignal, WidgetVerdict, WidgetVerdictTone, } from '@ekanos/integration-schema';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=widget-verdict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-verdict.js","sourceRoot":"","sources":["../../src/types/widget-verdict.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * A widget's verdict (the design system's widget rules). The canonical shapes live\n * in `@ekanos/integration-schema`, beside the `attention` handler that returns\n * them; the SDK re-exports them for widget code.\n */\nexport type {\n AttentionSignal,\n WidgetVerdict,\n WidgetVerdictTone,\n} from '@ekanos/integration-schema';\n"]}
@@ -0,0 +1,226 @@
1
+ /**
2
+ * @ekanos/sdk/eslint/design — the partner STYLE preset.
3
+ *
4
+ * This is a style preset, and it is deliberately a SEPARATE export from
5
+ * `@ekanos/sdk/eslint`. That one is a security control: it bans the escapes
6
+ * that route around the capability layer, it sets `noInlineConfig` so partner
7
+ * source cannot silence it, and every rule in it is core ESLint so it adds
8
+ * nothing to the SDK's build. Mixing a style rule into that file would make a
9
+ * load-bearing control look negotiable, and a partner who disabled the preset
10
+ * over a `<button>` complaint would have disabled the control with it.
11
+ *
12
+ * So the two ship apart, and a partner may adopt this one and not the other
13
+ * (never the reverse — the capability preset is not optional for a
14
+ * submission).
15
+ *
16
+ * ── What it enforces ─────────────────────────────────────────────────────
17
+ * Nine rules. One is local to this file: no raw interactive HTML element
18
+ * where `@ekanos/ui` publishes the host's component. A hand-rolled `<button>`
19
+ * is not a styling nit — it misses the kit's focus ring, its disabled and
20
+ * loading states, its size scale, and every per-Source retint that reaches a
21
+ * widget through the component rather than through a class. The same goes for
22
+ * inputs, selects, textareas and labels, each of which carries accessibility
23
+ * wiring a bare element does not.
24
+ *
25
+ * The other eight are the host's own `design/*` rules — palette-adjacent
26
+ * white/black utilities, Badge tone variants, overlay widths, rounded row
27
+ * hovers, faded muted text, viewport breakpoints in widgets, tinted chips,
28
+ * toolbar tabs — imported from `./eslint.design-rules.mjs`, the single
29
+ * implementation the host repo's oxlint plugin also loads (through the
30
+ * two-line re-export the host's lint config keeps). There is
31
+ * exactly one copy of each rule, so a partner is held to what the host holds
32
+ * itself to and the two cannot drift.
33
+ *
34
+ * `ekanos validate` still reports its own `design/*` findings from a
35
+ * parser-less text scan (`@ekanos/integration-schema`'s
36
+ * `collectDesignSourceFindings`). That is a different mechanism for a
37
+ * different place — the admission gate scans a submitted archive with no
38
+ * ESLint and no AST — not a second copy of these rules.
39
+ *
40
+ * ── Severity: warn, everywhere ───────────────────────────────────────────
41
+ * Every rule here reports `warn`, never `error`, for the same reason
42
+ * `collectDesignSourceFindings` does: off-system UI is a cohesion defect a
43
+ * reviewer weighs, not an unbuildable submission. The scaffold wires this
44
+ * preset up on day one, and a lint that fails a new partner's very first run
45
+ * teaches them to delete the config. The host lints itself at `error` — see
46
+ * `.oxlintrc.json` — which is the asymmetry we want.
47
+ *
48
+ * ── Dependency-free, like its sibling ────────────────────────────────────
49
+ * `react/forbid-elements` would need `eslint-plugin-react`, a runtime
50
+ * dependency this package does not have and should not grow for a lint
51
+ * preset. The rule below is the same check, the same option shape
52
+ * (`{ forbid: [{ element, message }] }`), implemented in ~20 lines against
53
+ * ESLint's own JSX AST.
54
+ *
55
+ * ── Usage (a partner's eslint.config.mjs) ────────────────────────────────
56
+ *
57
+ * import capabilityPreset from '@ekanos/sdk/eslint';
58
+ * import designPreset from '@ekanos/sdk/eslint/design';
59
+ *
60
+ * export default [
61
+ * ...someBaseConfig,
62
+ * ...capabilityPreset,
63
+ * ...designPreset,
64
+ * ];
65
+ *
66
+ * Inline disables DO work here, unlike the capability preset: a style rule
67
+ * with a real exception should be disabled at the line that needs it, where a
68
+ * reviewer reads the reason, rather than argued about.
69
+ */
70
+ import designRulesPlugin from './eslint.design-rules.mjs';
71
+
72
+ /**
73
+ * `.ts` as well as `.tsx`. The JSX-shaped rules find nothing in a `.ts`
74
+ * module and cost nothing there, but the class-string rules must see it: a
75
+ * `.ts` module can build UI with `createElement` and a `className` string,
76
+ * which the starter scaffold itself does.
77
+ */
78
+ const COMPONENT_GLOB = ['src/**/*.ts', 'src/**/*.tsx'];
79
+
80
+ /**
81
+ * The raw elements `@ekanos/ui` replaces, each with the module a partner
82
+ * imports instead. Kept equal to the host repo's own
83
+ * `react/forbid-elements` list for the example integrations by a parity test
84
+ * in `src/context/__tests__/design-preset-oxlint-parity.test.ts`.
85
+ */
86
+ export const FORBIDDEN_ELEMENTS = [
87
+ {
88
+ element: 'button',
89
+ message:
90
+ 'Use <Button> from @ekanos/ui/button. A bare <button> misses the focus ' +
91
+ 'ring, the disabled and loading states, the size scale, and the ' +
92
+ 'per-Source retint that only reaches a widget through the component.',
93
+ },
94
+ {
95
+ element: 'input',
96
+ message:
97
+ 'Use <Input> from @ekanos/ui/input, inside a <FormField> from ' +
98
+ '@ekanos/ui/form when it is part of a form.',
99
+ },
100
+ {
101
+ element: 'select',
102
+ message: 'Use <Select> from @ekanos/ui/select.',
103
+ },
104
+ {
105
+ element: 'textarea',
106
+ message: 'Use <Textarea> from @ekanos/ui/textarea.',
107
+ },
108
+ {
109
+ element: 'label',
110
+ message:
111
+ 'Use <Label> from @ekanos/ui/label, which wires itself to the control ' +
112
+ 'it names.',
113
+ },
114
+ {
115
+ element: 'table',
116
+ message:
117
+ 'Use <Table> from @ekanos/ui/table only for genuinely tabular data. ' +
118
+ 'For a list, prefer edge-to-edge rows: a container with no horizontal ' +
119
+ 'padding, each row `px-5 py-3` with a flat `hover:bg-accent/50`, ' +
120
+ 'separated by <Separator> from @ekanos/ui/separator. See ' +
121
+ "the design system's widget rules.",
122
+ },
123
+ ];
124
+
125
+ const FORBIDDEN_BY_ELEMENT = new Map(
126
+ FORBIDDEN_ELEMENTS.map((entry) => [entry.element, entry.message]),
127
+ );
128
+
129
+ /**
130
+ * ESLint's own `react/forbid-elements`, minus the plugin. A JSX element whose
131
+ * name is a lowercase HTML tag is a `JSXIdentifier`; a component reference is
132
+ * capitalised or a member expression, so neither can collide with this list.
133
+ */
134
+ const designPlugin = {
135
+ meta: { name: 'ekanos-design' },
136
+ rules: {
137
+ 'forbid-elements': {
138
+ meta: {
139
+ type: 'problem',
140
+ docs: {
141
+ description:
142
+ 'Disallow raw interactive HTML elements that @ekanos/ui publishes a component for.',
143
+ },
144
+ schema: [
145
+ {
146
+ type: 'object',
147
+ properties: {
148
+ forbid: {
149
+ type: 'array',
150
+ items: {
151
+ type: 'object',
152
+ properties: {
153
+ element: { type: 'string' },
154
+ message: { type: 'string' },
155
+ },
156
+ required: ['element'],
157
+ additionalProperties: false,
158
+ },
159
+ },
160
+ },
161
+ additionalProperties: false,
162
+ },
163
+ ],
164
+ },
165
+ /**
166
+ * Typed in JSDoc because a test imports this module, which pulls it
167
+ * into the package's TypeScript program (`eslint.preset.mjs` needs none
168
+ * — it declares no callbacks of its own).
169
+ *
170
+ * @param {{
171
+ * options?: Array<{ forbid?: Array<{ element: string; message?: string }> }>;
172
+ * report: (descriptor: { node: unknown; message: string }) => void;
173
+ * }} context
174
+ */
175
+ create(context) {
176
+ const configured = context.options?.[0]?.forbid;
177
+ const forbidden = Array.isArray(configured)
178
+ ? new Map(
179
+ configured.map((entry) => [
180
+ entry.element,
181
+ entry.message ?? `<${entry.element}> is forbidden.`,
182
+ ]),
183
+ )
184
+ : FORBIDDEN_BY_ELEMENT;
185
+
186
+ return {
187
+ /** @param {{ name?: { type: string; name: string } }} node */
188
+ JSXOpeningElement(node) {
189
+ if (node.name?.type !== 'JSXIdentifier') return;
190
+
191
+ const message = forbidden.get(node.name.name);
192
+ if (message) context.report({ node, message });
193
+ },
194
+ };
195
+ },
196
+ },
197
+ },
198
+ };
199
+
200
+ /**
201
+ * Every rule the shared plugin publishes, at `warn`. Derived from the plugin
202
+ * rather than listed, so a rule added to `eslint.design-rules.mjs` for the
203
+ * host reaches partners in the same commit — the drift this preset's whole
204
+ * arrangement exists to prevent.
205
+ */
206
+ const sharedDesignRules = Object.fromEntries(
207
+ Object.keys(designRulesPlugin.rules).map((rule) => [
208
+ `${designRulesPlugin.meta.name}/${rule}`,
209
+ 'warn',
210
+ ]),
211
+ );
212
+
213
+ export default [
214
+ {
215
+ name: '@ekanos/sdk/eslint/design',
216
+ files: COMPONENT_GLOB,
217
+ plugins: {
218
+ 'ekanos-design': designPlugin,
219
+ [designRulesPlugin.meta.name]: designRulesPlugin,
220
+ },
221
+ rules: {
222
+ ...sharedDesignRules,
223
+ 'ekanos-design/forbid-elements': ['warn', { forbid: FORBIDDEN_ELEMENTS }],
224
+ },
225
+ },
226
+ ];