@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.
Files changed (44) hide show
  1. package/README.md +7 -24
  2. package/dist/billing/billable.d.ts +1 -0
  3. package/dist/billing/billable.js +8 -0
  4. package/dist/billing/drivers/fake.d.ts +4 -0
  5. package/dist/billing/drivers/fake.js +5 -0
  6. package/dist/billing/drivers/stripe.d.ts +1 -0
  7. package/dist/billing/drivers/stripe.js +7 -0
  8. package/dist/billing/gateway.d.ts +7 -0
  9. package/dist/billing/index.d.ts +1 -1
  10. package/dist/core/index.d.ts +1 -1
  11. package/dist/core/index.js +1 -1
  12. package/dist/core/tokens.d.ts +2 -0
  13. package/dist/core/tokens.js +24 -0
  14. package/dist/gates/index.d.ts +10 -0
  15. package/dist/gates/index.js +9 -0
  16. package/dist/gates/models.d.ts +42 -0
  17. package/dist/gates/models.js +76 -0
  18. package/dist/gates/provider.d.ts +10 -0
  19. package/dist/gates/provider.js +13 -0
  20. package/dist/hosting/cloudflare.d.ts +43 -0
  21. package/dist/hosting/cloudflare.js +66 -0
  22. package/dist/hosting/dump.d.ts +7 -0
  23. package/dist/hosting/dump.js +58 -0
  24. package/dist/hosting/hostname.d.ts +7 -0
  25. package/dist/hosting/hostname.js +25 -0
  26. package/dist/hosting/index.d.ts +14 -0
  27. package/dist/hosting/index.js +12 -0
  28. package/dist/hosting/secrets.d.ts +15 -0
  29. package/dist/hosting/secrets.js +27 -0
  30. package/dist/mcp/cloud.d.ts +9 -0
  31. package/dist/mcp/cloud.js +179 -0
  32. package/dist/mcp/server.d.ts +2 -0
  33. package/dist/mcp/server.js +20 -1
  34. package/docs/ai-manifest.json +20 -1
  35. package/docs/ai.md +36 -0
  36. package/docs/billing.md +10 -1
  37. package/docs/changelog.md +30 -0
  38. package/docs/examples/gates.ts +30 -0
  39. package/docs/examples/hosting.ts +37 -0
  40. package/docs/gates.md +75 -0
  41. package/docs/hosting.md +97 -0
  42. package/llms-full.txt +232 -1
  43. package/llms.txt +4 -0
  44. package/package.json +9 -1
package/README.md CHANGED
@@ -161,6 +161,8 @@ src/db/ Database adapters (D1, Postgres, libSQL)
161
161
  src/api/ CRUD REST resources from a model
162
162
  src/openapi/ Generates an OpenAPI spec from the routes
163
163
  src/billing/ Subscription billing — Stripe + Paddle
164
+ src/gates/ Signup gates — invite codes & email allowlist
165
+ src/hosting/ Cloudflare / dump / secrets helpers for hosted apps
164
166
  src/watch/ The debug dashboard
165
167
  src/mcp/ The MCP server (docs + API for AI agents)
166
168
  src/vite/ The Vite plugin
@@ -203,7 +205,9 @@ See [docs/architecture.md](./docs/architecture.md) for the full picture.
203
205
  | [Lifecycle Hooks](./docs/hooks.md) | onReady/onShutdown, graceful shutdown, onRoute |
204
206
  | [Sessions](./docs/sessions.md) | Cookie-backed sessions, flash messages |
205
207
  | [Authentication](./docs/authentication.md) | Session auth, guards, user provider |
206
- | [Authorization](./docs/authorization.md) | Gates & policies, `can`/`authorize` |
208
+ | [Gates](./docs/gates.md) | Signup gating — invite codes & email allowlist |
209
+ | [Hosting](./docs/hosting.md) | Cloudflare, SQL dump, secrets for hosted Workers |
210
+ | [Authorization](./docs/authorization.md) | Ability gates & policies, `can`/`authorize` |
207
211
  | [Database](./docs/database.md) | Driver-agnostic query builder |
