@donkeylabs/cli 2.0.15 → 2.0.16

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 (84) hide show
  1. package/package.json +1 -1
  2. package/src/commands/config.ts +610 -0
  3. package/src/commands/deploy-enhanced.ts +354 -0
  4. package/src/commands/deploy.ts +204 -0
  5. package/src/commands/init-enhanced.ts +1994 -0
  6. package/src/deployment/manager.ts +356 -0
  7. package/src/index.ts +47 -19
  8. package/templates/starter/.env.example +0 -44
  9. package/templates/starter/.gitignore.template +0 -4
  10. package/templates/starter/donkeylabs.config.ts +0 -6
  11. package/templates/starter/package.json +0 -21
  12. package/templates/starter/src/index.ts +0 -54
  13. package/templates/starter/src/plugins/stats/index.ts +0 -105
  14. package/templates/starter/src/routes/health/handlers/ping.ts +0 -22
  15. package/templates/starter/src/routes/health/index.ts +0 -19
  16. package/templates/starter/tsconfig.json +0 -27
  17. package/templates/sveltekit-app/.env.example +0 -59
  18. package/templates/sveltekit-app/README.md +0 -103
  19. package/templates/sveltekit-app/bun.lock +0 -683
  20. package/templates/sveltekit-app/donkeylabs.config.ts +0 -12
  21. package/templates/sveltekit-app/package.json +0 -38
  22. package/templates/sveltekit-app/src/app.css +0 -40
  23. package/templates/sveltekit-app/src/app.html +0 -12
  24. package/templates/sveltekit-app/src/hooks.server.ts +0 -4
  25. package/templates/sveltekit-app/src/lib/components/ui/badge/badge.svelte +0 -30
  26. package/templates/sveltekit-app/src/lib/components/ui/badge/index.ts +0 -3
  27. package/templates/sveltekit-app/src/lib/components/ui/button/button.svelte +0 -48
  28. package/templates/sveltekit-app/src/lib/components/ui/button/index.ts +0 -9
  29. package/templates/sveltekit-app/src/lib/components/ui/card/card-content.svelte +0 -18
  30. package/templates/sveltekit-app/src/lib/components/ui/card/card-description.svelte +0 -18
  31. package/templates/sveltekit-app/src/lib/components/ui/card/card-footer.svelte +0 -18
  32. package/templates/sveltekit-app/src/lib/components/ui/card/card-header.svelte +0 -18
  33. package/templates/sveltekit-app/src/lib/components/ui/card/card-title.svelte +0 -18
  34. package/templates/sveltekit-app/src/lib/components/ui/card/card.svelte +0 -21
  35. package/templates/sveltekit-app/src/lib/components/ui/card/index.ts +0 -21
  36. package/templates/sveltekit-app/src/lib/components/ui/index.ts +0 -4
  37. package/templates/sveltekit-app/src/lib/components/ui/input/index.ts +0 -2
  38. package/templates/sveltekit-app/src/lib/components/ui/input/input.svelte +0 -20
  39. package/templates/sveltekit-app/src/lib/permissions.ts +0 -213
  40. package/templates/sveltekit-app/src/lib/utils/index.ts +0 -6
  41. package/templates/sveltekit-app/src/routes/+layout.svelte +0 -8
  42. package/templates/sveltekit-app/src/routes/+page.server.ts +0 -25
  43. package/templates/sveltekit-app/src/routes/+page.svelte +0 -680
  44. package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +0 -23
  45. package/templates/sveltekit-app/src/routes/workflows/+page.svelte +0 -522
  46. package/templates/sveltekit-app/src/server/events.ts +0 -28
  47. package/templates/sveltekit-app/src/server/index.ts +0 -124
  48. package/templates/sveltekit-app/src/server/plugins/auth/auth.test.ts +0 -377
  49. package/templates/sveltekit-app/src/server/plugins/auth/index.ts +0 -815
  50. package/templates/sveltekit-app/src/server/plugins/auth/migrations/001_create_users.ts +0 -25
  51. package/templates/sveltekit-app/src/server/plugins/auth/migrations/002_create_sessions.ts +0 -32
  52. package/templates/sveltekit-app/src/server/plugins/auth/migrations/003_create_refresh_tokens.ts +0 -33
  53. package/templates/sveltekit-app/src/server/plugins/auth/migrations/004_create_passkeys.ts +0 -60
  54. package/templates/sveltekit-app/src/server/plugins/auth/schema.ts +0 -65
  55. package/templates/sveltekit-app/src/server/plugins/demo/index.ts +0 -262
  56. package/templates/sveltekit-app/src/server/plugins/email/email.test.ts +0 -369
  57. package/templates/sveltekit-app/src/server/plugins/email/index.ts +0 -411
  58. package/templates/sveltekit-app/src/server/plugins/email/migrations/001_create_email_tokens.ts +0 -33
  59. package/templates/sveltekit-app/src/server/plugins/email/schema.ts +0 -24
  60. package/templates/sveltekit-app/src/server/plugins/permissions/index.ts +0 -1048
  61. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/001_create_tenants.ts +0 -63
  62. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/002_create_roles.ts +0 -90
  63. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/003_create_resource_grants.ts +0 -50
  64. package/templates/sveltekit-app/src/server/plugins/permissions/permissions.test.ts +0 -566
  65. package/templates/sveltekit-app/src/server/plugins/permissions/schema.ts +0 -67
  66. package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +0 -198
  67. package/templates/sveltekit-app/src/server/routes/auth/auth.schemas.ts +0 -66
  68. package/templates/sveltekit-app/src/server/routes/auth/handlers/login.handler.ts +0 -18
  69. package/templates/sveltekit-app/src/server/routes/auth/handlers/logout.handler.ts +0 -16
  70. package/templates/sveltekit-app/src/server/routes/auth/handlers/me.handler.ts +0 -20
  71. package/templates/sveltekit-app/src/server/routes/auth/handlers/refresh.handler.ts +0 -17
  72. package/templates/sveltekit-app/src/server/routes/auth/handlers/register.handler.ts +0 -19
  73. package/templates/sveltekit-app/src/server/routes/auth/handlers/update-profile.handler.ts +0 -21
  74. package/templates/sveltekit-app/src/server/routes/auth/index.ts +0 -73
  75. package/templates/sveltekit-app/src/server/routes/demo.ts +0 -464
  76. package/templates/sveltekit-app/src/server/routes/example/example.schemas.ts +0 -22
  77. package/templates/sveltekit-app/src/server/routes/example/handlers/greet.handler.ts +0 -21
  78. package/templates/sveltekit-app/src/server/routes/example/index.ts +0 -28
  79. package/templates/sveltekit-app/src/server/routes/permissions/index.ts +0 -248
  80. package/templates/sveltekit-app/src/server/routes/tenants/index.ts +0 -339
  81. package/templates/sveltekit-app/static/robots.txt +0 -3
  82. package/templates/sveltekit-app/svelte.config.ts +0 -17
  83. package/templates/sveltekit-app/tsconfig.json +0 -20
  84. package/templates/sveltekit-app/vite.config.ts +0 -12
