@cronvello/sdk 0.2.1 → 0.4.0

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { T as Transport, P as PublicJob, J as JobCreateBody, a as JobUpdateBody, b as PublicTask, c as TaskCreateBody, A as AccountTasksQuery, d as TasksPage, e as TaskUpdateBody, R as RunNowResult, f as RunsQuery, g as RunsPage, h as AccountRunsQuery, i as PublicRun, M as MeResponse, U as UsageResponse, F as FetchLike, j as RegistryReconcileRequest, k as RegistryReconcileResponse, l as ResolvedJob, S as SyncOptions, m as ReconcileResult, D as DispatchRequest, n as DispatchResponse, C as CronvelloAppConfig } from './dispatch-handler-BoadpsL9.js';
2
- export { o as CallbackStatus, p as CronvelloHooks, q as CronvelloJobConfig, r as CronvelloJobConfigWithKey, s as CronvelloJobContext, t as CronvelloJobHandler, u as CronvelloJobsInput, v as CronvelloLogger, w as CronvelloRunSource, E as ExecutionMode, H as HttpMethod, x as JobStatus, y as Pagination, z as ReconcileTaskChange, B as RegistryReconcileChange, G as RegistryTaskInput, I as RunStatus, K as RunType, L as SuccessCriteria, N as Urgency } from './dispatch-handler-BoadpsL9.js';
1
+ import { T as Transport, P as PublicJob, J as JobCreateBody, a as JobUpdateBody, b as PublicTask, c as TaskCreateBody, A as AccountTasksQuery, d as TasksPage, e as TaskUpdateBody, R as RunNowResult, f as RunsQuery, g as RunsPage, h as AccountRunsQuery, i as PublicRun, M as MeResponse, U as UsageResponse, F as FetchLike, j as RegistryReconcileRequest, k as RegistryReconcileResponse, l as ResolvedJob, S as SyncOptions, m as ReconcileResult, D as DispatchRequest, n as DispatchResponse, C as CronvelloAppConfig, o as Urgency } from './dispatch-handler-yHIFEnNG.js';
2
+ export { p as CallbackStatus, q as CronvelloHooks, r as CronvelloJobConfig, s as CronvelloJobConfigWithKey, t as CronvelloJobContext, u as CronvelloJobHandler, v as CronvelloJobsInput, w as CronvelloLogger, x as CronvelloRunSource, E as ExecutionMode, H as HttpMethod, y as JobStatus, z as Pagination, B as ReconcileTaskChange, G as RegistryReconcileChange, I as RegistryTaskInput, K as RunStatus, L as RunType, N as SuccessCriteria } from './dispatch-handler-yHIFEnNG.js';
3
3
  import { ExpressDispatchHandler } from './express.js';
4
4
  import { NextRouteHandler } from './next.js';
5
5
  import { LocalEngineOptions, DashboardOptions, LocalEngine } from './dev.js';