208
212
  | [Models](./docs/models.md) | Active-record: find/create/save, casts, relations |
209
213
  | [Migrations](./docs/migrations.md) | Schema builder + migrator, dialect-aware |
@@ -266,29 +270,8 @@ lines / ~91% branches**.
266
270
 
267
271
  ## Roadmap
268
272
 
269
- Keel's first release is the **MVP core**: container, routing, middleware,
270
- config, and the console. On deck:
271
-
272
- - [x] View / templating layer (Hono JSX) — **v0.2.0**
273
- - [x] Cloudflare Workers–safe core — **v0.2.0**
274
- - [x] Global helpers: `config()`, `app()`, `view()` — **v0.3.0 / v0.4.0**
275
- - [x] Error & exception handling — **v0.5.0**
276
- - [x] Request/response + container helpers — **v0.6.0–v0.9.0**
277
- - [x] Validation (Zod-compatible) — **v0.10.0**
278
- - [x] First-class routing (groups, resources, named routes) — **v0.11.0**
279
- - [x] Domain routing, matchers, Inertia adapter — **v0.12.0**
280
- - [x] Test suite (~99% coverage)
281
- - [x] Query builder (driver-agnostic) — **v0.28.0**
282
- - [x] Active-record Model layer — **v0.29.0**
283
- - [x] Migrations (schema builder) — **v0.30.0**
284
- - [x] Model relationships (hasMany / belongsTo / belongsToMany) — **v0.31.0**
285
- - [x] Factories & seeders (built-in Faker) — **v0.32.0**
286
- - [x] Model attribute casts + mass-assignment guarding — **v0.33.0**
287
- - [x] Mail (fluent mailer, pluggable transports) — **v0.34.0**
288
- - [x] Queues / background jobs (pluggable drivers) — **v0.35.0**
289
- - [x] Notifications (multi-channel, queueable) — **v0.36.0**
290
- - [ ] Publish `src/core` as the `@keel/core` package
291
-
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.
292
275
  See [CHANGELOG.md](./CHANGELOG.md) for release history.
293
276
 
294
277
  ## Contributing
