@ekanos/sdk 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,48 +79,44 @@ Three of five icons rendered as `□`. The monorepo could not reproduce it at al
79
79
  visible from an installed tarball. If you bundle `@ekanos/*` into your own
80
80
  output, keep these singletons.
81
81
 
82
- ### What is published, and what is not
82
+ ### What is published
83
83
 
84
- Being straight about this up front, because the alternative is you finding out
85
- by running a command that 404s:
84
+ Everything you need is on npm:
86
85
 
87
- | Package | On npm |
86
+ | Package | Role |
88
87
  |---|---|
89
- | `@ekanos/sdk`, `@ekanos/ui`, `@ekanos/integration-schema` | **yes** — `0.1.2` |
90
- | `@ekanos/cli`, `@ekanos/harness` | **not yet** |
88
+ | `@ekanos/sdk`, `@ekanos/ui`, `@ekanos/integration-schema` | the authoring API, the widget UI slice, the schema |
89
+ | `@ekanos/cli` | `ekanos init`, `dev`, `test`, `validate`, `publish` |
90
+ | `@ekanos/harness` | the local dev harness `ekanos dev` runs |
91
91
 
92
- So `npx @ekanos/cli init` and `pnpm add -D @ekanos/harness` do not resolve
93
- today. The toolchain and the local dev harness are written and documented —
94
- their READMEs are in this repo and describe what will ship — but you cannot
95
- install them yet.
92
+ Start a project with:
96
93
 
97
- **What that costs you is seeing, not building.** Everything in this document
98
- works against `@ekanos/sdk` alone:
94
+ ```bash
95
+ npx @ekanos/cli init
96
+ ```
97
+
98
+ `ekanos init` scaffolds a project that already depends on the matching
99
+ versions, and `ekanos dev` renders every surface of your integration in real
100
+ Ekanos chrome — the dashboard grid, the marketplace tile, the activation form,
101
+ the Triggers panel — from fixtures, or against your real vendor API in live
102
+ mode. The `@ekanos/harness` and `@ekanos/cli` READMEs document both.
103
+
104
+ You can also use `@ekanos/sdk` on its own:
99
105
 
100
106
  ```bash
101
107
  npm install @ekanos/sdk zod
102
108
  ```
103
109
 
104
110
  - `defineIntegration()` validates at import time and throws with a remediation
