@getjack/jack 0.1.30 → 0.1.32

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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Cron schedule creation logic for jack services cron create
3
+ *
4
+ * Validates cron expression, checks minimum interval, and calls control plane API.
5
+ * Only supported for managed (Jack Cloud) projects.
6
+ */
7
+
8
+ import { createCronSchedule as createCronScheduleApi } from "../control-plane.ts";
9
+ import { readProjectLink } from "../project-link.ts";
10
+ import { checkMinimumInterval, validateCronExpression } from "./cron-utils.ts";
11
+
12
+ // Minimum interval between cron runs (in minutes)
13
+ const MIN_INTERVAL_MINUTES = 15;
14
+
15
+ export interface CreateCronScheduleOptions {
16
+ interactive?: boolean;
17
+ }
18
+
19
+ export interface CreateCronScheduleResult {
20
+ id: string;
21
+ expression: string;
22
+ description: string;
23
+ nextRunAt: string;
24
+ created: boolean;
25
+ }
26
+
27
+ /**
28
+ * Create a cron schedule for the current project.
29
+ *
30
+ * For managed projects: calls control plane POST /v1/projects/:id/crons
31
+ * For BYO projects: throws error (not supported)
32
+ */
33
+ export async function createCronSchedule(
34
+ projectDir: string,
35
+ expression: string,
36
+ options: CreateCronScheduleOptions = {},
37
+ ): Promise<CreateCronScheduleResult> {
38
+ // Validate expression
39
+ const validation = validateCronExpression(expression);
40
+ if (!validation.valid) {
41
+ throw new Error(`Invalid cron expression: ${validation.error}`);
42
+ }
43
+
44
+ const normalizedExpression = validation.normalized!;
45
+
46
+ // Check minimum interval (15 minutes)
47
+ if (!checkMinimumInterval(normalizedExpression, MIN_INTERVAL_MINUTES)) {
48
+ throw new Error(
49
+ `Cron schedules must run at least ${MIN_INTERVAL_MINUTES} minutes apart. ` +
50
+ "This limit helps ensure reliable execution.",
51
+ );
52
+ }
53
+
54
+ // Must be managed mode
55
+ const link = await readProjectLink(projectDir);
56
+ if (!link || link.deploy_mode !== "managed") {
57
+ throw new Error(
58
+ "Cron schedules are only supported for Jack Cloud (managed) projects.\n" +
59
+ "BYO projects can use native Cloudflare cron triggers in wrangler.toml.",
60
+ );
61
+ }
62
+
63
+ // Create via control plane
64
+ const result = await createCronScheduleApi(link.project_id, normalizedExpression);
65
+
66
+ return {
67
+ id: result.id,
68
+ expression: result.expression,
69
+ description: result.description,
70
+ nextRunAt: result.next_run_at,
71
+ created: true,
72
+ };
73
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Cron schedule deletion logic for jack services cron delete
3
+ *
4
+ * Finds schedule by expression and deletes via control plane API.
5
+ * Only supported for managed (Jack Cloud) projects.
6
+ */
7
+
8
+ import {
9
+ deleteCronSchedule as deleteCronScheduleApi,
10
+ listCronSchedules as listCronSchedulesApi,
11
+ } from "../control-plane.ts";
12
+ import { readProjectLink } from "../project-link.ts";
13
+ import { normalizeCronExpression } from "./cron-utils.ts";
14
+
15
+ export interface DeleteCronScheduleOptions {
16
+ interactive?: boolean;
17
+ }
18
+
19
+ export interface DeleteCronScheduleResult {
20
+ expression: string;
21
+ deleted: boolean;
22
+ }
23
+
24
+ /**
25
+ * Delete a cron schedule by its expression.
26
+ *
27
+ * For managed projects: finds the schedule by expression and calls DELETE API
28
+ * For BYO projects: throws error (not supported)
29
+ */
30
+ export async function deleteCronSchedule(
31
+ projectDir: string,
32
+ expression: string,
33
+ options: DeleteCronScheduleOptions = {},
34
+ ): Promise<DeleteCronScheduleResult> {
35
+ const normalizedExpression = normalizeCronExpression(expression);
36
+
37
+ // Must be managed mode
38
+ const link = await readProjectLink(projectDir);
39
+ if (!link || link.deploy_mode !== "managed") {
40
+ throw new Error(
41
+ "Cron schedules are only supported for Jack Cloud (managed) projects.\n" +
42
+ "BYO projects can use native Cloudflare cron triggers in wrangler.toml.",
43
+ );
44
+ }
45
+
46
+ // Find the schedule by expression
47
+ const schedules = await listCronSchedulesApi(link.project_id);
48
+ const schedule = schedules.find(
49
+ (s) => normalizeCronExpression(s.expression) === normalizedExpression,
50
+ );
51
+
52
+ if (!schedule) {
53
+ throw new Error(
54
+ `No cron schedule found with expression "${normalizedExpression}".\n` +
55
+ "Use 'jack services cron list' to see all schedules.",
56
+ );
57
+ }
58
+
59
+ // Delete via control plane
60
+ await deleteCronScheduleApi(link.project_id, schedule.id);
61
+
62
+ return {
63
+ expression: normalizedExpression,
64
+ deleted: true,
65
+ };
66
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Cron schedule listing logic for jack services cron list
3
+ *
4
+ * Fetches cron schedules from control plane API.
5
+ * Only supported for managed (Jack Cloud) projects.
6
+ */
7
+
8
+ import {
9
+ type CronScheduleInfo,
10
+ listCronSchedules as listCronSchedulesApi,
11
+ } from "../control-plane.ts";
12
+ import { readProjectLink } from "../project-link.ts";
13
+
14
+ export interface CronScheduleListEntry {
15
+ id: string;
16
+ expression: string;
17
+ description: string;
18
+ enabled: boolean;
19
+ nextRunAt: string;
20
+ lastRunAt: string | null;
21
+ lastRunStatus: string | null;
22
+ lastRunDurationMs: number | null;
23
+ consecutiveFailures: number;
24
+ createdAt: string;
25
+ }
26
+
27
+ /**
28
+ * List all cron schedules for the current project.
29
+ *
30
+ * For managed projects: calls control plane GET /v1/projects/:id/crons
31
+ * For BYO projects: throws error (not supported)
32
+ */
33
+ export async function listCronSchedules(projectDir: string): Promise<CronScheduleListEntry[]> {
34
+ // Must be managed mode
35
+ const link = await readProjectLink(projectDir);
36
+ if (!link || link.deploy_mode !== "managed") {
37
+ throw new Error(
38
+ "Cron schedules are only supported for Jack Cloud (managed) projects.\n" +
39
+ "BYO projects can use native Cloudflare cron triggers in wrangler.toml.",
40
+ );
41
+ }
42
+
43
+ // Fetch from control plane
44
+ const schedules = await listCronSchedulesApi(link.project_id);
45
+
46
+ // Map to our format
47
+ return schedules.map((s: CronScheduleInfo) => ({
48
+ id: s.id,
49
+ expression: s.expression,
50
+ description: s.description,
51
+ enabled: s.enabled,
52
+ nextRunAt: s.next_run_at,
53
+ lastRunAt: s.last_run_at,
54
+ lastRunStatus: s.last_run_status,
55
+ lastRunDurationMs: s.last_run_duration_ms,
56
+ consecutiveFailures: s.consecutive_failures,
57
+ createdAt: s.created_at,
58
+ }));
59
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Cron schedule testing logic for jack services cron test
3
+ *
4
+ * Validates expression, shows human-readable description and next times.
5
+ * Optionally triggers the schedule on production (managed projects only).
6
+ */
7
+
8
+ import { triggerCronSchedule as triggerCronScheduleApi } from "../control-plane.ts";
9
+ import { readProjectLink } from "../project-link.ts";
10
+ import {
11
+ describeCronExpression,
12
+ getNextScheduledTimes,
13
+ validateCronExpression,
14
+ } from "./cron-utils.ts";
15
+
16
+ export interface CronTestOptions {
17
+ triggerProduction?: boolean;
18
+ interactive?: boolean;
19
+ }
20
+
21
+ export interface CronTestResult {
22
+ valid: boolean;
23
+ error?: string;
24
+ expression?: string;
25
+ description?: string;
26
+ nextTimes?: Date[];
27
+ triggerResult?: {
28
+ triggered: boolean;
29
+ status: string;
30
+ durationMs: number;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Test a cron expression: validate, describe, and show next run times.
36
+ * Optionally trigger the schedule handler on production.
37
+ */
38
+ export async function testCronExpression(
39
+ projectDir: string,
40
+ expression: string,
41
+ options: CronTestOptions = {},
42
+ ): Promise<CronTestResult> {
43
+ // Validate expression
44
+ const validation = validateCronExpression(expression);
45
+ if (!validation.valid) {
46
+ return {
47
+ valid: false,
48
+ error: validation.error,
49
+ };
50
+ }
51
+
52
+ const normalizedExpression = validation.normalized!;
53
+
54
+ // Get description and next times
55
+ const description = describeCronExpression(normalizedExpression);
56
+ const nextTimes = getNextScheduledTimes(normalizedExpression, 5);
57
+
58
+ const result: CronTestResult = {
59
+ valid: true,
60
+ expression: normalizedExpression,
61
+ description,
62
+ nextTimes,
63
+ };
64
+
65
+ // Optionally trigger production
66
+ if (options.triggerProduction) {
67
+ const link = await readProjectLink(projectDir);
68
+ if (!link || link.deploy_mode !== "managed") {
69
+ throw new Error("Production trigger is only available for Jack Cloud (managed) projects.");
70
+ }
71
+
72
+ const triggerResponse = await triggerCronScheduleApi(link.project_id, normalizedExpression);
73
+ result.triggerResult = {
74
+ triggered: triggerResponse.triggered,
75
+ status: triggerResponse.status,
76
+ durationMs: triggerResponse.duration_ms,
77
+ };
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ /**
84
+ * Common cron expression patterns for help output.
85
+ */
86
+ export const COMMON_CRON_PATTERNS = [
87
+ { expression: "*/15 * * * *", description: "Every 15 minutes" },
88
+ { expression: "0 * * * *", description: "Every hour" },
89
+ { expression: "0 0 * * *", description: "Daily at midnight" },
90
+ { expression: "0 9 * * *", description: "Daily at 9am" },
91
+ { expression: "0 9 * * 1", description: "Every Monday at 9am" },
92
+ { expression: "0 0 1 * *", description: "First day of every month" },
93
+ ];
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Cron expression utilities for validation, parsing, and human-readable descriptions.
3
+ */
4
+
5
+ import parser from "cron-parser";
6
+ import cronstrue from "cronstrue";
7
+
8
+ /**
9
+ * Normalize a cron expression by collapsing whitespace.
10
+ * "0 * * * *" -> "0 * * * *"
11
+ */
12
+ export function normalizeCronExpression(expression: string): string {
13
+ return expression.trim().replace(/\s+/g, " ");
14
+ }
15
+
16
+ /**
17
+ * Validate a cron expression.
18
+ * Returns normalized expression if valid, error message if invalid.
19
+ */
20
+ export function validateCronExpression(expression: string): {
21
+ valid: boolean;
22
+ error?: string;
23
+ normalized?: string;
24
+ } {
25
+ try {
26
+ const normalized = normalizeCronExpression(expression);
27
+ parser.parseExpression(normalized);
28
+ return { valid: true, normalized };
29
+ } catch (e) {
30
+ return { valid: false, error: e instanceof Error ? e.message : String(e) };
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Check if a cron expression has at least the minimum interval between runs.
36
+ * Returns true if the interval is >= minMinutes.
37
+ */
38
+ export function checkMinimumInterval(expression: string, minMinutes: number): boolean {
39
+ try {
40
+ const interval = parser.parseExpression(expression);
41
+ const first = interval.next().toDate();
42
+ const second = interval.next().toDate();
43
+ return (second.getTime() - first.getTime()) / 1000 / 60 >= minMinutes;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get the next N scheduled times for a cron expression.
51
+ */
52
+ export function getNextScheduledTimes(expression: string, count: number): Date[] {
53
+ const interval = parser.parseExpression(expression);
54
+ const times: Date[] = [];
55
+ for (let i = 0; i < count; i++) {
56
+ times.push(interval.next().toDate());
57
+ }
58
+ return times;
59
+ }
60
+
61
+ /**
62
+ * Get a human-readable description of a cron expression.
63
+ * e.g., "0 * * * *" -> "At minute 0"
64
+ */
65
+ export function describeCronExpression(expression: string): string {
66
+ try {
67
+ return cronstrue.toString(expression);
68
+ } catch {
69
+ return expression;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Compute the next run time for a cron expression as ISO string.
75
+ */
76
+ export function computeNextRun(expression: string): string {
77
+ return parser.parseExpression(expression).next().toDate().toISOString();
78
+ }
@@ -15,14 +15,18 @@ import { JackError, JackErrorCode } from "../errors.ts";
15
15
 
16
16
  export type DomainStatus =
17
17
  | "claimed"
18
+ | "unassigned"
18
19
  | "pending"
20
+ | "pending_dns"
19
21
  | "pending_owner"
20
22
  | "pending_ssl"
21
23
  | "active"
22
24
  | "blocked"
23
25
  | "moved"
24
26
  | "failed"
25
- | "deleting";
27
+ | "deleting"
28
+ | "expired"
29
+ | "deleted";
26
30
 
27
31
  export interface DomainVerification {
28
32
  type: "cname";
@@ -36,6 +40,22 @@ export interface DomainOwnershipVerification {
36
40
  value: string;
37
41
  }
38
42
 
43
+ export interface DomainDns {
44
+ verified: boolean;
45
+ checked_at: string | null;
46
+ current_target: string | null;
47
+ expected_target: string | null;
48
+ error: string | null;
49
+ }
50
+
51
+ export interface DomainNextStep {
52
+ action: string;
53
+ record_type?: string;
54
+ record_name?: string;
55
+ record_value?: string;
56
+ message?: string;
57
+ }
58
+
39
59
  export interface DomainInfo {
40
60
  id: string;
41
61
  hostname: string;
@@ -45,6 +65,8 @@ export interface DomainInfo {
45
65
  project_slug: string | null;
46
66
  verification?: DomainVerification;
47
67
  ownership_verification?: DomainOwnershipVerification;
68
+ dns?: DomainDns;
69
+ next_step?: DomainNextStep;
48
70
  created_at: string;
49
71
  }
50
72
 
@@ -96,16 +118,20 @@ interface ListDomainsApiResponse {
96
118
  }
97
119
 
98
120
  interface ConnectDomainApiResponse {
99
- id: string;
100
- hostname: string;
101
- status: string;
121
+ domain: {
122
+ id: string;
123
+ hostname: string;
124
+ status: string;
125
+ };
102
126
  }
103
127
 
104
128
  interface AssignDomainApiResponse {
105
- id: string;
106
- hostname: string;
107
- status: string;
108
- ssl_status: string | null;
129
+ domain: {
130
+ id: string;
131
+ hostname: string;
132
+ status: string;
133
+ ssl_status: string | null;
134
+ };
109
135
  verification?: DomainVerification;
110
136
  ownership_verification?: DomainOwnershipVerification;
111
137
  }
@@ -215,9 +241,9 @@ export async function connectDomain(hostname: string): Promise<ConnectDomainResu
215
241
 
216
242
  const data = (await response.json()) as ConnectDomainApiResponse;
217
243
  return {
218
- id: data.id,
219
- hostname: data.hostname,
220
- status: data.status as DomainStatus,
244
+ id: data.domain.id,
245
+ hostname: data.domain.hostname,
246
+ status: data.domain.status as DomainStatus,
221
247
  };
222
248
  }
223
249
 
@@ -282,10 +308,10 @@ export async function assignDomain(
282
308
 
283
309
  const data = (await response.json()) as AssignDomainApiResponse;
284
310
  return {
285
- id: data.id,
286
- hostname: data.hostname,
287
- status: data.status as DomainStatus,
288
- ssl_status: data.ssl_status,
311
+ id: data.domain.id,
312
+ hostname: data.domain.hostname,
313
+ status: data.domain.status as DomainStatus,
314
+ ssl_status: data.domain.ssl_status,
289
315
  project_id: project.id,
290
316
  project_slug: projectSlug,
291
317
  verification: data.verification,
@@ -334,10 +360,13 @@ export async function unassignDomain(hostname: string): Promise<UnassignDomainRe
334
360
  );
335
361
  }
336
362
 
363
+ const data = (await response.json()) as {
364
+ domain: { id: string; hostname: string; status: string };
365
+ };
337
366
  return {
338
- id: domain.id,
339
- hostname: domain.hostname,
340
- status: "claimed" as DomainStatus,
367
+ id: data.domain.id,
368
+ hostname: data.domain.hostname,
369
+ status: data.domain.status as DomainStatus,
341
370
  };
342
371
  }
343
372
 
@@ -377,3 +406,45 @@ export async function disconnectDomain(hostname: string): Promise<DisconnectDoma
377
406
  hostname: domain.hostname,
378
407
  };
379
408
  }
409
+
410
+ /**
411
+ * Verify DNS configuration for a domain.
412
+ *
413
+ * @throws JackError with RESOURCE_NOT_FOUND if domain not found
414
+ */
415
+ export interface VerifyDomainResult {
416
+ domain: DomainInfo;
417
+ dns_check?: {
418
+ verified: boolean;
419
+ target: string | null;
420
+ error: string | null;
421
+ };
422
+ }
423
+
424
+ export async function verifyDomain(hostname: string): Promise<VerifyDomainResult> {
425
+ const domain = await getDomainByHostname(hostname);
426
+ if (!domain) {
427
+ throw new JackError(
428
+ JackErrorCode.PROJECT_NOT_FOUND,
429
+ `Domain not found: ${hostname}`,
430
+ "Run 'jack domain' to see all domains",
431
+ { exitCode: 1 },
432
+ );
433
+ }
434
+
435
+ const response = await authFetch(`${getControlApiUrl()}/v1/domains/${domain.id}/verify`, {
436
+ method: "POST",
437
+ });
438
+
439
+ if (!response.ok) {
440
+ const err = (await response
441
+ .json()
442
+ .catch(() => ({ message: "Unknown error" }))) as ApiErrorResponse;
443
+ throw new JackError(
444
+ JackErrorCode.INTERNAL_ERROR,
445
+ err.message || `Failed to verify domain: ${response.status}`,
446
+ );
447
+ }
448
+
449
+ return (await response.json()) as VerifyDomainResult;
450
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Token operations service layer for jack cloud
3
+ *
4
+ * Provides shared API token management functions for both CLI and MCP.
5
+ * Returns pure data - no console.log or process.exit.
6
+ */
7
+
8
+ import { authFetch } from "../auth/index.ts";
9
+ import { getControlApiUrl } from "../control-plane.ts";
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export interface CreateTokenResult {
16
+ token: string;
17
+ id: string;
18
+ name: string;
19
+ created_at: string;
20
+ expires_at: string | null;
21
+ }
22
+
23
+ export interface TokenInfo {
24
+ id: string;
25
+ name: string;
26
+ id_prefix: string;
27
+ created_at: string;
28
+ last_used_at: string | null;
29
+ expires_at: string | null;
30
+ }
31
+
32
+ // ============================================================================
33
+ // Service Functions
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Create a new API token for headless authentication.
38
+ */
39
+ export async function createApiToken(
40
+ name: string,
41
+ expiresInDays?: number,
42
+ ): Promise<CreateTokenResult> {
43
+ const response = await authFetch(`${getControlApiUrl()}/v1/tokens`, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({ name, expires_in_days: expiresInDays }),
47
+ });
48
+
49
+ if (!response.ok) {
50
+ const err = (await response.json().catch(() => ({}))) as { message?: string };
51
+ throw new Error(err.message || `Failed to create token: ${response.status}`);
52
+ }
53
+
54
+ return response.json() as Promise<CreateTokenResult>;
55
+ }
56
+
57
+ /**
58
+ * List all active API tokens for the current user.
59
+ */
60
+ export async function listApiTokens(): Promise<TokenInfo[]> {
61
+ const response = await authFetch(`${getControlApiUrl()}/v1/tokens`);
62
+
63
+ if (!response.ok) {
64
+ const err = (await response.json().catch(() => ({}))) as { message?: string };
65
+ throw new Error(err.message || `Failed to list tokens: ${response.status}`);
66
+ }
67
+
68
+ const data = (await response.json()) as { tokens: TokenInfo[] };
69
+ return data.tokens;
70
+ }
71
+
72
+ /**
73
+ * Revoke an API token by ID.
74
+ */
75
+ export async function revokeApiToken(tokenId: string): Promise<void> {
76
+ const response = await authFetch(`${getControlApiUrl()}/v1/tokens/${tokenId}`, {
77
+ method: "DELETE",
78
+ });
79
+
80
+ if (!response.ok) {
81
+ const err = (await response.json().catch(() => ({}))) as { message?: string };
82
+ throw new Error(err.message || `Failed to revoke token: ${response.status}`);
83
+ }
84
+ }
@@ -44,6 +44,9 @@ export const Events = {
44
44
  BYO_DEPLOY_STARTED: "byo_deploy_started",
45
45
  BYO_DEPLOY_COMPLETED: "byo_deploy_completed",
46
46
  BYO_DEPLOY_FAILED: "byo_deploy_failed",
47
+ // Token management events
48
+ TOKEN_CREATED: "token_created",
49
+ TOKEN_REVOKED: "token_revoked",
47
50
  } as const;
48
51
 
49
52
  type EventName = (typeof Events)[keyof typeof Events];
@@ -127,6 +130,7 @@ export interface UserProperties {
127
130
  is_tty?: boolean;
128
131
  locale?: string;
129
132
  config_style?: "byoc" | "jack-cloud";
133
+ auth_method?: "oauth" | "token";
130
134
  }
131
135
 
132
136
  // Detect environment properties (for user profile - stable properties)