@@ -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
@@ -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 {
@@ -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";
@@ -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";
@@ -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";
@@ -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;
@@ -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 Gates — private-alpha / waitlist signup gating.
3
+ *
4
+ * import { GatesServiceProvider, canRegister, redeemInvite } from "@shaferllc/keel/gates";
5
+ *
6
+ * Not the same as team invitations in `@shaferllc/keel/teams`.
7
+ */
8
+ export { GatesServiceProvider } from "./provider.js";
9
+ export { InviteCode, EmailAllowlist, canRegister, redeemInvite, gatesMigration, } from "./models.js";
10
+ export type { GateCheck } from "./models.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Keel Gates — private-alpha / waitlist signup gating.
3
+ *
4
+ * import { GatesServiceProvider, canRegister, redeemInvite } from "@shaferllc/keel/gates";
5
+ *
6
+ * Not the same as team invitations in `@shaferllc/keel/teams`.
7
+ */
8
+ export { GatesServiceProvider } from "./provider.js";
9
+ export { InviteCode, EmailAllowlist, canRegister, redeemInvite, gatesMigration, } from "./models.js";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Signup gates — invite codes + email allowlist for private alpha / waitlist.
3
+ * Distinct from team invitations (`@shaferllc/keel/teams`).
4
+ */
5
+ import { Model } from "../core/model.js";
6
+ import type { Migration } from "../core/migrations.js";
7
+ export declare class InviteCode extends Model {
8
+ static table: string;
9
+ static fillable: string[];
10
+ static timestamps: boolean;
11
+ id: number;
12
+ code: string;
13
+ max_uses: number;
14
+ uses: number;
15
+ created_by: number | null;
16
+ expires_at: string | null;
17
+ }
18
+ export declare class EmailAllowlist extends Model {
19
+ static table: string;
20
+ static fillable: string[];
21
+ static timestamps: boolean;
22
+ id: number;
23
+ email: string;
24
+ created_by: number | null;
25
+ }
26
+ export type GateCheck = {
27
+ ok: true;
28
+ via: "allowlist" | "code";
29
+ invite?: InviteCode;
30
+ } | {
31
+ ok: false;
32
+ reason: string;
33
+ };
34
+ /** Private alpha: allowlisted email OR a valid invite code. */
35
+ export declare function canRegister(email: string, inviteCode?: string): Promise<GateCheck>;
36
+ export declare function redeemInvite(invite: InviteCode): Promise<void>;
37
+ /**
38
+ * Schema for invite codes + allowlist.
39
+ * Uses IF NOT EXISTS so apps that already created these tables (e.g. early
40
+ * keel-cloud) can still register the provider safely.
41
+ */
42
+ export declare function gatesMigration(): Migration;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Signup gates — invite codes + email allowlist for private alpha / waitlist.
3
+ * Distinct from team invitations (`@shaferllc/keel/teams`).
4
+ */
5
+ import { Model } from "../core/model.js";
6
+ export class InviteCode extends Model {
7
+ static table = "invite_codes";
8
+ static fillable = ["code", "max_uses", "uses", "created_by", "expires_at"];
9
+ static timestamps = true;
10
+ }
11
+ export class EmailAllowlist extends Model {
12
+ static table = "email_allowlist";
13
+ static fillable = ["email", "created_by"];
14
+ static timestamps = true;
15
+ }
16
+ /** Private alpha: allowlisted email OR a valid invite code. */
17
+ export async function canRegister(email, inviteCode) {
18
+ const normalized = email.toLowerCase().trim();
19
+ const allowed = await EmailAllowlist.newQuery().where("email", normalized).first();
20
+ if (allowed)
21
+ return { ok: true, via: "allowlist" };
22
+ const code = String(inviteCode ?? "").trim();
23
+ if (!code) {
24
+ return { ok: false, reason: "Registration requires an invite code or allowlisted email." };
25
+ }
26
+ const invite = await InviteCode.newQuery().where("code", code).first();
27
+ if (!invite)
28
+ return { ok: false, reason: "That invite code is invalid." };
29
+ if (invite.expires_at && new Date(invite.expires_at).getTime() < Date.now()) {
30
+ return { ok: false, reason: "That invite code has expired." };
31
+ }
32
+ if (invite.uses >= invite.max_uses) {
33
+ return { ok: false, reason: "That invite code has already been used up." };
34
+ }
35
+ return { ok: true, via: "code", invite };
36
+ }
37
+ export async function redeemInvite(invite) {
38
+ await invite.update({ uses: invite.uses + 1 });
39
+ }
40
+ /**
41
+ * Schema for invite codes + allowlist.
42
+ * Uses IF NOT EXISTS so apps that already created these tables (e.g. early
43
+ * keel-cloud) can still register the provider safely.
44
+ */
45
+ export function gatesMigration() {
46
+ return {
47
+ name: "gates_00_invite_allowlist",
48
+ async up(schema) {
49
+ await schema.raw(`
50
+ CREATE TABLE IF NOT EXISTS invite_codes (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ code VARCHAR(255) NOT NULL UNIQUE,
53
+ max_uses INTEGER DEFAULT 1,
54
+ uses INTEGER DEFAULT 0,
55
+ created_by INTEGER,
56
+ expires_at TIMESTAMP,
57
+ created_at TIMESTAMP,
58
+ updated_at TIMESTAMP
59
+ )
60
+ `);
61
+ await schema.raw(`
62
+ CREATE TABLE IF NOT EXISTS email_allowlist (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ email VARCHAR(255) NOT NULL UNIQUE,
65
+ created_by INTEGER,
66
+ created_at TIMESTAMP,
67
+ updated_at TIMESTAMP
68
+ )
69
+ `);
70
+ },
71
+ async down(schema) {
72
+ await schema.dropTable("email_allowlist").catch(() => { });
73
+ await schema.dropTable("invite_codes").catch(() => { });
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,10 @@
1
+ import { PackageProvider } from "../core/package.js";
2
+ /**
3
+ * Signup gates — invite codes + email allowlist.
4
+ *
5
+ * app.register(GatesServiceProvider)
6
+ */
7
+ export declare class GatesServiceProvider extends PackageProvider {
8
+ readonly name = "gates";
9
+ register(): void;
10
+ }
@@ -0,0 +1,13 @@
1
+ import { PackageProvider } from "../core/package.js";
2
+ import { gatesMigration } from "./models.js";
3
+ /**
4
+ * Signup gates — invite codes + email allowlist.
5
+ *
6
+ * app.register(GatesServiceProvider)
7
+ */
8
+ export class GatesServiceProvider extends PackageProvider {
9
+ name = "gates";
10
+ register() {
11
+ this.migrations([gatesMigration()]);
12
+ }
13
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Cloudflare REST client — D1 + Workers Custom Domains.
3
+ * Credentials are constructor args (no app config coupling).
4
+ */
5
+ export type CloudflareCredentials = {
6
+ accountId: string;
7
+ apiToken: string;
8
+ /** Optional pin for a primary zone name (e.g. sites.example.com). */
9
+ pinnedZoneId?: string;
10
+ pinnedZoneName?: string;
11
+ };
12
+ export type WorkerDomain = {
13
+ id: string;
14
+ hostname: string;
15
+ service: string;
16
+ zone_id?: string;
17
+ zone_name?: string;
18
+ };
19
+ export declare function cloudflareConfigured(creds: Partial<CloudflareCredentials>): boolean;
20
+ export declare class CloudflareClient {
21
+ private creds;
22
+ private baseUrl;
23
+ constructor(creds: CloudflareCredentials);
24
+ private accountId;
25
+ private apiToken;
26
+ private request;
27
+ createD1Database(name: string): Promise<{
28
+ uuid: string;
29
+ name: string;
30
+ }>;
31
+ resolveZoneId(zoneName: string): Promise<{
32
+ id: string;
33
+ name: string;
34
+ } | null>;
35
+ attachWorkerDomain(input: {
36
+ hostname: string;
37
+ service: string;
38
+ zoneId: string;
39
+ zoneName: string;
40
+ }): Promise<WorkerDomain>;
41
+ detachWorkerDomain(domainId: string): Promise<void>;
42
+ findWorkerDomain(hostname: string): Promise<WorkerDomain | null>;
43
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Cloudflare REST client — D1 + Workers Custom Domains.
3
+ * Credentials are constructor args (no app config coupling).
4
+ */
5
+ export function cloudflareConfigured(creds) {
6
+ return Boolean(creds.accountId && creds.apiToken);
7
+ }
8
+ export class CloudflareClient {
9
+ creds;
10
+ baseUrl = "https://api.cloudflare.com/client/v4";
11
+ constructor(creds) {
12
+ this.creds = creds;
13
+ }
14
+ accountId() {
15
+ return this.creds.accountId;
16
+ }
17
+ apiToken() {
18
+ return this.creds.apiToken;
19
+ }
20
+ async request(method, path, body) {
21
+ const response = await fetch(`${this.baseUrl}${path}`, {
22
+ method,
23
+ headers: {
24
+ authorization: `Bearer ${this.apiToken()}`,
25
+ "content-type": "application/json",
26
+ },
27
+ body: body === undefined ? undefined : JSON.stringify(body),
28
+ });
29
+ const payload = (await response.json());
30
+ if (!response.ok || !payload.success) {
31
+ const message = payload.errors?.map((e) => `${e.code}: ${e.message}`).join("; ") ??
32
+ `Cloudflare request failed (${response.status})`;
33
+ throw new Error(message);
34
+ }
35
+ return payload.result;
36
+ }
37
+ async createD1Database(name) {
38
+ return this.request("POST", `/accounts/${this.accountId()}/d1/database`, { name });
39
+ }
40
+ async resolveZoneId(zoneName) {
41
+ const pinned = this.creds.pinnedZoneId;
42
+ const pinnedName = this.creds.pinnedZoneName;
43
+ if (pinned && pinnedName && zoneName === pinnedName) {
44
+ return { id: pinned, name: zoneName };
45
+ }
46
+ const result = await this.request("GET", `/zones?name=${encodeURIComponent(zoneName)}&status=active&per_page=1`);
47
+ const zone = result?.[0];
48
+ return zone ? { id: zone.id, name: zone.name } : null;
49
+ }
50
+ async attachWorkerDomain(input) {
51
+ return this.request("PUT", `/accounts/${this.accountId()}/workers/domains`, {
52
+ hostname: input.hostname,
53
+ service: input.service,
54
+ environment: "production",
55
+ zone_id: input.zoneId,
56
+ zone_name: input.zoneName,
57
+ });
58
+ }
59
+ async detachWorkerDomain(domainId) {
60
+ await this.request("DELETE", `/accounts/${this.accountId()}/workers/domains/${domainId}`);
61
+ }
62
+ async findWorkerDomain(hostname) {
63
+ const result = await this.request("GET", `/accounts/${this.accountId()}/workers/domains?hostname=${encodeURIComponent(hostname)}`);
64
+ return result?.[0] ?? null;
65
+ }
66
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Dump a SQLite-compatible connection to a portable .sql script.
3
+ */
4
+ import type { Connection } from "../core/database.js";
5
+ export declare function dumpConnection(conn: Connection, header: string, opts?: {
6
+ generatedBy?: string;
7
+ }): Promise<string>;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Dump a SQLite-compatible connection to a portable .sql script.
3
+ */
4
+ function sqlLiteral(value) {
5
+ if (value === null || value === undefined)
6
+ return "NULL";
7
+ if (typeof value === "number" && Number.isFinite(value))
8
+ return String(value);
9
+ if (typeof value === "bigint")
10
+ return String(value);
11
+ if (typeof value === "boolean")
12
+ return value ? "1" : "0";
13
+ if (value instanceof Uint8Array) {
14
+ const hex = Array.from(value, (b) => b.toString(16).padStart(2, "0")).join("");
15
+ return `X'${hex}'`;
16
+ }
17
+ const str = String(value);
18
+ return `'${str.replace(/'/g, "''")}'`;
19
+ }
20
+ function quoteIdent(name) {
21
+ return `"${name.replace(/"/g, '""')}"`;
22
+ }
23
+ export async function dumpConnection(conn, header, opts = {}) {
24
+ const by = opts.generatedBy ?? "Keel";
25
+ const lines = [
26
+ `-- ${header}`,
27
+ `-- Generated by ${by} at ${new Date().toISOString()}`,
28
+ "PRAGMA foreign_keys=OFF;",
29
+ "BEGIN TRANSACTION;",
30
+ "",
31
+ ];
32
+ const objects = await conn.select("SELECT type, name, sql FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' AND sql IS NOT NULL ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'index' THEN 1 ELSE 2 END, name", []);
33
+ const tables = objects.filter((o) => o.type === "table" && typeof o.name === "string");
34
+ for (const table of tables) {
35
+ const name = String(table.name);
36
+ if (table.sql) {
37
+ lines.push(`DROP TABLE IF EXISTS ${quoteIdent(name)};`);
38
+ lines.push(`${String(table.sql)};`);
39
+ lines.push("");
40
+ }
41
+ const rows = await conn.select(`SELECT * FROM ${quoteIdent(name)}`, []);
42
+ if (rows.length === 0)
43
+ continue;
44
+ const columns = Object.keys(rows[0]);
45
+ for (const row of rows) {
46
+ const values = columns.map((col) => sqlLiteral(row[col]));
47
+ lines.push(`INSERT INTO ${quoteIdent(name)} (${columns.map(quoteIdent).join(", ")}) VALUES (${values.join(", ")});`);
48
+ }
49
+ lines.push("");
50
+ }
51
+ const indexes = objects.filter((o) => o.type === "index" && o.sql);
52
+ for (const index of indexes) {
53
+ lines.push(`${String(index.sql)};`);
54
+ }
55
+ lines.push("COMMIT;");
56
+ lines.push("");
57
+ return lines.join("\n");
58
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Hostname helpers for custom domains / Workers Custom Domains.
3
+ */
4
+ export declare function normalizeHostname(raw: string): string;
5
+ export declare function isValidHostname(hostname: string): boolean;
6
+ /** Zone name candidates from most specific to apex (e.g. app.ex.com → app.ex.com, ex.com). */
7
+ export declare function zoneCandidates(hostname: string): string[];
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Hostname helpers for custom domains / Workers Custom Domains.
3
+ */
4
+ const HOSTNAME_RE = /^(?=.{1,253}$)(?!-)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.(?!-)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
5
+ export function normalizeHostname(raw) {
6
+ return raw.trim().toLowerCase().replace(/\.$/, "").replace(/^https?:\/\//, "").split("/")[0];
7
+ }
8
+ export function isValidHostname(hostname) {
9
+ if (!HOSTNAME_RE.test(hostname))
10
+ return false;
11
+ if (hostname.includes(".."))
12
+ return false;
13
+ return true;
14
+ }
15
+ /** Zone name candidates from most specific to apex (e.g. app.ex.com → app.ex.com, ex.com). */
16
+ export function zoneCandidates(hostname) {
17
+ const parts = hostname.split(".").filter(Boolean);
18
+ if (parts.length < 2)
19
+ return [];
20
+ const out = [];
21
+ for (let i = 0; i <= parts.length - 2; i++) {
22
+ out.push(parts.slice(i).join("."));
23
+ }
24
+ return out;
25
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Keel Hosting — Cloudflare client, hostname helpers, SQL dump, secrets vault.
3
+ *
4
+ * import { CloudflareClient, dumpConnection, normalizeHostname } from "@shaferllc/keel/hosting";
5
+ *
6
+ * This is infrastructure for hosted Workers/D1 apps — not a full control plane.
7
+ * Product orchestration (sites, plans, deploy loops) stays in the app.
8
+ */
9
+ export { CloudflareClient, cloudflareConfigured, } from "./cloudflare.js";
10
+ export type { CloudflareCredentials, WorkerDomain } from "./cloudflare.js";
11
+ export { normalizeHostname, isValidHostname, zoneCandidates } from "./hostname.js";
12
+ export { dumpConnection } from "./dump.js";
13
+ export { normalizeSecretKey, encryptSecretValue, decryptSecretValue, resolveSecretRows, } from "./secrets.js";
14
+ export type { SecretRow } from "./secrets.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Keel Hosting — Cloudflare client, hostname helpers, SQL dump, secrets vault.
3
+ *
4
+ * import { CloudflareClient, dumpConnection, normalizeHostname } from "@shaferllc/keel/hosting";
5
+ *
6
+ * This is infrastructure for hosted Workers/D1 apps — not a full control plane.
7
+ * Product orchestration (sites, plans, deploy loops) stays in the app.
8
+ */
9
+ export { CloudflareClient, cloudflareConfigured, } from "./cloudflare.js";
10
+ export { normalizeHostname, isValidHostname, zoneCandidates } from "./hostname.js";
11
+ export { dumpConnection } from "./dump.js";
12
+ export { normalizeSecretKey, encryptSecretValue, decryptSecretValue, resolveSecretRows, } from "./secrets.js";
@@ -0,0 +1,15 @@
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
+ export type SecretRow = {
8
+ key: string;
9
+ value_encrypted: string;
10
+ };
11
+ export declare function normalizeSecretKey(key: string): string;
12
+ export declare function encryptSecretValue(value: string, purpose?: string): Promise<string>;
13
+ export declare function decryptSecretValue(payload: string, purpose?: string): Promise<string | null>;
14
+ /** Decrypt a list of encrypted rows into a plain key→value map. */
15
+ export declare function resolveSecretRows(rows: SecretRow[], purpose?: string): Promise<Record<string, string>>;