105
- message, so your definition is its own feedback loop — you do not need
106
- `ekanos validate` to know whether it is correct.
111
+ message, so your definition is its own feedback loop.
107
112
  - The whole [Testing](#testing) section needs nothing but vitest.
108
113
  `createMockContext()` gives you the same context production builds, and
109
- `invokeWebhook()` / `invokeSchedule()` run your event handlers with the same
110
- payload validation and egress allowlist the real transports will.
111
-
112
- **What you genuinely cannot do yet** is see any of it rendered — widgets in the
113
- dashboard grid, the marketplace tile, the activation form, the Triggers panel.
114
- Those are the harness, and the harness is not installable. Webhook, schedule
115
- and OAuth work collapses back to unit tests, which is a real loss but a
116
- recoverable one. Widget *appearance* does not: you will be writing it blind
117
- until the harness ships.
118
-
119
- One hard stop worth naming: `ekanos.json`, the project file the CLI reads. Its
120
- schema and the full generated-shell file list are documented in the
121
- `@ekanos/cli` README — which ships inside the CLI tarball, so today you can
122
- only read it if you have this repo. Nothing in this document needs that file;
123
- if you are hand-rolling something that does, wait for the CLI.
114
+ `invokeWebhook()` / `invokeSchedule()` / `invokeActivate()` run your handlers
115
+ with the same payload validation and egress allowlist the real transports
116
+ will.
117
+
118
+ `ekanos.json` is the project file the CLI reads; its schema and the generated
119
+ shell's file list are documented in the `@ekanos/cli` README.
124
120
 
125
121
  ## The shape of an integration
126
122
 
@@ -316,6 +312,15 @@ Four things the signature does not tell you:
316
312
  Key names are `[a-z0-9_-]+` with at most one `/` segment: `config`,
317
313
  `settings/location`, `cache/recent-payments`. Two slashes is a validation error.
318
314
 
315
+ **Limits.** The host caps one stored value at **64 KB serialized**, one
316
+ secret value at 64 KB, the number of distinct secret names an integration
317
+ writes at **32**, and a key segment at 100 characters. The same numbers are
318
+ exported as `PARTNER_STORAGE_LIMITS` from `@ekanos/integration-schema` and
319
+ enforced by `createMockContext()`, so a test that writes an oversized value
320
+ fails locally the way the host would. A snapshot of vendor records reaches
321
+ 64 KB around 120–150 modest rows: trim to a byte budget and flag it rather
322
+ than finding out on the host.
323
+
319
324
  ### `egress`
320
325
 
321
326
  An origin-only https allowlist. `ctx.fetch` refuses anything not covered
@@ -913,6 +918,13 @@ Signature verification is the transport's job in every environment — the host
913
918
  ingress enforces it, the local harness logs it as skipped. Do not verify it
914
919
  yourself; you would be verifying a header the transport already consumed.
915
920
 
921
+ If a vendor's scheme is not expressible yet and you declare `'none'`, the
922
+ delivery is authenticated only by the endpoint token in its URL. Treat the
923
+ body as a hint: take the ids from it and re-fetch the item through
924
+ `ctx.fetch` before writing anything a widget renders, and confirm removals
925
+ with the vendor too. A forged delivery then costs one read instead of a
926
+ planted name or link on the dashboard.
927
+
916
928
  #### `signature.scheme` — when the provider is not a plain body digest
917
929
 
918
930
  Without `scheme`, the transport expects the header to be an HMAC-SHA256 of the
@@ -1114,14 +1126,19 @@ Register this with your provider, against whichever host will run the flow:
1114
1126
  https://<your-ekanos-host>/api/integrations/<your-slug>/callback
1115
1127
  ```
1116
1128
 
1117
- The dev harness does not serve that route, so you cannot complete a round trip
1118
- locally. What it *can* check is that both endpoints you declared are inside
1119
- your egress list.
1129
+ The dev harness serves its own loopback callback, so you can complete a real
1130
+ round trip locally in live mode: register
1131
+ `http://localhost:3100/api/integrations/<your-slug>/oauth/callback` with your
1132
+ provider as well. See "Local OAuth" in the `@ekanos/harness` README for the
1133
+ secrets file and the connect flow.
1120
1134
 
1121
- **Honest limitation:** there is no local OAuth loop yet. The harness's Triggers
1122
- panel renders the declaration and checks its egress coverage, but it cannot walk
1123
- you through an authorize redirect. Until that lands, unit-test `onTokens`
1124
- directly against `createMockContext()` — it is an ordinary async function.
1135
+ In **live mode** the harness runs this loop for real on localhost — authorize
1136
+ redirect, single-use `state`, code exchange through your declared egress, then
1137
+ your `onTokens`. Client credentials come from `.ekanos/local-secrets.json`,
1138
+ which the harness generates and ignores in version control. In fixtures mode it
1139
+ answers connection status from the harness entry instead, and you can still
1140
+ unit-test `onTokens` directly against `createMockContext()` — it is an ordinary
1141
+ async function.
1125
1142
 
1126
1143
  ### `proposes`
1127
1144
 
@@ -1312,12 +1329,11 @@ whether it is you or us costs an afternoon.
1312
1329
  get an allowlisted fetch, so it needs a small seam of its own — a React
1313
1330
  context whose default is the browser's `fetch`. The `@ekanos/harness` README
1314
1331
  has the whole ~20-line pattern; the dev harness's live mode mounts it for you.
1315
- - **No local OAuth loop.** See [`oauth`](#oauth) above.
1316
- - **`onActivate` is declared and testable, but the host does not call it yet.**
1317
- It runs in the `@ekanos/harness` activation surface and under
1318
- `invokeActivate()`, but production has no wired call site as of this
1319
- writing — declare it, test it locally, and it starts running the moment host
1320
- wiring lands with no change on your side.
1332
+ - **`onActivate` errors are non-fatal.** The host runs it after an activation
1333
+ is first persisted and again when activation data changes, but a throw is
1334
+ logged and the activation stays connected — use it to seed caches and
1335
+ validate eagerly, never as a connect gate. It also runs in the
1336
+ `@ekanos/harness` activation surface and under `invokeActivate()`.
1321
1337
  - **`useActivateIntegration()` is inert without the host's provider.** It reads
1322
1338
  server actions out of `IntegrationActivationProvider`. Nothing warns you at
1323
1339
  build time.
package/api-report.md CHANGED
@@ -9,7 +9,7 @@ This is the complete surface of every entrypoint. The authoring guide
9
9
  for these symbols is README.md. Entrypoints marked WORKSPACE-ONLY are
10
10
  not reachable from a published install — see the note in each section.
11
11
 
12
- Total exported names: **171** across 7 entrypoints.
12
+ Total exported names: **172** across 7 entrypoints.
13
13
 
14
14
  ## `@ekanos/sdk` (32 exports)
15
15
 
@@ -191,7 +191,7 @@ Total exported names: **171** across 7 entrypoints.
191
191
  | `invokeSchedule` | function |
192
192
  | `invokeWebhook` | function |
193
193
 
194
- ## `@ekanos/sdk/integration` (30 exports)
194
+ ## `@ekanos/sdk/integration` (31 exports)
195
195
 
196
196
  | Export | Kind |
197
197
  | --- | --- |
@@ -207,6 +207,7 @@ Total exported names: **171** across 7 entrypoints.
207
207
  | `OAuthProviderDeclaration` | interface |
208
208
  | `OAuthTokens` | interface |
209
209
  | `OnActivateHandler` | type alias |
210
+ | `PARTNER_STORAGE_LIMITS` | variable |
210
211
  | `PartnerActionDeclaration` | interface |
211
212
  | `PartnerOAuthDeclaration` | interface |
212
213
  | `PartnerScheduleDeclaration` | interface |
@@ -30,9 +30,9 @@ export function AiPromptChip({ label, prompt, context, sourceWidget, title, onAf
30
30
  return (_jsx(Button, { variant: "ghost", size: "sm", className: cn('h-8 w-8 p-0', className), onClick: handleClick, "aria-label": `Ask AI: ${label}`, children: _jsx(Icon, { name: "fa-light fa-sparkles", className: "h-4 w-4" }) }));
31
31
  }
32
32
  if (variant === 'footer') {
33
- return (_jsxs("button", { type: "button", onClick: handleClick, className: cn('flex w-full items-center gap-2 rounded-lg border border-dashed px-4 py-3', 'border-primary/20 bg-primary/5 text-primary text-sm font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "text-left", children: label })] }));
33
+ return (_jsxs("button", { type: "button", onClick: handleClick, className: cn('flex w-full items-center gap-2 rounded-lg border border-dashed px-4 py-3', 'border-primary/20 bg-primary/5 text-primary-strong text-sm font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "text-left", children: label })] }));
34
34
  }
35
35
  // inline (Type B)
36
- return (_jsxs("button", { type: "button", onClick: handleClick, className: cn('inline-flex items-center gap-1.5 rounded-full px-3 py-1.5', 'border-primary/20 bg-primary/5 text-primary border text-xs font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: label })] }));
36
+ return (_jsxs("button", { type: "button", onClick: handleClick, className: cn('inline-flex items-center gap-1.5 rounded-full px-3 py-1.5', 'border-primary/20 bg-primary/5 text-primary-strong border text-xs font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: label })] }));
37
37
  }
38
38
  //# sourceMappingURL=ai-prompt-chip.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ai-prompt-chip.js","sourceRoot":"","sources":["../../../src/components/widgets/ai-prompt-chip.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAqBpE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,KAAK,EACL,MAAM,EACN,OAAO,EACP,YAAY,EACZ,KAAK,EACL,UAAU,EACV,OAAO,GAAG,QAAQ,EAClB,SAAS,GACS;IAClB,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;IAEvC,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,YAAY,CAAC;YACX,QAAQ,EAAE,YAAY;YACtB,KAAK,EAAE,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,KAAK;YACrB,QAAQ,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE;YACvB,eAAe,EAAE,MAAM;SACxB,CAAC,CAAC;QACH,UAAU,aAAV,UAAU,uBAAV,UAAU,EAAI,CAAC;IACjB,CAAC,CAAC;IAEF,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,EACvC,OAAO,EAAE,WAAW,gBACR,WAAW,KAAK,EAAE,YAE9B,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,SAAS,GAAG,GACjD,CACV,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CACX,0EAA0E,EAC1E,iEAAiE,EACjE,+DAA+D,EAC/D,4EAA4E,EAC5E,SAAS,CACV,gBACW,KAAK,aAEjB,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,kBAAkB,GAAG,EACjE,eAAM,SAAS,EAAC,WAAW,YAAE,KAAK,GAAQ,IACnC,CACV,CAAC;IACJ,CAAC;IAED,kBAAkB;IAClB,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CACX,2DAA2D,EAC3D,wEAAwE,EACxE,+DAA+D,EAC/D,4EAA4E,EAC5E,SAAS,CACV,gBACW,KAAK,aAEjB,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,sBAAsB,GAAG,EACrE,yBAAO,KAAK,GAAQ,IACb,CACV,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { Button } from '@ekanos/ui/button';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\n\ntype AiPromptChipProps = {\n label: string;\n prompt: string;\n context?: Record<string, unknown>;\n sourceWidget: string;\n /**\n * Human-readable label for the dock's ArrivalChip (\"Asking about …\").\n * Falls back to the chip's display label when omitted.\n */\n title?: string;\n /**\n * Fires after the assistant has been invoked. Drawers pass their close\n * handler here so the dock takes over the screen on click.\n */\n onAfterAsk?: () => void;\n variant?: 'header' | 'inline' | 'footer';\n className?: string;\n};\n\n/**\n * AI handoff chip that opens the assistant dock pre-seeded with this\n * widget/drawer's snapshot. Routes through `useAskAssistant` so the dock\n * receives the standard `{widgetId, title, snapshot, suggestedPrompt}`\n * shape used by all AI handoffs in the app.\n *\n * Variants:\n * - header (Type A): Compact icon button for widget headers\n * - inline (Type B): Chip with sparkle icon + text, below data sections\n * - footer (Type C): Full-width chip in drawer footers\n */\nexport function AiPromptChip({\n label,\n prompt,\n context,\n sourceWidget,\n title,\n onAfterAsk,\n variant = 'inline',\n className,\n}: AiPromptChipProps) {\n const askAssistant = useAskAssistant();\n\n const handleClick = () => {\n askAssistant({\n widgetId: sourceWidget,\n title: title ?? label,\n snapshot: context ?? {},\n suggestedPrompt: prompt,\n });\n onAfterAsk?.();\n };\n\n if (variant === 'header') {\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className={cn('h-8 w-8 p-0', className)}\n onClick={handleClick}\n aria-label={`Ask AI: ${label}`}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-4 w-4\" />\n </Button>\n );\n }\n\n if (variant === 'footer') {\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg border border-dashed px-4 py-3',\n 'border-primary/20 bg-primary/5 text-primary text-sm font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n className,\n )}\n aria-label={label}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-4 w-4 shrink-0\" />\n <span className=\"text-left\">{label}</span>\n </button>\n );\n }\n\n // inline (Type B)\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',\n 'border-primary/20 bg-primary/5 text-primary border text-xs font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n className,\n )}\n aria-label={label}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-3.5 w-3.5 shrink-0\" />\n <span>{label}</span>\n </button>\n );\n}\n"]}
1
+ {"version":3,"file":"ai-prompt-chip.js","sourceRoot":"","sources":["../../../src/components/widgets/ai-prompt-chip.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAqBpE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,KAAK,EACL,MAAM,EACN,OAAO,EACP,YAAY,EACZ,KAAK,EACL,UAAU,EACV,OAAO,GAAG,QAAQ,EAClB,SAAS,GACS;IAClB,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;IAEvC,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,YAAY,CAAC;YACX,QAAQ,EAAE,YAAY;YACtB,KAAK,EAAE,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,KAAK;YACrB,QAAQ,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE;YACvB,eAAe,EAAE,MAAM;SACxB,CAAC,CAAC;QACH,UAAU,aAAV,UAAU,uBAAV,UAAU,EAAI,CAAC;IACjB,CAAC,CAAC;IAEF,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,EACvC,OAAO,EAAE,WAAW,gBACR,WAAW,KAAK,EAAE,YAE9B,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,SAAS,GAAG,GACjD,CACV,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CACX,0EAA0E,EAC1E,wEAAwE,EACxE,+DAA+D,EAC/D,4EAA4E,EAC5E,SAAS,CACV,gBACW,KAAK,aAEjB,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,kBAAkB,GAAG,EACjE,eAAM,SAAS,EAAC,WAAW,YAAE,KAAK,GAAQ,IACnC,CACV,CAAC;IACJ,CAAC;IAED,kBAAkB;IAClB,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CACX,2DAA2D,EAC3D,+EAA+E,EAC/E,+DAA+D,EAC/D,4EAA4E,EAC5E,SAAS,CACV,gBACW,KAAK,aAEjB,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,sBAAsB,GAAG,EACrE,yBAAO,KAAK,GAAQ,IACb,CACV,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { Button } from '@ekanos/ui/button';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\n\ntype AiPromptChipProps = {\n label: string;\n prompt: string;\n context?: Record<string, unknown>;\n sourceWidget: string;\n /**\n * Human-readable label for the dock's ArrivalChip (\"Asking about …\").\n * Falls back to the chip's display label when omitted.\n */\n title?: string;\n /**\n * Fires after the assistant has been invoked. Drawers pass their close\n * handler here so the dock takes over the screen on click.\n */\n onAfterAsk?: () => void;\n variant?: 'header' | 'inline' | 'footer';\n className?: string;\n};\n\n/**\n * AI handoff chip that opens the assistant dock pre-seeded with this\n * widget/drawer's snapshot. Routes through `useAskAssistant` so the dock\n * receives the standard `{widgetId, title, snapshot, suggestedPrompt}`\n * shape used by all AI handoffs in the app.\n *\n * Variants:\n * - header (Type A): Compact icon button for widget headers\n * - inline (Type B): Chip with sparkle icon + text, below data sections\n * - footer (Type C): Full-width chip in drawer footers\n */\nexport function AiPromptChip({\n label,\n prompt,\n context,\n sourceWidget,\n title,\n onAfterAsk,\n variant = 'inline',\n className,\n}: AiPromptChipProps) {\n const askAssistant = useAskAssistant();\n\n const handleClick = () => {\n askAssistant({\n widgetId: sourceWidget,\n title: title ?? label,\n snapshot: context ?? {},\n suggestedPrompt: prompt,\n });\n onAfterAsk?.();\n };\n\n if (variant === 'header') {\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className={cn('h-8 w-8 p-0', className)}\n onClick={handleClick}\n aria-label={`Ask AI: ${label}`}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-4 w-4\" />\n </Button>\n );\n }\n\n if (variant === 'footer') {\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg border border-dashed px-4 py-3',\n 'border-primary/20 bg-primary/5 text-primary-strong text-sm font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n className,\n )}\n aria-label={label}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-4 w-4 shrink-0\" />\n <span className=\"text-left\">{label}</span>\n </button>\n );\n }\n\n // inline (Type B)\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',\n 'border-primary/20 bg-primary/5 text-primary-strong border text-xs font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n className,\n )}\n aria-label={label}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-3.5 w-3.5 shrink-0\" />\n <span>{label}</span>\n </button>\n );\n}\n"]}
@@ -52,7 +52,7 @@ export function WidgetAskBar({ context }) {
52
52
  }, [dismiss, isActive]);
53
53
  return (_jsxs("div", { ref: containerRef, className: "border-border/60 shrink-0 border-t", "data-test": "widget-ask-bar", children: [_jsxs("button", { type: "button", onClick: () => (isActive ? dismiss() : setIsActive(true)), "aria-expanded": isActive, "aria-label": isActive
54
54
  ? `Close ask input for ${context.title}`
55
- : `Ask about ${context.title}`, "data-test": "widget-ask-bar-toggle", className: cn('group/tag hover:bg-accent/40 flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors sm:px-5', isActive && 'bg-accent/30'), children: [_jsx(AskIcon, { className: cn('text-primary h-5 w-5 shrink-0 transition-transform duration-200', isActive && 'scale-110') }), _jsx("span", { className: "text-muted-foreground group-hover/tag:text-foreground flex-1 text-base font-medium transition-colors", children: "Ask about this" }), _jsx(Icon, { name: "fa-light fa-chevron-up", className: cn('text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200', isActive && 'rotate-180') })] }), isActive && (_jsxs("div", { className: "border-border/40 space-y-3 border-t p-3 sm:px-5", "data-test": "widget-ask-bar-input", children: [suggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", "data-test": "widget-ask-bar-suggestions", children: suggestions.map((s) => (_jsxs("button", { type: "button", onClick: () => handleSuggestion(s.prompt), className: cn('inline-flex items-center gap-1.5 rounded-full px-3 py-1.5', 'border-primary/20 bg-primary/5 text-primary border text-xs font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none'), "aria-label": s.label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "text-left", children: s.label })] }, s.label))) })), _jsxs(PromptInput, { onSubmit: handleSubmit, children: [_jsx(PromptInputBody, { children: _jsx(PromptInputTextarea, { autoFocus: true, placeholder: `Ask about ${context.title.toLowerCase()}…`, "aria-label": `Ask about ${context.title}`, onKeyDown: (e) => {
55
+ : `Ask about ${context.title}`, "data-test": "widget-ask-bar-toggle", className: cn('group/tag hover:bg-accent/40 flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors sm:px-5', isActive && 'bg-accent/30'), children: [_jsx(AskIcon, { className: cn('text-primary h-5 w-5 shrink-0 transition-transform duration-200', isActive && 'scale-110') }), _jsx("span", { className: "text-muted-foreground group-hover/tag:text-foreground flex-1 text-base font-medium transition-colors", children: "Ask about this" }), _jsx(Icon, { name: "fa-light fa-chevron-up", className: cn('text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200', isActive && 'rotate-180') })] }), isActive && (_jsxs("div", { className: "border-border/40 space-y-3 border-t p-3 sm:px-5", "data-test": "widget-ask-bar-input", children: [suggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", "data-test": "widget-ask-bar-suggestions", children: suggestions.map((s) => (_jsxs("button", { type: "button", onClick: () => handleSuggestion(s.prompt), className: cn('inline-flex items-center gap-1.5 rounded-full px-3 py-1.5', 'border-primary/20 bg-primary/5 text-primary-strong border text-xs font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none'), "aria-label": s.label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "text-left", children: s.label })] }, s.label))) })), _jsxs(PromptInput, { onSubmit: handleSubmit, children: [_jsx(PromptInputBody, { children: _jsx(PromptInputTextarea, { autoFocus: true, placeholder: `Ask about ${context.title.toLowerCase()}…`, "aria-label": `Ask about ${context.title}`, onKeyDown: (e) => {
56
56
  if (e.key === 'Escape') {
57
57
  e.preventDefault();
58
58
  dismiss();
@@ -1 +1 @@
1
- {"version":3,"file":"widget-ask-bar.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-ask-bar.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n PromptInput,\n PromptInputBody,\n PromptInputFooter,\n PromptInputSubmit,\n PromptInputTextarea,\n PromptInputTools,\n} from '@ekanos/ui/ai-prompt-input';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport { AskIcon } from './ask-icon';\n\ninterface Props {\n context: WidgetAskContext;\n}\n\n/**\n * Widget footer tag + expanding prompt input.\n *\n * - closed: a quiet, always-visible tag row at the bottom of the widget\n * (\"Aperture icon · Ask about this · chevron up\")\n * - open: clicking the tag reveals an inline `PromptInput` (the canonical\n * AI input component) below it. The tag itself doubles as the\n * close affordance (chevron rotates).\n *\n * Dismisses on click-outside or Escape. Submitting sends the user's typed\n * question (or the widget's suggestedPrompt fallback) to the assistant dock.\n */\nexport function WidgetAskBar({ context }: Props) {\n const [isActive, setIsActive] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const askAssistant = useAskAssistant();\n\n const dismiss = useCallback(() => {\n setIsActive(false);\n }, []);\n\n const handleSubmit = useCallback(\n (message: { text: string }) => {\n const prompt = message.text.trim() || context.suggestedPrompt;\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const handleSuggestion = useCallback(\n (prompt: string) => {\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const suggestions = context.suggestedPrompts ?? [];\n\n useEffect(() => {\n if (!isActive) return;\n const onPointerDown = (e: PointerEvent) => {\n const el = containerRef.current;\n if (!el) return;\n if (el.contains(e.target as Node)) return;\n dismiss();\n };\n document.addEventListener('pointerdown', onPointerDown);\n return () => document.removeEventListener('pointerdown', onPointerDown);\n }, [dismiss, isActive]);\n\n return (\n <div\n ref={containerRef}\n className=\"border-border/60 shrink-0 border-t\"\n data-test=\"widget-ask-bar\"\n >\n <button\n type=\"button\"\n onClick={() => (isActive ? dismiss() : setIsActive(true))}\n aria-expanded={isActive}\n aria-label={\n isActive\n ? `Close ask input for ${context.title}`\n : `Ask about ${context.title}`\n }\n data-test=\"widget-ask-bar-toggle\"\n className={cn(\n 'group/tag hover:bg-accent/40 flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors sm:px-5',\n isActive && 'bg-accent/30',\n )}\n >\n <AskIcon\n className={cn(\n 'text-primary h-5 w-5 shrink-0 transition-transform duration-200',\n isActive && 'scale-110',\n )}\n />\n <span className=\"text-muted-foreground group-hover/tag:text-foreground flex-1 text-base font-medium transition-colors\">\n Ask about this\n </span>\n <Icon\n name=\"fa-light fa-chevron-up\"\n className={cn(\n 'text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200',\n isActive && 'rotate-180',\n )}\n />\n </button>\n\n {isActive && (\n <div\n className=\"border-border/40 space-y-3 border-t p-3 sm:px-5\"\n data-test=\"widget-ask-bar-input\"\n >\n {suggestions.length > 0 && (\n <div\n className=\"flex flex-wrap gap-2\"\n data-test=\"widget-ask-bar-suggestions\"\n >\n {suggestions.map((s) => (\n <button\n key={s.label}\n type=\"button\"\n onClick={() => handleSuggestion(s.prompt)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',\n 'border-primary/20 bg-primary/5 text-primary border text-xs font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n )}\n aria-label={s.label}\n >\n <Icon\n name=\"fa-light fa-sparkles\"\n className=\"h-3.5 w-3.5 shrink-0\"\n />\n <span className=\"text-left\">{s.label}</span>\n </button>\n ))}\n </div>\n )}\n <PromptInput onSubmit={handleSubmit}>\n <PromptInputBody>\n <PromptInputTextarea\n autoFocus\n placeholder={`Ask about ${context.title.toLowerCase()}…`}\n aria-label={`Ask about ${context.title}`}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n dismiss();\n }\n }}\n />\n </PromptInputBody>\n <PromptInputFooter>\n <PromptInputTools />\n <PromptInputSubmit data-test=\"widget-ask-bar-send\" />\n </PromptInputFooter>\n </PromptInput>\n </div>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"widget-ask-bar.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-ask-bar.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n PromptInput,\n PromptInputBody,\n PromptInputFooter,\n PromptInputSubmit,\n PromptInputTextarea,\n PromptInputTools,\n} from '@ekanos/ui/ai-prompt-input';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport { AskIcon } from './ask-icon';\n\ninterface Props {\n context: WidgetAskContext;\n}\n\n/**\n * Widget footer tag + expanding prompt input.\n *\n * - closed: a quiet, always-visible tag row at the bottom of the widget\n * (\"Aperture icon · Ask about this · chevron up\")\n * - open: clicking the tag reveals an inline `PromptInput` (the canonical\n * AI input component) below it. The tag itself doubles as the\n * close affordance (chevron rotates).\n *\n * Dismisses on click-outside or Escape. Submitting sends the user's typed\n * question (or the widget's suggestedPrompt fallback) to the assistant dock.\n */\nexport function WidgetAskBar({ context }: Props) {\n const [isActive, setIsActive] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const askAssistant = useAskAssistant();\n\n const dismiss = useCallback(() => {\n setIsActive(false);\n }, []);\n\n const handleSubmit = useCallback(\n (message: { text: string }) => {\n const prompt = message.text.trim() || context.suggestedPrompt;\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const handleSuggestion = useCallback(\n (prompt: string) => {\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const suggestions = context.suggestedPrompts ?? [];\n\n useEffect(() => {\n if (!isActive) return;\n const onPointerDown = (e: PointerEvent) => {\n const el = containerRef.current;\n if (!el) return;\n if (el.contains(e.target as Node)) return;\n dismiss();\n };\n document.addEventListener('pointerdown', onPointerDown);\n return () => document.removeEventListener('pointerdown', onPointerDown);\n }, [dismiss, isActive]);\n\n return (\n <div\n ref={containerRef}\n className=\"border-border/60 shrink-0 border-t\"\n data-test=\"widget-ask-bar\"\n >\n <button\n type=\"button\"\n onClick={() => (isActive ? dismiss() : setIsActive(true))}\n aria-expanded={isActive}\n aria-label={\n isActive\n ? `Close ask input for ${context.title}`\n : `Ask about ${context.title}`\n }\n data-test=\"widget-ask-bar-toggle\"\n className={cn(\n 'group/tag hover:bg-accent/40 flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors sm:px-5',\n isActive && 'bg-accent/30',\n )}\n >\n <AskIcon\n className={cn(\n 'text-primary h-5 w-5 shrink-0 transition-transform duration-200',\n isActive && 'scale-110',\n )}\n />\n <span className=\"text-muted-foreground group-hover/tag:text-foreground flex-1 text-base font-medium transition-colors\">\n Ask about this\n </span>\n <Icon\n name=\"fa-light fa-chevron-up\"\n className={cn(\n 'text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200',\n isActive && 'rotate-180',\n )}\n />\n </button>\n\n {isActive && (\n <div\n className=\"border-border/40 space-y-3 border-t p-3 sm:px-5\"\n data-test=\"widget-ask-bar-input\"\n >\n {suggestions.length > 0 && (\n <div\n className=\"flex flex-wrap gap-2\"\n data-test=\"widget-ask-bar-suggestions\"\n >\n {suggestions.map((s) => (\n <button\n key={s.label}\n type=\"button\"\n onClick={() => handleSuggestion(s.prompt)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',\n 'border-primary/20 bg-primary/5 text-primary-strong border text-xs font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n )}\n aria-label={s.label}\n >\n <Icon\n name=\"fa-light fa-sparkles\"\n className=\"h-3.5 w-3.5 shrink-0\"\n />\n <span className=\"text-left\">{s.label}</span>\n </button>\n ))}\n </div>\n )}\n <PromptInput onSubmit={handleSubmit}>\n <PromptInputBody>\n <PromptInputTextarea\n autoFocus\n placeholder={`Ask about ${context.title.toLowerCase()}…`}\n aria-label={`Ask about ${context.title}`}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n dismiss();\n }\n }}\n />\n </PromptInputBody>\n <PromptInputFooter>\n <PromptInputTools />\n <PromptInputSubmit data-test=\"widget-ask-bar-send\" />\n </PromptInputFooter>\n </PromptInput>\n </div>\n )}\n </div>\n );\n}\n"]}
@@ -17,5 +17,5 @@
17
17
  export { defineIntegration } from './define-integration.js';
18
18
  export { parseCronExpression } from './cron.js';
19
19
  export { PublicActionError } from '../errors/public-action-error.js';
20
- export { DEFAULT_SIGNATURE_TOLERANCE_SECONDS, IntegrationDefinitionSchema, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
20
+ export { DEFAULT_SIGNATURE_TOLERANCE_SECONDS, IntegrationDefinitionSchema, PARTNER_STORAGE_LIMITS, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
21
21
  export type { IntegrationDefinition, IntegrationComponentDeclarations, IntegrationProposals, IntegrationCapabilityDeclaration, IntegrationPermissionDeclaration, PartnerWidgetDeclaration, PartnerToolModule, PartnerToolParameters, PartnerActionDeclaration, ToolClassificationProposal, DefinitionCollisionInput, FirstPartyInventory, PartnerWebhookDeclaration, WebhookSignatureDeclaration, WebhookSignatureScheme, WebhookEvent, WebhookResult, PartnerScheduleDeclaration, ScheduleInvocation, ScheduleResult, PartnerOAuthDeclaration, OAuthProviderDeclaration, OAuthTokens, OnActivateHandler, } from '@ekanos/integration-schema';
@@ -23,5 +23,5 @@ export { parseCronExpression } from './cron.js';
23
23
  // discipline — see the class's own doc comment
24
24
  // (the widget-actions opaque-failure ruling).
25
25
  export { PublicActionError } from '../errors/public-action-error.js';
26
- export { DEFAULT_SIGNATURE_TOLERANCE_SECONDS, IntegrationDefinitionSchema, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
26
+ export { DEFAULT_SIGNATURE_TOLERANCE_SECONDS, IntegrationDefinitionSchema, PARTNER_STORAGE_LIMITS, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
27
27
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/integration/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,mEAAmE;AACnE,0EAA0E;AAC1E,0EAA0E;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAE7C,oEAAoE;AACpE,+CAA+C;AAC/C,8CAA8C;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,OAAO,EACL,mCAAmC,EACnC,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,4BAA4B,CAAC","sourcesContent":["/**\n * @ekanos/sdk/integration — the partner authoring contract.\n *\n * `defineIntegration()` is THE way a partner declares an integration\n * Partners never extend a base class.\n * The host adapts the returned definition internally via\n * `registerPartnerIntegration` in `@kit/integrations-core`, which re-parses\n * against the SAME canonical schema (`@ekanos/integration-schema`) at the\n * trust boundary — one contract, no structural twin (F3/F9).\n *\n * Dependency-pure and isomorphic: zod plus the schema package, no `@kit/*`,\n * no `server-only`.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { defineIntegration } from './define-integration';\n\n// Event-surface validation values: the 5-field cron parser backing\n// `defineIntegration()`'s schedule validation, exported so validators and\n// the harness can check/describe an expression the same way the SDK does.\nexport { parseCronExpression } from './cron';\n\n// The one escape hatch from the actions host route's opaque-failure\n// discipline — see the class's own doc comment\n// (the widget-actions opaque-failure ruling).\nexport { PublicActionError } from '../errors/public-action-error';\n\nexport {\n DEFAULT_SIGNATURE_TOLERANCE_SECONDS,\n IntegrationDefinitionSchema,\n validateIntegrationDefinitions,\n} from '@ekanos/integration-schema';\n\nexport type {\n IntegrationDefinition,\n IntegrationComponentDeclarations,\n IntegrationProposals,\n IntegrationCapabilityDeclaration,\n IntegrationPermissionDeclaration,\n PartnerWidgetDeclaration,\n PartnerToolModule,\n PartnerToolParameters,\n // Widget-invoked server handlers (`actions`): the declaration type a\n // partner needs to export a handler shared between an action and a tool.\n PartnerActionDeclaration,\n ToolClassificationProposal,\n DefinitionCollisionInput,\n FirstPartyInventory,\n // Event surfaces (webhooks, schedules, OAuth): declared in the definition,\n // executed locally by the harness/testing helpers today; the host's real\n // transports (public ingress, scheduler, hosted OAuth callback) bind to\n // these exact declarations later with no partner code change.\n PartnerWebhookDeclaration,\n WebhookSignatureDeclaration,\n WebhookSignatureScheme,\n WebhookEvent,\n WebhookResult,\n PartnerScheduleDeclaration,\n ScheduleInvocation,\n ScheduleResult,\n PartnerOAuthDeclaration,\n OAuthProviderDeclaration,\n OAuthTokens,\n // Activation lifecycle hook: cache seeding / eager validation at connect\n // time. Runs after first persist and after every activationData update;\n // v1 errors are non-fatal (logged as a warning, activation stays active).\n OnActivateHandler,\n} from '@ekanos/integration-schema';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/integration/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,mEAAmE;AACnE,0EAA0E;AAC1E,0EAA0E;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAE7C,oEAAoE;AACpE,+CAA+C;AAC/C,8CAA8C;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,OAAO,EACL,mCAAmC,EACnC,2BAA2B,EAC3B,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,4BAA4B,CAAC","sourcesContent":["/**\n * @ekanos/sdk/integration — the partner authoring contract.\n *\n * `defineIntegration()` is THE way a partner declares an integration\n * Partners never extend a base class.\n * The host adapts the returned definition internally via\n * `registerPartnerIntegration` in `@kit/integrations-core`, which re-parses\n * against the SAME canonical schema (`@ekanos/integration-schema`) at the\n * trust boundary — one contract, no structural twin (F3/F9).\n *\n * Dependency-pure and isomorphic: zod plus the schema package, no `@kit/*`,\n * no `server-only`.\n *\n * Surface discipline: additions require an entry in\n * api-report.md, regenerated by `pnpm --filter @ekanos/sdk api-report`.\n */\n\nexport { defineIntegration } from './define-integration';\n\n// Event-surface validation values: the 5-field cron parser backing\n// `defineIntegration()`'s schedule validation, exported so validators and\n// the harness can check/describe an expression the same way the SDK does.\nexport { parseCronExpression } from './cron';\n\n// The one escape hatch from the actions host route's opaque-failure\n// discipline — see the class's own doc comment\n// (the widget-actions opaque-failure ruling).\nexport { PublicActionError } from '../errors/public-action-error';\n\nexport {\n DEFAULT_SIGNATURE_TOLERANCE_SECONDS,\n IntegrationDefinitionSchema,\n PARTNER_STORAGE_LIMITS,\n validateIntegrationDefinitions,\n} from '@ekanos/integration-schema';\n\nexport type {\n IntegrationDefinition,\n IntegrationComponentDeclarations,\n IntegrationProposals,\n IntegrationCapabilityDeclaration,\n IntegrationPermissionDeclaration,\n PartnerWidgetDeclaration,\n PartnerToolModule,\n PartnerToolParameters,\n // Widget-invoked server handlers (`actions`): the declaration type a\n // partner needs to export a handler shared between an action and a tool.\n PartnerActionDeclaration,\n ToolClassificationProposal,\n DefinitionCollisionInput,\n FirstPartyInventory,\n // Event surfaces (webhooks, schedules, OAuth): declared in the definition,\n // executed locally by the harness/testing helpers today; the host's real\n // transports (public ingress, scheduler, hosted OAuth callback) bind to\n // these exact declarations later with no partner code change.\n PartnerWebhookDeclaration,\n WebhookSignatureDeclaration,\n WebhookSignatureScheme,\n WebhookEvent,\n WebhookResult,\n PartnerScheduleDeclaration,\n ScheduleInvocation,\n ScheduleResult,\n PartnerOAuthDeclaration,\n OAuthProviderDeclaration,\n OAuthTokens,\n // Activation lifecycle hook: cache seeding / eager validation at connect\n // time. Runs after first persist and after every activationData update;\n // v1 errors are non-fatal (logged as a warning, activation stays active).\n OnActivateHandler,\n} from '@ekanos/integration-schema';\n"]}
@@ -1,5 +1,6 @@
1
+ import { PARTNER_STORAGE_LIMITS } from '@ekanos/integration-schema';
1
2
  import { isEgressAllowed } from '../context/egress.js';
2
- import { EgressDeniedError, SecretAccessError } from '../context/errors.js';
3
+ import { EgressDeniedError, SecretAccessError, StorageValidationError, } from '../context/errors.js';
3
4
  import { assertStorageKeyDeclared, assertStorageKeyWritable, parseStorageValue, } from '../context/storage-validation.js';
4
5
  import { matchFetchHandler } from './fetch-matching.js';
5
6
  function isExpired(row) {
@@ -53,6 +54,20 @@ function createInMemoryStore(args) {
53
54
  schemas,
54
55
  value: data,
55
56
  });
57
+ // The host's per-value cap (finding E), enforced here with the host's
58
+ // own wording so `ekanos test` fails where production would.
59
+ const serializedBytes = utf8Bytes(JSON.stringify(parsed));
60
+ if (serializedBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {
61
+ throw new StorageValidationError({
62
+ scope,
63
+ key,
64
+ hint: `The value written to ${scope} storage key "${key}" serializes ` +
65
+ `to ${serializedBytes} bytes, above the ` +
66
+ `${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte per-value ` +
67
+ `limit. Store less per key: trim cached payloads, or split state ` +
68
+ `across purpose-named keys.`,
69
+ });
70
+ }
56
71
  rows.set(key, {
57
72
  data: parsed,
58
73
  externalId: (_a = options === null || options === void 0 ? void 0 : options.externalId) !== null && _a !== void 0 ? _a : null,
@@ -68,6 +83,10 @@ function createInMemoryStore(args) {
68
83
  },
69
84
  };
70
85
  }
86
+ /** Byte length of a string as UTF-8, without Buffer (this module runs in the browser-side harness too). */
87
+ function utf8Bytes(value) {
88
+ return new TextEncoder().encode(value).length;
89
+ }
71
90
  function dumpScope(rows) {
72
91
  const out = {};
73
92
  for (const [key, row] of rows) {
@@ -145,6 +164,21 @@ export function createMockContext(options) {
145
164
  if (!accountSecrets.has(name) && adminSecrets.has(name)) {
146
165
  throw new SecretAccessError(name);
147
166
  }
167
+ // The host's caps (finding E): one Vault record per value, one row +
168
+ // record per distinct name. Same wording as the host.
169
+ const valueBytes = utf8Bytes(value);
170
+ if (valueBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {
171
+ throw new TypeError(`secrets.set("${name}", …): the value is ${valueBytes} bytes, ` +
172
+ `above the ${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte ` +
173
+ `per-secret limit. Secrets hold credentials, not payloads — store ` +
174
+ `bulk data under a ctx.storage key instead.`);
175
+ }
176
+ if (!accountSecrets.has(name) &&
177
+ accountSecrets.size >= PARTNER_STORAGE_LIMITS.maxSecretNames) {
178
+ throw new Error(`secrets.set("${name}", …): this integration already stores ` +
179
+ `${PARTNER_STORAGE_LIMITS.maxSecretNames} distinct secret names, ` +
180
+ `the per-integration limit. Reuse or rotate an existing name.`);
181
+ }
148
182
  accountSecrets.set(name, value);
149
183
  },
150
184
  };
@@ -1 +1 @@
1
- {"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `createMockContext()` — the day-1 in-memory `IntegrationContext` for the\n * partner harness (proposal §4, amended by rulings 1–4). Self-contained:\n * nothing here touches Supabase, Vault, or the network. Enforcement is the\n * SAME shared implementation the production wrapper uses\n * (`@ekanos/sdk/context`): schema validation via `parseStorageValue`, egress\n * via `isEgressAllowed` — a test that passes the mock cannot be a schema or\n * egress surprise later.\n */\nimport type { z } from 'zod';\n\nimport { isEgressAllowed } from '../context/egress';\nimport { EgressDeniedError, SecretAccessError } from '../context/errors';\nimport {\n assertStorageKeyDeclared,\n assertStorageKeyWritable,\n parseStorageValue,\n} from '../context/storage-validation';\nimport type {\n IntegrationActor,\n IntegrationContext,\n IntegrationFetch,\n IntegrationLogger,\n IntegrationSecrets,\n IntegrationStorage,\n ScopedStore,\n StorageEntry,\n StorageKeySchema,\n StorageSchemaMap,\n StorageSchemas,\n StorageWriteOptions,\n} from '../context/types';\nimport { type MockFetchHandler, matchFetchHandler } from './fetch-matching';\n\ntype StorageScope = 'account' | 'user';\n\n// Seeds are typed off the DECLARED schema, resolved out of whichever\n// declaration form the key used — a bare zod schema or a\n// `{ schema, clientReadable }` descriptor.\ntype ScopeSeed<M> = M extends StorageSchemaMap\n ? { [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>> }\n : Record<never, never>;\n\n// Re-exported from here for backward compatibility — `MockFetchHandler`'s\n// canonical home is now `./fetch-matching`, shared with `createMockFetchProvider`.\nexport type { MockFetchHandler } from './fetch-matching';\n\nexport interface MockContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n accountId?: string;\n accountSlug?: string | null;\n userId?: string | null;\n actor?: IntegrationActor;\n sourceId?: string | null;\n timezone?: string | null;\n integration: { slug: string; productId?: string };\n /**\n * The storage declaration `defineIntegration({ storage })` will carry —\n * the mock enforces the identical schemas via the same shared validator\n * (ruling 1). Omit it and every storage access throws.\n */\n storageSchemas?: Schemas;\n /** Seed rows: key → data, per scope. Validated on construction via the write path. */\n storage?: {\n account?: ScopeSeed<Schemas['account']>;\n user?: ScopeSeed<Schemas['user']>;\n };\n /**\n * Seed secrets, by tier (ruling 2): `account` values are the rows this\n * integration stored — writable via `secrets.set()`; `admin` values stand\n * in for admin-issued source/global-tier credentials — readable, and a\n * `set()` against a name resolvable only there throws `SecretAccessError`.\n */\n secrets?: {\n account?: Record<string, string>;\n admin?: Record<string, string>;\n };\n /** Same allowlist the manifest would declare — enforced by the stub. */\n egress?: readonly string[];\n /** Programmable responses; unmatched allowed calls resolve 200 `{}`. */\n fetchHandlers?: MockFetchHandler[];\n}\n\nexport interface RecordedFetchCall {\n url: string;\n init?: RequestInit;\n denied: boolean;\n}\n\nexport interface RecordedLog {\n level: 'debug' | 'info' | 'warn' | 'error';\n context: Record<string, unknown>;\n message: string;\n}\n\nexport interface MockIntegrationContext<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends IntegrationContext<Schemas> {\n /** Every ctx.fetch call, allowed or denied, in order. */\n readonly fetchCalls: readonly RecordedFetchCall[];\n /** Every log line, per level. */\n readonly logs: readonly RecordedLog[];\n /** Current readable storage state (expired entries excluded), key → data. */\n dumpStorage(): {\n account: Record<string, unknown>;\n user: Record<string, unknown>;\n };\n}\n\ninterface StoredRow {\n data: unknown;\n externalId: string | null;\n expiresAt: string | null;\n updatedAt: string;\n}\n\nfunction isExpired(row: StoredRow): boolean {\n return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();\n}\n\nfunction parseExpiresAt(\n options: StorageWriteOptions | undefined,\n): string | null {\n const expiresAt = options?.expiresAt;\n if (expiresAt === undefined) return null;\n if (Number.isNaN(Date.parse(expiresAt))) {\n throw new TypeError(\n `expiresAt \"${expiresAt}\" is not a parseable timestamp. Pass an ` +\n `ISO-8601 string, e.g. new Date(Date.now() + 60_000).toISOString().`,\n );\n }\n return expiresAt;\n}\n\nfunction createInMemoryStore(args: {\n scope: StorageScope;\n schemas: StorageSchemaMap | undefined;\n rows: Map<string, StoredRow>;\n guard: () => void;\n}): ScopedStore {\n const { scope, schemas, rows, guard } = args;\n\n return {\n async get(key: string): Promise<StorageEntry<unknown> | null> {\n guard();\n assertStorageKeyDeclared({ scope, key, schemas });\n const row = rows.get(key);\n if (!row) return null;\n if (isExpired(row)) {\n rows.delete(key);\n return null;\n }\n const data = parseStorageValue({\n scope,\n key,\n operation: 'read',\n schemas,\n value: row.data,\n });\n return {\n data,\n externalId: row.externalId,\n expiresAt: row.expiresAt,\n updatedAt: row.updatedAt,\n };\n },\n\n async set(\n key: string,\n data: unknown,\n options?: StorageWriteOptions,\n ): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas,\n value: data,\n });\n rows.set(key, {\n data: parsed,\n externalId: options?.externalId ?? null,\n expiresAt: parseExpiresAt(options),\n updatedAt: new Date().toISOString(),\n });\n },\n\n async delete(key: string): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n assertStorageKeyDeclared({ scope, key, schemas });\n rows.delete(key);\n },\n };\n}\n\nfunction dumpScope(rows: Map<string, StoredRow>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, row] of rows) {\n if (!isExpired(row)) out[key] = row.data;\n }\n return out;\n}\n\nexport function createMockContext<\n Schemas extends StorageSchemas = StorageSchemas,\n>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas> {\n const userId = options.userId !== undefined ? options.userId : 'mock-user-id';\n const actor: IntegrationActor =\n options.actor ??\n (userId !== null\n ? { kind: 'user', userId }\n : { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });\n\n const schemas: StorageSchemas = options.storageSchemas ?? {};\n\n const accountRows = new Map<string, StoredRow>();\n const userRows = new Map<string, StoredRow>();\n\n const seedScope = (\n scope: StorageScope,\n rows: Map<string, StoredRow>,\n seed: Record<string, unknown> | undefined,\n ) => {\n for (const [key, value] of Object.entries(seed ?? {})) {\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas: schemas[scope],\n value,\n });\n rows.set(key, {\n data: parsed,\n externalId: null,\n expiresAt: null,\n updatedAt: new Date().toISOString(),\n });\n }\n };\n seedScope('account', accountRows, options.storage?.account);\n seedScope('user', userRows, options.storage?.user);\n\n const guardUserScope = () => {\n if (actor.kind === 'machine') {\n throw new Error(\n 'ctx.storage.user is unavailable for machine actors: there is no ' +\n 'user to scope the row to. Store machine-written state in ' +\n 'ctx.storage.account instead.',\n );\n }\n };\n\n // The stores validate every key and value at runtime via the shared\n // validator; the cast narrows the runtime-checked store to the\n // schema-typed interface the declaration promises.\n const storage = {\n account: createInMemoryStore({\n scope: 'account',\n schemas: schemas.account,\n rows: accountRows,\n guard: () => void 0,\n }),\n user: createInMemoryStore({\n scope: 'user',\n schemas: schemas.user,\n rows: userRows,\n guard: guardUserScope,\n }),\n } as unknown as IntegrationStorage<Schemas>;\n\n const accountSecrets = new Map(\n Object.entries(options.secrets?.account ?? {}),\n );\n const adminSecrets = new Map(Object.entries(options.secrets?.admin ?? {}));\n\n const secrets: IntegrationSecrets = {\n async get(name) {\n return accountSecrets.get(name) ?? adminSecrets.get(name) ?? null;\n },\n async names() {\n return [\n ...new Set([...accountSecrets.keys(), ...adminSecrets.keys()]),\n ].sort();\n },\n async set(name, value) {\n if (!accountSecrets.has(name) && adminSecrets.has(name)) {\n throw new SecretAccessError(name);\n }\n accountSecrets.set(name, value);\n },\n };\n\n const fetchCalls: RecordedFetchCall[] = [];\n const egress = options.egress ?? [];\n const fetchHandlers = options.fetchHandlers ?? [];\n\n // Not an async function: a denied call must throw synchronously, before\n // any I/O (proposal §2.3) — same behavior the production wrapper has.\n const fetch: IntegrationFetch = (input, init) => {\n const url = new URL(input).toString();\n const allowed = isEgressAllowed(url, egress);\n fetchCalls.push(\n init === undefined\n ? { url, denied: !allowed }\n : { url, init, denied: !allowed },\n );\n if (!allowed) {\n throw new EgressDeniedError(new URL(url).origin);\n }\n const handler = matchFetchHandler(url, fetchHandlers);\n if (handler) {\n return Promise.resolve(handler.respond(new Request(url, init)));\n }\n return Promise.resolve(\n new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n }),\n );\n };\n\n const logs: RecordedLog[] = [];\n const record =\n (level: RecordedLog['level']) =>\n (context: Record<string, unknown>, message: string) => {\n logs.push({ level, context, message });\n };\n const logger: IntegrationLogger = {\n debug: record('debug'),\n info: record('info'),\n warn: record('warn'),\n error: record('error'),\n };\n\n return {\n accountId: options.accountId ?? 'mock-account-id',\n accountSlug: options.accountSlug ?? null,\n userId: actor.kind === 'machine' ? null : userId,\n actor,\n sourceId: options.sourceId ?? null,\n integration: {\n slug: options.integration.slug,\n productId: options.integration.productId ?? 'mock-product-id',\n },\n timezone: options.timezone ?? null,\n storage,\n secrets,\n fetch,\n logger,\n fetchCalls,\n logs,\n dumpStorage() {\n return { account: dumpScope(accountRows), user: dumpScope(userRows) };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAYpE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,+BAA+B,CAAC;AAevC,OAAO,EAAyB,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAqF5E,SAAS,SAAS,CAAC,GAAc;IAC/B,OAAO,GAAG,CAAC,SAAS,KAAK,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;AAC3E,CAAC;AAED,SAAS,cAAc,CACrB,OAAwC;IAExC,MAAM,SAAS,GAAG,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,SAAS,CAAC;IACrC,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,SAAS,CACjB,cAAc,SAAS,0CAA0C;YAC/D,oEAAoE,CACvE,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,mBAAmB,CAAC,IAK5B;IACC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IAE7C,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAW;YACnB,KAAK,EAAE,CAAC;YACR,wBAAwB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;YAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,CAAC,GAAG;gBAAE,OAAO,IAAI,CAAC;YACtB,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;gBACnB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,IAAI,GAAG,iBAAiB,CAAC;gBAC7B,KAAK;gBACL,GAAG;gBACH,SAAS,EAAE,MAAM;gBACjB,OAAO;gBACP,KAAK,EAAE,GAAG,CAAC,IAAI;aAChB,CAAC,CAAC;YACH,OAAO;gBACL,IAAI;gBACJ,UAAU,EAAE,GAAG,CAAC,UAAU;gBAC1B,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,SAAS,EAAE,GAAG,CAAC,SAAS;aACzB,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,GAAG,CACP,GAAW,EACX,IAAa,EACb,OAA6B;;YAE7B,KAAK,EAAE,CAAC;YACR,wBAAwB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,iBAAiB,CAAC;gBAC/B,KAAK;gBACL,GAAG;gBACH,SAAS,EAAE,OAAO;gBAClB,OAAO;gBACP,KAAK,EAAE,IAAI;aACZ,CAAC,CAAC;YACH,sEAAsE;YACtE,6DAA6D;YAC7D,MAAM,eAAe,GAAG,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;YAC1D,IAAI,eAAe,GAAG,sBAAsB,CAAC,uBAAuB,EAAE,CAAC;gBACrE,MAAM,IAAI,sBAAsB,CAAC;oBAC/B,KAAK;oBACL,GAAG;oBACH,IAAI,EACF,wBAAwB,KAAK,iBAAiB,GAAG,eAAe;wBAChE,MAAM,eAAe,oBAAoB;wBACzC,GAAG,sBAAsB,CAAC,uBAAuB,kBAAkB;wBACnE,kEAAkE;wBAClE,4BAA4B;iBAC/B,CAAC,CAAC;YACL,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;gBACZ,IAAI,EAAE,MAAM;gBACZ,UAAU,QAAE,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU,mCAAI,IAAI;gBACvC,SAAS,EAAE,cAAc,CAAC,OAAO,CAAC;gBAClC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,GAAW;YACtB,KAAK,EAAE,CAAC;YACR,wBAAwB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACzC,wBAAwB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,2GAA2G;AAC3G,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;AAChD,CAAC;AAED,SAAS,SAAS,CAAC,IAA4B;IAC7C,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;IAC3C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,iBAAiB,CAE/B,OAAoC;;;IACpC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC;IAC9E,MAAM,KAAK,SACT,OAAO,CAAC,KAAK,mCACb,CAAC,MAAM,KAAK,IAAI;QACd,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE;QAC1B,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtE,MAAM,OAAO,SAAmB,OAAO,CAAC,cAAc,mCAAI,EAAE,CAAC;IAE7D,MAAM,WAAW,GAAG,IAAI,GAAG,EAAqB,CAAC;IACjD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAqB,CAAC;IAE9C,MAAM,SAAS,GAAG,CAChB,KAAmB,EACnB,IAA4B,EAC5B,IAAyC,EACzC,EAAE;QACF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,aAAJ,IAAI,cAAJ,IAAI,GAAI,EAAE,CAAC,EAAE,CAAC;YACtD,MAAM,MAAM,GAAG,iBAAiB,CAAC;gBAC/B,KAAK;gBACL,GAAG;gBACH,SAAS,EAAE,OAAO;gBAClB,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC;gBACvB,KAAK;aACN,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;gBACZ,IAAI,EAAE,MAAM;gBACZ,UAAU,EAAE,IAAI;gBAChB,SAAS,EAAE,IAAI;gBACf,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC;IACF,SAAS,CAAC,SAAS,EAAE,WAAW,EAAE,MAAA,OAAO,CAAC,OAAO,0CAAE,OAAO,CAAC,CAAC;IAC5D,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAA,OAAO,CAAC,OAAO,0CAAE,IAAI,CAAC,CAAC;IAEnD,MAAM,cAAc,GAAG,GAAG,EAAE;QAC1B,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,kEAAkE;gBAChE,2DAA2D;gBAC3D,8BAA8B,CACjC,CAAC;QACJ,CAAC;IACH,CAAC,CAAC;IAEF,oEAAoE;IACpE,+DAA+D;IAC/D,mDAAmD;IACnD,MAAM,OAAO,GAAG;QACd,OAAO,EAAE,mBAAmB,CAAC;YAC3B,KAAK,EAAE,SAAS;YAChB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC;SACpB,CAAC;QACF,IAAI,EAAE,mBAAmB,CAAC;YACxB,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,OAAO,CAAC,IAAI;YACrB,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,cAAc;SACtB,CAAC;KACuC,CAAC;IAE5C,MAAM,cAAc,GAAG,IAAI,GAAG,CAC5B,MAAM,CAAC,OAAO,OAAC,MAAA,OAAO,CAAC,OAAO,0CAAE,OAAO,mCAAI,EAAE,CAAC,CAC/C,CAAC;IACF,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,OAAC,MAAA,OAAO,CAAC,OAAO,0CAAE,KAAK,mCAAI,EAAE,CAAC,CAAC,CAAC;IAE3E,MAAM,OAAO,GAAuB;QAClC,KAAK,CAAC,GAAG,CAAC,IAAI;;YACZ,mBAAO,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,IAAI,CAAC;QACpE,CAAC;QACD,KAAK,CAAC,KAAK;YACT,OAAO;gBACL,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,cAAc,CAAC,IAAI,EAAE,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;aAC/D,CAAC,IAAI,EAAE,CAAC;QACX,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK;YACnB,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxD,MAAM,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC;YACpC,CAAC;YACD,qEAAqE;YACrE,sDAAsD;YACtD,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YACpC,IAAI,UAAU,GAAG,sBAAsB,CAAC,uBAAuB,EAAE,CAAC;gBAChE,MAAM,IAAI,SAAS,CACjB,gBAAgB,IAAI,uBAAuB,UAAU,UAAU;oBAC7D,aAAa,sBAAsB,CAAC,uBAAuB,QAAQ;oBACnE,mEAAmE;oBACnE,4CAA4C,CAC/C,CAAC;YACJ,CAAC;YACD,IACE,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;gBACzB,cAAc,CAAC,IAAI,IAAI,sBAAsB,CAAC,cAAc,EAC5D,CAAC;gBACD,MAAM,IAAI,KAAK,CACb,gBAAgB,IAAI,yCAAyC;oBAC3D,GAAG,sBAAsB,CAAC,cAAc,0BAA0B;oBAClE,8DAA8D,CACjE,CAAC;YACJ,CAAC;YACD,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAClC,CAAC;KACF,CAAC;IAEF,MAAM,UAAU,GAAwB,EAAE,CAAC;IAC3C,MAAM,MAAM,SAAG,OAAO,CAAC,MAAM,mCAAI,EAAE,CAAC;IACpC,MAAM,aAAa,SAAG,OAAO,CAAC,aAAa,mCAAI,EAAE,CAAC;IAElD,wEAAwE;IACxE,sEAAsE;IACtE,MAAM,KAAK,GAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAC9C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC7C,UAAU,CAAC,IAAI,CACb,IAAI,KAAK,SAAS;YAChB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE;YAC3B,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,CACpC,CAAC;QACF,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,iBAAiB,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC;QACD,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QACtD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAClE,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CACpB,IAAI,QAAQ,CAAC,IAAI,EAAE;YACjB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CACH,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,IAAI,GAAkB,EAAE,CAAC;IAC/B,MAAM,MAAM,GACV,CAAC,KAA2B,EAAE,EAAE,CAChC,CAAC,OAAgC,EAAE,OAAe,EAAE,EAAE;QACpD,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACzC,CAAC,CAAC;IACJ,MAAM,MAAM,GAAsB;QAChC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC;KACvB,CAAC;IAEF,OAAO;QACL,SAAS,QAAE,OAAO,CAAC,SAAS,mCAAI,iBAAiB;QACjD,WAAW,QAAE,OAAO,CAAC,WAAW,mCAAI,IAAI;QACxC,MAAM,EAAE,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;QAChD,KAAK;QACL,QAAQ,QAAE,OAAO,CAAC,QAAQ,mCAAI,IAAI;QAClC,WAAW,EAAE;YACX,IAAI,EAAE,OAAO,CAAC,WAAW,CAAC,IAAI;YAC9B,SAAS,QAAE,OAAO,CAAC,WAAW,CAAC,SAAS,mCAAI,iBAAiB;SAC9D;QACD,QAAQ,QAAE,OAAO,CAAC,QAAQ,mCAAI,IAAI;QAClC,OAAO;QACP,OAAO;QACP,KAAK;QACL,MAAM;QACN,UAAU;QACV,IAAI;QACJ,WAAW;YACT,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxE,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The official SDK for building Ekanos integrations.",
5
5
  "bugs": {
6
6
  "email": "npm@govastly.com"
@@ -65,8 +65,8 @@
65
65
  "access": "public"
66
66
  },
67
67
  "dependencies": {
68
- "@ekanos/integration-schema": "0.4.0",
69
- "@ekanos/ui": "0.4.0",
68
+ "@ekanos/integration-schema": "0.5.0",
69
+ "@ekanos/ui": "0.5.0",
70
70
  "@supabase/supabase-js": "2.87.1",
71
71
  "server-only": "^0.0.1"
72
72
  },