@ekanos/sdk 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +1093 -140
  2. package/api-report.md +202 -0
  3. package/dist/components/index.d.ts +3 -2
  4. package/dist/components/index.js +3 -2
  5. package/dist/components/index.js.map +1 -1
  6. package/dist/components/widgets/widget-context.js.map +1 -1
  7. package/dist/context/index.d.ts +1 -1
  8. package/dist/context/index.js +1 -1
  9. package/dist/context/index.js.map +1 -1
  10. package/dist/context/types.d.ts +1 -1
  11. package/dist/context/types.js.map +1 -1
  12. package/dist/hooks/activation-actions-context.d.ts +4 -4
  13. package/dist/hooks/activation-actions-context.js +1 -1
  14. package/dist/hooks/activation-actions-context.js.map +1 -1
  15. package/dist/hooks/index.d.ts +2 -2
  16. package/dist/hooks/index.js +2 -2
  17. package/dist/hooks/index.js.map +1 -1
  18. package/dist/hooks/use-oauth-connection-status.d.ts +18 -72
  19. package/dist/hooks/use-oauth-connection-status.js +1 -1
  20. package/dist/hooks/use-oauth-connection-status.js.map +1 -1
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.js +3 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/integration/define-integration.d.ts +2 -3
  25. package/dist/integration/define-integration.js +2 -3
  26. package/dist/integration/define-integration.js.map +1 -1
  27. package/dist/integration/index.d.ts +2 -2
  28. package/dist/integration/index.js +2 -2
  29. package/dist/integration/index.js.map +1 -1
  30. package/dist/testing/index.d.ts +1 -1
  31. package/dist/testing/index.js +1 -1
  32. package/dist/testing/index.js.map +1 -1
  33. package/dist/types/index.d.ts +3 -4
  34. package/dist/types/index.js +3 -4
  35. package/dist/types/index.js.map +1 -1
  36. package/dist/types/integration.d.ts +2 -2
  37. package/dist/types/integration.js.map +1 -1
  38. package/dist/types/widget-ask-context.d.ts +1 -1
  39. package/dist/types/widget-ask-context.js.map +1 -1
  40. package/eslint.preset.mjs +373 -0
  41. package/package.json +18 -18
  42. package/dist/mcp/guards.d.ts +0 -40
  43. package/dist/mcp/guards.js +0 -99
  44. package/dist/mcp/guards.js.map +0 -1
  45. package/dist/mcp/index.d.ts +0 -22
  46. package/dist/mcp/index.js +0 -22
  47. package/dist/mcp/index.js.map +0 -1
  48. package/dist/mcp/ownership.d.ts +0 -74
  49. package/dist/mcp/ownership.js +0 -83
  50. package/dist/mcp/ownership.js.map +0 -1
  51. package/dist/mcp/types.d.ts +0 -145
  52. package/dist/mcp/types.js +0 -2
  53. package/dist/mcp/types.js.map +0 -1