@@ -115,23 +115,40 @@ declare class AccountResource {
115
115
  /**
116
116
  * `defineCronvello` — the high-level, code-first entry point.
117
117
  *
118
+ * Jobs alone are enough. This runs on your machine with no account and no network:
119
+ *
118
120
  * export const cronvello = defineCronvello({
119
121
  * appName: "my-app",
120
- * appUrl: process.env.APP_URL!,
121
- * apiKey: process.env.CRONVELLO_API_KEY!,
122
- * dispatchSecret: process.env.CRONVELLO_DISPATCH_SECRET!,
123
122
  * jobs: {
124
123
  * "send-daily-digest": { schedule: "0 8 * * *", handler: async () => { … } },
125
124
  * "cleanup-temp": { schedule: "*\/15 * * * *", handler: async () => { … } },
126
125
  * },
127
126
  * });
128
127
  *
128
+ * npx cronvello dev // real scheduler, locally
129
+ * await cronvello.trigger("cleanup-temp") // run one handler now
130
+ *
131
+ * Add the hosted side later, when you want run history, alerts on missed runs and replay.
132
+ * `apiKey`, `appUrl` and `dispatchSecret` are needed only from that point on, and only the
133
+ * calls that use them complain if they're absent:
134
+ *
135
+ * export const cronvello = defineCronvello({
136
+ * appName: "my-app",
137
+ * appUrl: process.env.APP_URL!,
138
+ * apiKey: process.env.CRONVELLO_API_KEY!,
139
+ * dispatchSecret: process.env.CRONVELLO_DISPATCH_SECRET!,
140
+ * jobs: { … },
141
+ * });
142
+ *
129
143
  * await cronvello.sync(); // reconcile the registry to Cronvello (idempotent)
130
144
  * app.post(cronvello.dispatchPath, express.json(), cronvello.expressHandler());
131
145
  */
132
146
 
133
147
  interface CronvelloApp {
134
- /** The underlying low-level client for ad-hoc `/v1` calls. */
148
+ /**
149
+ * The underlying low-level client for ad-hoc `/v1` calls. Built on first access — reading it
150
+ * on an app configured without an `apiKey` throws {@link CronvelloConfigError}.
151
+ */
135
152
  readonly client: CronvelloClient;
136
153
  /** Resolved jobs, keyed by their stable registry key. */
137
154
  readonly jobs: ReadonlyMap<string, ResolvedJob>;
@@ -139,8 +156,13 @@ interface CronvelloApp {
139
156
  readonly appName: string;
140
157
  /** Path the dispatch handler should be mounted at (e.g. "/cronvello/dispatch"). */
141
158
  readonly dispatchPath: string;
142
- /** Absolute URL Cronvello calls back (appUrl + dispatchPath). */
159
+ /**
160
+ * Absolute URL Cronvello calls back (appUrl + dispatchPath). Reading it on an app configured
161
+ * without an `appUrl` throws {@link CronvelloConfigError} — a local-only app is never called back.
162
+ */
143
163
  readonly dispatchUrl: string;
164
+ /** True when this app has everything the hosted side needs (`apiKey`, `appUrl`, `dispatchSecret`). */
165
+ readonly isCloudConfigured: boolean;
144
166
  /** Reconcile the registry into Cronvello. Idempotent — safe to call on every boot/deploy. */
145
167
  sync(options?: SyncOptions): Promise<ReconcileResult>;
146
168
  /** Trigger one job immediately via Cronvello (manual run). Requires a prior `sync()`. */
@@ -180,8 +202,8 @@ interface DevOptions extends LocalEngineOptions {
180
202
  */
181
203
  dashboard?: boolean | DashboardOptions;
182
204
  }
183
- /** Config for `defineCronvello.fromEnv` — the three secret fields become optional (read from env). */
184
- type CronvelloEnvConfig = Omit<CronvelloAppConfig, "apiKey" | "dispatchSecret" | "appUrl"> & Partial<Pick<CronvelloAppConfig, "apiKey" | "dispatchSecret" | "appUrl">>;
205
+ /** Config for `defineCronvello.fromEnv` — the three hosted-side fields are read from the environment. */
206
+ type CronvelloEnvConfig = CronvelloAppConfig;
185
207
  declare function defineCronvello(config: CronvelloAppConfig): CronvelloApp;
186
208
  declare namespace defineCronvello {
187
209
  /**
@@ -282,6 +304,251 @@ declare function validateCron(expr: string): CronValidation;
282
304
  /** True if `tz` is a valid IANA timezone (uses the runtime's Intl database). "UTC" always passes. */
283
305
  declare function isValidTimeZone(tz: string): boolean;
284
306
 
307
+ /**
308
+ * Wire types for Cronvello's operator ("service") API — `/external-apps/service/*`.
309
+ *
310
+ * This is NOT the per-account `/v1` API. It is the backend-to-backend control plane that
311
+ * registers, inspects, re-keys and removes the *external apps* Cronvello schedules jobs for.
312
+ * It lives on the same host as `/v1` but behind a different credential — see
313
+ * {@link ../client/admin-client.js CronvelloAdminClient}.
314
+ *
315
+ * Hand-authored plain TypeScript (mirroring the server's Zod DTOs and route contracts) so the
316
+ * SDK keeps ZERO runtime dependencies. When the server contract changes, update these to match.
317
+ */
318
+
319
+ /** How Cronvello authenticates itself towards a registered external app. */
320
+ type ExternalAppAuthMethod = "api_key" | "oauth";
321
+ /**
322
+ * Why the hourly catalog poll was retired for a registration.
323
+ *
324
+ * - `self_managed` — the app drives its own tasks through `@cronvello/sdk`; polling would delete them.
325
+ * - `catalog_gone` — the job-catalog route 404s.
326
+ * - `auth_rejected` — the catalog route rejected our credentials over a sustained period.
327
+ *
328
+ * Widened to `string` so a newly introduced server-side reason does not break the type.
329
+ */
330
+ type CatalogRetiredReason = "self_managed" | "catalog_gone" | "auth_rejected" | (string & {});
331
+ /**
332
+ * A registered external app as the server returns it.
333
+ *
334
+ * Secrets are never returned in plaintext: `apiKey` and `oauthClientSecret` are always `null`,
335
+ * with masked previews alongside them. The one-time plaintext token appears only in
336
+ * {@link ExternalAppRegisterResult.generatedApiKey} and {@link ExternalAppRotateKeyResult.newApiKey}.
337
+ *
338
+ * Timestamps arrive as JSON strings.
339
+ */
340
+ interface ExternalApp {
341
+ /** Numeric primary key. This — not {@link appId} — is what `externalApps.delete()` takes. */
342
+ id: number;
343
+ /** Caller-chosen stable string identifier, e.g. `"node-shop"`. Unique. */
344
+ appId: string;
345
+ name: string;
346
+ description: string | null;
347
+ /** Always `null`; the plaintext key is never echoed back. See {@link apiKeyMasked}. */
348
+ apiKey: null;
349
+ apiKeyMasked: string | null;
350
+ hasApiKey: boolean;
351
+ oauthClientId: string | null;
352
+ /** Always `null`. See {@link oauthClientSecretMasked}. */
353
+ oauthClientSecret: null;
354
+ oauthClientSecretMasked: string | null;
355
+ hasOAuthClientSecret: boolean;
356
+ authMethod: ExternalAppAuthMethod;
357
+ /** Base URL Cronvello calls back on. Snake_case on the wire — the server's column name. */
358
+ base_url: string;
359
+ createdAt: string;
360
+ isActive: boolean;
361
+ /** Maintained by the server's liveness monitor; not something a caller sets directly. */
362
+ isLive: boolean;
363
+ /** FK into the server's job-target-url table. */
364
+ targetUrl: number;
365
+ /** Path under `base_url` that serves the app's job catalog. Server default: `/cron-jobs`. */
366
+ jobRoutePath: string | null;
367
+ urgency: Urgency;
368
+ lastSyncedAt: string | null;
369
+ lastHealthCheckAt: string | null;
370
+ lastHealthCheckMs: number | null;
371
+ totalJobCount: number | null;
372
+ activeJobCount: number | null;
373
+ livenessMonitoringEnabled: boolean;
374
+ consecutiveUnreachableCount: number;
375
+ consecutiveSyncFailureCount: number;
376
+ lastSyncFailureAt: string | null;
377
+ lastSyncError: string | null;
378
+ catalogRetiredAt: string | null;
379
+ catalogRetiredReason: CatalogRetiredReason | null;
380
+ webhookCallbackEnabled: boolean;
381
+ /** Stored encrypted at rest; the server does not mask this field. */
382
+ webhookCallbackSecret: string | null;
383
+ webhookCallbackTimeoutMs: number | null;
384
+ /** `null` for system/B2B apps owned internally rather than by an account. */
385
+ accountId: number | null;
386
+ }
387
+ /**
388
+ * Body of `POST /external-apps/service/register`.
389
+ *
390
+ * The server validates this strictly — unknown properties are rejected. Cross-field rules
391
+ * (also checked client-side before the request leaves, so you get a `CronvelloConfigError`
392
+ * instead of an opaque 400):
393
+ *
394
+ * - exactly one of `base_url` or `targetUrl` must be present (at least one);
395
+ * - with `authMethod: "api_key"` (the default) you must supply either `apiKey` or `generateApiKey: true`;
396
+ * - with `authMethod: "oauth"` both `oauthClientId` and `oauthClientSecret` are required.
397
+ */
398
+ interface ExternalAppRegisterInput {
399
+ /** Stable string identifier. Existing value ⇒ update; new value ⇒ create. */
400
+ appId: string;
401
+ name: string;
402
+ description?: string | null;
403
+ /** Bring your own per-app token. Mutually exclusive with `generateApiKey`. */
404
+ apiKey?: string;
405
+ /**
406
+ * Let Cronvello mint the per-app token. It comes back exactly once as
407
+ * {@link ExternalAppRegisterResult.generatedApiKey} — persist it there and then, or rotate.
408
+ */
409
+ generateApiKey?: boolean;
410
+ base_url?: string;
411
+ /** Existing job-target-url id, as an alternative to `base_url`. */
412
+ targetUrl?: number;
413
+ /** Defaults to `/cron-jobs` server-side when omitted. */
414
+ jobRoutePath?: string;
415
+ authMethod?: ExternalAppAuthMethod;
416
+ oauthClientId?: string;
417
+ oauthClientSecret?: string;
418
+ /** Defaults to `true` server-side when omitted. */
419
+ isActive?: boolean;
420
+ webhookCallbackEnabled?: boolean;
421
+ /** Required by the server when `webhookCallbackEnabled` is true. Minimum 32 characters. */
422
+ webhookCallbackSecret?: string;
423
+ /** Server-enforced range: 10_000 – 3_600_000 ms. */
424
+ webhookCallbackTimeoutMs?: number;
425
+ }
426
+ /**
427
+ * Result of a register (upsert) call.
428
+ *
429
+ * `generatedApiKey` carries the plaintext token **once**, and only when the server minted it
430
+ * for a newly created app via `generateApiKey`. On an update, or when you supplied your own
431
+ * `apiKey`, it is `null`. There is no second chance to read it — use `rotateKey()` if it is lost.
432
+ */
433
+ interface ExternalAppRegisterResult extends ExternalApp {
434
+ generatedApiKey: string | null;
435
+ }
436
+ /** Result of a key rotation. `newApiKey` is plaintext and returned exactly once. */
437
+ interface ExternalAppRotateKeyResult extends ExternalApp {
438
+ newApiKey: string;
439
+ }
440
+ /** Registration status for one app, keyed by its string `appId`. */
441
+ interface ExternalAppStatus {
442
+ /** `false` when no app with that `appId` exists; every other field is then at its zero value. */
443
+ registered: boolean;
444
+ isActive: boolean;
445
+ lastSyncedAt: string | null;
446
+ isLive: boolean;
447
+ jobCount: number;
448
+ }
449
+
450
+ /**
451
+ * Operator-side client for Cronvello's backend-to-backend API (`/external-apps/service/*`).
452
+ *
453
+ * ## Why this is a separate class from {@link CronvelloClient}
454
+ *
455
+ * Both surfaces live on the same host and share the same transport, `{ success, message, data }`
456
+ * envelope and `Authorization: Bearer` scheme — but they take **different credentials**:
457
+ *
458
+ * | Surface | Credential | Scope |
459
+ * | -------------------------- | ----------------------------------- | --------------------------- |
460
+ * | `/v1/*` | per-account API key (`crn_live_…`) | one account's jobs and runs |
461
+ * | `/external-apps/service/*` | Cronvello's service key | every registered app |
462
+ *
463
+ * The service key is far broader than an account key. Folding both into one object would make it
464
+ * easy to send the wrong one — or to leak the service key onto an account-scoped call. Keeping
465
+ * them in two objects, with two differently named options (`apiKey` vs `serviceKey`), makes that
466
+ * mistake impossible to express.
467
+ *
468
+ * Nothing here is needed to *use* Cronvello. It is for the service that provisions apps into it.
469
+ */
470
+
471
+ interface CronvelloAdminClientOptions {
472
+ /**
473
+ * Cronvello's backend-to-backend service key. Required.
474
+ *
475
+ * This is **not** an account API key — it authorizes operations across every registered app.
476
+ * Keep it server-side only.
477
+ */
478
+ serviceKey: string;
479
+ /** API base URL. Defaults to https://api.cronvello.com. */
480
+ baseUrl?: string;
481
+ /** Per-request timeout in ms (default 30_000). */
482
+ timeoutMs?: number;
483
+ /** Retry attempts for 429/5xx/network errors (default 2). */
484
+ maxRetries?: number;
485
+ /** Custom fetch implementation (default: global fetch). */
486
+ fetch?: FetchLike;
487
+ /** Telemetry hook for every request attempt. */
488
+ onRequest?: (info: {
489
+ method: string;
490
+ path: string;
491
+ status: number;
492
+ attempt: number;
493
+ durationMs: number;
494
+ }) => void;
495
+ }
496
+ /**
497
+ * Operator access to Cronvello's app registry.
498
+ *
499
+ * ```ts
500
+ * const admin = new CronvelloAdminClient({ serviceKey: process.env.CRONVELLO_SERVICE_KEY! });
501
+ * const app = await admin.externalApps.register({
502
+ * appId: "node-shop",
503
+ * name: "Shop",
504
+ * base_url: "https://shop.example.com",
505
+ * generateApiKey: true,
506
+ * });
507
+ * // app.generatedApiKey is plaintext and shown exactly once.
508
+ * ```
509
+ *
510
+ * Errors follow the rest of the SDK: `CronvelloConfigError` for anything caught before the
511
+ * request leaves, `CronvelloApiError` for a non-2xx response, `CronvelloNetworkError` for a
512
+ * transport failure.
513
+ */
514
+ declare class CronvelloAdminClient {
515
+ private readonly transport;
516
+ /** The resolved base URL in use. */
517
+ readonly baseUrl: string;
518
+ /** Registration, status, key rotation and removal of external apps. */
519
+ readonly externalApps: ExternalAppsResource;
520
+ constructor(options: CronvelloAdminClientOptions);
521
+ }
522
+ declare class ExternalAppsResource {
523
+ private readonly t;
524
+ constructor(t: Transport);
525
+ /**
526
+ * Idempotent upsert, keyed on the string `appId`: creates when unknown, updates credentials
527
+ * and URL when it already exists. Safe to retry and to re-run on every provisioning pass.
528
+ *
529
+ * When the server mints the token (`generateApiKey: true` on a *new* app) it comes back as
530
+ * `generatedApiKey` — plaintext, exactly once. On an update it is `null`.
531
+ */
532
+ register(input: ExternalAppRegisterInput): Promise<ExternalAppRegisterResult>;
533
+ /** Registration status, last-sync info and job count for one app, by its string `appId`. */
534
+ status(appId: string): Promise<ExternalAppStatus>;
535
+ /**
536
+ * Mint a fresh per-app token, by string `appId`. The new token is returned once as `newApiKey`
537
+ * and must be written into the app's environment — the previous one stops working.
538
+ *
539
+ * Use this for drift recovery when the current token is no longer known.
540
+ */
541
+ rotateKey(appId: string): Promise<ExternalAppRotateKeyResult>;
542
+ /**
543
+ * Delete a registration, cascading its jobs and tasks.
544
+ *
545
+ * ⚠ This one takes the **numeric** {@link ExternalApp.id}, not the string `appId` the other
546
+ * three methods take — an asymmetry in the server's route contract. Read the id off a
547
+ * `register()` result (or a prior lookup); passing a string `appId` here is rejected locally.
548
+ */
549
+ delete(id: number): Promise<void>;
550
+ }
551
+
285
552
  /**
286
553
  * Error taxonomy for the SDK. Everything thrown by the public surface is a
287
554
  * `CronvelloError` (or subclass), so callers can `catch (e) { if (e instanceof CronvelloError) … }`.
@@ -331,6 +598,9 @@ declare class CronvelloConfigError extends CronvelloError {
331
598
  * • High-level registry: `defineCronvello({ jobs })` → `.sync()` + `.expressHandler()` / `.nextHandler()`.
332
599
  * Define jobs in code; the SDK reconciles them to https://api.cronvello.com and runs them.
333
600
  * • Low-level client: `new CronvelloClient({ apiKey })` → typed access to the whole `/v1` API.
601
+ *
602
+ * Plus an operator surface, `new CronvelloAdminClient({ serviceKey })`, for the service that
603
+ * registers apps into Cronvello. Separate class, separate credential — see its doc comment.
334
604
  */
335
605
 
336
606
  /**
@@ -339,4 +609,4 @@ declare class CronvelloConfigError extends CronvelloError {
339
609
  */
340
610
  declare function generateDispatchSecret(bytes?: number): string;
341
611
 
342
- export { AccountRunsQuery, AccountTasksQuery, CRONVELLO_DEFAULT_BASE_URL, type CronValidation, CronvelloApiError, type CronvelloApp, CronvelloAppConfig, CronvelloClient, type CronvelloClientOptions, CronvelloConfigError, type CronvelloEnvConfig, CronvelloError, CronvelloNetworkError, DispatchRequest, DispatchResponse, type FormatOptions, JobCreateBody, JobUpdateBody, MeResponse, PublicJob, PublicRun, PublicTask, ReconcileResult, RegistryReconcileRequest, RegistryReconcileResponse, RunNowResult, RunsPage, RunsQuery, SyncOptions, TaskCreateBody, TaskUpdateBody, TasksPage, UsageResponse, type Weekday, cron, daily, defineCronvello, every, everyHours, everyMinutes, formatSyncResult, generateDispatchSecret, hourly, isValidTimeZone, monthly, schedule, validateCron, weekdays, weekends, weekly };
612
+ export { AccountRunsQuery, AccountTasksQuery, CRONVELLO_DEFAULT_BASE_URL, type CatalogRetiredReason, type CronValidation, CronvelloAdminClient, type CronvelloAdminClientOptions, CronvelloApiError, type CronvelloApp, CronvelloAppConfig, CronvelloClient, type CronvelloClientOptions, CronvelloConfigError, type CronvelloEnvConfig, CronvelloError, CronvelloNetworkError, DispatchRequest, DispatchResponse, type ExternalApp, type ExternalAppAuthMethod, type ExternalAppRegisterInput, type ExternalAppRegisterResult, type ExternalAppRotateKeyResult, type ExternalAppStatus, type FormatOptions, JobCreateBody, JobUpdateBody, MeResponse, PublicJob, PublicRun, PublicTask, ReconcileResult, RegistryReconcileRequest, RegistryReconcileResponse, RunNowResult, RunsPage, RunsQuery, SyncOptions, TaskCreateBody, TaskUpdateBody, TasksPage, Urgency, UsageResponse, type Weekday, cron, daily, defineCronvello, every, everyHours, everyMinutes, formatSyncResult, generateDispatchSecret, hourly, isValidTimeZone, monthly, schedule, validateCron, weekdays, weekends, weekly };