@clawnify/connections 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/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @clawnify/connections
2
+
3
+ One declaration, one accessor for every credential a Clawnify app needs.
4
+
5
+ ```ts
6
+ import { connect, secret, describe } from "@clawnify/connections";
7
+
8
+ // OAuth or managed — app code is identical either way:
9
+ const accounts = await connect("metaads", env).get("/me/adaccounts", { fields: "name,id" });
10
+ const rows = await connect("googleads", env).query("SELECT customer.id FROM customer");
11
+
12
+ // Provider keys / secrets:
13
+ const key = secret("OPENROUTER_API_KEY", env);
14
+
15
+ // What's wired, for an agent to read before writing code:
16
+ const status = await describe(env, env.CLAWNIFY_ORG_ID, REQUIRES);
17
+ ```
18
+
19
+ `env` is your worker's `env` (it carries the `CREDENTIALS` binding and any
20
+ injected keys). Pass `orgId` explicitly or let it default to
21
+ `env.CLAWNIFY_ORG_ID`.
22
+
23
+ ## Why
24
+
25
+ Before this, every app hand-wrote its credential plumbing: which integration is
26
+ OAuth-direct vs. managed, the exact env var name for each key, the response
27
+ shape of each managed call. That knowledge belongs in **one** place. This SDK is
28
+ the accessor; [`@clawnify/integrations`](../integrations) is the descriptor
29
+ registry it reads. The broker that actually holds a credential is invisible to
30
+ your app — `connect()` routes to it; `describe()` never names it.
31
+
32
+ ## The registry is an enhancement, not a gate
33
+
34
+ A descriptor exists only to give an integration a **typed, ergonomic client**
35
+ (`connect("googleads").query(gaql)`). Functionality never depends on one:
36
+
37
+ - `connect("anything", env)` with no descriptor returns a `GenericClient` with
38
+ `token()` and `run(action, args)` — every integration supports those two
39
+ operations, so a toolkit **just added in the dashboard works immediately**.
40
+ - `secret(name, env)` reads any key by name — names come from the dashboard,
41
+ not from a release.
42
+ - `describe(env, org, requires)` reports unregistered integrations too
43
+ (connectivity + a generic accessor).
44
+
45
+ So you publish a new version of `@clawnify/integrations` **only when you want to
46
+ upgrade a specific integration from generic to first-class** — never just to
47
+ make a new integration usable. Adding the descriptor is opt-in sugar for the
48
+ handful of integrations where a typed client earns its keep.
49
+
50
+ ## API
51
+
52
+ | Call | Returns | Use for |
53
+ |------|---------|---------|
54
+ | `connect(service, env, orgId?)` | typed client (`ServiceClients[service]`) | OAuth / managed integrations |
55
+ | `secret(name, env)` | `string \| null` | provider keys & user secrets |
56
+ | `isConnected(service, env, orgId?)` | `Promise<boolean>` | gating a feature on a connection |
57
+ | `describe(env, orgId?, requires?)` | `Promise<DescribeEntry[]>` | agent-legible readiness snapshot |
58
+
59
+ ### Declaring requirements
60
+
61
+ In `clawnify.json`:
62
+
63
+ ```jsonc
64
+ {
65
+ "requires": [
66
+ { "service": "metaads", "as": "integration" },
67
+ { "service": "googleads", "as": "integration" },
68
+ { "name": "OPENROUTER_API_KEY", "as": "key" }
69
+ ]
70
+ }
71
+ ```
72
+
73
+ `requires` only **declares** — it never provisions. Connections and keys are
74
+ added in the Clawnify dashboard; when one is missing, `describe()` returns the
75
+ exact dashboard step in its `hint`.
76
+
77
+ ## Local dev
78
+
79
+ Two ways to run off-platform:
80
+
81
+ 1. **One org token (recommended).** Put your org's Clawnify token in `.dev.vars`:
82
+
83
+ ```
84
+ CLAWNIFY_TOKEN=clw_…
85
+ ```
86
+
87
+ With no in-process `CREDENTIALS` binding, the SDK resolves connections over
88
+ HTTP via the Clawnify API — `connect("googleads")`, `connect("metaads")`,
89
+ `describe()` all hit your **real** org connections, including managed ones.
90
+ The org is derived from the token server-side, so you don't set an org id and
91
+ you paste no per-service secrets. Override the API base with `CLAWNIFY_API_URL`.
92
+
93
+ 2. **Bare env tokens.** Without `CLAWNIFY_TOKEN`, the SDK falls back to
94
+ `METAADS_BEARER_TOKEN` / `OPENROUTER_API_KEY` / etc. from `.dev.vars`. Managed
95
+ integrations can't resolve this way and report not-ready.
96
+
97
+ In deployed apps neither is needed — Clawnify injects the in-process binding.
@@ -0,0 +1,129 @@
1
+ import { ConnectionKind, ServiceClients } from '@clawnify/integrations';
2
+ export * from '@clawnify/integrations';
3
+
4
+ /**
5
+ * @clawnify/connections — the accessor SDK.
6
+ *
7
+ * Three calls cover every credential an app needs:
8
+ *
9
+ * connect("googleads", env) → a typed client (oauth or managed alike)
10
+ * secret("OPENROUTER_API_KEY", env)→ an injected provider key / secret
11
+ * describe(env, orgId, requires) → agent-legible "what's wired" snapshot
12
+ *
13
+ * App code is identical whether a connection is managed or self-served, OAuth or
14
+ * API key, dynamic or static. The broker that actually holds a credential lives
15
+ * behind the Clawnify credentials binding and is never named here — `describe()`
16
+ * speaks only in capabilities (`oauth`/`managed`/`key`/`secret`).
17
+ */
18
+
19
+ /**
20
+ * The credentials broker RPC contract — the Clawnify `CREDENTIALS` service
21
+ * binding injected into every app. These method names are Clawnify-internal;
22
+ * the provider behind them (managed or self-configured) is not part of the
23
+ * contract.
24
+ */
25
+ interface CredentialBinding {
26
+ getToken(service: string, orgId: string): Promise<string | null>;
27
+ executeTool(service: string, toolSlug: string, args: Record<string, unknown>, orgId: string): Promise<{
28
+ data: unknown;
29
+ error: string | null;
30
+ successful: boolean;
31
+ }>;
32
+ listConnected(orgId: string): Promise<string[]>;
33
+ }
34
+ /** The slice of a worker's `env` the SDK reads. */
35
+ interface ConnectionsEnv {
36
+ /** The in-process credentials binding. Present in deployed apps. */
37
+ CREDENTIALS?: CredentialBinding;
38
+ /** The org the app is serving, injected by the Clawnify builder. */
39
+ CLAWNIFY_ORG_ID?: string;
40
+ /**
41
+ * A Clawnify token: either the OAuth bearer `clawnify login` holds, or a
42
+ * long-lived org service token (clw_…). When set and no in-process
43
+ * CREDENTIALS binding exists, the SDK resolves connections over HTTP via the
44
+ * Clawnify API — so local `pnpm dev` / scripts reach real org connections
45
+ * with no per-service secrets. With an OAuth bearer spanning several orgs,
46
+ * CLAWNIFY_ORG_ID picks one; a service token fixes the org itself.
47
+ */
48
+ CLAWNIFY_TOKEN?: string;
49
+ /** Override the Clawnify API base for the HTTP binding (defaults to prod). */
50
+ CLAWNIFY_API_URL?: string;
51
+ /** Injected provider keys / secrets / local-dev token fallbacks. */
52
+ [key: string]: unknown;
53
+ }
54
+ /**
55
+ * An HTTP-backed {@link CredentialBinding} for environments without the
56
+ * in-process binding (local `pnpm dev`, scripts, Claude Code).
57
+ *
58
+ * `token` is either a Clawnify OAuth bearer (the session `clawnify login`
59
+ * holds) or a long-lived org service token (clw_…). With an OAuth bearer that
60
+ * belongs to several orgs, pass `orgId` to target one (sent as `x-org-id`);
61
+ * with a service token the org is fixed and `orgId` is ignored. Managed
62
+ * credentials stay server-side — only `getToken` returns a bearer.
63
+ */
64
+ declare function clawnifyBinding(opts: {
65
+ token: string;
66
+ baseUrl?: string;
67
+ orgId?: string;
68
+ }): CredentialBinding;
69
+ /**
70
+ * Low-level client returned for any integration without a bespoke descriptor.
71
+ * Every integration supports these two operations, so a new dashboard
72
+ * integration is usable immediately — no descriptor, no package release.
73
+ */
74
+ interface GenericClient {
75
+ /** Current access token (OAuth / API key) for this integration. */
76
+ token(): Promise<string | null>;
77
+ /** Run a managed action scoped to the org; returns its data, throws on failure. */
78
+ run(action: string, args?: Record<string, unknown>): Promise<unknown>;
79
+ }
80
+ /**
81
+ * Get a client for an integration.
82
+ *
83
+ * - Registered integrations return their typed client — `connect("googleads",
84
+ * env)` is a GoogleAdsClient, no casting.
85
+ * - Any other (e.g. a toolkit just added in the dashboard) returns a
86
+ * {@link GenericClient} with `token()` + `run()`. The registry is an
87
+ * ergonomics layer, never a gate: functionality never depends on a release.
88
+ *
89
+ * @throws only when the service is a known key/secret — read those with secret().
90
+ */
91
+ declare function connect<K extends keyof ServiceClients>(service: K, env: ConnectionsEnv, orgId?: string): ServiceClients[K];
92
+ declare function connect(service: string, env: ConnectionsEnv, orgId?: string): GenericClient;
93
+ /** Read an injected provider key / secret env var. Returns null when absent. */
94
+ declare function secret(name: string, env: ConnectionsEnv): string | null;
95
+ /** True when a usable credential exists for this service right now. */
96
+ declare function isConnected(service: string, env: ConnectionsEnv, orgId?: string): Promise<boolean>;
97
+ /** One entry in clawnify.json's `requires` block. */
98
+ type RequireSpec = {
99
+ service: string;
100
+ as: "integration";
101
+ } | {
102
+ name: string;
103
+ as: "key" | "secret";
104
+ };
105
+ /** Per-capability readiness, written for an agent to read before wiring code. */
106
+ interface DescribeEntry {
107
+ /** Service id (integrations) or env var name (keys/secrets). */
108
+ id: string;
109
+ label: string;
110
+ kind: ConnectionKind;
111
+ /** Whether a credential is present for this capability. */
112
+ connected: boolean;
113
+ /** Whether app code can use it right now (same as connected today). */
114
+ ready: boolean;
115
+ /** One-line snippet showing how to access it in app code. */
116
+ accessor: string;
117
+ /** What to do when it isn't ready — always dashboard-driven. */
118
+ hint?: string;
119
+ }
120
+ /**
121
+ * Report what's wired for an org. Pass the app's `requires` to report exactly
122
+ * those capabilities (the agent-facing case); omit it to report every known
123
+ * integration's status for the org.
124
+ *
125
+ * Never names the underlying broker — only `kind` and readiness.
126
+ */
127
+ declare function describe(env: ConnectionsEnv, orgId?: string, requires?: RequireSpec[]): Promise<DescribeEntry[]>;
128
+
129
+ export { type ConnectionsEnv, type CredentialBinding, type DescribeEntry, type GenericClient, type RequireSpec, clawnifyBinding, connect, describe, isConnected, secret };
package/dist/index.js ADDED
@@ -0,0 +1,155 @@
1
+ // src/index.ts
2
+ import {
3
+ getDescriptor,
4
+ allDescriptors
5
+ } from "@clawnify/integrations";
6
+ export * from "@clawnify/integrations";
7
+ var DEFAULT_API_BASE = "https://provision.clawnify.com";
8
+ function clawnifyBinding(opts) {
9
+ const base = (opts.baseUrl ?? DEFAULT_API_BASE).replace(/\/+$/, "");
10
+ const headers = {
11
+ Authorization: `Bearer ${opts.token}`,
12
+ "Content-Type": "application/json",
13
+ ...opts.orgId ? { "x-org-id": opts.orgId } : {}
14
+ };
15
+ return {
16
+ async getToken(service) {
17
+ const res = await fetch(`${base}/v1/connections/token`, {
18
+ method: "POST",
19
+ headers,
20
+ body: JSON.stringify({ service })
21
+ });
22
+ if (!res.ok) return null;
23
+ const j = await res.json();
24
+ return j.token ?? null;
25
+ },
26
+ async executeTool(service, toolSlug, args) {
27
+ const res = await fetch(`${base}/v1/connections/execute`, {
28
+ method: "POST",
29
+ headers,
30
+ body: JSON.stringify({ service, action: toolSlug, args })
31
+ });
32
+ const j = await res.json().catch(() => ({}));
33
+ if (!res.ok) return { data: null, error: j.error ?? `connections ${res.status}`, successful: false };
34
+ return { data: j.data ?? null, error: j.error ?? null, successful: j.successful ?? true };
35
+ },
36
+ async listConnected() {
37
+ const res = await fetch(`${base}/v1/connections/connected`, { headers });
38
+ if (!res.ok) return [];
39
+ const j = await res.json();
40
+ return j.services ?? [];
41
+ }
42
+ };
43
+ }
44
+ function resolveBinding(env) {
45
+ if (env.CREDENTIALS) return { binding: env.CREDENTIALS, remote: false };
46
+ const token = typeof env.CLAWNIFY_TOKEN === "string" ? env.CLAWNIFY_TOKEN : "";
47
+ if (token) {
48
+ const baseUrl = typeof env.CLAWNIFY_API_URL === "string" ? env.CLAWNIFY_API_URL : void 0;
49
+ const orgId = typeof env.CLAWNIFY_ORG_ID === "string" ? env.CLAWNIFY_ORG_ID : void 0;
50
+ return { binding: clawnifyBinding({ token, baseUrl, orgId }), remote: true };
51
+ }
52
+ return { remote: false };
53
+ }
54
+ function resolveOrg(env, orgId) {
55
+ return orgId ?? env.CLAWNIFY_ORG_ID ?? null;
56
+ }
57
+ function buildContext(service, env, orgId) {
58
+ const { binding, remote } = resolveBinding(env);
59
+ const canBroker = !!binding && (remote || !!orgId);
60
+ return {
61
+ orgId,
62
+ managed: canBroker,
63
+ env,
64
+ async token() {
65
+ if (binding && canBroker) {
66
+ try {
67
+ return await binding.getToken(service, orgId ?? "");
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ const up = service.toUpperCase();
73
+ const local = env[`${up}_BEARER_TOKEN`] ?? env[`${up}_ACCESS_TOKEN`];
74
+ return typeof local === "string" ? local : null;
75
+ },
76
+ async run(action, args = {}) {
77
+ if (!binding || !canBroker) {
78
+ throw new Error(`"${service}" needs the Clawnify credentials binding or CLAWNIFY_TOKEN`);
79
+ }
80
+ const r = await binding.executeTool(service, action, args, orgId ?? "");
81
+ if (!r.successful) throw new Error(r.error ?? `${service} action "${action}" failed`);
82
+ return r.data;
83
+ }
84
+ };
85
+ }
86
+ function connect(service, env, orgId) {
87
+ const ctx = buildContext(service, env, resolveOrg(env, orgId));
88
+ const desc = getDescriptor(service);
89
+ if (desc?.client) return desc.client(ctx);
90
+ if (desc && (desc.kind === "key" || desc.kind === "secret")) {
91
+ throw new Error(`"${service}" is a ${desc.kind}; read it with secret("${desc.envName ?? service}")`);
92
+ }
93
+ return { token: ctx.token, run: ctx.run };
94
+ }
95
+ function secret(name, env) {
96
+ const v = env[name];
97
+ return typeof v === "string" && v.length > 0 ? v : null;
98
+ }
99
+ async function isConnected(service, env, orgId) {
100
+ const desc = getDescriptor(service);
101
+ if (!desc) return false;
102
+ const org = resolveOrg(env, orgId);
103
+ if (desc.kind === "key" || desc.kind === "secret") {
104
+ return !!secret(desc.envName ?? service, env);
105
+ }
106
+ const { binding, remote } = resolveBinding(env);
107
+ if (binding && (remote || org)) {
108
+ try {
109
+ if ((await binding.listConnected(org ?? "")).includes(service)) return true;
110
+ } catch {
111
+ }
112
+ }
113
+ return !!await buildContext(service, env, org).token();
114
+ }
115
+ async function describe(env, orgId, requires) {
116
+ const org = resolveOrg(env, orgId);
117
+ const fromDescriptor = (d) => {
118
+ const isKey = d.kind === "key" || d.kind === "secret";
119
+ return { id: isKey ? d.envName ?? d.service : d.service, label: d.label, kind: d.kind, accessor: d.accessor, isKey };
120
+ };
121
+ let items;
122
+ if (requires?.length) {
123
+ items = requires.map((r) => {
124
+ if ("name" in r) {
125
+ const d2 = getDescriptor(r.name);
126
+ return d2 ? fromDescriptor(d2) : { id: r.name, label: r.name, kind: r.as, accessor: `secret("${r.name}")`, isKey: true };
127
+ }
128
+ const d = getDescriptor(r.service);
129
+ return d ? fromDescriptor(d) : { id: r.service, label: r.service, kind: "managed", accessor: `connect("${r.service}")`, isKey: false };
130
+ });
131
+ } else {
132
+ items = allDescriptors().map(fromDescriptor);
133
+ }
134
+ let connectedList = [];
135
+ const { binding, remote } = resolveBinding(env);
136
+ if (binding && (remote || org) && items.some((i) => !i.isKey)) {
137
+ try {
138
+ connectedList = await binding.listConnected(org ?? "");
139
+ } catch {
140
+ }
141
+ }
142
+ return items.map((i) => {
143
+ const has = i.isKey ? !!secret(i.id, env) : connectedList.includes(i.id);
144
+ const hint = has ? void 0 : i.isKey ? `Add ${i.id} in the dashboard \u2192 API Keys / Environment Variables.` : `Connect ${i.label} in the dashboard \u2192 Integrations.`;
145
+ return { id: i.id, label: i.label, kind: i.kind, connected: has, ready: has, accessor: i.accessor, hint };
146
+ });
147
+ }
148
+ export {
149
+ clawnifyBinding,
150
+ connect,
151
+ describe,
152
+ isConnected,
153
+ secret
154
+ };
155
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @clawnify/connections — the accessor SDK.\n *\n * Three calls cover every credential an app needs:\n *\n * connect(\"googleads\", env) → a typed client (oauth or managed alike)\n * secret(\"OPENROUTER_API_KEY\", env)→ an injected provider key / secret\n * describe(env, orgId, requires) → agent-legible \"what's wired\" snapshot\n *\n * App code is identical whether a connection is managed or self-served, OAuth or\n * API key, dynamic or static. The broker that actually holds a credential lives\n * behind the Clawnify credentials binding and is never named here — `describe()`\n * speaks only in capabilities (`oauth`/`managed`/`key`/`secret`).\n */\n\nimport {\n getDescriptor,\n allDescriptors,\n type ServiceClients,\n type ConnectionDescriptor,\n type ConnectionKind,\n type AccessContext,\n} from \"@clawnify/integrations\";\n\n// Re-export the descriptor layer so consumers get client types (MetaAdsClient,\n// GoogleAdsRow, …) and descriptor types from one import.\nexport * from \"@clawnify/integrations\";\n\n/**\n * The credentials broker RPC contract — the Clawnify `CREDENTIALS` service\n * binding injected into every app. These method names are Clawnify-internal;\n * the provider behind them (managed or self-configured) is not part of the\n * contract.\n */\nexport interface CredentialBinding {\n getToken(service: string, orgId: string): Promise<string | null>;\n executeTool(\n service: string,\n toolSlug: string,\n args: Record<string, unknown>,\n orgId: string,\n ): Promise<{ data: unknown; error: string | null; successful: boolean }>;\n listConnected(orgId: string): Promise<string[]>;\n}\n\n/** The slice of a worker's `env` the SDK reads. */\nexport interface ConnectionsEnv {\n /** The in-process credentials binding. Present in deployed apps. */\n CREDENTIALS?: CredentialBinding;\n /** The org the app is serving, injected by the Clawnify builder. */\n CLAWNIFY_ORG_ID?: string;\n /**\n * A Clawnify token: either the OAuth bearer `clawnify login` holds, or a\n * long-lived org service token (clw_…). When set and no in-process\n * CREDENTIALS binding exists, the SDK resolves connections over HTTP via the\n * Clawnify API — so local `pnpm dev` / scripts reach real org connections\n * with no per-service secrets. With an OAuth bearer spanning several orgs,\n * CLAWNIFY_ORG_ID picks one; a service token fixes the org itself.\n */\n CLAWNIFY_TOKEN?: string;\n /** Override the Clawnify API base for the HTTP binding (defaults to prod). */\n CLAWNIFY_API_URL?: string;\n /** Injected provider keys / secrets / local-dev token fallbacks. */\n [key: string]: unknown;\n}\n\nconst DEFAULT_API_BASE = \"https://provision.clawnify.com\";\n\n/**\n * An HTTP-backed {@link CredentialBinding} for environments without the\n * in-process binding (local `pnpm dev`, scripts, Claude Code).\n *\n * `token` is either a Clawnify OAuth bearer (the session `clawnify login`\n * holds) or a long-lived org service token (clw_…). With an OAuth bearer that\n * belongs to several orgs, pass `orgId` to target one (sent as `x-org-id`);\n * with a service token the org is fixed and `orgId` is ignored. Managed\n * credentials stay server-side — only `getToken` returns a bearer.\n */\nexport function clawnifyBinding(opts: { token: string; baseUrl?: string; orgId?: string }): CredentialBinding {\n const base = (opts.baseUrl ?? DEFAULT_API_BASE).replace(/\\/+$/, \"\");\n const headers: Record<string, string> = {\n Authorization: `Bearer ${opts.token}`,\n \"Content-Type\": \"application/json\",\n ...(opts.orgId ? { \"x-org-id\": opts.orgId } : {}),\n };\n return {\n async getToken(service) {\n const res = await fetch(`${base}/v1/connections/token`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ service }),\n });\n if (!res.ok) return null;\n const j = (await res.json()) as { token?: string | null };\n return j.token ?? null;\n },\n async executeTool(service, toolSlug, args) {\n const res = await fetch(`${base}/v1/connections/execute`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ service, action: toolSlug, args }),\n });\n const j = (await res.json().catch(() => ({}))) as {\n data?: unknown;\n error?: string | null;\n successful?: boolean;\n };\n if (!res.ok) return { data: null, error: j.error ?? `connections ${res.status}`, successful: false };\n return { data: j.data ?? null, error: j.error ?? null, successful: j.successful ?? true };\n },\n async listConnected() {\n const res = await fetch(`${base}/v1/connections/connected`, { headers });\n if (!res.ok) return [];\n const j = (await res.json()) as { services?: string[] };\n return j.services ?? [];\n },\n };\n}\n\n/**\n * The binding to use this request: the in-process one if present, else an HTTP\n * binding built from CLAWNIFY_TOKEN. `remote` binding resolves the org from the\n * token, so it works without a local org id.\n */\nfunction resolveBinding(env: ConnectionsEnv): { binding?: CredentialBinding; remote: boolean } {\n if (env.CREDENTIALS) return { binding: env.CREDENTIALS, remote: false };\n const token = typeof env.CLAWNIFY_TOKEN === \"string\" ? env.CLAWNIFY_TOKEN : \"\";\n if (token) {\n const baseUrl = typeof env.CLAWNIFY_API_URL === \"string\" ? env.CLAWNIFY_API_URL : undefined;\n const orgId = typeof env.CLAWNIFY_ORG_ID === \"string\" ? env.CLAWNIFY_ORG_ID : undefined;\n return { binding: clawnifyBinding({ token, baseUrl, orgId }), remote: true };\n }\n return { remote: false };\n}\n\nfunction resolveOrg(env: ConnectionsEnv, orgId?: string): string | null {\n return orgId ?? env.CLAWNIFY_ORG_ID ?? null;\n}\n\nfunction buildContext(service: string, env: ConnectionsEnv, orgId: string | null): AccessContext {\n const { binding, remote } = resolveBinding(env);\n // A remote binding derives the org from the token, so it works without a\n // local org id; an in-process binding needs one.\n const canBroker = !!binding && (remote || !!orgId);\n return {\n orgId,\n managed: canBroker,\n env: env as Record<string, string | undefined>,\n async token() {\n if (binding && canBroker) {\n try {\n return await binding.getToken(service, orgId ?? \"\");\n } catch {\n return null;\n }\n }\n // Local-dev fallback: a bare token in env, e.g. METAADS_BEARER_TOKEN.\n const up = service.toUpperCase();\n const local = env[`${up}_BEARER_TOKEN`] ?? env[`${up}_ACCESS_TOKEN`];\n return typeof local === \"string\" ? local : null;\n },\n async run(action, args = {}) {\n if (!binding || !canBroker) {\n throw new Error(`\"${service}\" needs the Clawnify credentials binding or CLAWNIFY_TOKEN`);\n }\n const r = await binding.executeTool(service, action, args, orgId ?? \"\");\n if (!r.successful) throw new Error(r.error ?? `${service} action \"${action}\" failed`);\n return r.data;\n },\n };\n}\n\n/**\n * Low-level client returned for any integration without a bespoke descriptor.\n * Every integration supports these two operations, so a new dashboard\n * integration is usable immediately — no descriptor, no package release.\n */\nexport interface GenericClient {\n /** Current access token (OAuth / API key) for this integration. */\n token(): Promise<string | null>;\n /** Run a managed action scoped to the org; returns its data, throws on failure. */\n run(action: string, args?: Record<string, unknown>): Promise<unknown>;\n}\n\n/**\n * Get a client for an integration.\n *\n * - Registered integrations return their typed client — `connect(\"googleads\",\n * env)` is a GoogleAdsClient, no casting.\n * - Any other (e.g. a toolkit just added in the dashboard) returns a\n * {@link GenericClient} with `token()` + `run()`. The registry is an\n * ergonomics layer, never a gate: functionality never depends on a release.\n *\n * @throws only when the service is a known key/secret — read those with secret().\n */\nexport function connect<K extends keyof ServiceClients>(service: K, env: ConnectionsEnv, orgId?: string): ServiceClients[K];\nexport function connect(service: string, env: ConnectionsEnv, orgId?: string): GenericClient;\nexport function connect(service: string, env: ConnectionsEnv, orgId?: string): unknown {\n const ctx = buildContext(service, env, resolveOrg(env, orgId));\n const desc = getDescriptor(service);\n if (desc?.client) return desc.client(ctx);\n if (desc && (desc.kind === \"key\" || desc.kind === \"secret\")) {\n throw new Error(`\"${service}\" is a ${desc.kind}; read it with secret(\"${desc.envName ?? service}\")`);\n }\n return { token: ctx.token, run: ctx.run } satisfies GenericClient;\n}\n\n/** Read an injected provider key / secret env var. Returns null when absent. */\nexport function secret(name: string, env: ConnectionsEnv): string | null {\n const v = env[name];\n return typeof v === \"string\" && v.length > 0 ? v : null;\n}\n\n/** True when a usable credential exists for this service right now. */\nexport async function isConnected(service: string, env: ConnectionsEnv, orgId?: string): Promise<boolean> {\n const desc = getDescriptor(service);\n if (!desc) return false;\n const org = resolveOrg(env, orgId);\n if (desc.kind === \"key\" || desc.kind === \"secret\") {\n return !!secret(desc.envName ?? service, env);\n }\n const { binding, remote } = resolveBinding(env);\n if (binding && (remote || org)) {\n try {\n if ((await binding.listConnected(org ?? \"\")).includes(service)) return true;\n } catch {\n /* fall through to token probe */\n }\n }\n return !!(await buildContext(service, env, org).token());\n}\n\n/** One entry in clawnify.json's `requires` block. */\nexport type RequireSpec =\n | { service: string; as: \"integration\" }\n | { name: string; as: \"key\" | \"secret\" };\n\n/** Per-capability readiness, written for an agent to read before wiring code. */\nexport interface DescribeEntry {\n /** Service id (integrations) or env var name (keys/secrets). */\n id: string;\n label: string;\n kind: ConnectionKind;\n /** Whether a credential is present for this capability. */\n connected: boolean;\n /** Whether app code can use it right now (same as connected today). */\n ready: boolean;\n /** One-line snippet showing how to access it in app code. */\n accessor: string;\n /** What to do when it isn't ready — always dashboard-driven. */\n hint?: string;\n}\n\n/**\n * Report what's wired for an org. Pass the app's `requires` to report exactly\n * those capabilities (the agent-facing case); omit it to report every known\n * integration's status for the org.\n *\n * Never names the underlying broker — only `kind` and readiness.\n */\nexport async function describe(\n env: ConnectionsEnv,\n orgId?: string,\n requires?: RequireSpec[],\n): Promise<DescribeEntry[]> {\n const org = resolveOrg(env, orgId);\n\n // Normalize the work list. A required service with no descriptor is reported\n // generically (still fully usable via connect()'s GenericClient) — the\n // registry enriches, it never gates.\n type Item = { id: string; label: string; kind: ConnectionKind; accessor: string; isKey: boolean };\n const fromDescriptor = (d: ConnectionDescriptor): Item => {\n const isKey = d.kind === \"key\" || d.kind === \"secret\";\n return { id: isKey ? d.envName ?? d.service : d.service, label: d.label, kind: d.kind, accessor: d.accessor, isKey };\n };\n\n let items: Item[];\n if (requires?.length) {\n items = requires.map((r) => {\n if (\"name\" in r) {\n const d = getDescriptor(r.name);\n return d ? fromDescriptor(d) : { id: r.name, label: r.name, kind: r.as, accessor: `secret(\"${r.name}\")`, isKey: true };\n }\n const d = getDescriptor(r.service);\n // Unknown integration: kind is genuinely unknown without a descriptor;\n // \"managed\" is the broadest accessor (token() + run() both work).\n return d ? fromDescriptor(d) : { id: r.service, label: r.service, kind: \"managed\" as ConnectionKind, accessor: `connect(\"${r.service}\")`, isKey: false };\n });\n } else {\n items = allDescriptors().map(fromDescriptor);\n }\n\n // One broker round-trip for all integration kinds.\n let connectedList: string[] = [];\n const { binding, remote } = resolveBinding(env);\n if (binding && (remote || org) && items.some((i) => !i.isKey)) {\n try {\n connectedList = await binding.listConnected(org ?? \"\");\n } catch {\n /* leave empty — everything reports not-connected */\n }\n }\n\n return items.map((i) => {\n const has = i.isKey ? !!secret(i.id, env) : connectedList.includes(i.id);\n const hint = has\n ? undefined\n : i.isKey\n ? `Add ${i.id} in the dashboard → API Keys / Environment Variables.`\n : `Connect ${i.label} in the dashboard → Integrations.`;\n return { id: i.id, label: i.label, kind: i.kind, connected: has, ready: has, accessor: i.accessor, hint };\n });\n}\n"],"mappings":"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","names":["d"]}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@clawnify/connections",
3
+ "version": "0.1.0",
4
+ "description": "One-declaration, one-accessor credentials SDK for Clawnify apps. connect(service) returns a typed client, secret(name) reads an injected key, and describe(org) tells an agent what's wired — all over the Clawnify credentials binding, with the broker kept invisible.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "dependencies": {
21
+ "@clawnify/integrations": "0.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "@cloudflare/workers-types": "^4.20260405.1",
25
+ "tsup": "^8.0.0",
26
+ "typescript": "^5.7.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "dev": "tsup --watch"
31
+ }
32
+ }