@shaferllc/keel 0.83.3 → 0.83.5
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 +3 -23
- package/dist/accounts/provider.js +12 -2
- 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/billing/provider.js +12 -2
- 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/mcp/cloud.d.ts +10 -0
- package/dist/mcp/cloud.js +288 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +22 -1
- package/dist/teams/provider.js +12 -2
- package/docs/ai-manifest.json +13 -1
- package/docs/ai.md +46 -0
- package/docs/billing.md +10 -1
- package/docs/changelog.md +28 -0
- package/docs/examples/hosting.ts +37 -0
- package/docs/hosting.md +97 -0
- package/llms-full.txt +160 -1
- package/llms.txt +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -206,6 +206,7 @@ See [docs/architecture.md](./docs/architecture.md) for the full picture.
|
|
|
206
206
|
| [Sessions](./docs/sessions.md) | Cookie-backed sessions, flash messages |
|
|
207
207
|
| [Authentication](./docs/authentication.md) | Session auth, guards, user provider |
|
|
208
208
|
| [Gates](./docs/gates.md) | Signup gating — invite codes & email allowlist |
|
|
209
|
+
| [Hosting](./docs/hosting.md) | Cloudflare, SQL dump, secrets for hosted Workers |
|
|
209
210
|
| [Authorization](./docs/authorization.md) | Ability gates & policies, `can`/`authorize` |
|
|
210
211
|
| [Database](./docs/database.md) | Driver-agnostic query builder |
|
|
211
212
|
| [Models](./docs/models.md) | Active-record: find/create/save, casts, relations |
|
|
@@ -269,29 +270,8 @@ lines / ~91% branches**.
|
|
|
269
270
|
|
|
270
271
|
## Roadmap
|
|
271
272
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
- [x] View / templating layer (Hono JSX) — **v0.2.0**
|
|
276
|
-
- [x] Cloudflare Workers–safe core — **v0.2.0**
|
|
277
|
-
- [x] Global helpers: `config()`, `app()`, `view()` — **v0.3.0 / v0.4.0**
|
|
278
|
-
- [x] Error & exception handling — **v0.5.0**
|
|
279
|
-
- [x] Request/response + container helpers — **v0.6.0–v0.9.0**
|
|
280
|
-
- [x] Validation (Zod-compatible) — **v0.10.0**
|
|
281
|
-
- [x] First-class routing (groups, resources, named routes) — **v0.11.0**
|
|
282
|
-
- [x] Domain routing, matchers, Inertia adapter — **v0.12.0**
|
|
283
|
-
- [x] Test suite (~99% coverage)
|
|
284
|
-
- [x] Query builder (driver-agnostic) — **v0.28.0**
|
|
285
|
-
- [x] Active-record Model layer — **v0.29.0**
|
|
286
|
-
- [x] Migrations (schema builder) — **v0.30.0**
|
|
287
|
-
- [x] Model relationships (hasMany / belongsTo / belongsToMany) — **v0.31.0**
|
|
288
|
-
- [x] Factories & seeders (built-in Faker) — **v0.32.0**
|
|
289
|
-
- [x] Model attribute casts + mass-assignment guarding — **v0.33.0**
|
|
290
|
-
- [x] Mail (fluent mailer, pluggable transports) — **v0.34.0**
|
|
291
|
-
- [x] Queues / background jobs (pluggable drivers) — **v0.35.0**
|
|
292
|
-
- [x] Notifications (multi-channel, queueable) — **v0.36.0**
|
|
293
|
-
- [ ] Publish `src/core` as the `@keel/core` package
|
|
294
|
-
|
|
273
|
+
The MVP core and the major subsystems are shipped. Recent focus is Keel Cloud
|
|
274
|
+
support (gates, hosting, MCP Cloud tools, billing portal) and starter kits.
|
|
295
275
|
See [CHANGELOG.md](./CHANGELOG.md) for release history.
|
|
296
276
|
|
|
297
277
|
## Contributing
|
|
@@ -15,7 +15,17 @@ import { defaultConfig, resolveConfig } from "./config.js";
|
|
|
15
15
|
import { accountsMigration } from "./migration.js";
|
|
16
16
|
import { registerAccountsRoutes } from "./routes.js";
|
|
17
17
|
import { setAccountStore, tableStore } from "./store.js";
|
|
18
|
-
|
|
18
|
+
function packageDir() {
|
|
19
|
+
try {
|
|
20
|
+
const url = import.meta.url;
|
|
21
|
+
if (!url)
|
|
22
|
+
return ".";
|
|
23
|
+
return dirname(fileURLToPath(url));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return ".";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
19
29
|
export class AccountsServiceProvider extends PackageProvider {
|
|
20
30
|
name = "accounts";
|
|
21
31
|
config;
|
|
@@ -24,7 +34,7 @@ export class AccountsServiceProvider extends PackageProvider {
|
|
|
24
34
|
this.config = resolveConfig();
|
|
25
35
|
setAccountStore(tableStore(this.config.userTable));
|
|
26
36
|
this.migrations([accountsMigration(this.config.userTable)]);
|
|
27
|
-
this.publishes({ [join(
|
|
37
|
+
this.publishes({ [join(packageDir(), "accounts.config.stub")]: "config/accounts.ts" }, "accounts-config");
|
|
28
38
|
}
|
|
29
39
|
boot() {
|
|
30
40
|
if (!this.config.routes.enabled)
|
|
@@ -67,6 +67,7 @@ export interface BillableInstance {
|
|
|
67
67
|
checkout(prices: string | PriceRef | (string | PriceRef)[], options?: ProductCheckoutOptions): Promise<CheckoutSession>;
|
|
68
68
|
createSetupIntent(): Promise<SetupIntent>;
|
|
69
69
|
paymentMethods(): Promise<PaymentMethod[]>;
|
|
70
|
+
billingPortal(returnUrl: string): Promise<import("./gateway.js").BillingPortalSession>;
|
|
70
71
|
}
|
|
71
72
|
/**
|
|
72
73
|
* The class a `Billable(Base)` call returns: billing instances plus all of
|
package/dist/billing/billable.js
CHANGED
|
@@ -171,6 +171,14 @@ export function Billable(Base) {
|
|
|
171
171
|
return [];
|
|
172
172
|
return gateway.paymentMethods(this.billing_customer_id);
|
|
173
173
|
}
|
|
174
|
+
/** Open the hosted customer portal (manage card / cancel). */
|
|
175
|
+
async billingPortal(returnUrl) {
|
|
176
|
+
const gateway = this.billingGateway();
|
|
177
|
+
if (!gateway.createBillingPortalSession) {
|
|
178
|
+
throw new BillingError(`The "${gateway.name}" gateway does not support a billing portal.`, gateway.name);
|
|
179
|
+
}
|
|
180
|
+
return gateway.createBillingPortalSession(await this.getCustomerId(), returnUrl);
|
|
181
|
+
}
|
|
174
182
|
}
|
|
175
183
|
return BillableModel;
|
|
176
184
|
}
|
|
@@ -42,6 +42,10 @@ export declare class FakeGateway implements BillingGateway {
|
|
|
42
42
|
refund(params: RefundParams): Promise<GatewayRefund>;
|
|
43
43
|
listInvoices(customer: string): Promise<GatewayInvoice[]>;
|
|
44
44
|
createCheckoutSession(params: CheckoutParams): Promise<CheckoutSession>;
|
|
45
|
+
createBillingPortalSession(customer: string, returnUrl: string): Promise<{
|
|
46
|
+
id: string;
|
|
47
|
+
url: string;
|
|
48
|
+
}>;
|
|
45
49
|
createSetupIntent(customer: string): Promise<SetupIntent>;
|
|
46
50
|
paymentMethods(customer: string): Promise<PaymentMethod[]>;
|
|
47
51
|
verifyWebhook(rawBody: string, headers: HeaderBag, secret: string): Promise<import("../gateway.js").WebhookEvent | null>;
|
|
@@ -129,6 +129,11 @@ export class FakeGateway {
|
|
|
129
129
|
const id = this.id("cs");
|
|
130
130
|
return { id, url: `https://fake.checkout/${id}`, clientToken: `ctok_${id}` };
|
|
131
131
|
}
|
|
132
|
+
async createBillingPortalSession(customer, returnUrl) {
|
|
133
|
+
this.record("createBillingPortalSession", customer, returnUrl);
|
|
134
|
+
const id = this.id("bps");
|
|
135
|
+
return { id, url: `https://fake.portal/${id}?return=${encodeURIComponent(returnUrl)}` };
|
|
136
|
+
}
|
|
132
137
|
async createSetupIntent(customer) {
|
|
133
138
|
this.record("createSetupIntent", customer);
|
|
134
139
|
const id = this.id("seti");
|
|
@@ -28,6 +28,7 @@ export declare class StripeGateway implements BillingGateway {
|
|
|
28
28
|
createCheckoutSession(params: CheckoutParams): Promise<CheckoutSession>;
|
|
29
29
|
createSetupIntent(customer: string): Promise<SetupIntent>;
|
|
30
30
|
paymentMethods(customer: string): Promise<PaymentMethod[]>;
|
|
31
|
+
createBillingPortalSession(customer: string, returnUrl: string): Promise<import("../gateway.js").BillingPortalSession>;
|
|
31
32
|
verifyWebhook(rawBody: string, headers: HeaderBag, secret: string): Promise<WebhookEvent | null>;
|
|
32
33
|
private normalize;
|
|
33
34
|
}
|
|
@@ -219,6 +219,13 @@ export class StripeGateway {
|
|
|
219
219
|
};
|
|
220
220
|
});
|
|
221
221
|
}
|
|
222
|
+
async createBillingPortalSession(customer, returnUrl) {
|
|
223
|
+
const session = await this.call("POST", "/billing_portal/sessions", {
|
|
224
|
+
customer,
|
|
225
|
+
return_url: returnUrl,
|
|
226
|
+
});
|
|
227
|
+
return { id: String(session.id), url: String(session.url ?? "") };
|
|
228
|
+
}
|
|
222
229
|
/* ------------------------------ webhooks ------------------------------- */
|
|
223
230
|
async verifyWebhook(rawBody, headers, secret) {
|
|
224
231
|
const header = headers("stripe-signature");
|
|
@@ -162,6 +162,13 @@ export interface BillingGateway {
|
|
|
162
162
|
verifyWebhook(rawBody: string, headers: HeaderBag, secret: string): Promise<WebhookEvent | null>;
|
|
163
163
|
createSetupIntent?(customer: string): Promise<SetupIntent>;
|
|
164
164
|
paymentMethods?(customer: string): Promise<PaymentMethod[]>;
|
|
165
|
+
/** Hosted customer portal (manage card / cancel). Stripe-style. */
|
|
166
|
+
createBillingPortalSession?(customer: string, returnUrl: string): Promise<BillingPortalSession>;
|
|
167
|
+
}
|
|
168
|
+
/** A hosted billing-portal redirect. */
|
|
169
|
+
export interface BillingPortalSession {
|
|
170
|
+
id: string;
|
|
171
|
+
url: string;
|
|
165
172
|
}
|
|
166
173
|
/** Raised for gateway/config problems and unsupported-capability calls. */
|
|
167
174
|
export declare class BillingError extends Error {
|
package/dist/billing/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type { BillableTarget, CheckoutOptions } from "./builder.js";
|
|
|
16
16
|
export { BillingManager, billing, setBilling } from "./manager.js";
|
|
17
17
|
export type { GatewayFactory } from "./manager.js";
|
|
18
18
|
export { BillingError } from "./gateway.js";
|
|
19
|
-
export type { BillingGateway, HeaderBag, CustomerDetails, GatewayCustomer, PriceRef, CreateSubscriptionParams, GatewaySubscription, GatewaySubscriptionItem, SwapParams, CancelParams, ChargeParams, GatewayCharge, RefundParams, GatewayRefund, GatewayInvoice, CheckoutParams, CheckoutSession, SetupIntent, PaymentMethod, WebhookEvent, } from "./gateway.js";
|
|
19
|
+
export type { BillingGateway, HeaderBag, CustomerDetails, GatewayCustomer, PriceRef, CreateSubscriptionParams, GatewaySubscription, GatewaySubscriptionItem, SwapParams, CancelParams, ChargeParams, GatewayCharge, RefundParams, GatewayRefund, GatewayInvoice, CheckoutParams, CheckoutSession, SetupIntent, PaymentMethod, BillingPortalSession, WebhookEvent, } from "./gateway.js";
|
|
20
20
|
export { registerDefaultGateways, StripeGateway, PaddleGateway, FakeGateway } from "./drivers/index.js";
|
|
21
21
|
export type { FakeCall } from "./drivers/index.js";
|
|
22
22
|
export { handleWebhook, resolveBillableUsing } from "./webhooks.js";
|
package/dist/billing/provider.js
CHANGED
|
@@ -17,7 +17,17 @@ import { BillingManager, setBilling } from "./manager.js";
|
|
|
17
17
|
import { registerDefaultGateways } from "./drivers/index.js";
|
|
18
18
|
import { billingMigration } from "./migration.js";
|
|
19
19
|
import { registerBillingRoutes } from "./routes.js";
|
|
20
|
-
|
|
20
|
+
function packageDir() {
|
|
21
|
+
try {
|
|
22
|
+
const url = import.meta.url;
|
|
23
|
+
if (!url)
|
|
24
|
+
return ".";
|
|
25
|
+
return dirname(fileURLToPath(url));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return ".";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
21
31
|
export class BillingServiceProvider extends PackageProvider {
|
|
22
32
|
name = "billing";
|
|
23
33
|
config;
|
|
@@ -31,7 +41,7 @@ export class BillingServiceProvider extends PackageProvider {
|
|
|
31
41
|
this.app.instance(BillingManager, this.manager);
|
|
32
42
|
// Default billable table is `users`.
|
|
33
43
|
this.migrations([billingMigration("users")]);
|
|
34
|
-
this.publishes({ [join(
|
|
44
|
+
this.publishes({ [join(packageDir(), "billing.config.stub")]: "config/billing.ts" }, "billing-config");
|
|
35
45
|
}
|
|
36
46
|
boot() {
|
|
37
47
|
this.routes((r) => registerBillingRoutes(r, this.config.webhook.path));
|
package/dist/core/index.d.ts
CHANGED
|
@@ -102,7 +102,7 @@ export { Session, session, sessionMiddleware } from "./session.js";
|
|
|
102
102
|
export type { SessionOptions } from "./session.js";
|
|
103
103
|
export { Auth, auth, authGuard, bearerAuth, basicAuth, tokenAuth, token, tokenCan, setUserProvider } from "./auth.js";
|
|
104
104
|
export type { UserProvider, BasicVerifier } from "./auth.js";
|
|
105
|
-
export { createToken, verifyToken, revokeToken, revokeTokens, listTokens, tokenAllows, tokenDenies, setTokensTable, } from "./tokens.js";
|
|
105
|
+
export { createToken, verifyToken, revokeToken, revokeTokens, listTokens, tokenAllows, tokenDenies, setTokensTable, tokensMigration, } from "./tokens.js";
|
|
106
106
|
export type { AccessToken, CreateTokenOptions, IssuedToken } from "./tokens.js";
|
|
107
107
|
export { social, github, google, discord, oauthDriver, oauthState, OAuthDriver, OAuthError, twitter, oauth1Driver, oauth1Signature, OAuth1Driver, } from "./social.js";
|
|
108
108
|
export type { SocialUser, OAuthToken, OAuthConfig, ProviderSpec, RedirectOptions, OAuth1Config, OAuth1Token, OAuth1ProviderSpec, } from "./social.js";
|
package/dist/core/index.js
CHANGED
|
@@ -54,7 +54,7 @@ export { HttpException, BadRequestException, UnauthorizedException, PaymentRequi
|
|
|
54
54
|
export { validate, validateRequest, validated } from "./validation.js";
|
|
55
55
|
export { Session, session, sessionMiddleware } from "./session.js";
|
|
56
56
|
export { Auth, auth, authGuard, bearerAuth, basicAuth, tokenAuth, token, tokenCan, setUserProvider } from "./auth.js";
|
|
57
|
-
export { createToken, verifyToken, revokeToken, revokeTokens, listTokens, tokenAllows, tokenDenies, setTokensTable, } from "./tokens.js";
|
|
57
|
+
export { createToken, verifyToken, revokeToken, revokeTokens, listTokens, tokenAllows, tokenDenies, setTokensTable, tokensMigration, } from "./tokens.js";
|
|
58
58
|
export { social, github, google, discord, oauthDriver, oauthState, OAuthDriver, OAuthError, twitter, oauth1Driver, oauth1Signature, OAuth1Driver, } from "./social.js";
|
|
59
59
|
export { define, policy, gateBefore, gateAfter, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
|
|
60
60
|
export { Transformer } from "./transformer.js";
|
package/dist/core/tokens.d.ts
CHANGED
|
@@ -72,3 +72,5 @@ export declare function revokeToken(selector: string, connection?: string): Prom
|
|
|
72
72
|
export declare function revokeTokens(tokenableId: string | number, connection?: string): Promise<void>;
|
|
73
73
|
/** List an entity's tokens (metadata only — the secret is never stored). */
|
|
74
74
|
export declare function listTokens(tokenableId: string | number, connection?: string): Promise<AccessToken[]>;
|
|
75
|
+
/** Schema for personal access tokens (SQLite-friendly; works on Postgres with minor tweaks). */
|
|
76
|
+
export declare function tokensMigration(tableName?: string): import("./migrations.js").Migration;
|
package/dist/core/tokens.js
CHANGED
|
@@ -153,3 +153,27 @@ function parseAbilities(value) {
|
|
|
153
153
|
return ["*"];
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
/** Schema for personal access tokens (SQLite-friendly; works on Postgres with minor tweaks). */
|
|
157
|
+
export function tokensMigration(tableName = "personal_access_tokens") {
|
|
158
|
+
return {
|
|
159
|
+
name: `tokens_00_${tableName}`,
|
|
160
|
+
async up(schema) {
|
|
161
|
+
await schema.raw(`
|
|
162
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
163
|
+
selector TEXT NOT NULL UNIQUE,
|
|
164
|
+
hash TEXT NOT NULL,
|
|
165
|
+
tokenable_id TEXT NOT NULL,
|
|
166
|
+
name TEXT,
|
|
167
|
+
abilities TEXT,
|
|
168
|
+
last_used_at INTEGER,
|
|
169
|
+
expires_at INTEGER,
|
|
170
|
+
created_at INTEGER
|
|
171
|
+
)
|
|
172
|
+
`);
|
|
173
|
+
await schema.raw(`CREATE INDEX IF NOT EXISTS idx_${tableName}_tokenable ON ${tableName} (tokenable_id)`);
|
|
174
|
+
},
|
|
175
|
+
async down(schema) {
|
|
176
|
+
await schema.dropTable(tableName).catch(() => { });
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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, manage secrets/domains/billing, and export
|
|
6
|
+
* without leaving the IDE.
|
|
7
|
+
*/
|
|
8
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
/** Register Cloud tools on an existing MCP server when a Cloud token is present. */
|
|
10
|
+
export declare function registerCloudTools(server: McpServer): boolean;
|
|
@@ -0,0 +1,288 @@
|
|
|
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, manage secrets/domains/billing, and export
|
|
6
|
+
* without leaving the IDE.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
const PRESETS = ["minimal", "api", "app", "saas"];
|
|
10
|
+
function cloudConfig() {
|
|
11
|
+
const token = String(process.env.KEEL_CLOUD_TOKEN ?? "").trim();
|
|
12
|
+
if (!token)
|
|
13
|
+
return null;
|
|
14
|
+
const url = String(process.env.KEEL_CLOUD_URL ?? "http://localhost:3000").replace(/\/$/, "");
|
|
15
|
+
return { url, token };
|
|
16
|
+
}
|
|
17
|
+
async function cloudFetch(path, init = {}) {
|
|
18
|
+
const cfg = cloudConfig();
|
|
19
|
+
if (!cfg)
|
|
20
|
+
throw new Error("KEEL_CLOUD_TOKEN is not set");
|
|
21
|
+
const response = await fetch(`${cfg.url}${path}`, {
|
|
22
|
+
...init,
|
|
23
|
+
headers: {
|
|
24
|
+
authorization: `Bearer ${cfg.token}`,
|
|
25
|
+
accept: "application/json",
|
|
26
|
+
...(init.body ? { "content-type": "application/json" } : {}),
|
|
27
|
+
...(init.headers ?? {}),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const text = await response.text();
|
|
31
|
+
let body = text;
|
|
32
|
+
try {
|
|
33
|
+
body = text ? JSON.parse(text) : null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* keep text */
|
|
37
|
+
}
|
|
38
|
+
return { ok: response.ok, status: response.status, body };
|
|
39
|
+
}
|
|
40
|
+
function jsonText(data, isError = false) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }],
|
|
43
|
+
...(isError ? { isError: true } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function requireConfirm(confirm, action) {
|
|
47
|
+
if (confirm)
|
|
48
|
+
return null;
|
|
49
|
+
return jsonText({
|
|
50
|
+
error: `${action} requires confirm=true. Ask the user to confirm before calling again.`,
|
|
51
|
+
}, true);
|
|
52
|
+
}
|
|
53
|
+
/** Register Cloud tools on an existing MCP server when a Cloud token is present. */
|
|
54
|
+
export function registerCloudTools(server) {
|
|
55
|
+
if (!cloudConfig())
|
|
56
|
+
return false;
|
|
57
|
+
server.registerTool("keel_cloud_me", {
|
|
58
|
+
title: "Keel Cloud — who am I",
|
|
59
|
+
description: "Return the Keel Cloud user authenticated by KEEL_CLOUD_TOKEN (plan, site_limit, team_id). Token binds to the user's first team.",
|
|
60
|
+
inputSchema: {},
|
|
61
|
+
}, async () => {
|
|
62
|
+
const res = await cloudFetch("/api/v1/me");
|
|
63
|
+
return jsonText(res.body, !res.ok);
|
|
64
|
+
});
|
|
65
|
+
server.registerTool("keel_cloud_billing", {
|
|
66
|
+
title: "Keel Cloud — billing status",
|
|
67
|
+
description: "Current team plan (free|pro), site limits, custom-domain eligibility, and whether the caller is the owner.",
|
|
68
|
+
inputSchema: {},
|
|
69
|
+
}, async () => {
|
|
70
|
+
const res = await cloudFetch("/api/v1/billing");
|
|
71
|
+
return jsonText(res.body, !res.ok);
|
|
72
|
+
});
|
|
73
|
+
server.registerTool("keel_cloud_billing_checkout", {
|
|
74
|
+
title: "Keel Cloud — Stripe checkout URL",
|
|
75
|
+
description: "Create a Stripe Checkout session for Keel Cloud Pro (owner only). Return the url for the user to open — do not scrape cards.",
|
|
76
|
+
inputSchema: {},
|
|
77
|
+
}, async () => {
|
|
78
|
+
const res = await cloudFetch("/api/v1/billing/checkout", { method: "POST", body: "{}" });
|
|
79
|
+
return jsonText(res.body, !res.ok);
|
|
80
|
+
});
|
|
81
|
+
server.registerTool("keel_cloud_billing_portal", {
|
|
82
|
+
title: "Keel Cloud — Stripe customer portal URL",
|
|
83
|
+
description: "Open the Stripe customer portal URL for the team (owner only) to update card / cancel. Return the url for the user.",
|
|
84
|
+
inputSchema: {},
|
|
85
|
+
}, async () => {
|
|
86
|
+
const res = await cloudFetch("/api/v1/billing/portal", { method: "POST", body: "{}" });
|
|
87
|
+
return jsonText(res.body, !res.ok);
|
|
88
|
+
});
|
|
89
|
+
server.registerTool("keel_cloud_list_sites", {
|
|
90
|
+
title: "Keel Cloud — list sites",
|
|
91
|
+
description: "List sites on the current Keel Cloud team. Includes storage_path for local editing and hostnames.",
|
|
92
|
+
inputSchema: {},
|
|
93
|
+
}, async () => {
|
|
94
|
+
const res = await cloudFetch("/api/v1/sites");
|
|
95
|
+
return jsonText(res.body, !res.ok);
|
|
96
|
+
});
|
|
97
|
+
server.registerTool("keel_cloud_get_site", {
|
|
98
|
+
title: "Keel Cloud — get site",
|
|
99
|
+
description: "Fetch one Keel Cloud site by id (paths, hostnames, status, git).",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
site_id: z.number().int().describe("Site id from keel_cloud_list_sites"),
|
|
102
|
+
},
|
|
103
|
+
}, async ({ site_id }) => {
|
|
104
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}`);
|
|
105
|
+
return jsonText(res.body, !res.ok);
|
|
106
|
+
});
|
|
107
|
+
server.registerTool("keel_cloud_create_site", {
|
|
108
|
+
title: "Keel Cloud — create site",
|
|
109
|
+
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.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
name: z.string().min(1).describe("Human-readable site name"),
|
|
112
|
+
preset: z.enum(PRESETS).optional().describe("Keel preset (default minimal)"),
|
|
113
|
+
},
|
|
114
|
+
}, async ({ name, preset }) => {
|
|
115
|
+
const res = await cloudFetch("/api/v1/sites", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
body: JSON.stringify({ name, preset: preset ?? "minimal" }),
|
|
118
|
+
});
|
|
119
|
+
return jsonText(res.body, !res.ok);
|
|
120
|
+
});
|
|
121
|
+
server.registerTool("keel_cloud_delete_site", {
|
|
122
|
+
title: "Keel Cloud — soft-delete site",
|
|
123
|
+
description: "Soft-delete a site (detaches hostnames; recoverable within retention). Requires confirm=true.",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
site_id: z.number().int().describe("Site id"),
|
|
126
|
+
confirm: z.boolean().describe("Must be true — ask the user before setting this"),
|
|
127
|
+
},
|
|
128
|
+
}, async ({ site_id, confirm }) => {
|
|
129
|
+
const blocked = requireConfirm(confirm, "Delete");
|
|
130
|
+
if (blocked)
|
|
131
|
+
return blocked;
|
|
132
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/delete`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
body: JSON.stringify({ confirm: true }),
|
|
135
|
+
});
|
|
136
|
+
return jsonText(res.body, !res.ok);
|
|
137
|
+
});
|
|
138
|
+
server.registerTool("keel_cloud_restore_site", {
|
|
139
|
+
title: "Keel Cloud — restore soft-deleted site",
|
|
140
|
+
description: "Restore a soft-deleted site within the retention window. Redeploy to reattach hostnames.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
site_id: z.number().int().describe("Site id"),
|
|
143
|
+
},
|
|
144
|
+
}, async ({ site_id }) => {
|
|
145
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/restore`, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: "{}",
|
|
148
|
+
});
|
|
149
|
+
return jsonText(res.body, !res.ok);
|
|
150
|
+
});
|
|
151
|
+
server.registerTool("keel_cloud_preview", {
|
|
152
|
+
title: "Keel Cloud — deploy preview",
|
|
153
|
+
description: "Deploy the site's preview Worker (and attach preview hostname when Cloudflare is configured). Safe to call freely while iterating.",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
site_id: z.number().int().describe("Site id"),
|
|
156
|
+
},
|
|
157
|
+
}, async ({ site_id }) => {
|
|
158
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/preview`, { method: "POST", body: "{}" });
|
|
159
|
+
return jsonText(res.body, !res.ok);
|
|
160
|
+
});
|
|
161
|
+
server.registerTool("keel_cloud_publish", {
|
|
162
|
+
title: "Keel Cloud — publish production",
|
|
163
|
+
description: "Publish the site to its production Worker/hostname. Requires confirm=true (explicit user approval).",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
site_id: z.number().int().describe("Site id"),
|
|
166
|
+
confirm: z
|
|
167
|
+
.boolean()
|
|
168
|
+
.describe("Must be true to publish — ask the user before setting this"),
|
|
169
|
+
},
|
|
170
|
+
}, async ({ site_id, confirm }) => {
|
|
171
|
+
const blocked = requireConfirm(confirm, "Publish");
|
|
172
|
+
if (blocked)
|
|
173
|
+
return blocked;
|
|
174
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/publish`, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
body: JSON.stringify({ confirm: true }),
|
|
177
|
+
});
|
|
178
|
+
return jsonText(res.body, !res.ok);
|
|
179
|
+
});
|
|
180
|
+
server.registerTool("keel_cloud_deploys", {
|
|
181
|
+
title: "Keel Cloud — deploy history",
|
|
182
|
+
description: "List recent preview/production deploys and logs for a site.",
|
|
183
|
+
inputSchema: {
|
|
184
|
+
site_id: z.number().int().describe("Site id"),
|
|
185
|
+
},
|
|
186
|
+
}, async ({ site_id }) => {
|
|
187
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/deploys`);
|
|
188
|
+
return jsonText(res.body, !res.ok);
|
|
189
|
+
});
|
|
190
|
+
server.registerTool("keel_cloud_list_secrets", {
|
|
191
|
+
title: "Keel Cloud — list secret keys",
|
|
192
|
+
description: "List vault key names for a site (values are never returned).",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
site_id: z.number().int().describe("Site id"),
|
|
195
|
+
},
|
|
196
|
+
}, async ({ site_id }) => {
|
|
197
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/secrets`);
|
|
198
|
+
return jsonText(res.body, !res.ok);
|
|
199
|
+
});
|
|
200
|
+
server.registerTool("keel_cloud_set_secret", {
|
|
201
|
+
title: "Keel Cloud — set secret",
|
|
202
|
+
description: "Store a secret in the site vault (never in git). Injected into the Worker on the next preview/publish.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
site_id: z.number().int().describe("Site id"),
|
|
205
|
+
key: z.string().min(1).describe("Env-style key, e.g. STRIPE_SECRET_KEY"),
|
|
206
|
+
value: z.string().describe("Secret value (will not be echoed by the dashboard)"),
|
|
207
|
+
},
|
|
208
|
+
}, async ({ site_id, key, value }) => {
|
|
209
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/secrets`, {
|
|
210
|
+
method: "PUT",
|
|
211
|
+
body: JSON.stringify({ key, value }),
|
|
212
|
+
});
|
|
213
|
+
return jsonText(res.body, !res.ok);
|
|
214
|
+
});
|
|
215
|
+
server.registerTool("keel_cloud_delete_secret", {
|
|
216
|
+
title: "Keel Cloud — delete secret",
|
|
217
|
+
description: "Remove a secret key from the site vault.",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
site_id: z.number().int().describe("Site id"),
|
|
220
|
+
key: z.string().min(1).describe("Secret key to remove"),
|
|
221
|
+
},
|
|
222
|
+
}, async ({ site_id, key }) => {
|
|
223
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/secrets?key=${encodeURIComponent(key)}`, { method: "DELETE" });
|
|
224
|
+
return jsonText(res.body, !res.ok);
|
|
225
|
+
});
|
|
226
|
+
server.registerTool("keel_cloud_set_custom_domain", {
|
|
227
|
+
title: "Keel Cloud — set custom domain",
|
|
228
|
+
description: "Set a Pro custom hostname (CNAME instructions returned). Optionally attach=true to verify DNS and attach to the production Worker.",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
site_id: z.number().int().describe("Site id"),
|
|
231
|
+
hostname: z.string().min(1).describe("Customer hostname, e.g. app.example.com"),
|
|
232
|
+
attach: z
|
|
233
|
+
.boolean()
|
|
234
|
+
.optional()
|
|
235
|
+
.describe("If true, verify DNS and attach Workers Custom Domain now"),
|
|
236
|
+
},
|
|
237
|
+
}, async ({ site_id, hostname, attach }) => {
|
|
238
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/custom-domain`, {
|
|
239
|
+
method: "PUT",
|
|
240
|
+
body: JSON.stringify({ hostname, attach: Boolean(attach) }),
|
|
241
|
+
});
|
|
242
|
+
return jsonText(res.body, !res.ok);
|
|
243
|
+
});
|
|
244
|
+
server.registerTool("keel_cloud_clear_custom_domain", {
|
|
245
|
+
title: "Keel Cloud — clear custom domain",
|
|
246
|
+
description: "Detach and clear the site's custom hostname (Pro).",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
site_id: z.number().int().describe("Site id"),
|
|
249
|
+
},
|
|
250
|
+
}, async ({ site_id }) => {
|
|
251
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/custom-domain`, { method: "DELETE" });
|
|
252
|
+
return jsonText(res.body, !res.ok);
|
|
253
|
+
});
|
|
254
|
+
server.registerTool("keel_cloud_export", {
|
|
255
|
+
title: "Keel Cloud — export manifest",
|
|
256
|
+
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.",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
site_id: z.number().int().describe("Site id"),
|
|
259
|
+
},
|
|
260
|
+
}, async ({ site_id }) => {
|
|
261
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/export`);
|
|
262
|
+
return jsonText(res.body, !res.ok);
|
|
263
|
+
});
|
|
264
|
+
server.registerTool("keel_cloud_export_sql", {
|
|
265
|
+
title: "Keel Cloud — export SQL dump",
|
|
266
|
+
description: "Download a portable .sql dump for local sqlite, preview D1, or production D1. Prefer production after publish for a full escape hatch.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
site_id: z.number().int().describe("Site id"),
|
|
269
|
+
env: z
|
|
270
|
+
.enum(["local", "preview", "production"])
|
|
271
|
+
.optional()
|
|
272
|
+
.describe("Which database to dump (default local)"),
|
|
273
|
+
},
|
|
274
|
+
}, async ({ site_id, env }) => {
|
|
275
|
+
const environment = env ?? "local";
|
|
276
|
+
const res = await cloudFetch(`/api/v1/sites/${site_id}/export/sql?env=${environment}`);
|
|
277
|
+
if (!res.ok)
|
|
278
|
+
return jsonText(res.body, true);
|
|
279
|
+
const sql = typeof res.body === "string" ? res.body : JSON.stringify(res.body);
|
|
280
|
+
return jsonText({
|
|
281
|
+
site_id,
|
|
282
|
+
env: environment,
|
|
283
|
+
sql,
|
|
284
|
+
hint: "Write this to a .sql file and restore with sqlite3 / D1 import on a real Keel app.",
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
return true;
|
|
288
|
+
}
|
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,20 @@ 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. Token binds to the user's first team.",
|
|
194
|
+
"Typical loop:",
|
|
195
|
+
"1. keel_cloud_create_site { name, preset }",
|
|
196
|
+
"2. Edit files at the returned storage_path (real Keel app)",
|
|
197
|
+
"3. keel_cloud_preview { site_id }",
|
|
198
|
+
"4. keel_cloud_publish { site_id, confirm: true } after user approval",
|
|
199
|
+
"Also: me, billing(+checkout/portal), list/get/delete/restore sites,",
|
|
200
|
+
"secrets (list/set/delete), custom domain (set/clear), deploys, export(+sql).",
|
|
201
|
+
]
|
|
202
|
+
: []),
|
|
186
203
|
].join("\n");
|
|
187
204
|
// ---- tools ----
|
|
188
205
|
server.registerTool("keel_overview", {
|
|
@@ -343,6 +360,7 @@ export async function createServer() {
|
|
|
343
360
|
for (const doc of manifest.docs) {
|
|
344
361
|
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
362
|
}
|
|
363
|
+
registerCloudTools(server);
|
|
346
364
|
return server;
|
|
347
365
|
}
|
|
348
366
|
/** Boot the server on stdio. Called by the `keel-mcp` bin and `keel mcp`. */
|
|
@@ -351,5 +369,8 @@ export async function runMcpServer() {
|
|
|
351
369
|
const transport = new StdioServerTransport();
|
|
352
370
|
await server.connect(transport);
|
|
353
371
|
// stdout is the protocol channel — log to stderr only.
|
|
354
|
-
|
|
372
|
+
const cloud = Boolean(process.env.KEEL_CLOUD_TOKEN?.trim());
|
|
373
|
+
console.error(cloud
|
|
374
|
+
? "⚓ Keel MCP server running on stdio (Cloud tools enabled)"
|
|
375
|
+
: "⚓ Keel MCP server running on stdio");
|
|
355
376
|
}
|
package/dist/teams/provider.js
CHANGED
|
@@ -14,7 +14,17 @@ import { dirname, join } from "node:path";
|
|
|
14
14
|
import { PackageProvider } from "../core/package.js";
|
|
15
15
|
import { defaultConfig, resolveConfig } from "./config.js";
|
|
16
16
|
import { teamsMigration } from "./migration.js";
|
|
17
|
-
|
|
17
|
+
function packageDir() {
|
|
18
|
+
try {
|
|
19
|
+
const url = import.meta.url;
|
|
20
|
+
if (!url)
|
|
21
|
+
return ".";
|
|
22
|
+
return dirname(fileURLToPath(url));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return ".";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
18
28
|
export class TeamsServiceProvider extends PackageProvider {
|
|
19
29
|
name = "teams";
|
|
20
30
|
config;
|
|
@@ -22,6 +32,6 @@ export class TeamsServiceProvider extends PackageProvider {
|
|
|
22
32
|
this.mergeConfig("teams", defaultConfig);
|
|
23
33
|
this.config = resolveConfig();
|
|
24
34
|
this.migrations([teamsMigration(this.config.userTable)]);
|
|
25
|
-
this.publishes({ [join(
|
|
35
|
+
this.publishes({ [join(packageDir(), "teams.config.stub")]: "config/teams.ts" }, "teams-config");
|
|
26
36
|
}
|
|
27
37
|
}
|
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.5",
|
|
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",
|
|
@@ -208,6 +208,13 @@
|
|
|
208
208
|
"path": "docs/hooks.md",
|
|
209
209
|
"example": "docs/examples/hooks.ts"
|
|
210
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
|
+
},
|
|
211
218
|
{
|
|
212
219
|
"slug": "i18n",
|
|
213
220
|
"title": "Internationalization",
|
|
@@ -3084,6 +3091,11 @@
|
|
|
3084
3091
|
"kind": "value",
|
|
3085
3092
|
"module": "tokens"
|
|
3086
3093
|
},
|
|
3094
|
+
{
|
|
3095
|
+
"name": "tokensMigration",
|
|
3096
|
+
"kind": "value",
|
|
3097
|
+
"module": "tokens"
|
|
3098
|
+
},
|
|
3087
3099
|
{
|
|
3088
3100
|
"name": "TooManyRequestsException",
|
|
3089
3101
|
"kind": "value",
|
package/docs/ai.md
CHANGED
|
@@ -61,6 +61,42 @@ 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. Create a token at `/tokens` on the control plane — the plaintext
|
|
66
|
+
looks like `keel_<selector>.<verifier>`.
|
|
67
|
+
|
|
68
|
+
| Tool | What it does |
|
|
69
|
+
|------|--------------|
|
|
70
|
+
| `keel_cloud_me` | Authenticated Cloud user (plan, site limit, team) |
|
|
71
|
+
| `keel_cloud_billing` | Team plan / limits / owner flag |
|
|
72
|
+
| `keel_cloud_billing_checkout` / `_portal` | Stripe Checkout or Customer Portal URL (owner) |
|
|
73
|
+
| `keel_cloud_list_sites` / `keel_cloud_get_site` | List or fetch a site (`storage_path`, hostnames, git) |
|
|
74
|
+
| `keel_cloud_create_site` | Create from preset (`minimal` \| `api` \| `app` \| `saas`) |
|
|
75
|
+
| `keel_cloud_delete_site` / `keel_cloud_restore_site` | Soft-delete (confirm) / restore |
|
|
76
|
+
| `keel_cloud_preview` | Deploy preview Worker |
|
|
77
|
+
| `keel_cloud_publish` | Publish production — requires `confirm: true` |
|
|
78
|
+
| `keel_cloud_deploys` | Deploy history + logs |
|
|
79
|
+
| `keel_cloud_list_secrets` / `keel_cloud_set_secret` / `keel_cloud_delete_secret` | Vault keys (values never returned) |
|
|
80
|
+
| `keel_cloud_set_custom_domain` / `keel_cloud_clear_custom_domain` | Pro custom hostname (+ optional attach) |
|
|
81
|
+
| `keel_cloud_export` / `keel_cloud_export_sql` | Export manifest / portable `.sql` dump |
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"keel": {
|
|
87
|
+
"command": "npx",
|
|
88
|
+
"args": ["-y", "keel-mcp"],
|
|
89
|
+
"env": {
|
|
90
|
+
"KEEL_CLOUD_TOKEN": "keel_….…",
|
|
91
|
+
"KEEL_CLOUD_URL": "https://app.keeljs.cloud"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The token binds to the user's **first team**. Switch teams in the dashboard before
|
|
99
|
+
minting a token if you need a different team context.
|
|
64
100
|
### Resources
|
|
65
101
|
|
|
66
102
|
- `keel://overview` — the same orientation text as `keel_overview`
|
|
@@ -75,6 +111,16 @@ package, so it always matches your installed version.
|
|
|
75
111
|
4. `keel_scaffold { kind: "controller", name: "Post", resource: true }` → get the stub.
|
|
76
112
|
5. Write the file, add the route, run `npm run typecheck`.
|
|
77
113
|
|
|
114
|
+
### A typical Keel Cloud loop
|
|
115
|
+
|
|
116
|
+
1. `keel_cloud_create_site { name: "Acme", preset: "app" }`
|
|
117
|
+
2. Edit the returned `storage_path` (real Keel app + git)
|
|
118
|
+
3. `keel_cloud_set_secret` for env the Worker needs
|
|
119
|
+
4. `keel_cloud_preview { site_id }`
|
|
120
|
+
5. `keel_cloud_publish { site_id, confirm: true }` after the user approves
|
|
121
|
+
6. Optional Pro: `keel_cloud_set_custom_domain { hostname, attach: true }`
|
|
122
|
+
7. Escape hatch anytime: `keel_cloud_export` + `keel_cloud_export_sql`
|
|
123
|
+
|
|
78
124
|
## `llms.txt` and `llms-full.txt`
|
|
79
125
|
|
|
80
126
|
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,34 @@ 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.5] — 2026-07-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Full Keel Cloud MCP surface** — billing status/checkout/portal, soft-delete
|
|
12
|
+
and restore sites, list/delete secrets, set/clear custom domains. Documented in
|
|
13
|
+
[Building with AI](./docs/ai.md). Pairs with expanded `/api/v1` routes on Keel
|
|
14
|
+
Cloud.
|
|
15
|
+
|
|
16
|
+
[0.83.5]: https://github.com/shaferllc/keel/releases/tag/v0.83.5
|
|
17
|
+
|
|
18
|
+
## [0.83.4] — 2026-07-13
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **MCP Cloud tools** (`keel_cloud_*`) — when `KEEL_CLOUD_TOKEN` is set,
|
|
23
|
+
`keel-mcp` registers create/list/preview/publish/secrets/export tools against
|
|
24
|
+
a Keel Cloud control plane. Documented in [Building with AI](./docs/ai.md).
|
|
25
|
+
- **`tokensMigration()`** — personal access tokens schema helper for apps that
|
|
26
|
+
need the table via `keel migrate`.
|
|
27
|
+
- **Billing customer portal** — `user.billingPortal(returnUrl)` (Stripe + Fake
|
|
28
|
+
gateway); opens the hosted portal to manage card / cancel.
|
|
29
|
+
- **Hosting guide** — [`docs/hosting.md`](./docs/hosting.md) + example harness
|
|
30
|
+
for `@shaferllc/keel/hosting`.
|
|
31
|
+
- **Tests** for gates, hosting, billing portal, and `tokensMigration`.
|
|
32
|
+
|
|
33
|
+
[0.83.4]: https://github.com/shaferllc/keel/releases/tag/v0.83.4
|
|
34
|
+
|
|
7
35
|
## [0.83.3] — 2026-07-12
|
|
8
36
|
|
|
9
37
|
### Added
|
|
@@ -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/hosting.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Hosting
|
|
2
|
+
|
|
3
|
+
Keel Hosting is a small toolkit for **hosted Workers / D1 apps**: a Cloudflare
|
|
4
|
+
REST client, hostname helpers, a SQLite-compatible SQL dump, and purpose-scoped
|
|
5
|
+
secret encryption. It ships as `@shaferllc/keel/hosting`.
|
|
6
|
+
|
|
7
|
+
This is infrastructure — not a control plane. Site orchestration, plans, and
|
|
8
|
+
deploy loops live in your app (for example Keel Cloud).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import {
|
|
14
|
+
CloudflareClient,
|
|
15
|
+
cloudflareConfigured,
|
|
16
|
+
normalizeHostname,
|
|
17
|
+
isValidHostname,
|
|
18
|
+
zoneCandidates,
|
|
19
|
+
dumpConnection,
|
|
20
|
+
normalizeSecretKey,
|
|
21
|
+
encryptSecretValue,
|
|
22
|
+
decryptSecretValue,
|
|
23
|
+
resolveSecretRows,
|
|
24
|
+
} from "@shaferllc/keel/hosting";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No service provider — import what you need.
|
|
28
|
+
|
|
29
|
+
## Cloudflare
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const creds = {
|
|
33
|
+
accountId: process.env.CF_ACCOUNT_ID!,
|
|
34
|
+
apiToken: process.env.CF_API_TOKEN!,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (!cloudflareConfigured(creds)) {
|
|
38
|
+
throw new Error("Cloudflare credentials missing");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cf = new CloudflareClient(creds);
|
|
42
|
+
const db = await cf.createD1Database("kc-acme");
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Credentials are constructor args — no app config coupling. Optional
|
|
46
|
+
`pinnedZoneId` / `pinnedZoneName` skip a zone lookup when you already know the
|
|
47
|
+
primary zone.
|
|
48
|
+
|
|
49
|
+
## Hostnames
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const host = normalizeHostname("https://App.Example.com/"); // "app.example.com"
|
|
53
|
+
isValidHostname(host); // true
|
|
54
|
+
zoneCandidates(host); // ["app.example.com", "example.com"]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`zoneCandidates` walks from most-specific to apex — useful when attaching a
|
|
58
|
+
Workers Custom Domain and you need to find which zone owns the name.
|
|
59
|
+
|
|
60
|
+
## SQL dump
|
|
61
|
+
|
|
62
|
+
Dump any SQLite-compatible `Connection` to a portable `.sql` script (schema +
|
|
63
|
+
data). Useful for export / escape hatches:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { db } from "@shaferllc/keel/core";
|
|
67
|
+
import { dumpConnection } from "@shaferllc/keel/hosting";
|
|
68
|
+
|
|
69
|
+
const sql = await dumpConnection(db(), "Acme local D1", { generatedBy: "Keel Cloud" });
|
|
70
|
+
// write sql to a .sql file; restore with sqlite3 / D1 import
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Secrets
|
|
74
|
+
|
|
75
|
+
Encrypt vault values with Keel's purpose-scoped encryption (`config('app.key')`
|
|
76
|
+
must be set). Keys are normalized to `ENV_STYLE` identifiers:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const key = normalizeSecretKey("stripe-secret-key"); // "STRIPE_SECRET_KEY"
|
|
80
|
+
const encrypted = await encryptSecretValue(secret, "app-secret");
|
|
81
|
+
const plain = await decryptSecretValue(encrypted, "app-secret");
|
|
82
|
+
|
|
83
|
+
const env = await resolveSecretRows(
|
|
84
|
+
[{ key: "STRIPE_SECRET_KEY", value_encrypted: encrypted }],
|
|
85
|
+
"app-secret",
|
|
86
|
+
);
|
|
87
|
+
// { STRIPE_SECRET_KEY: "…" }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Your app owns the table of rows (`owner_id`, `key`, `value_encrypted`); hosting
|
|
91
|
+
only encrypts and decrypts.
|
|
92
|
+
|
|
93
|
+
## Related
|
|
94
|
+
|
|
95
|
+
- [Gates](./gates.md) — private-alpha signup gating used by hosted control planes
|
|
96
|
+
- [Starter kits](./starter-kits.md) — presets Cloud scaffolds from
|
|
97
|
+
- [Building with AI](./ai.md) — MCP Cloud tools (`keel_cloud_*`) that drive hosting
|
package/llms-full.txt
CHANGED
|
@@ -4725,6 +4725,42 @@ package, so it always matches your installed version.
|
|
|
4725
4725
|
| `keel_list_generators` | The `keel make:*` generators, what they produce, and their flags. |
|
|
4726
4726
|
| `keel_scaffold` | Generate a controller/provider/middleware/factory/seeder/job/notification/transformer stub. Returns code + target path — it does **not** write to disk. |
|
|
4727
4727
|
|
|
4728
|
+
When `KEEL_CLOUD_TOKEN` (and optional `KEEL_CLOUD_URL`) is set, Cloud tools are
|
|
4729
|
+
also registered. Create a token at `/tokens` on the control plane — the plaintext
|
|
4730
|
+
looks like `keel_<selector>.<verifier>`.
|
|
4731
|
+
|
|
4732
|
+
| Tool | What it does |
|
|
4733
|
+
|------|--------------|
|
|
4734
|
+
| `keel_cloud_me` | Authenticated Cloud user (plan, site limit, team) |
|
|
4735
|
+
| `keel_cloud_billing` | Team plan / limits / owner flag |
|
|
4736
|
+
| `keel_cloud_billing_checkout` / `_portal` | Stripe Checkout or Customer Portal URL (owner) |
|
|
4737
|
+
| `keel_cloud_list_sites` / `keel_cloud_get_site` | List or fetch a site (`storage_path`, hostnames, git) |
|
|
4738
|
+
| `keel_cloud_create_site` | Create from preset (`minimal` \| `api` \| `app` \| `saas`) |
|
|
4739
|
+
| `keel_cloud_delete_site` / `keel_cloud_restore_site` | Soft-delete (confirm) / restore |
|
|
4740
|
+
| `keel_cloud_preview` | Deploy preview Worker |
|
|
4741
|
+
| `keel_cloud_publish` | Publish production — requires `confirm: true` |
|
|
4742
|
+
| `keel_cloud_deploys` | Deploy history + logs |
|
|
4743
|
+
| `keel_cloud_list_secrets` / `keel_cloud_set_secret` / `keel_cloud_delete_secret` | Vault keys (values never returned) |
|
|
4744
|
+
| `keel_cloud_set_custom_domain` / `keel_cloud_clear_custom_domain` | Pro custom hostname (+ optional attach) |
|
|
4745
|
+
| `keel_cloud_export` / `keel_cloud_export_sql` | Export manifest / portable `.sql` dump |
|
|
4746
|
+
|
|
4747
|
+
```json
|
|
4748
|
+
{
|
|
4749
|
+
"mcpServers": {
|
|
4750
|
+
"keel": {
|
|
4751
|
+
"command": "npx",
|
|
4752
|
+
"args": ["-y", "keel-mcp"],
|
|
4753
|
+
"env": {
|
|
4754
|
+
"KEEL_CLOUD_TOKEN": "keel_….…",
|
|
4755
|
+
"KEEL_CLOUD_URL": "https://app.keeljs.cloud"
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
```
|
|
4761
|
+
|
|
4762
|
+
The token binds to the user's **first team**. Switch teams in the dashboard before
|
|
4763
|
+
minting a token if you need a different team context.
|
|
4728
4764
|
### Resources
|
|
4729
4765
|
|
|
4730
4766
|
- `keel://overview` — the same orientation text as `keel_overview`
|
|
@@ -4739,6 +4775,16 @@ package, so it always matches your installed version.
|
|
|
4739
4775
|
4. `keel_scaffold { kind: "controller", name: "Post", resource: true }` → get the stub.
|
|
4740
4776
|
5. Write the file, add the route, run `npm run typecheck`.
|
|
4741
4777
|
|
|
4778
|
+
### A typical Keel Cloud loop
|
|
4779
|
+
|
|
4780
|
+
1. `keel_cloud_create_site { name: "Acme", preset: "app" }`
|
|
4781
|
+
2. Edit the returned `storage_path` (real Keel app + git)
|
|
4782
|
+
3. `keel_cloud_set_secret` for env the Worker needs
|
|
4783
|
+
4. `keel_cloud_preview { site_id }`
|
|
4784
|
+
5. `keel_cloud_publish { site_id, confirm: true }` after the user approves
|
|
4785
|
+
6. Optional Pro: `keel_cloud_set_custom_domain { hostname, attach: true }`
|
|
4786
|
+
7. Escape hatch anytime: `keel_cloud_export` + `keel_cloud_export_sql`
|
|
4787
|
+
|
|
4742
4788
|
## `llms.txt` and `llms-full.txt`
|
|
4743
4789
|
|
|
4744
4790
|
At the package root:
|
|
@@ -5716,6 +5762,15 @@ const intent = await user.createSetupIntent(); // return intent.clientSecret to
|
|
|
5716
5762
|
const methods = await user.paymentMethods();
|
|
5717
5763
|
```
|
|
5718
5764
|
|
|
5765
|
+
### Customer portal (Stripe)
|
|
5766
|
+
|
|
5767
|
+
Send the customer to Stripe's hosted portal to update their card or cancel:
|
|
5768
|
+
|
|
5769
|
+
```ts
|
|
5770
|
+
const portal = await user.billingPortal("https://app.example.com/billing");
|
|
5771
|
+
// redirect to portal.url
|
|
5772
|
+
```
|
|
5773
|
+
|
|
5719
5774
|
These are Stripe-only capabilities; calling them on the Paddle gateway throws a
|
|
5720
5775
|
`BillingError` (Paddle collects cards in its own hosted checkout).
|
|
5721
5776
|
|
|
@@ -5810,7 +5865,7 @@ real gateway from `config/billing.ts` and `.env`.
|
|
|
5810
5865
|
|---------|--------|--------|
|
|
5811
5866
|
| Create a subscription server-side | `create(pmId)` | Not supported — use `checkout()`; the webhook creates the local row |
|
|
5812
5867
|
| One-off `charge()` | Confirms a PaymentIntent | Not supported — use `checkout({ mode })` / transactions |
|
|
5813
|
-
| SetupIntent / `paymentMethods()` | Supported | Throws `BillingError` (hosted checkout) |
|
|
5868
|
+
| SetupIntent / `paymentMethods()` / `billingPortal()` | Supported | Throws `BillingError` (hosted checkout) |
|
|
5814
5869
|
| Checkout handle | `session.url` (redirect) | `session.clientToken` (overlay/inline) |
|
|
5815
5870
|
| Webhook signature | `Stripe-Signature: t=…,v1=…` | `Paddle-Signature: ts=…;h1=…` |
|
|
5816
5871
|
|
|
@@ -10679,6 +10734,110 @@ The signature of `onReady` / `onShutdown` hooks.
|
|
|
10679
10734
|
|
|
10680
10735
|
|
|
10681
10736
|
|
|
10737
|
+
---
|
|
10738
|
+
|
|
10739
|
+
<!-- source: docs/hosting.md -->
|
|
10740
|
+
|
|
10741
|
+
# Hosting
|
|
10742
|
+
|
|
10743
|
+
Keel Hosting is a small toolkit for **hosted Workers / D1 apps**: a Cloudflare
|
|
10744
|
+
REST client, hostname helpers, a SQLite-compatible SQL dump, and purpose-scoped
|
|
10745
|
+
secret encryption. It ships as `@shaferllc/keel/hosting`.
|
|
10746
|
+
|
|
10747
|
+
This is infrastructure — not a control plane. Site orchestration, plans, and
|
|
10748
|
+
deploy loops live in your app (for example Keel Cloud).
|
|
10749
|
+
|
|
10750
|
+
## Install
|
|
10751
|
+
|
|
10752
|
+
```ts
|
|
10753
|
+
import {
|
|
10754
|
+
CloudflareClient,
|
|
10755
|
+
cloudflareConfigured,
|
|
10756
|
+
normalizeHostname,
|
|
10757
|
+
isValidHostname,
|
|
10758
|
+
zoneCandidates,
|
|
10759
|
+
dumpConnection,
|
|
10760
|
+
normalizeSecretKey,
|
|
10761
|
+
encryptSecretValue,
|
|
10762
|
+
decryptSecretValue,
|
|
10763
|
+
resolveSecretRows,
|
|
10764
|
+
} from "@shaferllc/keel/hosting";
|
|
10765
|
+
```
|
|
10766
|
+
|
|
10767
|
+
No service provider — import what you need.
|
|
10768
|
+
|
|
10769
|
+
## Cloudflare
|
|
10770
|
+
|
|
10771
|
+
```ts
|
|
10772
|
+
const creds = {
|
|
10773
|
+
accountId: process.env.CF_ACCOUNT_ID!,
|
|
10774
|
+
apiToken: process.env.CF_API_TOKEN!,
|
|
10775
|
+
};
|
|
10776
|
+
|
|
10777
|
+
if (!cloudflareConfigured(creds)) {
|
|
10778
|
+
throw new Error("Cloudflare credentials missing");
|
|
10779
|
+
}
|
|
10780
|
+
|
|
10781
|
+
const cf = new CloudflareClient(creds);
|
|
10782
|
+
const db = await cf.createD1Database("kc-acme");
|
|
10783
|
+
```
|
|
10784
|
+
|
|
10785
|
+
Credentials are constructor args — no app config coupling. Optional
|
|
10786
|
+
`pinnedZoneId` / `pinnedZoneName` skip a zone lookup when you already know the
|
|
10787
|
+
primary zone.
|
|
10788
|
+
|
|
10789
|
+
## Hostnames
|
|
10790
|
+
|
|
10791
|
+
```ts
|
|
10792
|
+
const host = normalizeHostname("https://App.Example.com/"); // "app.example.com"
|
|
10793
|
+
isValidHostname(host); // true
|
|
10794
|
+
zoneCandidates(host); // ["app.example.com", "example.com"]
|
|
10795
|
+
```
|
|
10796
|
+
|
|
10797
|
+
`zoneCandidates` walks from most-specific to apex — useful when attaching a
|
|
10798
|
+
Workers Custom Domain and you need to find which zone owns the name.
|
|
10799
|
+
|
|
10800
|
+
## SQL dump
|
|
10801
|
+
|
|
10802
|
+
Dump any SQLite-compatible `Connection` to a portable `.sql` script (schema +
|
|
10803
|
+
data). Useful for export / escape hatches:
|
|
10804
|
+
|
|
10805
|
+
```ts
|
|
10806
|
+
import { db } from "@shaferllc/keel/core";
|
|
10807
|
+
import { dumpConnection } from "@shaferllc/keel/hosting";
|
|
10808
|
+
|
|
10809
|
+
const sql = await dumpConnection(db(), "Acme local D1", { generatedBy: "Keel Cloud" });
|
|
10810
|
+
// write sql to a .sql file; restore with sqlite3 / D1 import
|
|
10811
|
+
```
|
|
10812
|
+
|
|
10813
|
+
## Secrets
|
|
10814
|
+
|
|
10815
|
+
Encrypt vault values with Keel's purpose-scoped encryption (`config('app.key')`
|
|
10816
|
+
must be set). Keys are normalized to `ENV_STYLE` identifiers:
|
|
10817
|
+
|
|
10818
|
+
```ts
|
|
10819
|
+
const key = normalizeSecretKey("stripe-secret-key"); // "STRIPE_SECRET_KEY"
|
|
10820
|
+
const encrypted = await encryptSecretValue(secret, "app-secret");
|
|
10821
|
+
const plain = await decryptSecretValue(encrypted, "app-secret");
|
|
10822
|
+
|
|
10823
|
+
const env = await resolveSecretRows(
|
|
10824
|
+
[{ key: "STRIPE_SECRET_KEY", value_encrypted: encrypted }],
|
|
10825
|
+
"app-secret",
|
|
10826
|
+
);
|
|
10827
|
+
// { STRIPE_SECRET_KEY: "…" }
|
|
10828
|
+
```
|
|
10829
|
+
|
|
10830
|
+
Your app owns the table of rows (`owner_id`, `key`, `value_encrypted`); hosting
|
|
10831
|
+
only encrypts and decrypts.
|
|
10832
|
+
|
|
10833
|
+
## Related
|
|
10834
|
+
|
|
10835
|
+
- [Gates](./gates.md) — private-alpha signup gating used by hosted control planes
|
|
10836
|
+
- [Starter kits](./starter-kits.md) — presets Cloud scaffolds from
|
|
10837
|
+
- [Building with AI](./ai.md) — MCP Cloud tools (`keel_cloud_*`) that drive hosting
|
|
10838
|
+
|
|
10839
|
+
|
|
10840
|
+
|
|
10682
10841
|
---
|
|
10683
10842
|
|
|
10684
10843
|
<!-- source: docs/i18n.md -->
|
package/llms.txt
CHANGED
|
@@ -37,6 +37,7 @@ Userland imports everything from `@shaferllc/keel/core`.
|
|
|
37
37
|
- [Helpers](https://github.com/shaferllc/keel/blob/main/docs/helpers.md): Keel gives you a handful of global helper functions so you can reach the running application from anywhere — a route handler, a model, a plain function — without threading a container reference through every call.
|
|
38
38
|
- [Built on Hono](https://github.com/shaferllc/keel/blob/main/docs/hono.md): Keel's HTTP layer is Hono — an ultrafast, web-standard router that runs on Node, Cloudflare Workers, Deno, Bun, and more.
|
|
39
39
|
- [Lifecycle Hooks](https://github.com/shaferllc/keel/blob/main/docs/hooks.md): Tap into the application lifecycle — run code once the app is ready, clean up on shutdown, and observe route registration.
|
|
40
|
+
- [Hosting](https://github.com/shaferllc/keel/blob/main/docs/hosting.md): 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.
|
|
40
41
|
- [Internationalization](https://github.com/shaferllc/keel/blob/main/docs/i18n.md): Translations with ICU message formatting, plus the Intl formatters that go with them.
|
|
41
42
|
- [Inertia](https://github.com/shaferllc/keel/blob/main/docs/inertia.md): Keel ships a server-side Inertia.js adapter.
|
|
42
43
|
- [Locks](https://github.com/shaferllc/keel/blob/main/docs/locks.md): "Only one of you may do this at a time" — across processes, across nodes.
|
|
@@ -104,6 +105,7 @@ Every topic has a runnable, type-checked example:
|
|
|
104
105
|
- [Helpers example](https://github.com/shaferllc/keel/blob/main/docs/examples/helpers.ts)
|
|
105
106
|
- [Built on Hono example](https://github.com/shaferllc/keel/blob/main/docs/examples/hono.ts)
|
|
106
107
|
- [Lifecycle Hooks example](https://github.com/shaferllc/keel/blob/main/docs/examples/hooks.ts)
|
|
108
|
+
- [Hosting example](https://github.com/shaferllc/keel/blob/main/docs/examples/hosting.ts)
|
|
107
109
|
- [Internationalization example](https://github.com/shaferllc/keel/blob/main/docs/examples/i18n.ts)
|
|
108
110
|
- [Inertia example](https://github.com/shaferllc/keel/blob/main/docs/examples/inertia.ts)
|
|
109
111
|
- [Locks example](https://github.com/shaferllc/keel/blob/main/docs/examples/locks.ts)
|
package/package.json
CHANGED