@keemakr/operator-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/package.json +33 -0
- package/src/index.ts +173 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keemakr/operator-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The keemakr connector contract: defineProvider + ConnectionProvider/ConnectionOperation types, shared by keemakr-core's built-in adapters and external connector repos. One shape, two consumers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"zod": "^4.4.3"
|
|
19
|
+
},
|
|
20
|
+
"peerDependenciesMeta": {
|
|
21
|
+
"zod": {
|
|
22
|
+
"optional": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/fsztpartners/keemakr.git",
|
|
31
|
+
"directory": "packages/operator-sdk"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// @keemakr/operator-sdk — the connector contract.
|
|
2
|
+
//
|
|
3
|
+
// The canonical home of the ConnectionProvider seam. keemakr-core's built-in
|
|
4
|
+
// adapters and external connector repos import the exact same symbols from this
|
|
5
|
+
// package. One shape, one validator, two consumers.
|
|
6
|
+
//
|
|
7
|
+
// Each connectable provider ships one adapter implementing ConnectionProvider.
|
|
8
|
+
// The adapter is the source of truth for the provider's metadata (synced into
|
|
9
|
+
// connections_registry) and for its auth lifecycle (build auth URL, exchange
|
|
10
|
+
// code, refresh, validate a credential, derive an account label).
|
|
11
|
+
|
|
12
|
+
export type AuthKind = 'oauth2' | 'api_key' | 'app_password';
|
|
13
|
+
|
|
14
|
+
// How proven the adapter's connect path is.
|
|
15
|
+
// 'ready' → the connect + validate path is tested and works; the user
|
|
16
|
+
// only needs to supply their own API key/credential.
|
|
17
|
+
// 'coming_soon' → code-complete but the live connect round-trip is NOT proven
|
|
18
|
+
// (OAuth-broker providers without a test app, token-paste
|
|
19
|
+
// stand-ins for a not-yet-wired OAuth flow, …). The UI badges
|
|
20
|
+
// these "Coming soon" and disables Connect.
|
|
21
|
+
// Omitted on an adapter ⇒ treated as 'ready' (the registry column also defaults
|
|
22
|
+
// to 'ready'), so legacy adapters stay connectable until told otherwise.
|
|
23
|
+
export type Maturity = 'ready' | 'coming_soon';
|
|
24
|
+
|
|
25
|
+
export interface TokenSet {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
refreshToken?: string | null;
|
|
28
|
+
scope?: string | null;
|
|
29
|
+
expiresAt?: string | null; // ISO 8601
|
|
30
|
+
meta?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TestResult {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
detail?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A single field in an inline connect form (api_key / app_password adapters).
|
|
39
|
+
// The UI renders these generically; the adapter owns the spec so the form and
|
|
40
|
+
// the route's expectations never drift. `target` says where the value goes in
|
|
41
|
+
// the POST body:
|
|
42
|
+
// 'access_token' → body.access_token (e.g. PostHog project key)
|
|
43
|
+
// 'username' → body.username (self-hosted WordPress)
|
|
44
|
+
// 'application_password'→ body.application_password (self-hosted WordPress)
|
|
45
|
+
// 'meta.<key>' → body.meta[<key>] (e.g. meta.host, meta.site_url)
|
|
46
|
+
export interface ConnectField {
|
|
47
|
+
target: string;
|
|
48
|
+
label: string;
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
secret?: boolean; // render as type=password
|
|
51
|
+
optional?: boolean;
|
|
52
|
+
help?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ConnectionProvider {
|
|
56
|
+
provider: string;
|
|
57
|
+
displayName: string;
|
|
58
|
+
authKind: AuthKind;
|
|
59
|
+
category?: string;
|
|
60
|
+
scopes?: string[];
|
|
61
|
+
icon?: string;
|
|
62
|
+
docUrl?: string;
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
// Connect-path maturity. Omitted ⇒ 'ready'. See Maturity above.
|
|
65
|
+
maturity?: Maturity;
|
|
66
|
+
|
|
67
|
+
// Inline connect-form spec for api_key / app_password shapes. Omitted by pure
|
|
68
|
+
// oauth2 adapters (they render a Connect button instead). For a dual-mode
|
|
69
|
+
// provider (WordPress), this describes the NON-oauth fallback form; the UI
|
|
70
|
+
// also offers the oauth2 button.
|
|
71
|
+
//
|
|
72
|
+
// This is the SINGLE source of truth for the connect form — the UI fetches it
|
|
73
|
+
// from the catalog (GET /api/connections), so there is no second place to
|
|
74
|
+
// declare fields. Add a field here and it renders; nothing else to touch.
|
|
75
|
+
connectFields?: ConnectField[];
|
|
76
|
+
|
|
77
|
+
// Dual-mode = authKind:'oauth2' as the PRIMARY shape, but ALSO offers the
|
|
78
|
+
// connectFields form as a fallback (WordPress: .com OAuth OR self-hosted
|
|
79
|
+
// app-password). When true, the connect panel renders both the OAuth button
|
|
80
|
+
// AND the inline form. A form posted for a dual-mode provider carries
|
|
81
|
+
// meta.mode='<fallbackMode>' so the route/adapter know which branch to take.
|
|
82
|
+
dualMode?: { fallbackMode: string; oauthLabel?: string; formLabel?: string };
|
|
83
|
+
|
|
84
|
+
// OAuth-only lifecycle. Omitted by api_key / app_password adapters.
|
|
85
|
+
buildAuthUrl?(args: { tenantId: string; state: string; callbackUrl: string }): string;
|
|
86
|
+
exchangeCode?(args: { code: string; callbackUrl: string }): Promise<TokenSet>;
|
|
87
|
+
refresh?(args: { refreshToken: string }): Promise<TokenSet>;
|
|
88
|
+
|
|
89
|
+
// All kinds — validate a credential at save time.
|
|
90
|
+
test(args: { accessToken: string; meta: Record<string, unknown> }): Promise<TestResult>;
|
|
91
|
+
|
|
92
|
+
// Derive a human-readable account_label + durable handle
|
|
93
|
+
// (stripe_user_id, store url, ...) to persist alongside the token.
|
|
94
|
+
extractMeta?(
|
|
95
|
+
token: TokenSet,
|
|
96
|
+
): Promise<{ accountLabel: string | null; meta: Record<string, unknown> }>;
|
|
97
|
+
|
|
98
|
+
// Named proxy operations a marketplace agent may invoke through the Capability
|
|
99
|
+
// API (L11). Co-located with the credential logic here so the platform owns
|
|
100
|
+
// them; agents call by name and never see the credential. Each operation:
|
|
101
|
+
// inputSchema validates the args the agent sends (zod or JSON-schema-ish);
|
|
102
|
+
// call(cred, args) runs the third-party request SERVER-SIDE with the
|
|
103
|
+
// resolved credential and returns the RESULT (never the
|
|
104
|
+
// credential). Throw OperationError for a typed failure.
|
|
105
|
+
// Omitted ⇒ the provider exposes no proxy operations (proxy calls 404).
|
|
106
|
+
operations?: Record<string, ConnectionOperation>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// One named, credential-backed proxy operation. `cred` is the resolved
|
|
110
|
+
// connection (access token + meta); the platform passes it in — the agent never
|
|
111
|
+
// sees it. Keep `call` pure-ish: do the request, return JSON-serializable data.
|
|
112
|
+
export interface ConnectionOperation {
|
|
113
|
+
description?: string;
|
|
114
|
+
// A Standard Schema (zod/valibot/…) validating the agent-supplied args. Typed
|
|
115
|
+
// loosely here so the connector contract doesn't hard-depend on zod; adapters
|
|
116
|
+
// pass a zod schema and the route validates with `.parse`/`.safeParse`.
|
|
117
|
+
inputSchema: {
|
|
118
|
+
safeParse(input: unknown): { success: boolean; data?: unknown; error?: unknown };
|
|
119
|
+
};
|
|
120
|
+
call(
|
|
121
|
+
cred: { accessToken: string; meta: Record<string, unknown> },
|
|
122
|
+
args: Record<string, unknown>,
|
|
123
|
+
): Promise<unknown>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Throw from a ConnectionOperation.call to surface a typed, agent-safe failure
|
|
127
|
+
// (maps to a 4xx with a clean message — never leaks the credential).
|
|
128
|
+
export class OperationError extends Error {
|
|
129
|
+
status: number;
|
|
130
|
+
constructor(message: string, status = 400) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.name = 'OperationError';
|
|
133
|
+
this.status = status;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Authoring helper ────────────────────────────────────────────────────────
|
|
138
|
+
//
|
|
139
|
+
// Define a provider adapter with this instead of a bare object literal. It is an
|
|
140
|
+
// identity function (returns its argument unchanged) whose only job is to give
|
|
141
|
+
// you full TypeScript checking + editor autocomplete on the ConnectionProvider
|
|
142
|
+
// shape, and to fail fast on a few mistakes the type system can't catch:
|
|
143
|
+
//
|
|
144
|
+
// - a non-oauth2 adapter that forgot connectFields (its form would be a single
|
|
145
|
+
// bare "API key" box — usually a bug)
|
|
146
|
+
// - an oauth2 adapter missing buildAuthUrl/exchangeCode (its Connect button
|
|
147
|
+
// would 500 at the broker)
|
|
148
|
+
// - a provider key that isn't lowercase-kebab (must match the URL segment and
|
|
149
|
+
// the connections_registry row)
|
|
150
|
+
//
|
|
151
|
+
// Usage: see keemakr-core's lib/connections/providers/_template.ts.
|
|
152
|
+
export function defineProvider(p: ConnectionProvider): ConnectionProvider {
|
|
153
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(p.provider)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`connections: provider key '${p.provider}' must be lowercase-kebab (e.g. 'mailchimp', 'google-ads')`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (p.authKind === 'oauth2') {
|
|
159
|
+
if (typeof p.buildAuthUrl !== 'function' || typeof p.exchangeCode !== 'function') {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`connections: oauth2 provider '${p.provider}' must implement buildAuthUrl() and exchangeCode()`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
} else if (!p.connectFields || p.connectFields.length === 0) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`connections: ${p.authKind} provider '${p.provider}' must declare connectFields (the inline connect form)`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (typeof p.test !== 'function') {
|
|
170
|
+
throw new Error(`connections: provider '${p.provider}' must implement test()`);
|
|
171
|
+
}
|
|
172
|
+
return p;
|
|
173
|
+
}
|