@shaferllc/keel 0.83.2 → 0.83.4
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 +7 -24
- package/dist/billing/billable.d.ts +1 -0
- package/dist/billing/billable.js +8 -0
- package/dist/billing/drivers/fake.d.ts +4 -0
- package/dist/billing/drivers/fake.js +5 -0
- package/dist/billing/drivers/stripe.d.ts +1 -0
- package/dist/billing/drivers/stripe.js +7 -0
- package/dist/billing/gateway.d.ts +7 -0
- package/dist/billing/index.d.ts +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/tokens.d.ts +2 -0
- package/dist/core/tokens.js +24 -0
- package/dist/gates/index.d.ts +10 -0
- package/dist/gates/index.js +9 -0
- package/dist/gates/models.d.ts +42 -0
- package/dist/gates/models.js +76 -0
- package/dist/gates/provider.d.ts +10 -0
- package/dist/gates/provider.js +13 -0
- package/dist/hosting/cloudflare.d.ts +43 -0
- package/dist/hosting/cloudflare.js +66 -0
- package/dist/hosting/dump.d.ts +7 -0
- package/dist/hosting/dump.js +58 -0
- package/dist/hosting/hostname.d.ts +7 -0
- package/dist/hosting/hostname.js +25 -0
- package/dist/hosting/index.d.ts +14 -0
- package/dist/hosting/index.js +12 -0
- package/dist/hosting/secrets.d.ts +15 -0
- package/dist/hosting/secrets.js +27 -0
- package/dist/mcp/cloud.d.ts +9 -0
- package/dist/mcp/cloud.js +179 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +20 -1
- package/docs/ai-manifest.json +20 -1
- package/docs/ai.md +36 -0
- package/docs/billing.md +10 -1
- package/docs/changelog.md +30 -0
- package/docs/examples/gates.ts +30 -0
- package/docs/examples/hosting.ts +37 -0
- package/docs/gates.md +75 -0
- package/docs/hosting.md +97 -0
- package/llms-full.txt +232 -1
- package/llms.txt +4 -0
- package/package.json +9 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypted key/value secrets vault — purpose-scoped encryption via Keel core.
|
|
3
|
+
*
|
|
4
|
+
* Apps supply a table of rows with `owner_id`, `key`, `value_encrypted`.
|
|
5
|
+
* This helper doesn't own the model; it encrypts/decrypts values.
|
|
6
|
+
*/
|
|
7
|
+
import { encryption } from "../core/crypto.js";
|
|
8
|
+
export function normalizeSecretKey(key) {
|
|
9
|
+
return key.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
10
|
+
}
|
|
11
|
+
export async function encryptSecretValue(value, purpose = "app-secret") {
|
|
12
|
+
return encryption.encrypt(value, { purpose });
|
|
13
|
+
}
|
|
14
|
+
export async function decryptSecretValue(payload, purpose = "app-secret") {
|
|
15
|
+
const value = await encryption.decrypt(payload, { purpose });
|
|
16
|
+
return typeof value === "string" ? value : null;
|
|
17
|
+
}
|
|
18
|
+
/** Decrypt a list of encrypted rows into a plain key→value map. */
|
|
19
|
+
export async function resolveSecretRows(rows, purpose = "app-secret") {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
const value = await decryptSecretValue(row.value_encrypted, purpose);
|
|
23
|
+
if (value !== null)
|
|
24
|
+
out[row.key] = value;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keel Cloud MCP tools — registered only when KEEL_CLOUD_TOKEN is set.
|
|
3
|
+
*
|
|
4
|
+
* Talks to a Keel Cloud control plane over HTTP (Bearer token). Agents use these
|
|
5
|
+
* to create sites, preview/publish, and manage secrets without leaving the IDE.
|
|
6
|
+
*/
|
|
7
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
/** Register Cloud tools on an existing MCP server when a Cloud token is present. */
|
|
9
|
+
export declare function registerCloudTools(server: McpServer): boolean;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keel Cloud MCP tools — registered only when KEEL_CLOUD_TOKEN is set.
|
|
3
|
+
*
|
|
4
|
+
* Talks to a Keel Cloud control plane over HTTP (Bearer token). Agents use these
|
|
5
|
+
* to create sites, preview/publish, and manage secrets without leaving the IDE.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
const PRESETS = ["minimal", "api", "app", "saas"];
|
|
9
|
+
function cloudConfig() {
|
|
10
|
+
const token = String(process.env.KEEL_CLOUD_TOKEN ?? "").trim();
|
|
11
|
+
if (!token)
|
|
12
|
+
return null;
|
|
13
|
+
const url = String(process.env.KEEL_CLOUD_URL ?? "http://localhost:3000").replace(/\/$/, "");
|
|
14
|
+
return { url, token };
|
|
15
|
+
}
|
|
16
|
+
async function cloudFetch(path, init = {}) {
|
|
17
|
+
const cfg = cloudConfig();
|
|
18
|
+
if (!cfg)
|
|
19
|
+
throw new Error("KEEL_CLOUD_TOKEN is not set");
|
|
20
|
+
const response = await fetch(`${cfg.url}${path}`, {
|
|
21
|
+
...init,
|
|
22
|
+
headers: {
|
|
23
|
+
authorization: `Bearer ${cfg.token}`,
|
|
24
|
+
accept: "application/json",
|
|
25
|
+
...(init.body ? { "content-type": "application/json" } : {}),
|
|
26
|
+
...(init.headers ?? {}),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const text = await response.text();
|
|
30
|
+
let body = text;
|
|
31
|
+
try {
|
|
32
|
+
body = text ? JSON.parse(text) : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* keep text */
|
|
36
|
+
}
|
|
37
|
+
return { ok: response.ok, status: response.status, body };
|
|
38
|
+
}
|
|
39
|
+
function jsonText(data, isError = false) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }],
|
|
42
|
+
...(isError ? { isError: true } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Register Cloud tools on an existing MCP server when a Cloud token is present. */
|
|
46
|
+
export function registerCloudTools(server) {
|
|
47
|
+
if (!cloudConfig())
|
|
48
|
+
return false;
|
|
49
|
+
server.registerTool("keel_cloud_me", {
|
|
50
|
+
title: "Keel Cloud — who am I",
|
|
51
|
+
description: "Return the Keel Cloud user authenticated by KEEL_CLOUD_TOKEN.",
|
|
52
|
+
inputSchema: {},
|
|
53
|
+
}, async () => {
|
|
54
|
+
const res = await cloudFetch("/api/v1/me");
|
|
55
|
+
return jsonText(res.body, !res.ok);
|
|
56
|
+
});
|
|
57
|
+
server.registerTool("keel_cloud_list_sites", {
|
|
58
|
+
title: "Keel Cloud — list sites",
|
|
59
|
+
description: "List sites on the current Keel Cloud team. Includes storage_path for local editing and hostnames.",
|
|
60
|
+
inputSchema: {},
|
|
61
|
+
}, async () => {
|
|
62
|
+
const res = await cloudFetch("/api/v1/sites");
|
|
63
|
+
return jsonText(res.body, !res.ok);
|
|
64
|
+
});
|
|
65
|
+
server.registerTool("keel_cloud_get_site", {
|
|
66
|
+
title: "Keel Cloud — get site",
|
|
67
|
+
description: "Fetch one Keel Cloud site by id (paths, hostnames, status).",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
site_id: z.number().int().describe("Site id from keel_cloud_list_sites"),
|
|
70
|
+
},
|
|
71
|
+
}, async ({ site_id }) => {
|
|
72
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}`);
|
|
73
|
+
return jsonText(res.body, !res.ok);
|
|
74
|
+
});
|
|
75
|
+
server.registerTool("keel_cloud_create_site", {
|
|
76
|
+
title: "Keel Cloud — create site",
|
|
77
|
+
description: "Create a new Keel Cloud site from a preset (minimal|api|app|saas). Scaffolds source under storage/sites/{slug} and returns storage_path for editing.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
name: z.string().min(1).describe("Human-readable site name"),
|
|
80
|
+
preset: z.enum(PRESETS).optional().describe("Keel preset (default minimal)"),
|
|
81
|
+
},
|
|
82
|
+
}, async ({ name, preset }) => {
|
|
83
|
+
const res = await cloudFetch("/api/v1/sites", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
body: JSON.stringify({ name, preset: preset ?? "minimal" }),
|
|
86
|
+
});
|
|
87
|
+
return jsonText(res.body, !res.ok);
|
|
88
|
+
});
|
|
89
|
+
server.registerTool("keel_cloud_preview", {
|
|
90
|
+
title: "Keel Cloud — deploy preview",
|
|
91
|
+
description: "Deploy the site's preview Worker (and attach preview hostname when Cloudflare is configured). Safe to call freely while iterating.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
site_id: z.number().int().describe("Site id"),
|
|
94
|
+
},
|
|
95
|
+
}, async ({ site_id }) => {
|
|
96
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/preview`, { method: "POST", body: "{}" });
|
|
97
|
+
return jsonText(res.body, !res.ok);
|
|
98
|
+
});
|
|
99
|
+
server.registerTool("keel_cloud_publish", {
|
|
100
|
+
title: "Keel Cloud — publish production",
|
|
101
|
+
description: "Publish the site to its production Worker/hostname. Requires confirm=true (explicit user approval).",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
site_id: z.number().int().describe("Site id"),
|
|
104
|
+
confirm: z
|
|
105
|
+
.boolean()
|
|
106
|
+
.describe("Must be true to publish — ask the user before setting this"),
|
|
107
|
+
},
|
|
108
|
+
}, async ({ site_id, confirm }) => {
|
|
109
|
+
if (!confirm) {
|
|
110
|
+
return jsonText({
|
|
111
|
+
error: "Publish requires confirm=true. Ask the user to confirm before calling again.",
|
|
112
|
+
}, true);
|
|
113
|
+
}
|
|
114
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/publish`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
body: JSON.stringify({ confirm: true }),
|
|
117
|
+
});
|
|
118
|
+
return jsonText(res.body, !res.ok);
|
|
119
|
+
});
|
|
120
|
+
server.registerTool("keel_cloud_deploys", {
|
|
121
|
+
title: "Keel Cloud — deploy history",
|
|
122
|
+
description: "List recent preview/production deploys and logs for a site.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
site_id: z.number().int().describe("Site id"),
|
|
125
|
+
},
|
|
126
|
+
}, async ({ site_id }) => {
|
|
127
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/deploys`);
|
|
128
|
+
return jsonText(res.body, !res.ok);
|
|
129
|
+
});
|
|
130
|
+
server.registerTool("keel_cloud_set_secret", {
|
|
131
|
+
title: "Keel Cloud — set secret",
|
|
132
|
+
description: "Store a secret in the site vault (never in git). Injected into the Worker on the next preview/publish.",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
site_id: z.number().int().describe("Site id"),
|
|
135
|
+
key: z.string().min(1).describe("Env-style key, e.g. STRIPE_SECRET_KEY"),
|
|
136
|
+
value: z.string().describe("Secret value (will not be echoed by the dashboard)"),
|
|
137
|
+
},
|
|
138
|
+
}, async ({ site_id, key, value }) => {
|
|
139
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/secrets`, {
|
|
140
|
+
method: "PUT",
|
|
141
|
+
body: JSON.stringify({ key, value }),
|
|
142
|
+
});
|
|
143
|
+
return jsonText(res.body, !res.ok);
|
|
144
|
+
});
|
|
145
|
+
server.registerTool("keel_cloud_export", {
|
|
146
|
+
title: "Keel Cloud — export manifest",
|
|
147
|
+
description: "Return the site's export manifest (paths, hostnames, Keel version, SQL dump URLs). Use storage_path / git_url for code; use keel_cloud_export_sql for data.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
site_id: z.number().int().describe("Site id"),
|
|
150
|
+
},
|
|
151
|
+
}, async ({ site_id }) => {
|
|
152
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/export`);
|
|
153
|
+
return jsonText(res.body, !res.ok);
|
|
154
|
+
});
|
|
155
|
+
server.registerTool("keel_cloud_export_sql", {
|
|
156
|
+
title: "Keel Cloud — export SQL dump",
|
|
157
|
+
description: "Download a portable .sql dump for local sqlite, preview D1, or production D1. Prefer production after publish for a full escape hatch.",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
site_id: z.number().int().describe("Site id"),
|
|
160
|
+
env: z
|
|
161
|
+
.enum(["local", "preview", "production"])
|
|
162
|
+
.optional()
|
|
163
|
+
.describe("Which database to dump (default local)"),
|
|
164
|
+
},
|
|
165
|
+
}, async ({ site_id, env }) => {
|
|
166
|
+
const environment = env ?? "local";
|
|
167
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/export/sql?env=${environment}`);
|
|
168
|
+
if (!res.ok)
|
|
169
|
+
return jsonText(res.body, true);
|
|
170
|
+
const sql = typeof res.body === "string" ? res.body : JSON.stringify(res.body);
|
|
171
|
+
return jsonText({
|
|
172
|
+
site_id,
|
|
173
|
+
env: environment,
|
|
174
|
+
sql,
|
|
175
|
+
hint: "Write this to a .sql file and restore with sqlite3 / D1 import on a real Keel app.",
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
return true;
|
|
179
|
+
}
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* keel_search_api search the public export surface
|
|
12
12
|
* keel_list_generators the `keel make:*` generators
|
|
13
13
|
* keel_scaffold generate a controller/provider/job/… stub (no write)
|
|
14
|
+
* When KEEL_CLOUD_TOKEN is set, also:
|
|
15
|
+
* keel_cloud_* create/list/preview/publish sites on Keel Cloud
|
|
14
16
|
* Resources: keel://overview, keel://llms-full, and keel://docs/<slug> per guide.
|
|
15
17
|
*/
|
|
16
18
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
package/dist/mcp/server.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* keel_search_api search the public export surface
|
|
12
12
|
* keel_list_generators the `keel make:*` generators
|
|
13
13
|
* keel_scaffold generate a controller/provider/job/… stub (no write)
|
|
14
|
+
* When KEEL_CLOUD_TOKEN is set, also:
|
|
15
|
+
* keel_cloud_* create/list/preview/publish sites on Keel Cloud
|
|
14
16
|
* Resources: keel://overview, keel://llms-full, and keel://docs/<slug> per guide.
|
|
15
17
|
*/
|
|
16
18
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
@@ -20,6 +22,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
20
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
23
|
import { z } from "zod";
|
|
22
24
|
import { controllerStub, resourceControllerStub, providerStub, middlewareStub, factoryStub, seederStub, jobStub, notificationStub, transformerStub, } from "../core/cli/stubs.js";
|
|
25
|
+
import { registerCloudTools } from "./cloud.js";
|
|
23
26
|
async function exists(p) {
|
|
24
27
|
try {
|
|
25
28
|
await stat(p);
|
|
@@ -183,6 +186,18 @@ export async function createServer() {
|
|
|
183
186
|
"",
|
|
184
187
|
"## Generators — use keel_scaffold or `keel make:*`",
|
|
185
188
|
...manifest.generators.map((g) => `- ${g.command} → ${g.produces}`),
|
|
189
|
+
...(process.env.KEEL_CLOUD_TOKEN?.trim()
|
|
190
|
+
? [
|
|
191
|
+
"",
|
|
192
|
+
"## Keel Cloud (token detected)",
|
|
193
|
+
"Cloud tools are enabled. Typical loop:",
|
|
194
|
+
"1. keel_cloud_create_site { name, preset }",
|
|
195
|
+
"2. Edit files at the returned storage_path (real Keel app)",
|
|
196
|
+
"3. keel_cloud_preview { site_id }",
|
|
197
|
+
"4. keel_cloud_publish { site_id, confirm: true } after user approval",
|
|
198
|
+
"Also: keel_cloud_list_sites, keel_cloud_get_site, keel_cloud_set_secret, keel_cloud_deploys, keel_cloud_export.",
|
|
199
|
+
]
|
|
200
|
+
: []),
|
|
186
201
|
].join("\n");
|
|
187
202
|
// ---- tools ----
|
|
188
203
|
server.registerTool("keel_overview", {
|
|
@@ -343,6 +358,7 @@ export async function createServer() {
|
|
|
343
358
|
for (const doc of manifest.docs) {
|
|
344
359
|
server.registerResource(`doc:${doc.slug}`, `keel://docs/${doc.slug}`, { title: doc.title, description: doc.summary || doc.title, mimeType: "text/markdown" }, async (uri) => ({ contents: [{ uri: uri.href, text: await readFile(join(root, doc.path), "utf8") }] }));
|
|
345
360
|
}
|
|
361
|
+
registerCloudTools(server);
|
|
346
362
|
return server;
|
|
347
363
|
}
|
|
348
364
|
/** Boot the server on stdio. Called by the `keel-mcp` bin and `keel mcp`. */
|
|
@@ -351,5 +367,8 @@ export async function runMcpServer() {
|
|
|
351
367
|
const transport = new StdioServerTransport();
|
|
352
368
|
await server.connect(transport);
|
|
353
369
|
// stdout is the protocol channel — log to stderr only.
|
|
354
|
-
|
|
370
|
+
const cloud = Boolean(process.env.KEEL_CLOUD_TOKEN?.trim());
|
|
371
|
+
console.error(cloud
|
|
372
|
+
? "⚓ Keel MCP server running on stdio (Cloud tools enabled)"
|
|
373
|
+
: "⚓ Keel MCP server running on stdio");
|
|
355
374
|
}
|
package/docs/ai-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shaferllc/keel",
|
|
3
|
-
"version": "0.83.
|
|
3
|
+
"version": "0.83.4",
|
|
4
4
|
"description": "The house framework for Node.js — a service container, providers, routing, JSX views, and a code-generating console.",
|
|
5
5
|
"repo": "https://github.com/shaferllc/keel",
|
|
6
6
|
"generated": "run `npm run build:ai` to regenerate",
|
|
@@ -159,6 +159,13 @@
|
|
|
159
159
|
"path": "docs/factories.md",
|
|
160
160
|
"example": "docs/examples/factories.ts"
|
|
161
161
|
},
|
|
162
|
+
{
|
|
163
|
+
"slug": "gates",
|
|
164
|
+
"title": "Gates",
|
|
165
|
+
"summary": "Keel Gates is a signup gate for private alpha / waitlist apps: an email allowlist, invite codes with use limits and expiry, and a single check that answers \"may this person register?\".",
|
|
166
|
+
"path": "docs/gates.md",
|
|
167
|
+
"example": "docs/examples/gates.ts"
|
|
168
|
+
},
|
|
162
169
|
{
|
|
163
170
|
"slug": "getting-started",
|
|
164
171
|
"title": "Getting Started",
|
|
@@ -201,6 +208,13 @@
|
|
|
201
208
|
"path": "docs/hooks.md",
|
|
202
209
|
"example": "docs/examples/hooks.ts"
|
|
203
210
|
},
|
|
211
|
+
{
|
|
212
|
+
"slug": "hosting",
|
|
213
|
+
"title": "Hosting",
|
|
214
|
+
"summary": "Keel Hosting is a small toolkit for hosted Workers / D1 apps: a Cloudflare REST client, hostname helpers, a SQLite-compatible SQL dump, and purpose-scoped secret encryption.",
|
|
215
|
+
"path": "docs/hosting.md",
|
|
216
|
+
"example": "docs/examples/hosting.ts"
|
|
217
|
+
},
|
|
204
218
|
{
|
|
205
219
|
"slug": "i18n",
|
|
206
220
|
"title": "Internationalization",
|
|
@@ -3077,6 +3091,11 @@
|
|
|
3077
3091
|
"kind": "value",
|
|
3078
3092
|
"module": "tokens"
|
|
3079
3093
|
},
|
|
3094
|
+
{
|
|
3095
|
+
"name": "tokensMigration",
|
|
3096
|
+
"kind": "value",
|
|
3097
|
+
"module": "tokens"
|
|
3098
|
+
},
|
|
3080
3099
|
{
|
|
3081
3100
|
"name": "TooManyRequestsException",
|
|
3082
3101
|
"kind": "value",
|
package/docs/ai.md
CHANGED
|
@@ -61,6 +61,35 @@ package, so it always matches your installed version.
|
|
|
61
61
|
| `keel_list_generators` | The `keel make:*` generators, what they produce, and their flags. |
|
|
62
62
|
| `keel_scaffold` | Generate a controller/provider/middleware/factory/seeder/job/notification/transformer stub. Returns code + target path — it does **not** write to disk. |
|
|
63
63
|
|
|
64
|
+
When `KEEL_CLOUD_TOKEN` (and optional `KEEL_CLOUD_URL`) is set, Cloud tools are
|
|
65
|
+
also registered:
|
|
66
|
+
|
|
67
|
+
| Tool | What it does |
|
|
68
|
+
|------|--------------|
|
|
69
|
+
| `keel_cloud_me` | Authenticated Cloud user |
|
|
70
|
+
| `keel_cloud_list_sites` / `keel_cloud_get_site` | List or fetch a site (includes `storage_path`) |
|
|
71
|
+
| `keel_cloud_create_site` | Create from preset (`minimal` \| `api` \| `app` \| `saas`) |
|
|
72
|
+
| `keel_cloud_preview` | Deploy preview Worker |
|
|
73
|
+
| `keel_cloud_publish` | Publish production — requires `confirm: true` |
|
|
74
|
+
| `keel_cloud_set_secret` | Vault a secret (injected on next deploy) |
|
|
75
|
+
| `keel_cloud_deploys` / `keel_cloud_export` | Deploy history and export manifest |
|
|
76
|
+
| `keel_cloud_export_sql` | Portable `.sql` dump (`local` \| `preview` \| `production`) |
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"keel": {
|
|
82
|
+
"command": "npx",
|
|
83
|
+
"args": ["-y", "keel-mcp"],
|
|
84
|
+
"env": {
|
|
85
|
+
"KEEL_CLOUD_TOKEN": "kc_…",
|
|
86
|
+
"KEEL_CLOUD_URL": "https://app.keeljs.cloud"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
64
93
|
### Resources
|
|
65
94
|
|
|
66
95
|
- `keel://overview` — the same orientation text as `keel_overview`
|
|
@@ -75,6 +104,13 @@ package, so it always matches your installed version.
|
|
|
75
104
|
4. `keel_scaffold { kind: "controller", name: "Post", resource: true }` → get the stub.
|
|
76
105
|
5. Write the file, add the route, run `npm run typecheck`.
|
|
77
106
|
|
|
107
|
+
### A typical Keel Cloud loop
|
|
108
|
+
|
|
109
|
+
1. `keel_cloud_create_site { name: "Acme", preset: "app" }`
|
|
110
|
+
2. Edit the returned `storage_path` (real Keel app + git)
|
|
111
|
+
3. `keel_cloud_preview { site_id }`
|
|
112
|
+
4. `keel_cloud_publish { site_id, confirm: true }` after the user approves
|
|
113
|
+
|
|
78
114
|
## `llms.txt` and `llms-full.txt`
|
|
79
115
|
|
|
80
116
|
At the package root:
|
package/docs/billing.md
CHANGED
|
@@ -157,6 +157,15 @@ const intent = await user.createSetupIntent(); // return intent.clientSecret to
|
|
|
157
157
|
const methods = await user.paymentMethods();
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
### Customer portal (Stripe)
|
|
161
|
+
|
|
162
|
+
Send the customer to Stripe's hosted portal to update their card or cancel:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const portal = await user.billingPortal("https://app.example.com/billing");
|
|
166
|
+
// redirect to portal.url
|
|
167
|
+
```
|
|
168
|
+
|
|
160
169
|
These are Stripe-only capabilities; calling them on the Paddle gateway throws a
|
|
161
170
|
`BillingError` (Paddle collects cards in its own hosted checkout).
|
|
162
171
|
|
|
@@ -251,7 +260,7 @@ real gateway from `config/billing.ts` and `.env`.
|
|
|
251
260
|
|---------|--------|--------|
|
|
252
261
|
| Create a subscription server-side | `create(pmId)` | Not supported — use `checkout()`; the webhook creates the local row |
|
|
253
262
|
| One-off `charge()` | Confirms a PaymentIntent | Not supported — use `checkout({ mode })` / transactions |
|
|
254
|
-
| SetupIntent / `paymentMethods()` | Supported | Throws `BillingError` (hosted checkout) |
|
|
263
|
+
| SetupIntent / `paymentMethods()` / `billingPortal()` | Supported | Throws `BillingError` (hosted checkout) |
|
|
255
264
|
| Checkout handle | `session.url` (redirect) | `session.clientToken` (overlay/inline) |
|
|
256
265
|
| Webhook signature | `Stripe-Signature: t=…,v1=…` | `Paddle-Signature: ts=…;h1=…` |
|
|
257
266
|
|
package/docs/changelog.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to Keel are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
|
|
5
5
|
adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.83.4] — 2026-07-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **MCP Cloud tools** (`keel_cloud_*`) — when `KEEL_CLOUD_TOKEN` is set,
|
|
12
|
+
`keel-mcp` registers create/list/preview/publish/secrets/export tools against
|
|
13
|
+
a Keel Cloud control plane. Documented in [Building with AI](./docs/ai.md).
|
|
14
|
+
- **`tokensMigration()`** — personal access tokens schema helper for apps that
|
|
15
|
+
need the table via `keel migrate`.
|
|
16
|
+
- **Billing customer portal** — `user.billingPortal(returnUrl)` (Stripe + Fake
|
|
17
|
+
gateway); opens the hosted portal to manage card / cancel.
|
|
18
|
+
- **Hosting guide** — [`docs/hosting.md`](./docs/hosting.md) + example harness
|
|
19
|
+
for `@shaferllc/keel/hosting`.
|
|
20
|
+
- **Tests** for gates, hosting, billing portal, and `tokensMigration`.
|
|
21
|
+
|
|
22
|
+
[0.83.4]: https://github.com/shaferllc/keel/releases/tag/v0.83.4
|
|
23
|
+
|
|
24
|
+
## [0.83.3] — 2026-07-12
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`@shaferllc/keel/gates`** — signup gating for private alpha / waitlist:
|
|
29
|
+
email allowlist + invite codes (`canRegister` / `redeemInvite`), shipped as a
|
|
30
|
+
package with a migration. Used by Keel Cloud; not the same as team invitations
|
|
31
|
+
or authorization gates.
|
|
32
|
+
- **`@shaferllc/keel/hosting`** — Cloudflare client, hostname helpers, SQL dump,
|
|
33
|
+
and secrets encryption helpers for hosted Workers/D1 apps.
|
|
34
|
+
|
|
35
|
+
[0.83.3]: https://github.com/shaferllc/keel/releases/tag/v0.83.3
|
|
36
|
+
|
|
7
37
|
## [0.83.2] — 2026-07-12
|
|
8
38
|
|
|
9
39
|
### Added
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Type-check harness for docs/gates.md. Compile-only — never executed.
|
|
2
|
+
import { Application, json } from "@shaferllc/keel/core";
|
|
3
|
+
import {
|
|
4
|
+
GatesServiceProvider,
|
|
5
|
+
InviteCode,
|
|
6
|
+
EmailAllowlist,
|
|
7
|
+
canRegister,
|
|
8
|
+
redeemInvite,
|
|
9
|
+
} from "@shaferllc/keel/gates";
|
|
10
|
+
|
|
11
|
+
export function install() {
|
|
12
|
+
const app = new Application();
|
|
13
|
+
app.register(GatesServiceProvider);
|
|
14
|
+
return app;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function register(email: string, inviteCode?: string) {
|
|
18
|
+
const gate = await canRegister(email, inviteCode);
|
|
19
|
+
if (!gate.ok) return json({ error: gate.reason }, 403);
|
|
20
|
+
|
|
21
|
+
if (gate.via === "code" && gate.invite) {
|
|
22
|
+
await redeemInvite(gate.invite);
|
|
23
|
+
}
|
|
24
|
+
return json({ ok: true, via: gate.via });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function seed() {
|
|
28
|
+
await InviteCode.create({ code: "ALPHA-42", max_uses: 10, uses: 0, expires_at: null });
|
|
29
|
+
await EmailAllowlist.create({ email: "ada@example.com" });
|
|
30
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Type-check harness for docs/hosting.md. Compile-only — never executed.
|
|
2
|
+
import type { Connection } from "@shaferllc/keel/core";
|
|
3
|
+
import {
|
|
4
|
+
CloudflareClient,
|
|
5
|
+
cloudflareConfigured,
|
|
6
|
+
normalizeHostname,
|
|
7
|
+
isValidHostname,
|
|
8
|
+
zoneCandidates,
|
|
9
|
+
dumpConnection,
|
|
10
|
+
normalizeSecretKey,
|
|
11
|
+
encryptSecretValue,
|
|
12
|
+
decryptSecretValue,
|
|
13
|
+
resolveSecretRows,
|
|
14
|
+
} from "@shaferllc/keel/hosting";
|
|
15
|
+
|
|
16
|
+
export function client() {
|
|
17
|
+
const creds = { accountId: "acct", apiToken: "tok" };
|
|
18
|
+
if (!cloudflareConfigured(creds)) throw new Error("missing");
|
|
19
|
+
return new CloudflareClient(creds);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hostnames(raw: string) {
|
|
23
|
+
const host = normalizeHostname(raw);
|
|
24
|
+
return { host, valid: isValidHostname(host), zones: zoneCandidates(host) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function dump(conn: Connection) {
|
|
28
|
+
return dumpConnection(conn, "Acme local D1", { generatedBy: "Keel Cloud" });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function vault(secret: string) {
|
|
32
|
+
const key = normalizeSecretKey("stripe-secret-key");
|
|
33
|
+
const encrypted = await encryptSecretValue(secret, "app-secret");
|
|
34
|
+
const plain = await decryptSecretValue(encrypted, "app-secret");
|
|
35
|
+
const env = await resolveSecretRows([{ key, value_encrypted: encrypted }], "app-secret");
|
|
36
|
+
return { key, plain, env };
|
|
37
|
+
}
|
package/docs/gates.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Gates
|
|
2
|
+
|
|
3
|
+
Keel Gates is a **signup gate** for private alpha / waitlist apps: an email
|
|
4
|
+
allowlist, invite codes with use limits and expiry, and a single check that
|
|
5
|
+
answers "may this person register?". It ships as `@shaferllc/keel/gates`.
|
|
6
|
+
|
|
7
|
+
This is **not** authorization (`can` / policies in [authorization](./authorization.md))
|
|
8
|
+
and **not** team invitations ([teams](./teams.md)). Those answer different
|
|
9
|
+
questions. Gates answers only: *is this email allowed to create an account?*
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// bootstrap/providers.ts
|
|
15
|
+
import { GatesServiceProvider } from "@shaferllc/keel/gates";
|
|
16
|
+
|
|
17
|
+
export const providers = [AppServiceProvider, GatesServiceProvider];
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then migrate — the provider contributes `invite_codes` and `email_allowlist`
|
|
21
|
+
tables (`CREATE TABLE IF NOT EXISTS`, so existing apps stay safe):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
keel migrate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Checking registration
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { canRegister, redeemInvite } from "@shaferllc/keel/gates";
|
|
31
|
+
|
|
32
|
+
const gate = await canRegister(email, inviteCode);
|
|
33
|
+
if (!gate.ok) {
|
|
34
|
+
return json({ error: gate.reason }, 403);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// …create the user…
|
|
38
|
+
|
|
39
|
+
if (gate.via === "code" && gate.invite) {
|
|
40
|
+
await redeemInvite(gate.invite); // increments uses
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`canRegister` returns:
|
|
45
|
+
|
|
46
|
+
| Result | Meaning |
|
|
47
|
+
|--------|---------|
|
|
48
|
+
| `{ ok: true, via: "allowlist" }` | Email is on `email_allowlist` |
|
|
49
|
+
| `{ ok: true, via: "code", invite }` | Valid invite code (not expired, uses left) |
|
|
50
|
+
| `{ ok: false, reason }` | Rejected — show `reason` to the user |
|
|
51
|
+
|
|
52
|
+
Allowlist wins over codes: if the email is allowlisted, the code is ignored.
|
|
53
|
+
|
|
54
|
+
## Managing codes and allowlist
|
|
55
|
+
|
|
56
|
+
The models are ordinary Keel models — create rows from an admin UI or a seeder:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { InviteCode, EmailAllowlist } from "@shaferllc/keel/gates";
|
|
60
|
+
|
|
61
|
+
await InviteCode.create({
|
|
62
|
+
code: "ALPHA-42",
|
|
63
|
+
max_uses: 10,
|
|
64
|
+
uses: 0,
|
|
65
|
+
expires_at: null,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await EmailAllowlist.create({ email: "ada@example.com" });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Related
|
|
72
|
+
|
|
73
|
+
- [Accounts](./accounts.md) — register / login flows that call `canRegister` first
|
|
74
|
+
- [Teams](./teams.md) — invitations *into* a team, after the user already exists
|
|
75
|
+
- [Authorization](./authorization.md) — ability checks once they're signed in
|