@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.
Files changed (2) hide show
  1. package/package.json +33 -0
  2. 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
+ }