@@ -0,0 +1,373 @@
1
+ /**
2
+ * @ekanos/sdk/eslint — the capability ESLint preset ("R5").
3
+ *
4
+ * This is a SECURITY CONTROL, not a style preset. Partner integration code
5
+ * runs UNSANDBOXED, in-process, inside the host: the generated
6
+ * `partner-integration-bootstrap.ts` statically imports each partner package,
7
+ * so a partner module graph evaluates with full host authority before any
8
+ * validation runs.
9
+ *
10
+ * The accepted T1 trust tier rests on exactly two controls:
11
+ *
12
+ * 1. the promotion gate — a human reads the SOURCE before it is built, and
13
+ * 2. THIS PRESET — which bans the escapes that route around the capability
14
+ * layer, so "reads the source" is a tractable review rather than an
15
+ * invitation to spot `globalThis.fetch` by eye.
16
+ *
17
+ * Runtime SANDBOXING of handlers is the deferred T3 tier. Until it exists,
18
+ * every rule below is load-bearing.
19
+ *
20
+ * ── Why it lives here ────────────────────────────────────────────────────
21
+ * A partner already installs `@ekanos/sdk`, so the control ships with the
22
+ * surface it protects and versions in lockstep with it: the day `ctx.fetch`
23
+ * grows a documented client-side twin, the fetch rule relaxes in the same
24
+ * release. It is dependency-free — every rule below is core ESLint — so it
25
+ * adds nothing to the SDK's build, `api-report`, or `pack:test`.
26
+ *
27
+ * ── Usage (a partner's eslint.config.mjs) ────────────────────────────────
28
+ *
29
+ * import capabilityPreset from '@ekanos/sdk/eslint';
30
+ *
31
+ * export default [
32
+ * ...someBaseConfig,
33
+ * ...capabilityPreset,
34
+ * ];
35
+ *
36
+ * ── Inline disables do not work, by design ───────────────────────────────
37
+ * The preset sets `linterOptions.noInlineConfig` over the source glob, so
38
+ * `// eslint-disable-next-line` cannot silence these rules from partner
39
+ * source — otherwise the control would be opt-out by the very code it
40
+ * constrains. A genuine exception must be a FILE-SCOPED OVERRIDE in the
41
+ * package's own `eslint.config.mjs`, where a reviewer reads it. Both
42
+ * exceptions currently granted in this repo live in the example packages'
43
+ * configs and are recorded there with the reason.
44
+ */
45
+
46
+ /**
47
+ * Glob for partner integration SOURCE. Build tooling — `vitest.config.ts`,
48
+ * `eslint.config.mjs`, `tsup.config.ts` — is deliberately OUT of scope: it
49
+ * runs on the partner's build machine, never in the host process, and it
50
+ * legitimately imports `node:path`/`node:url`. Narrowing the preset to source
51
+ * is what keeps it free of the false positives that get a control disabled.
52
+ */
53
+ const SOURCE_GLOB = ['src/**/*.ts', 'src/**/*.tsx'];
54
+
55
+ const CTX_FETCH_REMEDIATION =
56
+ 'Use `ctx.fetch` from the capability context (`IntegrationContext`) instead, ' +
57
+ "and declare every origin you call in your integration's `egress` " +
58
+ 'allowlist — `defineIntegration({ egress: [...] })`. `ctx.fetch` refuses an ' +
59
+ 'undeclared origin before any I/O, re-checks every redirect hop, and pins ' +
60
+ 'the vetted IP addresses so an allowlisted hostname cannot be rebound to a ' +
61
+ 'host-internal address. A global fetch has none of that, so it is an egress ' +
62
+ 'bypass. Widening `egress` is a reviewed security-posture change, not a ' +
63
+ 'config tweak.';
64
+
65
+ const CTX_SECRETS_REMEDIATION =
66
+ 'Use `ctx.secrets.get(name)` from the capability context instead, and ' +
67
+ "declare the credential in your integration's activation schema. The host " +
68
+ "process environment holds the platform's own credentials — it is not your " +
69
+ 'configuration store, and reading it is how an integration gets a secret it ' +
70
+ 'was never granted.';
71
+
72
+ /**
73
+ * Node builtins a partner integration must never reach, each with the
74
+ * capability that replaces it. Every entry is listed twice — bare and
75
+ * `node:`-prefixed — because both resolve.
76
+ *
77
+ * This is an explicit list rather than a blanket `node:*` ban on purpose: the
78
+ * SDK surface is isomorphic, and `node:crypto`/`node:url`-shaped imports are
79
+ * neither an escape nor worth training a partner to reach for a disable
80
+ * comment over. The generic escape hatch — a computed dynamic `import()` — is
81
+ * closed separately in `no-restricted-syntax` below, so the list cannot be
82
+ * routed around by building the specifier at runtime.
83
+ */
84
+ const BANNED_MODULES = [
85
+ {
86
+ names: ['fs', 'node:fs', 'fs/promises', 'node:fs/promises'],
87
+ message:
88
+ 'The host filesystem is not part of the capability surface. Use ' +
89
+ '`ctx.storage.account` / `ctx.storage.user` for integration state — it ' +
90
+ 'is scoped to the account the request is authorized for, quota-bounded, ' +
91
+ 'and schema-validated. Filesystem access reads and writes host state ' +
92
+ 'that no account owns.',
93
+ },
94
+ {
95
+ names: ['child_process', 'node:child_process'],
96
+ message:
97
+ 'Spawning a process escapes every capability control at once. There is ' +
98
+ 'no replacement: an integration is data-in / data-out. If you need work ' +
99
+ "the SDK cannot express, raise it as an SDK gap in your integration's " +
100
+ 'README rather than shelling out.',
101
+ },
102
+ {
103
+ names: [
104
+ 'net',
105
+ 'node:net',
106
+ 'tls',
107
+ 'node:tls',
108
+ 'dgram',
109
+ 'node:dgram',
110
+ 'http',
111
+ 'node:http',
112
+ 'https',
113
+ 'node:https',
114
+ 'http2',
115
+ 'node:http2',
116
+ ],
117
+ message:
118
+ 'Raw sockets and the `http`/`https` clients bypass the egress allowlist ' +
119
+ 'and its address guard entirely. ' +
120
+ CTX_FETCH_REMEDIATION,
121
+ },
122
+ {
123
+ names: ['dns', 'node:dns', 'dns/promises', 'node:dns/promises'],
124
+ message:
125
+ 'Resolving hostnames yourself is the first half of a DNS-rebinding ' +
126
+ 'egress bypass — `ctx.fetch` resolves ONCE and pins the vetted ' +
127
+ 'addresses precisely so that a name cannot resolve differently between ' +
128
+ 'the check and the connection. ' +
129
+ CTX_FETCH_REMEDIATION,
130
+ },
131
+ {
132
+ names: ['worker_threads', 'node:worker_threads'],
133
+ message:
134
+ 'A worker thread runs on a path where the capability context does not ' +
135
+ 'exist, so nothing inside it can reach `ctx.fetch`, `ctx.secrets` or ' +
136
+ '`ctx.storage`. Do the work in the handler, with `ctx`, and let the ' +
137
+ 'host own concurrency — batch or paginate rather than fanning out.',
138
+ },
139
+ {
140
+ names: ['vm', 'node:vm'],
141
+ message:
142
+ '`vm` executes code from a string, which makes the source a reviewer ' +
143
+ 'reads no longer the code that runs — the assumption the T1 trust tier ' +
144
+ 'is built on. Write the logic as source.',
145
+ },
146
+ {
147
+ names: ['module', 'node:module'],
148
+ message:
149
+ '`node:module` (`createRequire`) is a loader escape hatch: it resolves ' +
150
+ 'modules this preset bans by name at runtime. Use a static `import` of ' +
151
+ 'a package you declare in `dependencies`.',
152
+ },
153
+ {
154
+ names: ['process', 'node:process'],
155
+ message:
156
+ 'Importing `process` reaches the host environment. ' +
157
+ CTX_SECRETS_REMEDIATION,
158
+ },
159
+ {
160
+ names: ['os', 'node:os'],
161
+ message:
162
+ '`node:os` reports the host machine — hostname, users, network ' +
163
+ 'interfaces, home directories. None of it describes the account your ' +
164
+ 'integration is running for, and all of it is host reconnaissance. ' +
165
+ 'Read what you need from the capability context instead: ' +
166
+ '`ctx.accountId` for who this run is for, `ctx.storage` for state you ' +
167
+ 'persisted, `ctx.logger` for diagnostics.',
168
+ },
169
+ {
170
+ names: ['undici', 'node-fetch', 'axios', 'got', 'superagent', 'request'],
171
+ message:
172
+ 'A third-party HTTP client issues requests the egress allowlist never ' +
173
+ 'sees, which is the same bypass as a global `fetch` with an extra ' +
174
+ 'dependency. ' +
175
+ CTX_FETCH_REMEDIATION,
176
+ },
177
+ {
178
+ names: ['react-i18next'],
179
+ importNames: ['Trans'],
180
+ message:
181
+ 'Use `Trans` from `@ekanos/ui/trans` instead — it is wired to the ' +
182
+ "host's i18n instance, so your strings resolve against the same " +
183
+ 'namespaces and language the surrounding page already loaded.',
184
+ },
185
+ ];
186
+
187
+ /** Globals that reach the network without passing through `ctx.fetch`. */
188
+ const EGRESS_GLOBALS = ['fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource'];
189
+
190
+ /** Objects a `globalThis.fetch`-style member access can hide behind. */
191
+ const GLOBAL_OBJECTS = 'globalThis|window|self|global';
192
+
193
+ /** Members of those objects that are the very escapes banned as bare globals. */
194
+ const GLOBAL_ESCAPE_MEMBERS =
195
+ 'fetch|XMLHttpRequest|WebSocket|EventSource|process|eval|require|Function';
196
+
197
+ /**
198
+ * `@kit/*` is checked with a PATTERN rather than a path so every subpath is
199
+ * covered (`@kit/ui/badge`, `@kit/next/actions`, …). This is the rule that
200
+ * catches, at lint time in the partner's own editor, the failure that
201
+ * otherwise surfaces as an unresolvable module at install time — which is
202
+ * exactly how our own reference example turned out to be unbuildable outside
203
+ * the monorepo.
204
+ */
205
+ const KIT_PATTERN = {
206
+ group: ['@kit/*', '@kit/*/**'],
207
+ message:
208
+ '`@kit/*` packages are workspace-internal to the Fusion monorepo and do ' +
209
+ 'not exist on any registry, so this import cannot resolve for anyone ' +
210
+ 'outside it — including in the clean-room build your submission is gated ' +
211
+ 'on. Import UI primitives from `@ekanos/ui/*` (badge, button, card, form, ' +
212
+ 'input, select, icon, utils, …), integration surfaces from `@ekanos/sdk` ' +
213
+ 'and its `/components`, `/hooks`, `/mcp`, `/context`, `/integration` ' +
214
+ 'entrypoints, and nothing else from the host.',
215
+ };
216
+
217
+ const SUPABASE_PATTERN = {
218
+ group: ['@supabase/*', '@supabase/*/**'],
219
+ message:
220
+ 'A direct Supabase client talks to the database with whatever key it is ' +
221
+ 'given, outside the account scoping and RLS the host applies for you. Use ' +
222
+ '`ctx.storage.account` / `ctx.storage.user` for integration state — ' +
223
+ 'authorized for the account in the request and validated against your ' +
224
+ 'declared schema.',
225
+ };
226
+
227
+ /** Flattens BANNED_MODULES into `no-restricted-imports` `paths` entries. */
228
+ function bannedPaths() {
229
+ return BANNED_MODULES.flatMap((entry) =>
230
+ entry.names.map((name) => ({
231
+ name,
232
+ message: entry.message,
233
+ ...(entry.importNames ? { importNames: entry.importNames } : {}),
234
+ })),
235
+ );
236
+ }
237
+
238
+ /**
239
+ * The rules, exported separately so a consumer can compose them into its own
240
+ * config block (a different `files` glob, say) without re-deriving them.
241
+ */
242
+ export const capabilityRules = {
243
+ /**
244
+ * Bare global references only. ESLint resolves scope first, so a parameter
245
+ * or local named `fetch` — the SDK's own `IntegrationFetch` injection
246
+ * pattern, `getJson(fetch, url)` — is NOT reported. That is the intended
247
+ * shape: pass the mediated fetch in as a value.
248
+ */
249
+ 'no-restricted-globals': [
250
+ 'error',
251
+ ...EGRESS_GLOBALS.map((name) => ({
252
+ name,
253
+ message: `\`${name}\` reaches the network outside the capability layer. ${CTX_FETCH_REMEDIATION}`,
254
+ })),
255
+ {
256
+ name: 'process',
257
+ message: `\`process\` (including \`process.env\`) is host state, not integration state. ${CTX_SECRETS_REMEDIATION}`,
258
+ },
259
+ {
260
+ name: 'require',
261
+ message:
262
+ '`require` resolves modules at runtime, so no reviewer and no import ' +
263
+ 'rule can see what is actually loaded. Use a static `import` of a ' +
264
+ 'package declared in `dependencies`.',
265
+ },
266
+ {
267
+ name: 'eval',
268
+ message:
269
+ '`eval` runs code built at runtime, so the source a reviewer reads ' +
270
+ 'stops being the code that runs — and human review of source is half ' +
271
+ 'of what makes running your integration in-process acceptable. Write ' +
272
+ 'the logic as source.',
273
+ },
274
+ ],
275
+
276
+ 'no-restricted-imports': [
277
+ 'error',
278
+ { paths: bannedPaths(), patterns: [KIT_PATTERN, SUPABASE_PATTERN] },
279
+ ],
280
+
281
+ 'no-restricted-syntax': [
282
+ 'error',
283
+ {
284
+ // Closes the hole in no-restricted-globals: that rule resolves
285
+ // identifiers, so `globalThis.fetch` (a member access, not a global
286
+ // reference) is invisible to it.
287
+ selector: `MemberExpression[object.name=/^(${GLOBAL_OBJECTS})$/][property.name=/^(${GLOBAL_ESCAPE_MEMBERS})$/]`,
288
+ message:
289
+ 'Reaching an escape through the global object is the same bypass as ' +
290
+ 'naming it directly, and it is how a capability control gets routed ' +
291
+ 'around without tripping an import rule. Use the capability context: ' +
292
+ '`ctx.fetch` for network calls (with the origin declared in `egress`) ' +
293
+ 'and `ctx.secrets` for credentials.',
294
+ },
295
+ {
296
+ // Computed-string member access, e.g. `globalThis['fetch']`.
297
+ selector: `MemberExpression[computed=true][object.name=/^(${GLOBAL_OBJECTS})$/]`,
298
+ message:
299
+ 'Indexing the global object with a computed key hides which global is ' +
300
+ 'being reached, which defeats every rule in this preset. Reference ' +
301
+ 'what you need directly, through the capability context — `ctx.fetch` ' +
302
+ 'for network calls, `ctx.secrets` for credentials, `ctx.storage` for ' +
303
+ 'state.',
304
+ },
305
+ {
306
+ selector: 'NewExpression[callee.name="Function"]',
307
+ message:
308
+ '`new Function` compiles code from a string — the same problem as ' +
309
+ '`eval`: the source under review is no longer the code that runs. ' +
310
+ 'Write the logic as source.',
311
+ },
312
+ {
313
+ selector: 'CallExpression[callee.name="Function"]',
314
+ message:
315
+ '`Function(...)` compiles code from a string — the same problem as ' +
316
+ '`eval`: the source under review is no longer the code that runs. ' +
317
+ 'Write the logic as source.',
318
+ },
319
+ {
320
+ // The generic route around no-restricted-imports: build the specifier
321
+ // at runtime and the import rule has nothing to match.
322
+ selector: 'ImportExpression[source.type!="Literal"]',
323
+ message:
324
+ 'A dynamic `import()` with a computed specifier hides what is loaded ' +
325
+ 'from both the import rules and the human promotion review. Use a ' +
326
+ 'static `import`, or `import("literal-specifier")` if you genuinely ' +
327
+ 'need lazy loading.',
328
+ },
329
+ {
330
+ // `no-restricted-imports` does not inspect dynamic import specifiers,
331
+ // so the banned module list is re-applied to literal ones here.
332
+ selector:
333
+ 'ImportExpression[source.value=/^(node:)?(fs|child_process|net|tls|dgram|dns|http|https|http2|worker_threads|vm|module|process|os)$/]',
334
+ message:
335
+ 'This Node builtin is banned for integration code whether it is ' +
336
+ 'imported statically or dynamically — a dynamic `import()` of it is ' +
337
+ 'the same capability escape. See the static-import message for the ' +
338
+ 'capability that replaces it (`ctx.fetch`, `ctx.secrets`, ' +
339
+ '`ctx.storage`).',
340
+ },
341
+ {
342
+ // The `/promises` faces of the same builtins. Split out because a `/`
343
+ // cannot appear inside an esquery regex-literal attribute value.
344
+ selector:
345
+ 'ImportExpression[source.value=/^(node:)?(fs|dns)\\u002Fpromises$/]',
346
+ message:
347
+ 'This Node builtin is banned for integration code whether it is ' +
348
+ 'imported statically or dynamically. Use `ctx.storage` for state and ' +
349
+ '`ctx.fetch` for network calls.',
350
+ },
351
+ ],
352
+ };
353
+
354
+ /**
355
+ * The preset. Spread it AFTER any base config: it deliberately replaces a
356
+ * base `no-restricted-imports` / `no-restricted-globals` /
357
+ * `no-restricted-syntax` setting rather than merging with it (flat config does
358
+ * not merge rule options), and it re-states the one entry the Fusion base
359
+ * config carries — `react-i18next`'s `Trans` — in its partner-correct form.
360
+ */
361
+ export default [
362
+ {
363
+ name: '@ekanos/sdk/eslint:capabilities',
364
+ files: SOURCE_GLOB,
365
+ linterOptions: {
366
+ // A control the constrained code can switch off is not a control.
367
+ // Exceptions belong in the consumer's config, where review sees them.
368
+ // See the header.
369
+ noInlineConfig: true,
370
+ },
371
+ rules: capabilityRules,
372
+ },
373
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "The official SDK for building Ekanos integrations.",
6
6
  "license": "MIT",
@@ -12,8 +12,11 @@
12
12
  ],
