@ekanos/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/components/base-activation-dialog.d.ts +41 -0
- package/dist/components/base-activation-dialog.js +41 -0
- package/dist/components/base-activation-dialog.js.map +1 -0
- package/dist/components/base-activation-form.d.ts +36 -0
- package/dist/components/base-activation-form.js +21 -0
- package/dist/components/base-activation-form.js.map +1 -0
- package/dist/components/base-marketplace-tile.d.ts +34 -0
- package/dist/components/base-marketplace-tile.js +40 -0
- package/dist/components/base-marketplace-tile.js.map +1 -0
- package/dist/components/current-return-path.d.ts +19 -0
- package/dist/components/current-return-path.js +26 -0
- package/dist/components/current-return-path.js.map +1 -0
- package/dist/components/index.d.ts +40 -0
- package/dist/components/index.js +38 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/oauth-activation-form.d.ts +50 -0
- package/dist/components/oauth-activation-form.js +119 -0
- package/dist/components/oauth-activation-form.js.map +1 -0
- package/dist/components/widgets/ai-prompt-chip.d.ts +31 -0
- package/dist/components/widgets/ai-prompt-chip.js +38 -0
- package/dist/components/widgets/ai-prompt-chip.js.map +1 -0
- package/dist/components/widgets/ask-icon.d.ts +3 -0
- package/dist/components/widgets/ask-icon.js +5 -0
- package/dist/components/widgets/ask-icon.js.map +1 -0
- package/dist/components/widgets/use-animated-height.d.ts +12 -0
- package/dist/components/widgets/use-animated-height.js +39 -0
- package/dist/components/widgets/use-animated-height.js.map +1 -0
- package/dist/components/widgets/widget-ask-bar.d.ts +18 -0
- package/dist/components/widgets/widget-ask-bar.js +62 -0
- package/dist/components/widgets/widget-ask-bar.js.map +1 -0
- package/dist/components/widgets/widget-context.d.ts +38 -0
- package/dist/components/widgets/widget-context.js +4 -0
- package/dist/components/widgets/widget-context.js.map +1 -0
- package/dist/components/widgets/widget-state-components.d.ts +5 -0
- package/dist/components/widgets/widget-state-components.js +19 -0
- package/dist/components/widgets/widget-state-components.js.map +1 -0
- package/dist/components/widgets/widget.d.ts +91 -0
- package/dist/components/widgets/widget.js +205 -0
- package/dist/components/widgets/widget.js.map +1 -0
- package/dist/context/egress.d.ts +30 -0
- package/dist/context/egress.js +81 -0
- package/dist/context/egress.js.map +1 -0
- package/dist/context/errors.d.ts +57 -0
- package/dist/context/errors.js +78 -0
- package/dist/context/errors.js.map +1 -0
- package/dist/context/index.d.ts +19 -0
- package/dist/context/index.js +20 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/require-context.d.ts +26 -0
- package/dist/context/require-context.js +36 -0
- package/dist/context/require-context.js.map +1 -0
- package/dist/context/storage-validation.d.ts +71 -0
- package/dist/context/storage-validation.js +132 -0
- package/dist/context/storage-validation.js.map +1 -0
- package/dist/context/types.d.ts +11 -0
- package/dist/context/types.js +2 -0
- package/dist/context/types.js.map +1 -0
- package/dist/hooks/activation-actions-context.d.ts +80 -0
- package/dist/hooks/activation-actions-context.js +31 -0
- package/dist/hooks/activation-actions-context.js.map +1 -0
- package/dist/hooks/ask-assistant-context.d.ts +32 -0
- package/dist/hooks/ask-assistant-context.js +56 -0
- package/dist/hooks/ask-assistant-context.js.map +1 -0
- package/dist/hooks/fetch-integration-storage.d.ts +48 -0
- package/dist/hooks/fetch-integration-storage.js +68 -0
- package/dist/hooks/fetch-integration-storage.js.map +1 -0
- package/dist/hooks/index.d.ts +28 -0
- package/dist/hooks/index.js +32 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-activate-integration.d.ts +15 -0
- package/dist/hooks/use-activate-integration.js +61 -0
- package/dist/hooks/use-activate-integration.js.map +1 -0
- package/dist/hooks/use-oauth-connection-status.d.ts +360 -0
- package/dist/hooks/use-oauth-connection-status.js +108 -0
- package/dist/hooks/use-oauth-connection-status.js.map +1 -0
- package/dist/hooks/use-widget-events.d.ts +20 -0
- package/dist/hooks/use-widget-events.js +51 -0
- package/dist/hooks/use-widget-events.js.map +1 -0
- package/dist/hooks/widget-event-context.d.ts +22 -0
- package/dist/hooks/widget-event-context.js +75 -0
- package/dist/hooks/widget-event-context.js.map +1 -0
- package/dist/hooks/widget-event-types.d.ts +55 -0
- package/dist/hooks/widget-event-types.js +9 -0
- package/dist/hooks/widget-event-types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/cron.d.ts +29 -0
- package/dist/integration/cron.js +102 -0
- package/dist/integration/cron.js.map +1 -0
- package/dist/integration/define-integration.d.ts +28 -0
- package/dist/integration/define-integration.js +41 -0
- package/dist/integration/define-integration.js.map +1 -0
- package/dist/integration/event-surfaces.d.ts +15 -0
- package/dist/integration/event-surfaces.js +45 -0
- package/dist/integration/event-surfaces.js.map +1 -0
- package/dist/integration/index.d.ts +20 -0
- package/dist/integration/index.js +23 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/types.d.ts +7 -0
- package/dist/integration/types.js +2 -0
- package/dist/integration/types.js.map +1 -0
- package/dist/mcp/guards.d.ts +40 -0
- package/dist/mcp/guards.js +99 -0
- package/dist/mcp/guards.js.map +1 -0
- package/dist/mcp/index.d.ts +22 -0
- package/dist/mcp/index.js +22 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/ownership.d.ts +74 -0
- package/dist/mcp/ownership.js +83 -0
- package/dist/mcp/ownership.js.map +1 -0
- package/dist/mcp/types.d.ts +145 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/invoke.d.ts +77 -0
- package/dist/testing/invoke.js +83 -0
- package/dist/testing/invoke.js.map +1 -0
- package/dist/testing/mock-context.d.ts +79 -0
- package/dist/testing/mock-context.js +206 -0
- package/dist/testing/mock-context.js.map +1 -0
- package/dist/types/icon.d.ts +12 -0
- package/dist/types/icon.js +2 -0
- package/dist/types/icon.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/integration.d.ts +258 -0
- package/dist/types/integration.js +2 -0
- package/dist/types/integration.js.map +1 -0
- package/dist/types/widget-ask-context.d.ts +36 -0
- package/dist/types/widget-ask-context.js +2 -0
- package/dist/types/widget-ask-context.js.map +1 -0
- package/dist/types/workspace-target.d.ts +33 -0
- package/dist/types/workspace-target.js +2 -0
- package/dist/types/workspace-target.js.map +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ekanos/sdk/integration — the partner authoring contract.
|
|
3
|
+
*
|
|
4
|
+
* `defineIntegration()` is THE way a partner declares an integration
|
|
5
|
+
* (adversarial review outcome 3: partners never extend `BaseIntegration`).
|
|
6
|
+
* The host adapts the returned definition internally via
|
|
7
|
+
* `registerPartnerIntegration` in `@kit/integrations-core`, which re-parses
|
|
8
|
+
* against the SAME canonical schema (`@ekanos/integration-schema`) at the
|
|
9
|
+
* trust boundary — one contract, no structural twin (F3/F9).
|
|
10
|
+
*
|
|
11
|
+
* Dependency-pure and isomorphic: zod plus the schema package, no `@kit/*`,
|
|
12
|
+
* no `server-only`.
|
|
13
|
+
*
|
|
14
|
+
* Surface discipline: additions require an entry in
|
|
15
|
+
* docs/devex/sdk-export-map.md ("`@ekanos/sdk/integration`").
|
|
16
|
+
*/
|
|
17
|
+
export { defineIntegration } from './define-integration.js';
|
|
18
|
+
export { parseCronExpression } from './cron.js';
|
|
19
|
+
export { IntegrationDefinitionSchema, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
|
|
20
|
+
export type { IntegrationDefinition, IntegrationComponentDeclarations, IntegrationProposals, IntegrationCapabilityDeclaration, IntegrationPermissionDeclaration, PartnerWidgetDeclaration, PartnerToolModule, PartnerToolParameters, ToolClassificationProposal, DefinitionCollisionInput, FirstPartyInventory, PartnerWebhookDeclaration, WebhookSignatureDeclaration, WebhookEvent, WebhookResult, PartnerScheduleDeclaration, ScheduleInvocation, ScheduleResult, PartnerOAuthDeclaration, OAuthProviderDeclaration, OAuthTokens, } from '@ekanos/integration-schema';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ekanos/sdk/integration — the partner authoring contract.
|
|
3
|
+
*
|
|
4
|
+
* `defineIntegration()` is THE way a partner declares an integration
|
|
5
|
+
* (adversarial review outcome 3: partners never extend `BaseIntegration`).
|
|
6
|
+
* The host adapts the returned definition internally via
|
|
7
|
+
* `registerPartnerIntegration` in `@kit/integrations-core`, which re-parses
|
|
8
|
+
* against the SAME canonical schema (`@ekanos/integration-schema`) at the
|
|
9
|
+
* trust boundary — one contract, no structural twin (F3/F9).
|
|
10
|
+
*
|
|
11
|
+
* Dependency-pure and isomorphic: zod plus the schema package, no `@kit/*`,
|
|
12
|
+
* no `server-only`.
|
|
13
|
+
*
|
|
14
|
+
* Surface discipline: additions require an entry in
|
|
15
|
+
* docs/devex/sdk-export-map.md ("`@ekanos/sdk/integration`").
|
|
16
|
+
*/
|
|
17
|
+
export { defineIntegration } from './define-integration.js';
|
|
18
|
+
// Event-surface validation values: the 5-field cron parser backing
|
|
19
|
+
// `defineIntegration()`'s schedule validation, exported so validators and
|
|
20
|
+
// the harness can check/describe an expression the same way the SDK does.
|
|
21
|
+
export { parseCronExpression } from './cron.js';
|
|
22
|
+
export { IntegrationDefinitionSchema, validateIntegrationDefinitions, } from '@ekanos/integration-schema';
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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,OAAO,EACL,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 * (adversarial review outcome 3: partners never extend `BaseIntegration`).\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 * docs/devex/sdk-export-map.md (\"`@ekanos/sdk/integration`\").\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\nexport {\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 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 WebhookEvent,\n WebhookResult,\n PartnerScheduleDeclaration,\n ScheduleInvocation,\n ScheduleResult,\n PartnerOAuthDeclaration,\n OAuthProviderDeclaration,\n OAuthTokens,\n} from '@ekanos/integration-schema';\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The partner authoring types are owned by `@ekanos/integration-schema` (the
|
|
3
|
+
* dependency-pure package `@kit/integrations-core` also imports, so there is
|
|
4
|
+
* one contract and no structural twin — F9). This file re-exports them under
|
|
5
|
+
* `@ekanos/sdk/integration`.
|
|
6
|
+
*/
|
|
7
|
+
export type { IntegrationDefinition, IntegrationComponentDeclarations, IntegrationProposals, IntegrationCapabilityDeclaration, IntegrationPermissionDeclaration, PartnerWidgetDeclaration, PartnerToolModule, PartnerToolParameters, ToolClassificationProposal, PartnerWebhookDeclaration, WebhookSignatureDeclaration, WebhookEvent, WebhookResult, PartnerScheduleDeclaration, ScheduleInvocation, ScheduleResult, PartnerOAuthDeclaration, OAuthProviderDeclaration, OAuthTokens, } from '@ekanos/integration-schema';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/integration/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The partner authoring types are owned by `@ekanos/integration-schema` (the\n * dependency-pure package `@kit/integrations-core` also imports, so there is\n * one contract and no structural twin — F9). This file re-exports them under\n * `@ekanos/sdk/integration`.\n */\nexport type {\n IntegrationDefinition,\n IntegrationComponentDeclarations,\n IntegrationProposals,\n IntegrationCapabilityDeclaration,\n IntegrationPermissionDeclaration,\n PartnerWidgetDeclaration,\n PartnerToolModule,\n PartnerToolParameters,\n ToolClassificationProposal,\n PartnerWebhookDeclaration,\n WebhookSignatureDeclaration,\n WebhookEvent,\n WebhookResult,\n PartnerScheduleDeclaration,\n ScheduleInvocation,\n ScheduleResult,\n PartnerOAuthDeclaration,\n OAuthProviderDeclaration,\n OAuthTokens,\n} from '@ekanos/integration-schema';\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
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>;
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
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';
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership guards for member-facing MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* The LLM controls tool arguments. Any member-facing tool that accepts an
|
|
5
|
+
* external per-user identifier (a Wild Apricot contactId, a Discourse
|
|
6
|
+
* username, a LearnWorlds learner id, etc.) is a cross-member data-exposure
|
|
7
|
+
* risk: the caller can pass *someone else's* id and read or act on their
|
|
8
|
+
* data. The org-membership gate (`ensureAccountMembership`) does not catch
|
|
9
|
+
* this — it answers "is this person in the org?", never "is this person's
|
|
10
|
+
* own data?".
|
|
11
|
+
*
|
|
12
|
+
* These helpers make the safe path the default. A tool author reaches for
|
|
13
|
+
* `resolveSelfOnly` instead of writing a bespoke "default to me" resolver,
|
|
14
|
+
* and gets ownership enforcement for free.
|
|
15
|
+
*
|
|
16
|
+
* Resolving "who am I" is integration-specific (each integration maps the
|
|
17
|
+
* Supabase user to its own id space), so the caller supplies `resolveSelf`.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when a tool caller attempts to access or act on another user's
|
|
21
|
+
* data. Tool `run` bodies should let this propagate (or convert it to a
|
|
22
|
+
* `{ error }` result) — the assistant surfaces a "not permitted" message
|
|
23
|
+
* rather than the forbidden data.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ToolOwnershipError extends Error {
|
|
26
|
+
readonly code = "FORBIDDEN_OWNERSHIP";
|
|
27
|
+
constructor(resourceLabel?: string);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Self-only identity resolution for member-facing tools.
|
|
31
|
+
*
|
|
32
|
+
* - caller supplied no id → returns the caller's own id
|
|
33
|
+
* - caller supplied their own id → returns it
|
|
34
|
+
* - caller supplied a different id → throws ToolOwnershipError
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // In a Wild Apricot read tool:
|
|
38
|
+
* const contactId = await resolveSelfOnly({
|
|
39
|
+
* requestedId: args.contactId,
|
|
40
|
+
* resolveSelf: () => resolveWaContactId(accountId),
|
|
41
|
+
* resourceLabel: 'member data',
|
|
42
|
+
* });
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveSelfOnly<TId extends string | number>(params: {
|
|
45
|
+
/** The identifier the LLM supplied as a tool argument, if any. */
|
|
46
|
+
requestedId?: TId | null;
|
|
47
|
+
/** Integration-specific resolver for the calling user's own id. */
|
|
48
|
+
resolveSelf: () => Promise<TId>;
|
|
49
|
+
/** Human-readable noun for the error message, e.g. 'member profile'. */
|
|
50
|
+
resourceLabel?: string;
|
|
51
|
+
}): Promise<TId>;
|
|
52
|
+
/**
|
|
53
|
+
* Ownership verification for tools that act on an opaque resource id (an
|
|
54
|
+
* event registration, a saved document, an invoice) where ownership cannot
|
|
55
|
+
* be inferred from the id itself. Resolves the resource's owner and the
|
|
56
|
+
* caller's own id and throws if they differ (or if the resource has no
|
|
57
|
+
* resolvable owner — fail closed).
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // In a Wild Apricot cancel tool:
|
|
61
|
+
* await assertResourceOwnership({
|
|
62
|
+
* resolveSelf: () => resolveWaContactId(accountId),
|
|
63
|
+
* resolveOwner: async () => {
|
|
64
|
+
* const regs = await getContactRegistrations(accountId, await resolveWaContactId(accountId));
|
|
65
|
+
* return regs.find((r) => r.Id === registrationId)?.Contact.Id;
|
|
66
|
+
* },
|
|
67
|
+
* resourceLabel: 'event registration',
|
|
68
|
+
* });
|
|
69
|
+
*/
|
|
70
|
+
export declare function assertResourceOwnership<TId extends string | number>(params: {
|
|
71
|
+
resolveSelf: () => Promise<TId>;
|
|
72
|
+
resolveOwner: () => Promise<TId | null | undefined>;
|
|
73
|
+
resourceLabel?: string;
|
|
74
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership guards for member-facing MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* The LLM controls tool arguments. Any member-facing tool that accepts an
|
|
5
|
+
* external per-user identifier (a Wild Apricot contactId, a Discourse
|
|
6
|
+
* username, a LearnWorlds learner id, etc.) is a cross-member data-exposure
|
|
7
|
+
* risk: the caller can pass *someone else's* id and read or act on their
|
|
8
|
+
* data. The org-membership gate (`ensureAccountMembership`) does not catch
|
|
9
|
+
* this — it answers "is this person in the org?", never "is this person's
|
|
10
|
+
* own data?".
|
|
11
|
+
*
|
|
12
|
+
* These helpers make the safe path the default. A tool author reaches for
|
|
13
|
+
* `resolveSelfOnly` instead of writing a bespoke "default to me" resolver,
|
|
14
|
+
* and gets ownership enforcement for free.
|
|
15
|
+
*
|
|
16
|
+
* Resolving "who am I" is integration-specific (each integration maps the
|
|
17
|
+
* Supabase user to its own id space), so the caller supplies `resolveSelf`.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when a tool caller attempts to access or act on another user's
|
|
21
|
+
* data. Tool `run` bodies should let this propagate (or convert it to a
|
|
22
|
+
* `{ error }` result) — the assistant surfaces a "not permitted" message
|
|
23
|
+
* rather than the forbidden data.
|
|
24
|
+
*/
|
|
25
|
+
export class ToolOwnershipError extends Error {
|
|
26
|
+
constructor(resourceLabel = 'data') {
|
|
27
|
+
super(`Forbidden: you can only access your own ${resourceLabel}.`);
|
|
28
|
+
this.code = 'FORBIDDEN_OWNERSHIP';
|
|
29
|
+
this.name = 'ToolOwnershipError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Self-only identity resolution for member-facing tools.
|
|
34
|
+
*
|
|
35
|
+
* - caller supplied no id → returns the caller's own id
|
|
36
|
+
* - caller supplied their own id → returns it
|
|
37
|
+
* - caller supplied a different id → throws ToolOwnershipError
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // In a Wild Apricot read tool:
|
|
41
|
+
* const contactId = await resolveSelfOnly({
|
|
42
|
+
* requestedId: args.contactId,
|
|
43
|
+
* resolveSelf: () => resolveWaContactId(accountId),
|
|
44
|
+
* resourceLabel: 'member data',
|
|
45
|
+
* });
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveSelfOnly(params) {
|
|
48
|
+
const selfId = await params.resolveSelf();
|
|
49
|
+
if (params.requestedId !== undefined &&
|
|
50
|
+
params.requestedId !== null &&
|
|
51
|
+
params.requestedId !== selfId) {
|
|
52
|
+
throw new ToolOwnershipError(params.resourceLabel);
|
|
53
|
+
}
|
|
54
|
+
return selfId;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Ownership verification for tools that act on an opaque resource id (an
|
|
58
|
+
* event registration, a saved document, an invoice) where ownership cannot
|
|
59
|
+
* be inferred from the id itself. Resolves the resource's owner and the
|
|
60
|
+
* caller's own id and throws if they differ (or if the resource has no
|
|
61
|
+
* resolvable owner — fail closed).
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // In a Wild Apricot cancel tool:
|
|
65
|
+
* await assertResourceOwnership({
|
|
66
|
+
* resolveSelf: () => resolveWaContactId(accountId),
|
|
67
|
+
* resolveOwner: async () => {
|
|
68
|
+
* const regs = await getContactRegistrations(accountId, await resolveWaContactId(accountId));
|
|
69
|
+
* return regs.find((r) => r.Id === registrationId)?.Contact.Id;
|
|
70
|
+
* },
|
|
71
|
+
* resourceLabel: 'event registration',
|
|
72
|
+
* });
|
|
73
|
+
*/
|
|
74
|
+
export async function assertResourceOwnership(params) {
|
|
75
|
+
const [selfId, ownerId] = await Promise.all([
|
|
76
|
+
params.resolveSelf(),
|
|
77
|
+
params.resolveOwner(),
|
|
78
|
+
]);
|
|
79
|
+
if (ownerId === null || ownerId === undefined || ownerId !== selfId) {
|
|
80
|
+
throw new ToolOwnershipError(params.resourceLabel);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=ownership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.js","sourceRoot":"","sources":["../../src/mcp/ownership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;GAKG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAG3C,YAAY,aAAa,GAAG,MAAM;QAChC,KAAK,CAAC,2CAA2C,aAAa,GAAG,CAAC,CAAC;QAH5D,SAAI,GAAG,qBAAqB,CAAC;QAIpC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAA8B,MAOlE;IACC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;IAE1C,IACE,MAAM,CAAC,WAAW,KAAK,SAAS;QAChC,MAAM,CAAC,WAAW,KAAK,IAAI;QAC3B,MAAM,CAAC,WAAW,KAAK,MAAM,EAC7B,CAAC;QACD,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAE3C,MAID;IACC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC1C,MAAM,CAAC,WAAW,EAAE;QACpB,MAAM,CAAC,YAAY,EAAE;KACtB,CAAC,CAAC;IAEH,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACpE,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACrD,CAAC;AACH,CAAC","sourcesContent":["/**\n * Ownership guards for member-facing MCP tools.\n *\n * The LLM controls tool arguments. Any member-facing tool that accepts an\n * external per-user identifier (a Wild Apricot contactId, a Discourse\n * username, a LearnWorlds learner id, etc.) is a cross-member data-exposure\n * risk: the caller can pass *someone else's* id and read or act on their\n * data. The org-membership gate (`ensureAccountMembership`) does not catch\n * this — it answers \"is this person in the org?\", never \"is this person's\n * own data?\".\n *\n * These helpers make the safe path the default. A tool author reaches for\n * `resolveSelfOnly` instead of writing a bespoke \"default to me\" resolver,\n * and gets ownership enforcement for free.\n *\n * Resolving \"who am I\" is integration-specific (each integration maps the\n * Supabase user to its own id space), so the caller supplies `resolveSelf`.\n */\n\n/**\n * Thrown when a tool caller attempts to access or act on another user's\n * data. Tool `run` bodies should let this propagate (or convert it to a\n * `{ error }` result) — the assistant surfaces a \"not permitted\" message\n * rather than the forbidden data.\n */\nexport class ToolOwnershipError extends Error {\n readonly code = 'FORBIDDEN_OWNERSHIP';\n\n constructor(resourceLabel = 'data') {\n super(`Forbidden: you can only access your own ${resourceLabel}.`);\n this.name = 'ToolOwnershipError';\n }\n}\n\n/**\n * Self-only identity resolution for member-facing tools.\n *\n * - caller supplied no id → returns the caller's own id\n * - caller supplied their own id → returns it\n * - caller supplied a different id → throws ToolOwnershipError\n *\n * @example\n * // In a Wild Apricot read tool:\n * const contactId = await resolveSelfOnly({\n * requestedId: args.contactId,\n * resolveSelf: () => resolveWaContactId(accountId),\n * resourceLabel: 'member data',\n * });\n */\nexport async function resolveSelfOnly<TId extends string | number>(params: {\n /** The identifier the LLM supplied as a tool argument, if any. */\n requestedId?: TId | null;\n /** Integration-specific resolver for the calling user's own id. */\n resolveSelf: () => Promise<TId>;\n /** Human-readable noun for the error message, e.g. 'member profile'. */\n resourceLabel?: string;\n}): Promise<TId> {\n const selfId = await params.resolveSelf();\n\n if (\n params.requestedId !== undefined &&\n params.requestedId !== null &&\n params.requestedId !== selfId\n ) {\n throw new ToolOwnershipError(params.resourceLabel);\n }\n\n return selfId;\n}\n\n/**\n * Ownership verification for tools that act on an opaque resource id (an\n * event registration, a saved document, an invoice) where ownership cannot\n * be inferred from the id itself. Resolves the resource's owner and the\n * caller's own id and throws if they differ (or if the resource has no\n * resolvable owner — fail closed).\n *\n * @example\n * // In a Wild Apricot cancel tool:\n * await assertResourceOwnership({\n * resolveSelf: () => resolveWaContactId(accountId),\n * resolveOwner: async () => {\n * const regs = await getContactRegistrations(accountId, await resolveWaContactId(accountId));\n * return regs.find((r) => r.Id === registrationId)?.Contact.Id;\n * },\n * resourceLabel: 'event registration',\n * });\n */\nexport async function assertResourceOwnership<\n TId extends string | number,\n>(params: {\n resolveSelf: () => Promise<TId>;\n resolveOwner: () => Promise<TId | null | undefined>;\n resourceLabel?: string;\n}): Promise<void> {\n const [selfId, ownerId] = await Promise.all([\n params.resolveSelf(),\n params.resolveOwner(),\n ]);\n\n if (ownerId === null || ownerId === undefined || ownerId !== selfId) {\n throw new ToolOwnershipError(params.resourceLabel);\n }\n}\n"]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { IntegrationContext } from '@ekanos/integration-schema';
|
|
3
|
+
export type ToolParamsSchema = {
|
|
4
|
+
type: 'object';
|
|
5
|
+
properties?: Record<string, unknown>;
|
|
6
|
+
required?: string[];
|
|
7
|
+
additionalProperties?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type ToolContext = {
|
|
10
|
+
req: Request;
|
|
11
|
+
/**
|
|
12
|
+
* Admin Supabase client for in-process MCP execution.
|
|
13
|
+
* This bypasses RLS; tools must perform their own authorization checks before
|
|
14
|
+
* reading or mutating protected data.
|
|
15
|
+
*/
|
|
16
|
+
supabase: SupabaseClient;
|
|
17
|
+
user: {
|
|
18
|
+
id: string;
|
|
19
|
+
} & Record<string, unknown>;
|
|
20
|
+
/** The resolved account slug from the workspace context (injected by MCP route) */
|
|
21
|
+
accountSlug?: string;
|
|
22
|
+
/** The resolved account ID (UUID) from the workspace context (injected by MCP route) */
|
|
23
|
+
accountId?: string;
|
|
24
|
+
/** The resolved source ID (UUID) for the current tenant/workspace */
|
|
25
|
+
sourceId?: string | null;
|
|
26
|
+
/** The user's resolved IANA timezone (e.g. 'America/New_York') */
|
|
27
|
+
timezone?: string;
|
|
28
|
+
/** Whether the authenticated user is a platform super-admin */
|
|
29
|
+
isSuperAdmin?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Who is executing this tool call.
|
|
32
|
+
*
|
|
33
|
+
* Undefined (the default for every user-facing MCP path) is treated as
|
|
34
|
+
* `'user'` — the historical behavior. `'machine'` marks a non-interactive
|
|
35
|
+
* caller (the Workflow Gateway) whose identity IS the account-bound gateway
|
|
36
|
+
* token, not a signed-in session. See machineGrant below and the
|
|
37
|
+
* "Machine-principal design" section of context/N8N_INTEGRATION_PLAN.md.
|
|
38
|
+
*/
|
|
39
|
+
principal?: 'user' | 'machine';
|
|
40
|
+
/**
|
|
41
|
+
* Present only when `principal === 'machine'`. Carries the account the
|
|
42
|
+
* gateway token is bound to (the sole source of account identity for a
|
|
43
|
+
* machine call), the token row id, and the token creator for audit
|
|
44
|
+
* attribution. `getAccountId` / `ensureAccountMembership` read this instead
|
|
45
|
+
* of doing a DB membership lookup — the token itself is the authorization.
|
|
46
|
+
*/
|
|
47
|
+
machineGrant?: {
|
|
48
|
+
accountId: string;
|
|
49
|
+
tokenId: string;
|
|
50
|
+
createdBy: string | null;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The host-built capability context
|
|
54
|
+
* (docs/devex/capability-context-proposal.md), attached ADDITIVELY by the
|
|
55
|
+
* MCP route after authorization. This is the CANONICAL `IntegrationContext`
|
|
56
|
+
* from `@ekanos/integration-schema` — the same declaration `@ekanos/sdk`
|
|
57
|
+
* re-exports, so there is no structural twin to drift and no core→SDK
|
|
58
|
+
* package cycle. Migrated tools may still narrow it with
|
|
59
|
+
* `requireContext(ctx)` from `@ekanos/sdk/context`; existing tools that
|
|
60
|
+
* never touch this field are unaffected.
|
|
61
|
+
*/
|
|
62
|
+
integrationContext?: IntegrationContext;
|
|
63
|
+
};
|
|
64
|
+
export type ToolRunResult = unknown;
|
|
65
|
+
/**
|
|
66
|
+
* The partner execution channel — the ONLY way partner-authored tool code
|
|
67
|
+
* runs (capability-context proposal §3 + sdk-export-map.md adversarial
|
|
68
|
+
* review outcomes 1–2).
|
|
69
|
+
*
|
|
70
|
+
* The first argument is the host-built capability context — the canonical
|
|
71
|
+
* `IntegrationContext` from `@ekanos/integration-schema`, the one declaration
|
|
72
|
+
* both `@ekanos/sdk` (authoring) and this package (host) import. The
|
|
73
|
+
* client-bearing `ToolContext` (req, admin supabase, identity fields) is
|
|
74
|
+
* structurally absent from this signature — a partner tool cannot reach it.
|
|
75
|
+
*/
|
|
76
|
+
export type PartnerToolRun = (integrationContext: IntegrationContext, args: Record<string, unknown>) => Promise<ToolRunResult>;
|
|
77
|
+
export type ToolModule = {
|
|
78
|
+
name: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
parameters?: ToolParamsSchema;
|
|
81
|
+
run: (ctx: ToolContext, args: Record<string, unknown>) => Promise<ToolRunResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Partner EXECUTION function. Set by host adapter code
|
|
84
|
+
* (`registerPartnerIntegration`'s tool adapter) alongside the host BRAND
|
|
85
|
+
* (`brandPartnerTool`, mcp/helpers.ts) — never by first-party tool modules.
|
|
86
|
+
*
|
|
87
|
+
* IMPORTANT: this property is NOT the partner classification authority
|
|
88
|
+
* (that is the module-private WeakSet brand — a property can be forged,
|
|
89
|
+
* stripped, or varied between reads). For a host-branded tool the executor:
|
|
90
|
+
* - invokes `partnerRun(integrationContext, args)` with the host-built
|
|
91
|
+
* capability context and NOTHING else (no ToolContext, no prepareArgs,
|
|
92
|
+
* no toUi);
|
|
93
|
+
* - FAILS CLOSED if the capability context or this function is missing —
|
|
94
|
+
* there is no legacy-context fallback for a branded tool.
|
|
95
|
+
*
|
|
96
|
+
* An UNBRANDED object carrying this property is treated as first-party and
|
|
97
|
+
* can never reach the partner path — so a partner cannot escape into, and a
|
|
98
|
+
* spoofed property cannot fake, the capability-only channel.
|
|
99
|
+
*/
|
|
100
|
+
partnerRun?: PartnerToolRun;
|
|
101
|
+
prepareArgs?: (ctx: ToolContext, rawArgs: unknown) => Promise<Record<string, unknown>>;
|
|
102
|
+
ui?: UiComponentMeta | UiComponentMeta[];
|
|
103
|
+
toUi?: (ctx: ToolContext, result: unknown) => Promise<null | RenderInstruction | RenderInstruction[]>;
|
|
104
|
+
/**
|
|
105
|
+
* Side-effect classification — used by the SOC 2 audit log to decide
|
|
106
|
+
* whether the call is recorded. When omitted, the audit layer derives
|
|
107
|
+
* it from the tool name (read-verb prefixes → 'read', otherwise 'write').
|
|
108
|
+
* Tool authors should set this explicitly when the heuristic would be
|
|
109
|
+
* wrong (e.g. a `generate_*` tool that materially mutates server state).
|
|
110
|
+
* See packages/agents/src/security/tool-effect.ts.
|
|
111
|
+
*/
|
|
112
|
+
effect?: 'read' | 'write';
|
|
113
|
+
/**
|
|
114
|
+
* Data classification of what this tool exposes. Mirror of
|
|
115
|
+
* `Sensitivity` in packages/agents/src/security/redact.ts — kept inline
|
|
116
|
+
* here to avoid a backwards dependency from integrations-core onto agents.
|
|
117
|
+
* Reads that expose 'pii' or 'financial' data are recorded in the audit
|
|
118
|
+
* log even though they are not writes. Defaults to undefined (treated
|
|
119
|
+
* as 'internal' by the audit filter, 'pii' by the redaction layer).
|
|
120
|
+
*/
|
|
121
|
+
sensitivity?: 'public' | 'internal' | 'pii' | 'financial';
|
|
122
|
+
/**
|
|
123
|
+
* A REPRESENTATIVE example of this tool's SUCCESS return value (top-level
|
|
124
|
+
* shape, minimal realistic values, arrays trimmed to one element). Consumed
|
|
125
|
+
* by the workflow authoring layer two ways:
|
|
126
|
+
* 1. Codegen grounding — the example is embedded in the workflow-generation
|
|
127
|
+
* prompt so the model wires `{{stepId.field}}` refs against the tool's
|
|
128
|
+
* REAL output keys instead of guessing (`deals` vs `contacts`,
|
|
129
|
+
* `contact_id` vs `contactId` — observed failure class).
|
|
130
|
+
* 2. Ref linting — a workflow ref into this tool's output whose first path
|
|
131
|
+
* segment is not a key of this example is rejected at authoring time.
|
|
132
|
+
* Keep it SMALL (it ships in prompts), faithful to the actual run() return,
|
|
133
|
+
* and free of real customer data. Omitting it just skips both uses.
|
|
134
|
+
*/
|
|
135
|
+
outputExample?: Record<string, unknown>;
|
|
136
|
+
};
|
|
137
|
+
export type UiComponentMeta = {
|
|
138
|
+
component: string;
|
|
139
|
+
propsSchema: ToolParamsSchema;
|
|
140
|
+
doc?: string;
|
|
141
|
+
};
|
|
142
|
+
export type RenderInstruction = {
|
|
143
|
+
component: string;
|
|
144
|
+
props: unknown;
|
|
145
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/mcp/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { SupabaseClient } from '@supabase/supabase-js';\n\nimport type { IntegrationContext } from '@ekanos/integration-schema';\n\nexport type ToolParamsSchema = {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n additionalProperties?: boolean;\n};\n\nexport type ToolContext = {\n req: Request;\n /**\n * Admin Supabase client for in-process MCP execution.\n * This bypasses RLS; tools must perform their own authorization checks before\n * reading or mutating protected data.\n */\n supabase: SupabaseClient;\n user: { id: string } & Record<string, unknown>;\n /** The resolved account slug from the workspace context (injected by MCP route) */\n accountSlug?: string;\n /** The resolved account ID (UUID) from the workspace context (injected by MCP route) */\n accountId?: string;\n /** The resolved source ID (UUID) for the current tenant/workspace */\n sourceId?: string | null;\n /** The user's resolved IANA timezone (e.g. 'America/New_York') */\n timezone?: string;\n /** Whether the authenticated user is a platform super-admin */\n isSuperAdmin?: boolean;\n /**\n * Who is executing this tool call.\n *\n * Undefined (the default for every user-facing MCP path) is treated as\n * `'user'` — the historical behavior. `'machine'` marks a non-interactive\n * caller (the Workflow Gateway) whose identity IS the account-bound gateway\n * token, not a signed-in session. See machineGrant below and the\n * \"Machine-principal design\" section of context/N8N_INTEGRATION_PLAN.md.\n */\n principal?: 'user' | 'machine';\n /**\n * Present only when `principal === 'machine'`. Carries the account the\n * gateway token is bound to (the sole source of account identity for a\n * machine call), the token row id, and the token creator for audit\n * attribution. `getAccountId` / `ensureAccountMembership` read this instead\n * of doing a DB membership lookup — the token itself is the authorization.\n */\n machineGrant?: {\n accountId: string;\n tokenId: string;\n createdBy: string | null;\n };\n /**\n * The host-built capability context\n * (docs/devex/capability-context-proposal.md), attached ADDITIVELY by the\n * MCP route after authorization. This is the CANONICAL `IntegrationContext`\n * from `@ekanos/integration-schema` — the same declaration `@ekanos/sdk`\n * re-exports, so there is no structural twin to drift and no core→SDK\n * package cycle. Migrated tools may still narrow it with\n * `requireContext(ctx)` from `@ekanos/sdk/context`; existing tools that\n * never touch this field are unaffected.\n */\n integrationContext?: IntegrationContext;\n};\n\nexport type ToolRunResult = unknown;\n\n/**\n * The partner execution channel — the ONLY way partner-authored tool code\n * runs (capability-context proposal §3 + sdk-export-map.md adversarial\n * review outcomes 1–2).\n *\n * The first argument is the host-built capability context — the canonical\n * `IntegrationContext` from `@ekanos/integration-schema`, the one declaration\n * both `@ekanos/sdk` (authoring) and this package (host) import. The\n * client-bearing `ToolContext` (req, admin supabase, identity fields) is\n * structurally absent from this signature — a partner tool cannot reach it.\n */\nexport type PartnerToolRun = (\n integrationContext: IntegrationContext,\n args: Record<string, unknown>,\n) => Promise<ToolRunResult>;\n\nexport type ToolModule = {\n name: string;\n description?: string;\n parameters?: ToolParamsSchema;\n run: (\n ctx: ToolContext,\n args: Record<string, unknown>,\n ) => Promise<ToolRunResult>;\n\n /**\n * Partner EXECUTION function. Set by host adapter code\n * (`registerPartnerIntegration`'s tool adapter) alongside the host BRAND\n * (`brandPartnerTool`, mcp/helpers.ts) — never by first-party tool modules.\n *\n * IMPORTANT: this property is NOT the partner classification authority\n * (that is the module-private WeakSet brand — a property can be forged,\n * stripped, or varied between reads). For a host-branded tool the executor:\n * - invokes `partnerRun(integrationContext, args)` with the host-built\n * capability context and NOTHING else (no ToolContext, no prepareArgs,\n * no toUi);\n * - FAILS CLOSED if the capability context or this function is missing —\n * there is no legacy-context fallback for a branded tool.\n *\n * An UNBRANDED object carrying this property is treated as first-party and\n * can never reach the partner path — so a partner cannot escape into, and a\n * spoofed property cannot fake, the capability-only channel.\n */\n partnerRun?: PartnerToolRun;\n\n // Optional advanced features\n // Compute/override arguments from context so the model doesn't need to supply them\n prepareArgs?: (\n ctx: ToolContext,\n rawArgs: unknown,\n ) => Promise<Record<string, unknown>>;\n\n // UI rendering support: declare components and map results to render instructions\n ui?: UiComponentMeta | UiComponentMeta[];\n toUi?: (\n ctx: ToolContext,\n result: unknown,\n ) => Promise<null | RenderInstruction | RenderInstruction[]>;\n\n /**\n * Side-effect classification — used by the SOC 2 audit log to decide\n * whether the call is recorded. When omitted, the audit layer derives\n * it from the tool name (read-verb prefixes → 'read', otherwise 'write').\n * Tool authors should set this explicitly when the heuristic would be\n * wrong (e.g. a `generate_*` tool that materially mutates server state).\n * See packages/agents/src/security/tool-effect.ts.\n */\n effect?: 'read' | 'write';\n\n /**\n * Data classification of what this tool exposes. Mirror of\n * `Sensitivity` in packages/agents/src/security/redact.ts — kept inline\n * here to avoid a backwards dependency from integrations-core onto agents.\n * Reads that expose 'pii' or 'financial' data are recorded in the audit\n * log even though they are not writes. Defaults to undefined (treated\n * as 'internal' by the audit filter, 'pii' by the redaction layer).\n */\n sensitivity?: 'public' | 'internal' | 'pii' | 'financial';\n\n /**\n * A REPRESENTATIVE example of this tool's SUCCESS return value (top-level\n * shape, minimal realistic values, arrays trimmed to one element). Consumed\n * by the workflow authoring layer two ways:\n * 1. Codegen grounding — the example is embedded in the workflow-generation\n * prompt so the model wires `{{stepId.field}}` refs against the tool's\n * REAL output keys instead of guessing (`deals` vs `contacts`,\n * `contact_id` vs `contactId` — observed failure class).\n * 2. Ref linting — a workflow ref into this tool's output whose first path\n * segment is not a key of this example is rejected at authoring time.\n * Keep it SMALL (it ships in prompts), faithful to the actual run() return,\n * and free of real customer data. Omitting it just skips both uses.\n */\n outputExample?: Record<string, unknown>;\n};\n\nexport type UiComponentMeta = {\n component: string;\n propsSchema: ToolParamsSchema;\n doc?: string;\n};\n\nexport type RenderInstruction = {\n component: string;\n props: unknown;\n};\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ekanos/sdk/testing — the partner test harness surface.
|
|
3
|
+
*
|
|
4
|
+
* `createMockContext()` is the day-1 in-memory `IntegrationContext`
|
|
5
|
+
* (proposal §4): partners code and test against `ctx` here before anything
|
|
6
|
+
* lands in a sandbox. Self-contained — no Supabase, no Vault, no network —
|
|
7
|
+
* and enforcement (storage schemas, secret tiers, egress) is the same
|
|
8
|
+
* shared implementation from `@ekanos/sdk/context` that production uses.
|
|
9
|
+
*
|
|
10
|
+
* Surface discipline: additions require an entry in
|
|
11
|
+
* docs/devex/sdk-export-map.md ("Context & testing entrypoints (P1)").
|
|
12
|
+
*/
|
|
13
|
+
export { createMockContext } from './mock-context.js';
|
|
14
|
+
export type { MockContextOptions, MockFetchHandler, MockIntegrationContext, RecordedFetchCall, RecordedLog, } from './mock-context.js';
|
|
15
|
+
export { invokeWebhook, invokeSchedule } from './invoke.js';
|
|
16
|
+
export type { DefinitionContextOptions, InvokeWebhookOptions, InvokeScheduleOptions, WebhookInvocationOutcome, ScheduleInvocationOutcome, } from './invoke.js';
|