@ekanos/sdk 0.4.0 → 0.6.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 +62 -42
- package/api-report.md +3 -2
- package/dist/components/widgets/ai-prompt-chip.js +2 -2
- package/dist/components/widgets/ai-prompt-chip.js.map +1 -1
- package/dist/components/widgets/widget-ask-bar.js +1 -1
- package/dist/components/widgets/widget-ask-bar.js.map +1 -1
- package/dist/hooks/partner-oauth.js +1 -0
- package/dist/hooks/partner-oauth.js.map +1 -1
- package/dist/integration/index.d.ts +1 -1
- package/dist/integration/index.js +1 -1
- package/dist/integration/index.js.map +1 -1
- package/dist/testing/mock-context.js +35 -1
- package/dist/testing/mock-context.js.map +1 -1
- package/package.json +4 -4
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
|
|
82
|
+
### What is published
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
by running a command that 404s:
|
|
84
|
+
Everything you need is on npm:
|
|
86
85
|
|
|
87
|
-
| Package |
|
|
86
|
+
| Package | Role |
|
|
88
87
|
|---|---|
|
|
89
|
-
| `@ekanos/sdk`, `@ekanos/ui`, `@ekanos/integration-schema` |
|
|
90
|
-
| `@ekanos/cli`,
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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
|
|
110
|
-
payload validation and egress allowlist the real transports
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
@@ -1068,6 +1080,10 @@ export const acmeOAuth: PartnerOAuthDeclaration<MyStorage> = {
|
|
|
1068
1080
|
tokenUrl: 'https://connect.acme.example/oauth/token',
|
|
1069
1081
|
scopes: ['payments:read', 'refunds:write'],
|
|
1070
1082
|
pkce: true,
|
|
1083
|
+
// How the token endpoint wants client authentication. Defaults to
|
|
1084
|
+
// 'client_secret_basic' (HTTP Basic); LinkedIn needs 'client_secret_post'
|
|
1085
|
+
// (client_id and client_secret in the form body).
|
|
1086
|
+
tokenAuthMethod: 'client_secret_basic',
|
|
1071
1087
|
},
|
|
1072
1088
|
credentials: {
|
|
1073
1089
|
clientIdSecretName: 'acme_oauth_client_id',
|
|
@@ -1114,14 +1130,19 @@ Register this with your provider, against whichever host will run the flow:
|
|
|
1114
1130
|
https://<your-ekanos-host>/api/integrations/<your-slug>/callback
|
|
1115
1131
|
```
|
|
1116
1132
|
|
|
1117
|
-
The dev harness
|
|
1118
|
-
|
|
1119
|
-
your
|
|
1133
|
+
The dev harness serves its own loopback callback, so you can complete a real
|
|
1134
|
+
round trip locally in live mode: register
|
|
1135
|
+
`http://localhost:3100/api/integrations/<your-slug>/oauth/callback` with your
|
|
1136
|
+
provider as well. See "Local OAuth" in the `@ekanos/harness` README for the
|
|
1137
|
+
secrets file and the connect flow.
|
|
1120
1138
|
|
|
1121
|
-
**
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1139
|
+
In **live mode** the harness runs this loop for real on localhost — authorize
|
|
1140
|
+
redirect, single-use `state`, code exchange through your declared egress, then
|
|
1141
|
+
your `onTokens`. Client credentials come from `.ekanos/local-secrets.json`,
|
|
1142
|
+
which the harness generates and ignores in version control. In fixtures mode it
|
|
1143
|
+
answers connection status from the harness entry instead, and you can still
|
|
1144
|
+
unit-test `onTokens` directly against `createMockContext()` — it is an ordinary
|
|
1145
|
+
async function.
|
|
1125
1146
|
|
|
1126
1147
|
### `proposes`
|
|
1127
1148
|
|
|
@@ -1312,12 +1333,11 @@ whether it is you or us costs an afternoon.
|
|
|
1312
1333
|
get an allowlisted fetch, so it needs a small seam of its own — a React
|
|
1313
1334
|
context whose default is the browser's `fetch`. The `@ekanos/harness` README
|
|
1314
1335
|
has the whole ~20-line pattern; the dev harness's live mode mounts it for you.
|
|
1315
|
-
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
wiring lands with no change on your side.
|
|
1336
|
+
- **`onActivate` errors are non-fatal.** The host runs it after an activation
|
|
1337
|
+
is first persisted and again when activation data changes, but a throw is
|
|
1338
|
+
logged and the activation stays connected — use it to seed caches and
|
|
1339
|
+
validate eagerly, never as a connect gate. It also runs in the
|
|
1340
|
+
`@ekanos/harness` activation surface and under `invokeActivate()`.
|
|
1321
1341
|
- **`useActivateIntegration()` is inert without the host's provider.** It reads
|
|
1322
1342
|
server actions out of `IntegrationActivationProvider`. Nothing warns you at
|
|
1323
1343
|
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: **
|
|
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` (
|
|
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,
|
|
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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
|
|
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"]}
|
|
@@ -25,6 +25,7 @@ export function partnerOAuthErrorParam(integrationSlug) {
|
|
|
25
25
|
}
|
|
26
26
|
const PARTNER_OAUTH_ERROR_MESSAGES = {
|
|
27
27
|
denied: 'Access was denied at the provider. Try connecting again.',
|
|
28
|
+
provider_error: 'The provider refused the connection request. Trying again won’t help; contact support if it keeps happening.',
|
|
28
29
|
expired: 'The connection attempt expired. Try connecting again.',
|
|
29
30
|
invalid: 'The connection attempt could not be verified. Try connecting again.',
|
|
30
31
|
unauthorized: 'You need to be signed in as the person who started the connection.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"partner-oauth.js","sourceRoot":"","sources":["../../src/hooks/partner-oauth.ts"],"names":[],"mappings":"AAiCA,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAW7C,yEAAyE;AACzE,MAAM,UAAU,uBAAuB,CACrC,eAAuB,EACvB,OAAO,GAA4B,EAAE;;IAErC,MAAM,IAAI,SAAG,OAAO,CAAC,OAAO,mCAAI,gBAAgB,CAAC;IACjD,OAAO,GAAG,IAAI,IAAI,eAAe,gBAAgB,CAAC;AACpD,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,qBAAqB,CACnC,eAAuB,EACvB,OAAwD;;IAExD,MAAM,IAAI,SAAG,OAAO,CAAC,OAAO,mCAAI,gBAAgB,CAAC;IACjD,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACpE,OAAO,GAAG,IAAI,IAAI,eAAe,iBAAiB,KAAK,EAAE,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,eAAuB;IAEvB,OAAO;QACL,IAAI,EAAE,GAAG,eAAe,cAAc;QACtC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,wBAAC,4BAA4B,CAAC,KAAK,CAAC,mCAAI,IAAI,GAAA;KAClE,CAAC;AACJ,CAAC;AAED,MAAM,4BAA4B,GAA2B;IAC3D,MAAM,EAAE,0DAA0D;IAClE,OAAO,EAAE,uDAAuD;IAChE,OAAO,EACL,qEAAqE;IACvE,YAAY,EACV,oEAAoE;IACtE,cAAc,EACZ,+GAA+G;IACjH,eAAe,EACb,0FAA0F;IAC5F,WAAW,EAAE,+DAA+D;IAC5E,eAAe,EACb,sIAAsI;IACxI,cAAc,EACZ,mJAAmJ;IACrJ,mBAAmB,EACjB,mHAAmH;CACtH,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAC1C,eAAuB,EACvB,OAAO,GAA4B,EAAE;IAErC,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,qBAAqB,CAAC,eAAe,kCAC5C,OAAO,KACV,SAAS,IACT,CAAC;QAEH,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;YAC3C,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;SACxC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,uBAAuB,eAAe,wBAAwB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,CACzG,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAE/C,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,KAAK,CACb,QAAQ,eAAe,gDAAgD,CACxE,CAAC;QACJ,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAC9B,KAAc;IAEd,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAQ,KAA+B,CAAC,OAAO,KAAK,SAAS,CAC9D,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The browser half of the hosted partner OAuth transport.\n *\n * A partner's `oauth` declaration names the provider endpoints and the\n * client-credential secrets; the HOST owns the flow — authorize redirect,\n * `state`, callback, code exchange, and the `onTokens` call — behind three\n * generic routes it serves for every partner integration:\n *\n * GET /api/integrations/[slug]/oauth/connect?accountId=…&returnPath=…\n * GET /api/integrations/[slug]/oauth/callback (the provider redirects here)\n * GET /api/integrations/[slug]/oauth/status?accountId=…\n *\n * These helpers are the only place a partner package should reference those\n * paths, so a route move is one SDK change rather than a hunt through every\n * integration. `OAuthActivationForm`'s `connectPath` default still points at\n * the per-integration `/connect` route first-party packages ship themselves;\n * a partner passes `connectPath: partnerOAuthConnectPath(slug)` explicitly.\n *\n * ```tsx\n * <OAuthActivationForm\n * {...props}\n * config={{ slug, displayName: 'Acme', connectPath: partnerOAuthConnectPath(slug), … }}\n * checkConnection={createPartnerConnectionCheck(slug)}\n * errorParam={partnerOAuthErrorParam(slug)}\n * />\n * ```\n */\nimport type {\n ConnectionCheckAction,\n ConnectionCheckResult,\n OAuthErrorParam,\n} from './use-oauth-connection-status';\n\nconst DEFAULT_BASE_URL = '/api/integrations';\n\nexport interface PartnerOAuthPathOptions {\n /**\n * Overrides the base path. Exists for harnesses and tests that serve the\n * routes from somewhere other than the host's own origin; production code\n * should leave it alone.\n */\n baseUrl?: string;\n}\n\n/** The hosted connect route the activation form sends the browser to. */\nexport function partnerOAuthConnectPath(\n integrationSlug: string,\n options: PartnerOAuthPathOptions = {},\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n return `${base}/${integrationSlug}/oauth/connect`;\n}\n\n/** The hosted status route `createPartnerConnectionCheck` reads. */\nexport function partnerOAuthStatusUrl(\n integrationSlug: string,\n options: PartnerOAuthPathOptions & { accountId: string },\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n const query = new URLSearchParams({ accountId: options.accountId });\n return `${base}/${integrationSlug}/oauth/status?${query}`;\n}\n\n/**\n * The error query param the hosted callback appends on failure:\n * `<slug>_oauth_error=<code>`. Codes are the transport's, so the messages\n * live here rather than in every partner package.\n */\nexport function partnerOAuthErrorParam(\n integrationSlug: string,\n): OAuthErrorParam {\n return {\n name: `${integrationSlug}_oauth_error`,\n toMessage: (value) => PARTNER_OAUTH_ERROR_MESSAGES[value] ?? null,\n };\n}\n\nconst PARTNER_OAUTH_ERROR_MESSAGES: Record<string, string> = {\n denied: 'Access was denied at the provider. Try connecting again.',\n expired: 'The connection attempt expired. Try connecting again.',\n invalid:\n 'The connection attempt could not be verified. Try connecting again.',\n unauthorized:\n 'You need to be signed in as the person who started the connection.',\n not_configured:\n 'This integration is not configured for your workspace yet. Ask an administrator to add its OAuth credentials.',\n exchange_failed:\n 'The provider rejected the connection. Try again, or contact support if it keeps failing.',\n save_failed: 'Connected, but the credentials could not be saved. Try again.',\n wrong_workspace:\n 'You signed in to a different account or workspace than this one is set up for. Switch to that account at the provider and try again.',\n app_not_set_up:\n 'The provider refused this integration’s app settings, so trying again won’t help. Ask an administrator to check the app’s scopes and credentials.',\n organization_access:\n 'Connect with personal access — this integration only uses your own account. Try again and choose personal access.',\n};\n\n/**\n * A `ConnectionCheckAction` backed by the hosted status route. The route\n * answers 200 with the standard envelope for both outcomes — `{ success:\n * true, data: { obtainedAt } }` or `{ success: false, error }` — and a non-OK\n * status only when the request itself is unauthorized, so a query error\n * surfaces as an error rather than as a misleading \"not connected\".\n */\nexport function createPartnerConnectionCheck(\n integrationSlug: string,\n options: PartnerOAuthPathOptions = {},\n): ConnectionCheckAction {\n return async ({ accountId }) => {\n const url = partnerOAuthStatusUrl(integrationSlug, {\n ...options,\n accountId,\n });\n\n const response = await globalThis.fetch(url, {\n headers: { accept: 'application/json' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Could not read the '${integrationSlug}' connection status (${response.status} ${response.statusText}).`,\n );\n }\n\n const payload: unknown = await response.json();\n\n if (!isConnectionCheckResult(payload)) {\n throw new Error(\n `The '${integrationSlug}' status route returned an unexpected payload.`,\n );\n }\n\n return payload;\n };\n}\n\nfunction isConnectionCheckResult(\n value: unknown,\n): value is ConnectionCheckResult {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { success?: unknown }).success === 'boolean'\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"partner-oauth.js","sourceRoot":"","sources":["../../src/hooks/partner-oauth.ts"],"names":[],"mappings":"AAiCA,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAW7C,yEAAyE;AACzE,MAAM,UAAU,uBAAuB,CACrC,eAAuB,EACvB,OAAO,GAA4B,EAAE;;IAErC,MAAM,IAAI,SAAG,OAAO,CAAC,OAAO,mCAAI,gBAAgB,CAAC;IACjD,OAAO,GAAG,IAAI,IAAI,eAAe,gBAAgB,CAAC;AACpD,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,qBAAqB,CACnC,eAAuB,EACvB,OAAwD;;IAExD,MAAM,IAAI,SAAG,OAAO,CAAC,OAAO,mCAAI,gBAAgB,CAAC;IACjD,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACpE,OAAO,GAAG,IAAI,IAAI,eAAe,iBAAiB,KAAK,EAAE,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,eAAuB;IAEvB,OAAO;QACL,IAAI,EAAE,GAAG,eAAe,cAAc;QACtC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,wBAAC,4BAA4B,CAAC,KAAK,CAAC,mCAAI,IAAI,GAAA;KAClE,CAAC;AACJ,CAAC;AAED,MAAM,4BAA4B,GAA2B;IAC3D,MAAM,EAAE,0DAA0D;IAClE,cAAc,EACZ,8GAA8G;IAChH,OAAO,EAAE,uDAAuD;IAChE,OAAO,EACL,qEAAqE;IACvE,YAAY,EACV,oEAAoE;IACtE,cAAc,EACZ,+GAA+G;IACjH,eAAe,EACb,0FAA0F;IAC5F,WAAW,EAAE,+DAA+D;IAC5E,eAAe,EACb,sIAAsI;IACxI,cAAc,EACZ,mJAAmJ;IACrJ,mBAAmB,EACjB,mHAAmH;CACtH,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAC1C,eAAuB,EACvB,OAAO,GAA4B,EAAE;IAErC,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,qBAAqB,CAAC,eAAe,kCAC5C,OAAO,KACV,SAAS,IACT,CAAC;QAEH,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;YAC3C,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;SACxC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,uBAAuB,eAAe,wBAAwB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,CACzG,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAE/C,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,KAAK,CACb,QAAQ,eAAe,gDAAgD,CACxE,CAAC;QACJ,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAC9B,KAAc;IAEd,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAQ,KAA+B,CAAC,OAAO,KAAK,SAAS,CAC9D,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The browser half of the hosted partner OAuth transport.\n *\n * A partner's `oauth` declaration names the provider endpoints and the\n * client-credential secrets; the HOST owns the flow — authorize redirect,\n * `state`, callback, code exchange, and the `onTokens` call — behind three\n * generic routes it serves for every partner integration:\n *\n * GET /api/integrations/[slug]/oauth/connect?accountId=…&returnPath=…\n * GET /api/integrations/[slug]/oauth/callback (the provider redirects here)\n * GET /api/integrations/[slug]/oauth/status?accountId=…\n *\n * These helpers are the only place a partner package should reference those\n * paths, so a route move is one SDK change rather than a hunt through every\n * integration. `OAuthActivationForm`'s `connectPath` default still points at\n * the per-integration `/connect` route first-party packages ship themselves;\n * a partner passes `connectPath: partnerOAuthConnectPath(slug)` explicitly.\n *\n * ```tsx\n * <OAuthActivationForm\n * {...props}\n * config={{ slug, displayName: 'Acme', connectPath: partnerOAuthConnectPath(slug), … }}\n * checkConnection={createPartnerConnectionCheck(slug)}\n * errorParam={partnerOAuthErrorParam(slug)}\n * />\n * ```\n */\nimport type {\n ConnectionCheckAction,\n ConnectionCheckResult,\n OAuthErrorParam,\n} from './use-oauth-connection-status';\n\nconst DEFAULT_BASE_URL = '/api/integrations';\n\nexport interface PartnerOAuthPathOptions {\n /**\n * Overrides the base path. Exists for harnesses and tests that serve the\n * routes from somewhere other than the host's own origin; production code\n * should leave it alone.\n */\n baseUrl?: string;\n}\n\n/** The hosted connect route the activation form sends the browser to. */\nexport function partnerOAuthConnectPath(\n integrationSlug: string,\n options: PartnerOAuthPathOptions = {},\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n return `${base}/${integrationSlug}/oauth/connect`;\n}\n\n/** The hosted status route `createPartnerConnectionCheck` reads. */\nexport function partnerOAuthStatusUrl(\n integrationSlug: string,\n options: PartnerOAuthPathOptions & { accountId: string },\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n const query = new URLSearchParams({ accountId: options.accountId });\n return `${base}/${integrationSlug}/oauth/status?${query}`;\n}\n\n/**\n * The error query param the hosted callback appends on failure:\n * `<slug>_oauth_error=<code>`. Codes are the transport's, so the messages\n * live here rather than in every partner package.\n */\nexport function partnerOAuthErrorParam(\n integrationSlug: string,\n): OAuthErrorParam {\n return {\n name: `${integrationSlug}_oauth_error`,\n toMessage: (value) => PARTNER_OAUTH_ERROR_MESSAGES[value] ?? null,\n };\n}\n\nconst PARTNER_OAUTH_ERROR_MESSAGES: Record<string, string> = {\n denied: 'Access was denied at the provider. Try connecting again.',\n provider_error:\n 'The provider refused the connection request. Trying again won’t help; contact support if it keeps happening.',\n expired: 'The connection attempt expired. Try connecting again.',\n invalid:\n 'The connection attempt could not be verified. Try connecting again.',\n unauthorized:\n 'You need to be signed in as the person who started the connection.',\n not_configured:\n 'This integration is not configured for your workspace yet. Ask an administrator to add its OAuth credentials.',\n exchange_failed:\n 'The provider rejected the connection. Try again, or contact support if it keeps failing.',\n save_failed: 'Connected, but the credentials could not be saved. Try again.',\n wrong_workspace:\n 'You signed in to a different account or workspace than this one is set up for. Switch to that account at the provider and try again.',\n app_not_set_up:\n 'The provider refused this integration’s app settings, so trying again won’t help. Ask an administrator to check the app’s scopes and credentials.',\n organization_access:\n 'Connect with personal access — this integration only uses your own account. Try again and choose personal access.',\n};\n\n/**\n * A `ConnectionCheckAction` backed by the hosted status route. The route\n * answers 200 with the standard envelope for both outcomes — `{ success:\n * true, data: { obtainedAt } }` or `{ success: false, error }` — and a non-OK\n * status only when the request itself is unauthorized, so a query error\n * surfaces as an error rather than as a misleading \"not connected\".\n */\nexport function createPartnerConnectionCheck(\n integrationSlug: string,\n options: PartnerOAuthPathOptions = {},\n): ConnectionCheckAction {\n return async ({ accountId }) => {\n const url = partnerOAuthStatusUrl(integrationSlug, {\n ...options,\n accountId,\n });\n\n const response = await globalThis.fetch(url, {\n headers: { accept: 'application/json' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Could not read the '${integrationSlug}' connection status (${response.status} ${response.statusText}).`,\n );\n }\n\n const payload: unknown = await response.json();\n\n if (!isConnectionCheckResult(payload)) {\n throw new Error(\n `The '${integrationSlug}' status route returned an unexpected payload.`,\n );\n }\n\n return payload;\n };\n}\n\nfunction isConnectionCheckResult(\n value: unknown,\n): value is ConnectionCheckResult {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { success?: unknown }).success === 'boolean'\n );\n}\n"]}
|
|
@@ -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":"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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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekanos/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.
|
|
69
|
-
"@ekanos/ui": "0.
|
|
68
|
+
"@ekanos/integration-schema": "0.6.0",
|
|
69
|
+
"@ekanos/ui": "0.6.0",
|
|
70
70
|
"@supabase/supabase-js": "2.87.1",
|
|
71
71
|
"server-only": "^0.0.1"
|
|
72
72
|
},
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"@types/react": "19.2.18",
|
|
78
78
|
"@types/react-dom": "19.2.5",
|
|
79
79
|
"eslint": "^10.11.0",
|
|
80
|
-
"next": "16.
|
|
80
|
+
"next": "16.4.0",
|
|
81
81
|
"react": "19.2.8",
|
|
82
82
|
"react-dom": "19.2.8",
|
|
83
83
|
"react-hook-form": "^7.88.0",
|