13
13
  "files": [
14
14
  "dist",
15
+ "!dist/mcp",
15
16
  "README.md",
16
- "LICENSE"
17
+ "api-report.md",
18
+ "LICENSE",
19
+ "eslint.preset.mjs"
17
20
  ],
18
21
  "exports": {
19
22
  ".": {
@@ -28,10 +31,6 @@
28
31
  "types": "./dist/hooks/index.d.ts",
29
32
  "default": "./dist/hooks/index.js"
30
33
  },
31
- "./mcp": {
32
- "types": "./dist/mcp/index.d.ts",
33
- "default": "./dist/mcp/index.js"
34
- },
35
34
  "./context": {
36
35
  "types": "./dist/context/index.d.ts",
37
36
  "default": "./dist/context/index.js"
@@ -43,16 +42,17 @@
43
42
  "./integration": {
44
43
  "types": "./dist/integration/index.d.ts",
45
44
  "default": "./dist/integration/index.js"
46
- }
45
+ },
46
+ "./eslint": "./eslint.preset.mjs"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
+ "@ekanos/integration-schema": "0.1.3",
53
+ "@ekanos/ui": "0.1.3",
52
54
  "@supabase/supabase-js": "2.87.1",
53
- "server-only": "^0.0.1",
54
- "@ekanos/integration-schema": "0.1.1",
55
- "@ekanos/ui": "0.1.1"
55
+ "server-only": "^0.0.1"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@hookform/resolvers": "^5.2.2",
@@ -63,18 +63,18 @@
63
63
  "zod": "^3.25.76"
64
64
  },