@@ -1,411 +0,0 @@
1
- /**
2
- * Email Plugin - Transactional email with multiple providers
3
- *
4
- * Providers:
5
- * - resend: Resend.com API
6
- * - console: Log emails to console (development)
7
- *
8
- * Features:
9
- * - Magic link generation and validation
10
- * - Password reset tokens
11
- * - Email verification
12
- * - Template support
13
- */
14
-
15
- import { createPlugin } from "@donkeylabs/server";
16
- import type { ColumnType } from "kysely";
17
-
18
- // =============================================================================
19
- // CONFIGURATION TYPES
20
- // =============================================================================
21
-
22
- export type EmailProvider = "resend" | "console";
23
-
24
- export interface EmailConfig {
25
- /**
26
- * Email provider
27
- * @default "console"
28
- */
29
- provider?: EmailProvider;
30
-
31
- /**
32
- * Provider-specific configuration
33
- */
34
- resend?: {
35
- apiKey: string;
36
- };
37
-
38
- /**
39
- * Default from address
40
- */
41
- from: string;
42
-
43
- /**
44
- * Base URL for links (e.g., "https://myapp.com")
45
- */
46
- baseUrl: string;
47
-
48
- /**
49
- * Token expiry times
50
- */
51
- expiry?: {
52
- /** Magic link expiry @default "15m" */
53
- magicLink?: string;
54
- /** Password reset expiry @default "1h" */
55
- passwordReset?: string;
56
- /** Email verification expiry @default "24h" */
57
- emailVerification?: string;
58
- };
59
- }
60
-
61
- // =============================================================================
62
- // DATABASE SCHEMA
63
- // =============================================================================
64
-
65
- interface EmailTokensTable {
66
- id: string;
67
- type: "magic_link" | "password_reset" | "email_verification";
68
- email: string;
69
- token_hash: string;
70
- expires_at: string;
71
- used_at: string | null;
72
- created_at: ColumnType<string, string | undefined, never>;
73
- }
74
-
75
- interface EmailSchema {
76
- email_tokens: EmailTokensTable;
77
- }
78
-
79
- // =============================================================================
80
- // TYPES
81
- // =============================================================================
82
-
83
- export interface SendEmailOptions {
84
- to: string | string[];
85
- subject: string;
86
- html?: string;
87
- text?: string;
88
- from?: string;
89
- replyTo?: string;
90
- }
91
-
92
- export interface EmailResult {
93
- success: boolean;
94
- messageId?: string;
95
- error?: string;
96
- }
97
-
98
- // =============================================================================
99
- // HELPERS
100
- // =============================================================================
101
-
102
- function parseExpiry(expiry: string): number {
103
- const match = expiry.match(/^(\d+)(s|m|h|d)$/);
104
- if (!match) return 15 * 60 * 1000; // Default 15 minutes
105
-
106
- const value = parseInt(match[1]);
107
- const unit = match[2];
108
-
109
- switch (unit) {
110
- case "s": return value * 1000;
111
- case "m": return value * 60 * 1000;
112
- case "h": return value * 60 * 60 * 1000;
113
- case "d": return value * 24 * 60 * 60 * 1000;
114
- default: return 15 * 60 * 1000;
115
- }
116
- }
117
-
118
- function generateToken(): string {
119
- const bytes = new Uint8Array(32);
120
- crypto.getRandomValues(bytes);
121
- return Array.from(bytes)
122
- .map((b) => b.toString(16).padStart(2, "0"))
123
- .join("");
124
- }
125
-
126
- // =============================================================================
127
- // PLUGIN DEFINITION
128
- // =============================================================================
129
-
130
- export const emailPlugin = createPlugin
131
- .withSchema<EmailSchema>()
132
- .withConfig<EmailConfig>()
133
- .define({
134
- name: "email",
135
-
136
- customErrors: {
137
- InvalidToken: {
138
- status: 400,
139
- code: "INVALID_TOKEN",
140
- message: "Invalid or expired token",
141
- },
142
- TokenAlreadyUsed: {
143
- status: 400,
144
- code: "TOKEN_USED",
145
- message: "This token has already been used",
146
- },
147
- SendFailed: {
148
- status: 500,
149
- code: "EMAIL_SEND_FAILED",
150
- message: "Failed to send email",
151
- },
152
- },
153
-
154
- service: async (ctx) => {
155
- const config = ctx.config!;
156
- const provider = config.provider || "console";
157
- const from = config.from;
158
- const baseUrl = config.baseUrl.replace(/\/$/, ""); // Remove trailing slash
159
-
160
- const magicLinkExpiry = parseExpiry(config.expiry?.magicLink || "15m");
161
- const passwordResetExpiry = parseExpiry(config.expiry?.passwordReset || "1h");
162
- const emailVerificationExpiry = parseExpiry(config.expiry?.emailVerification || "24h");
163
-
164
- /**
165
- * Send email using configured provider
166
- */
167
- async function sendEmail(options: SendEmailOptions): Promise<EmailResult> {
168
- const emailFrom = options.from || from;
169
- const recipients = Array.isArray(options.to) ? options.to : [options.to];
170
-
171
- if (provider === "console") {
172
- ctx.core.logger.info("Email sent (console provider)", {
173
- to: recipients,
174
- subject: options.subject,
175
- from: emailFrom,
176
- });
177
- console.log("\n" + "=".repeat(60));
178
- console.log(`📧 EMAIL TO: ${recipients.join(", ")}`);
179
- console.log(` FROM: ${emailFrom}`);
180
- console.log(` SUBJECT: ${options.subject}`);
181
- console.log("-".repeat(60));
182
- console.log(options.text || options.html);
183
- console.log("=".repeat(60) + "\n");
184
- return { success: true, messageId: `console-${Date.now()}` };
185
- }
186
-
187
- if (provider === "resend") {
188
- if (!config.resend?.apiKey) {
189
- throw new Error("Resend API key not configured");
190
- }
191
-
192
- try {
193
- const response = await fetch("https://api.resend.com/emails", {
194
- method: "POST",
195
- headers: {
196
- "Authorization": `Bearer ${config.resend.apiKey}`,
197
- "Content-Type": "application/json",
198
- },
199
- body: JSON.stringify({
200
- from: emailFrom,
201
- to: recipients,
202
- subject: options.subject,
203
- html: options.html,
204
- text: options.text,
205
- reply_to: options.replyTo,
206
- }),
207
- });
208
-
209
- if (!response.ok) {
210
- const error = await response.text();
211
- ctx.core.logger.error("Resend API error", { error, status: response.status });
212
- return { success: false, error };
213
- }
214
-
215
- const data = await response.json() as { id: string };
216
- return { success: true, messageId: data.id };
217
- } catch (error) {
218
- ctx.core.logger.error("Failed to send email via Resend", { error });
219
- return { success: false, error: String(error) };
220
- }
221
- }
222
-
223
- return { success: false, error: `Unknown provider: ${provider}` };
224
- }
225
-
226
- /**
227
- * Create a token and store hash
228
- */
229
- async function createToken(
230
- type: "magic_link" | "password_reset" | "email_verification",
231
- email: string,
232
- expiryMs: number
233
- ): Promise<string> {
234
- const token = generateToken();
235
- const tokenHash = await Bun.password.hash(token, {
236
- algorithm: "bcrypt",
237
- cost: 4, // Low cost for tokens
238
- });
239
-
240
- await ctx.db
241
- .insertInto("email_tokens")
242
- .values({
243
- id: crypto.randomUUID(),
244
- type,
245
- email: email.toLowerCase(),
246
- token_hash: tokenHash,
247
- expires_at: new Date(Date.now() + expiryMs).toISOString(),
248
- used_at: null,
249
- created_at: new Date().toISOString(),
250
- })
251
- .execute();
252
-
253
- return token;
254
- }
255
-
256
- /**
257
- * Validate and consume a token
258
- */
259
- async function validateToken(
260
- type: "magic_link" | "password_reset" | "email_verification",
261
- email: string,
262
- token: string
263
- ): Promise<boolean> {
264
- const tokens = await ctx.db
265
- .selectFrom("email_tokens")
266
- .where("type", "=", type)
267
- .where("email", "=", email.toLowerCase())
268
- .where("used_at", "is", null)
269
- .where("expires_at", ">", new Date().toISOString())
270
- .selectAll()
271
- .execute();
272
-
273
- for (const stored of tokens) {
274
- const valid = await Bun.password.verify(token, stored.token_hash);
275
- if (valid) {
276
- // Mark as used
277
- await ctx.db
278
- .updateTable("email_tokens")
279
- .set({ used_at: new Date().toISOString() })
280
- .where("id", "=", stored.id)
281
- .execute();
282
- return true;
283
- }
284
- }
285
-
286
- return false;
287
- }
288
-
289
- return {
290
- /**
291
- * Send a raw email
292
- */
293
- send: sendEmail,
294
-
295
- /**
296
- * Send magic link for passwordless login
297
- */
298
- sendMagicLink: async (email: string, redirectPath: string = "/"): Promise<EmailResult> => {
299
- const token = await createToken("magic_link", email, magicLinkExpiry);
300
- const magicLink = `${baseUrl}/auth/magic?token=${token}&email=${encodeURIComponent(email)}&redirect=${encodeURIComponent(redirectPath)}`;
301
-
302
- return sendEmail({
303
- to: email,
304
- subject: "Sign in to your account",
305
- html: `
306
- <h2>Sign in to your account</h2>
307
- <p>Click the link below to sign in. This link expires in 15 minutes.</p>
308
- <p><a href="${magicLink}" style="display: inline-block; padding: 12px 24px; background: #0066cc; color: white; text-decoration: none; border-radius: 4px;">Sign In</a></p>
309
- <p style="color: #666; font-size: 14px;">Or copy this link: ${magicLink}</p>
310
- <p style="color: #999; font-size: 12px;">If you didn't request this, you can safely ignore this email.</p>
311
- `,
312
- text: `Sign in to your account\n\nClick here to sign in: ${magicLink}\n\nThis link expires in 15 minutes.\n\nIf you didn't request this, you can safely ignore this email.`,
313
- });
314
- },
315
-
316
- /**
317
- * Validate magic link token and return email if valid
318
- */
319
- validateMagicLink: async (email: string, token: string): Promise<boolean> => {
320
- return validateToken("magic_link", email, token);
321
- },
322
-
323
- /**
324
- * Send password reset email
325
- */
326
- sendPasswordReset: async (email: string): Promise<EmailResult> => {
327
- const token = await createToken("password_reset", email, passwordResetExpiry);
328
- const resetLink = `${baseUrl}/auth/reset-password?token=${token}&email=${encodeURIComponent(email)}`;
329
-
330
- return sendEmail({
331
- to: email,
332
- subject: "Reset your password",
333
- html: `
334
- <h2>Reset your password</h2>
335
- <p>Click the link below to reset your password. This link expires in 1 hour.</p>
336
- <p><a href="${resetLink}" style="display: inline-block; padding: 12px 24px; background: #0066cc; color: white; text-decoration: none; border-radius: 4px;">Reset Password</a></p>
337
- <p style="color: #666; font-size: 14px;">Or copy this link: ${resetLink}</p>
338
- <p style="color: #999; font-size: 12px;">If you didn't request this, you can safely ignore this email.</p>
339
- `,
340
- text: `Reset your password\n\nClick here to reset: ${resetLink}\n\nThis link expires in 1 hour.\n\nIf you didn't request this, you can safely ignore this email.`,
341
- });
342
- },
343
-
344
- /**
345
- * Validate password reset token
346
- */
347
- validatePasswordReset: async (email: string, token: string): Promise<boolean> => {
348
- return validateToken("password_reset", email, token);
349
- },
350
-
351
- /**
352
- * Send email verification
353
- */
354
- sendVerification: async (email: string): Promise<EmailResult> => {
355
- const token = await createToken("email_verification", email, emailVerificationExpiry);
356
- const verifyLink = `${baseUrl}/auth/verify-email?token=${token}&email=${encodeURIComponent(email)}`;
357
-
358
- return sendEmail({
359
- to: email,
360
- subject: "Verify your email address",
361
- html: `
362
- <h2>Verify your email address</h2>
363
- <p>Click the link below to verify your email. This link expires in 24 hours.</p>
364
- <p><a href="${verifyLink}" style="display: inline-block; padding: 12px 24px; background: #0066cc; color: white; text-decoration: none; border-radius: 4px;">Verify Email</a></p>
365
- <p style="color: #666; font-size: 14px;">Or copy this link: ${verifyLink}</p>
366
- <p style="color: #999; font-size: 12px;">If you didn't create an account, you can safely ignore this email.</p>
367
- `,
368
- text: `Verify your email address\n\nClick here to verify: ${verifyLink}\n\nThis link expires in 24 hours.\n\nIf you didn't create an account, you can safely ignore this email.`,
369
- });
370
- },
371
-
372
- /**
373
- * Validate email verification token
374
- */
375
- validateVerification: async (email: string, token: string): Promise<boolean> => {
376
- return validateToken("email_verification", email, token);
377
- },
378
-
379
- /**
380
- * Cleanup expired tokens
381
- */
382
- cleanup: async (): Promise<number> => {
383
- const result = await ctx.db
384
- .deleteFrom("email_tokens")
385
- .where("expires_at", "<", new Date().toISOString())
386
- .executeTakeFirst();
387
-
388
- const count = Number(result.numDeletedRows);
389
- if (count > 0) {
390
- ctx.core.logger.info("Cleaned up expired email tokens", { count });
391
- }
392
- return count;
393
- },
394
- };
395
- },
396
-
397
- init: async (ctx, service) => {
398
- // Cleanup expired tokens daily
399
- ctx.core.cron.schedule(
400
- "0 4 * * *", // Daily at 4am
401
- async () => {
402
- await service.cleanup();
403
- },
404
- { name: "email-token-cleanup" }
405
- );
406
-
407
- ctx.core.logger.info("Email plugin initialized", {
408
- provider: ctx.config?.provider || "console",
409
- });
410
- },
411
- });
@@ -1,33 +0,0 @@
1
- import type { Kysely } from "kysely";
2
-
3
- export async function up(db: Kysely<any>): Promise<void> {
4
- await db.schema
5
- .createTable("email_tokens")
6
- .ifNotExists()
7
- .addColumn("id", "text", (col) => col.primaryKey())
8
- .addColumn("type", "text", (col) => col.notNull()) // magic_link, password_reset, email_verification
9
- .addColumn("email", "text", (col) => col.notNull())
10
- .addColumn("token_hash", "text", (col) => col.notNull())
11
- .addColumn("expires_at", "text", (col) => col.notNull())
12
- .addColumn("used_at", "text")
13
- .addColumn("created_at", "text", (col) => col.notNull().defaultTo("CURRENT_TIMESTAMP"))
14
- .execute();
15
-
16
- await db.schema
17
- .createIndex("idx_email_tokens_email_type")
18
- .ifNotExists()
19
- .on("email_tokens")
20
- .columns(["email", "type"])
21
- .execute();
22
-
23
- await db.schema
24
- .createIndex("idx_email_tokens_expires_at")
25
- .ifNotExists()
26
- .on("email_tokens")
27
- .column("expires_at")
28
- .execute();
29
- }
30
-
31
- export async function down(db: Kysely<any>): Promise<void> {
32
- await db.schema.dropTable("email_tokens").ifExists().execute();
33
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * This file was generated by kysely-codegen.
3
- * Please do not edit it manually.
4
- */
5
-
6
- import type { ColumnType } from "kysely";
7
-
8
- export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
9
- ? ColumnType<S, I | undefined, U>
10
- : ColumnType<T, T | undefined, T>;
11
-
12
- export interface EmailTokens {
13
- created_at: Generated<string>;
14
- email: string;
15
- expires_at: string;
16
- id: string | null;
17
- token_hash: string;
18
- type: string;
19
- used_at: string | null;
20
- }
21
-
22
- export interface DB {
23
- email_tokens: EmailTokens;
24
- }