65
65
  "devDependencies": {
66
- "@hookform/resolvers": "^5.2.2",
67
- "@tanstack/react-query": "5.101.4",
66
+ "@hookform/resolvers": "^5.9.1",
67
+ "@kit/eslint-config": "0.2.0",
68
+ "@kit/prettier-config": "0.1.0",
69
+ "@kit/tsconfig": "0.1.0",
70
+ "@tanstack/react-query": "5.102.8",
68
71
  "@types/react": "19.2.18",
69
- "next": "16.3.1",
72
+ "next": "16.3.3",
70
73
  "react": "19.2.8",
71
- "react-hook-form": "^7.68.0",
74
+ "react-hook-form": "^7.87.0",
72
75
  "typescript": "^5.9.3",
73
76
  "vitest": "4.1.10",
74
- "zod": "^3.25.74",
75
- "@kit/eslint-config": "0.2.0",
76
- "@kit/prettier-config": "0.1.0",
77
- "@kit/tsconfig": "0.1.0"
77
+ "zod": "3.25.76"
78
78
  },
79
79
  "prettier": "@kit/prettier-config",
80
80
  "typesVersions": {
@@ -1,40 +0,0 @@
1
- import type { ToolContext } from './types.js';
2
- /**
3
- * Account-resolution and membership guards for MCP tool modules.
4
- *
5
- * Every tool module MUST resolve the acting account via `getAccountId` and
6
- * verify access with `ensureAccountMembership` (or the group variant) before
7
- * touching data. Moved here from `@kit/integrations-core/mcp` by the SDK
8
- * inversion; the HOST-only tool machinery (partner/first-party branding,
9
- * account_slug schema injection) deliberately stays in integrations-core —
10
- * branding is the host's provenance pen and must not ship in the
11
- * partner-facing SDK.
12
- */
13
- /**
14
- * Resolve the account ID from context.
15
- *
16
- * Machine principal (Workflow Gateway): the account is fixed by the gateway
17
- * token grant — never derived from args or the user. Otherwise: uses the
18
- * pre-injected accountId from the MCP route, falling back to the user's
19
- * personal account. See context/N8N_INTEGRATION_PLAN.md "Machine-principal design".
20
- */
21
- export declare function getAccountId(ctx: ToolContext): string;
22
- /**
23
- * Verify the caller has access to the specified account.
24
- *
25
- * Machine principal (Workflow Gateway): access is granted iff the requested
26
- * account matches the token's bound account — the token is the authorization,
27
- * so no DB membership lookup is performed (and none would be correct, since a
28
- * machine call has no signed-in user).
29
- *
30
- * User principal (default): unchanged — true for personal accounts
31
- * (accountId === user.id) or a valid team membership row.
32
- */
33
- export declare function ensureAccountMembership(ctx: ToolContext, accountId: string): Promise<boolean>;
34
- /**
35
- * Verify user is a member of a named group within the account.
36
- * Returns true only if the named group exists AND the user is a member.
37
- * Returns false if the group doesn't exist (deny by default — the group
38
- * must be created and the user added before they gain access).
39
- */
40
- export declare function ensureGroupMembership(ctx: ToolContext, accountId: string, groupName: string): Promise<boolean>;
@@ -1,99 +0,0 @@
1
- /**
2
- * Account-resolution and membership guards for MCP tool modules.
3
- *
4
- * Every tool module MUST resolve the acting account via `getAccountId` and
5
- * verify access with `ensureAccountMembership` (or the group variant) before
6
- * touching data. Moved here from `@kit/integrations-core/mcp` by the SDK
7
- * inversion; the HOST-only tool machinery (partner/first-party branding,
8
- * account_slug schema injection) deliberately stays in integrations-core —
9
- * branding is the host's provenance pen and must not ship in the
10
- * partner-facing SDK.
11
- */
12
- /**
13
- * Resolve the account ID from context.
14
- *
15
- * Machine principal (Workflow Gateway): the account is fixed by the gateway
16
- * token grant — never derived from args or the user. Otherwise: uses the
17
- * pre-injected accountId from the MCP route, falling back to the user's
18
- * personal account. See context/N8N_INTEGRATION_PLAN.md "Machine-principal design".
19
- */
20
- export function getAccountId(ctx) {
21
- if (ctx.principal === 'machine') {
22
- // A machine principal MUST carry a grant. Falling back to ctx.user.id here
23
- // would silently retarget the call at the token creator's PERSONAL account
24
- // — fail closed instead: a machine context without a grant is a bug.
25
- if (!ctx.machineGrant) {
26
- throw new Error('Machine principal context is missing its account grant');
27
- }
28
- return ctx.machineGrant.accountId;
29
- }
30
- return ctx.accountId || ctx.user.id;
31
- }
32
- /**
33
- * Verify the caller has access to the specified account.
34
- *
35
- * Machine principal (Workflow Gateway): access is granted iff the requested
36
- * account matches the token's bound account — the token is the authorization,
37
- * so no DB membership lookup is performed (and none would be correct, since a
38
- * machine call has no signed-in user).
39
- *
40
- * User principal (default): unchanged — true for personal accounts
41
- * (accountId === user.id) or a valid team membership row.
42
- */
43
- export async function ensureAccountMembership(ctx, accountId) {
44
- var _a;
45
- // Machine principal: the gateway token binds exactly one account. Access is
46
- // allowed only for that account; there is no membership table to consult.
47
- if (ctx.principal === 'machine') {
48
- return ((_a = ctx.machineGrant) === null || _a === void 0 ? void 0 : _a.accountId) === accountId;
49
- }
50
- // Personal account - user always has access
51
- if (accountId === ctx.user.id) {
52
- return true;
53
- }
54
- // Team account - check membership
55
- const { data: membershipRows } = await ctx.supabase
56
- .from('accounts_memberships')
57
- .select('account_id')
58
- .eq('account_id', accountId)
59
- .eq('user_id', ctx.user.id)
60
- .limit(1);
61
- return !!(membershipRows && membershipRows.length > 0);
62
- }
63
- /**
64
- * Verify user is a member of a named group within the account.
65
- * Returns true only if the named group exists AND the user is a member.
66
- * Returns false if the group doesn't exist (deny by default — the group
67
- * must be created and the user added before they gain access).
68
- */
69
- export async function ensureGroupMembership(ctx, accountId, groupName) {
70
- // Machine principal (Workflow Gateway): group membership is a per-user
71
- // concept and is not meaningful for a token-authenticated machine caller —
72
- // checking it against ctx.user.id (the token creator) would be both wrong
73
- // and a membership-probing leak. Deny; machine access is governed solely by
74
- // the token grant (ensureAccountMembership).
75
- if (ctx.principal === 'machine') {
76
- return false;
77
- }
78
- // Check if the named group exists for this account
79
- const { data: groups } = await ctx.supabase
80
- .from('account_groups')
81
- .select('id')
82
- .eq('account_id', accountId)
83
- .eq('name', groupName)
84
- .limit(1);
85
- // Group doesn't exist → deny (must be configured first)
86
- if (!groups || groups.length === 0) {
87
- return false;
88
- }
89
- const groupId = groups[0].id;
90
- // Group exists → check if user is a member
91
- const { data: membership } = await ctx.supabase
92
- .from('account_group_members')
93
- .select('id')
94
- .eq('group_id', groupId)
95
- .eq('user_id', ctx.user.id)
96
- .limit(1);
97
- return !!(membership && membership.length > 0);
98
- }
99
- //# sourceMappingURL=guards.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/mcp/guards.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,GAAgB;IAC3C,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChC,2EAA2E;QAC3E,2EAA2E;QAC3E,qEAAqE;QACrE,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC5E,CAAC;QAED,OAAO,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC;IACpC,CAAC;IAED,OAAO,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;AACtC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,GAAgB,EAChB,SAAiB;;IAEjB,4EAA4E;IAC5E,0EAA0E;IAC1E,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,CAAA,MAAA,GAAG,CAAC,YAAY,0CAAE,SAAS,MAAK,SAAS,CAAC;IACnD,CAAC;IAED,4CAA4C;IAC5C,IAAI,SAAS,KAAK,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,kCAAkC;IAClC,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,MAAM,GAAG,CAAC,QAAQ;SAChD,IAAI,CAAC,sBAAsB,CAAC;SAC5B,MAAM,CAAC,YAAY,CAAC;SACpB,EAAE,CAAC,YAAY,EAAE,SAAS,CAAC;SAC3B,EAAE,CAAC,SAAS,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;SAC1B,KAAK,CAAC,CAAC,CAAC,CAAC;IAEZ,OAAO,CAAC,CAAC,CAAC,cAAc,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,GAAgB,EAChB,SAAiB,EACjB,SAAiB;IAEjB,uEAAuE;IACvE,2EAA2E;IAC3E,0EAA0E;IAC1E,4EAA4E;IAC5E,6CAA6C;IAC7C,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,mDAAmD;IACnD,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,QAAQ;SACxC,IAAI,CAAC,gBAAgB,CAAC;SACtB,MAAM,CAAC,IAAI,CAAC;SACZ,EAAE,CAAC,YAAY,EAAE,SAAS,CAAC;SAC3B,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC;SACrB,KAAK,CAAC,CAAC,CAAC,CAAC;IAEZ,wDAAwD;IACxD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC,EAAY,CAAC;IAExC,2CAA2C;IAC3C,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,GAAG,CAAC,QAAQ;SAC5C,IAAI,CAAC,uBAAuB,CAAC;SAC7B,MAAM,CAAC,IAAI,CAAC;SACZ,EAAE,CAAC,UAAU,EAAE,OAAO,CAAC;SACvB,EAAE,CAAC,SAAS,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;SAC1B,KAAK,CAAC,CAAC,CAAC,CAAC;IAEZ,OAAO,CAAC,CAAC,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjD,CAAC","sourcesContent":["import type { ToolContext } from './types';\n\n/**\n * Account-resolution and membership guards for MCP tool modules.\n *\n * Every tool module MUST resolve the acting account via `getAccountId` and\n * verify access with `ensureAccountMembership` (or the group variant) before\n * touching data. Moved here from `@kit/integrations-core/mcp` by the SDK\n * inversion; the HOST-only tool machinery (partner/first-party branding,\n * account_slug schema injection) deliberately stays in integrations-core —\n * branding is the host's provenance pen and must not ship in the\n * partner-facing SDK.\n */\n\n/**\n * Resolve the account ID from context.\n *\n * Machine principal (Workflow Gateway): the account is fixed by the gateway\n * token grant — never derived from args or the user. Otherwise: uses the\n * pre-injected accountId from the MCP route, falling back to the user's\n * personal account. See context/N8N_INTEGRATION_PLAN.md \"Machine-principal design\".\n */\nexport function getAccountId(ctx: ToolContext): string {\n if (ctx.principal === 'machine') {\n // A machine principal MUST carry a grant. Falling back to ctx.user.id here\n // would silently retarget the call at the token creator's PERSONAL account\n // — fail closed instead: a machine context without a grant is a bug.\n if (!ctx.machineGrant) {\n throw new Error('Machine principal context is missing its account grant');\n }\n\n return ctx.machineGrant.accountId;\n }\n\n return ctx.accountId || ctx.user.id;\n}\n\n/**\n * Verify the caller has access to the specified account.\n *\n * Machine principal (Workflow Gateway): access is granted iff the requested\n * account matches the token's bound account — the token is the authorization,\n * so no DB membership lookup is performed (and none would be correct, since a\n * machine call has no signed-in user).\n *\n * User principal (default): unchanged — true for personal accounts\n * (accountId === user.id) or a valid team membership row.\n */\nexport async function ensureAccountMembership(\n ctx: ToolContext,\n accountId: string,\n): Promise<boolean> {\n // Machine principal: the gateway token binds exactly one account. Access is\n // allowed only for that account; there is no membership table to consult.\n if (ctx.principal === 'machine') {\n return ctx.machineGrant?.accountId === accountId;\n }\n\n // Personal account - user always has access\n if (accountId === ctx.user.id) {\n return true;\n }\n\n // Team account - check membership\n const { data: membershipRows } = await ctx.supabase\n .from('accounts_memberships')\n .select('account_id')\n .eq('account_id', accountId)\n .eq('user_id', ctx.user.id)\n .limit(1);\n\n return !!(membershipRows && membershipRows.length > 0);\n}\n\n/**\n * Verify user is a member of a named group within the account.\n * Returns true only if the named group exists AND the user is a member.\n * Returns false if the group doesn't exist (deny by default — the group\n * must be created and the user added before they gain access).\n */\nexport async function ensureGroupMembership(\n ctx: ToolContext,\n accountId: string,\n groupName: string,\n): Promise<boolean> {\n // Machine principal (Workflow Gateway): group membership is a per-user\n // concept and is not meaningful for a token-authenticated machine caller —\n // checking it against ctx.user.id (the token creator) would be both wrong\n // and a membership-probing leak. Deny; machine access is governed solely by\n // the token grant (ensureAccountMembership).\n if (ctx.principal === 'machine') {\n return false;\n }\n\n // Check if the named group exists for this account\n const { data: groups } = await ctx.supabase\n .from('account_groups')\n .select('id')\n .eq('account_id', accountId)\n .eq('name', groupName)\n .limit(1);\n\n // Group doesn't exist → deny (must be configured first)\n if (!groups || groups.length === 0) {\n return false;\n }\n\n const groupId = groups[0]!.id as string;\n\n // Group exists → check if user is a member\n const { data: membership } = await ctx.supabase\n .from('account_group_members')\n .select('id')\n .eq('group_id', groupId)\n .eq('user_id', ctx.user.id)\n .limit(1);\n\n return !!(membership && membership.length > 0);\n}\n"]}
@@ -1,22 +0,0 @@
1
- /**
2
- * @ekanos/sdk/mcp — MCP tool contract and security guards.
3
- *
4
- * Every tool module MUST resolve the acting account via `getAccountId` and
5
- * verify access with `ensureAccountMembership` (or the group/resource
6
- * variants) before touching data. `assertResourceOwnership` is exported so
7
- * tools never hand-roll ownership checks.
8
- *
9
- * The SDK OWNS these implementations (the inversion);
10
- * `@kit/integrations-core/mcp` re-exports them for first-party code. The
11
- * HOST-only machinery stays in integrations-core on purpose:
12
- * `brandPartnerTool` / `brandFirstPartyTool` (the provenance pen — exporting
13
- * it here would hand partners the classification authority) and
14
- * `injectAccountSlugParam(s)` (host route plumbing).
15
- *
16
- * Surface discipline: additions require an entry in
17
- * docs/devex/sdk-export-map.md ("`@ekanos/sdk/mcp`").
18
- */
19
- import 'server-only';
20
- export type * from './types.js';
21
- export { ensureAccountMembership, ensureGroupMembership, getAccountId, } from './guards.js';
22
- export { ToolOwnershipError, assertResourceOwnership, resolveSelfOnly, } from './ownership.js';
package/dist/mcp/index.js DELETED
@@ -1,22 +0,0 @@
1
- /**
2
- * @ekanos/sdk/mcp — MCP tool contract and security guards.
3
- *
4
- * Every tool module MUST resolve the acting account via `getAccountId` and
5
- * verify access with `ensureAccountMembership` (or the group/resource
6
- * variants) before touching data. `assertResourceOwnership` is exported so
7
- * tools never hand-roll ownership checks.
8
- *
9
- * The SDK OWNS these implementations (the inversion);
10
- * `@kit/integrations-core/mcp` re-exports them for first-party code. The
11
- * HOST-only machinery stays in integrations-core on purpose:
12
- * `brandPartnerTool` / `brandFirstPartyTool` (the provenance pen — exporting
13
- * it here would hand partners the classification authority) and
14
- * `injectAccountSlugParam(s)` (host route plumbing).
15
- *
16
- * Surface discipline: additions require an entry in
17
- * docs/devex/sdk-export-map.md ("`@ekanos/sdk/mcp`").
18
- */
19
- import 'server-only';
20
- export { ensureAccountMembership, ensureGroupMembership, getAccountId, } from './guards.js';
21
- export { ToolOwnershipError, assertResourceOwnership, resolveSelfOnly, } from './ownership.js';
22
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,aAAa,CAAC;AAIrB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,eAAe,GAChB,MAAM,aAAa,CAAC","sourcesContent":["/**\n * @ekanos/sdk/mcp — MCP tool contract and security guards.\n *\n * Every tool module MUST resolve the acting account via `getAccountId` and\n * verify access with `ensureAccountMembership` (or the group/resource\n * variants) before touching data. `assertResourceOwnership` is exported so\n * tools never hand-roll ownership checks.\n *\n * The SDK OWNS these implementations (the inversion);\n * `@kit/integrations-core/mcp` re-exports them for first-party code. The\n * HOST-only machinery stays in integrations-core on purpose:\n * `brandPartnerTool` / `brandFirstPartyTool` (the provenance pen — exporting\n * it here would hand partners the classification authority) and\n * `injectAccountSlugParam(s)` (host route plumbing).\n *\n * Surface discipline: additions require an entry in\n * docs/devex/sdk-export-map.md (\"`@ekanos/sdk/mcp`\").\n */\nimport 'server-only';\n\nexport type * from './types';\n\nexport {\n ensureAccountMembership,\n ensureGroupMembership,\n getAccountId,\n} from './guards';\n\nexport {\n ToolOwnershipError,\n assertResourceOwnership,\n resolveSelfOnly,\n} from './ownership';\n"]}