@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.
@@ -434,16 +434,28 @@ interface CronvelloAppConfig {
434
434
  /**
435
435
  * Public base URL of THIS app, where Cronvello delivers callbacks.
436
436
  * e.g. "https://app.example.com". The dispatch handler is mounted under it.
437
+ *
438
+ * Only needed for the hosted side (`sync()`, `dispatchUrl`). Local runs — `cronvello dev`,
439
+ * `trigger()` — never leave the machine, so you can leave it out entirely.
437
440
  */
438
- appUrl: string;
439
- /** Cronvello account API key (`crn_live_…`). */
440
- apiKey: string;
441
+ appUrl?: string;
442
+ /**
443
+ * Cronvello account API key (`crn_live_…`).
444
+ *
445
+ * Only needed for the hosted side (`sync()`, `run()`, `client`). Leave it out to run
446
+ * locally with no account.
447
+ */
448
+ apiKey?: string;
441
449
  /**
442
450
  * Shared secret. Cronvello sends it back as `Authorization: Bearer <secret>` on every
443
451
  * dispatch; the mounted handler verifies it in constant time. Generate a strong random
444
452
  * value and store it in your env (e.g. `openssl rand -hex 32`).
453
+ *
454
+ * Only needed to accept dispatches from the hosted side. Without it there is no HTTP
455
+ * entry point to protect, so local runs don't require one — and any mounted handler
456
+ * refuses every request rather than running unauthenticated.
445
457
  */
446
- dispatchSecret: string;
458
+ dispatchSecret?: string;
447
459
  /** Path the dispatch handler is mounted at. Default "/cronvello/dispatch". */
448
460
  dispatchPath?: string;
449
461
  /** Cronvello API base URL. Default "https://api.cronvello.com". */
@@ -550,4 +562,4 @@ interface DispatchHandler {
550
562
  handle(req: DispatchRequest): Promise<DispatchResponse>;
551
563
  }
552
564
 
553
- export { type AccountTasksQuery as A, type RegistryReconcileChange as B, type CronvelloAppConfig as C, type DispatchRequest as D, type ExecutionMode as E, type FetchLike as F, type RegistryTaskInput as G, type HttpMethod as H, type RunStatus as I, type JobCreateBody as J, type RunType as K, type SuccessCriteria as L, type MeResponse as M, type Urgency as N, type DispatchHandler as O, type PublicJob as P, type RunNowResult as R, type SyncOptions as S, Transport as T, type UsageResponse as U, type JobUpdateBody as a, type PublicTask as b, type TaskCreateBody as c, type TasksPage as d, type TaskUpdateBody as e, type RunsQuery as f, type RunsPage as g, type AccountRunsQuery as h, type PublicRun as i, type RegistryReconcileRequest as j, type RegistryReconcileResponse as k, type ResolvedJob as l, type ReconcileResult as m, type DispatchResponse as n, type CallbackStatus as o, type CronvelloHooks as p, type CronvelloJobConfig as q, type CronvelloJobConfigWithKey as r, type CronvelloJobContext as s, type CronvelloJobHandler as t, type CronvelloJobsInput as u, type CronvelloLogger as v, type CronvelloRunSource as w, type JobStatus as x, type Pagination as y, type ReconcileTaskChange as z };
565
+ export { type AccountTasksQuery as A, type ReconcileTaskChange as B, type CronvelloAppConfig as C, type DispatchRequest as D, type ExecutionMode as E, type FetchLike as F, type RegistryReconcileChange as G, type HttpMethod as H, type RegistryTaskInput as I, type JobCreateBody as J, type RunStatus as K, type RunType as L, type MeResponse as M, type SuccessCriteria as N, type DispatchHandler as O, type PublicJob as P, type RunNowResult as R, type SyncOptions as S, Transport as T, type UsageResponse as U, type JobUpdateBody as a, type PublicTask as b, type TaskCreateBody as c, type TasksPage as d, type TaskUpdateBody as e, type RunsQuery as f, type RunsPage as g, type AccountRunsQuery as h, type PublicRun as i, type RegistryReconcileRequest as j, type RegistryReconcileResponse as k, type ResolvedJob as l, type ReconcileResult as m, type DispatchResponse as n, type Urgency as o, type CallbackStatus as p, type CronvelloHooks as q, type CronvelloJobConfig as r, type CronvelloJobConfigWithKey as s, type CronvelloJobContext as t, type CronvelloJobHandler as u, type CronvelloJobsInput as v, type CronvelloLogger as w, type CronvelloRunSource as x, type JobStatus as y, type Pagination as z };
@@ -434,16 +434,28 @@ interface CronvelloAppConfig {
434
434
  /**
435
435
  * Public base URL of THIS app, where Cronvello delivers callbacks.
436
436
  * e.g. "https://app.example.com". The dispatch handler is mounted under it.
437
+ *
438
+ * Only needed for the hosted side (`sync()`, `dispatchUrl`). Local runs — `cronvello dev`,
439
+ * `trigger()` — never leave the machine, so you can leave it out entirely.
437
440
  */
438
- appUrl: string;
439
- /** Cronvello account API key (`crn_live_…`). */
440
- apiKey: string;
441
+ appUrl?: string;
442
+ /**
443
+ * Cronvello account API key (`crn_live_…`).
444
+ *
445
+ * Only needed for the hosted side (`sync()`, `run()`, `client`). Leave it out to run
446
+ * locally with no account.
447
+ */
448
+ apiKey?: string;
441
449
  /**
442
450
  * Shared secret. Cronvello sends it back as `Authorization: Bearer <secret>` on every
443
451
  * dispatch; the mounted handler verifies it in constant time. Generate a strong random
444
452
  * value and store it in your env (e.g. `openssl rand -hex 32`).
453
+ *
454
+ * Only needed to accept dispatches from the hosted side. Without it there is no HTTP
455
+ * entry point to protect, so local runs don't require one — and any mounted handler
456
+ * refuses every request rather than running unauthenticated.
445
457
  */
446
- dispatchSecret: string;
458
+ dispatchSecret?: string;
447
459
  /** Path the dispatch handler is mounted at. Default "/cronvello/dispatch". */
448
460
  dispatchPath?: string;
449
461
  /** Cronvello API base URL. Default "https://api.cronvello.com". */
@@ -550,4 +562,4 @@ interface DispatchHandler {
550
562
  handle(req: DispatchRequest): Promise<DispatchResponse>;
551
563
  }
552
564
 
553
- export { type AccountTasksQuery as A, type RegistryReconcileChange as B, type CronvelloAppConfig as C, type DispatchRequest as D, type ExecutionMode as E, type FetchLike as F, type RegistryTaskInput as G, type HttpMethod as H, type RunStatus as I, type JobCreateBody as J, type RunType as K, type SuccessCriteria as L, type MeResponse as M, type Urgency as N, type DispatchHandler as O, type PublicJob as P, type RunNowResult as R, type SyncOptions as S, Transport as T, type UsageResponse as U, type JobUpdateBody as a, type PublicTask as b, type TaskCreateBody as c, type TasksPage as d, type TaskUpdateBody as e, type RunsQuery as f, type RunsPage as g, type AccountRunsQuery as h, type PublicRun as i, type RegistryReconcileRequest as j, type RegistryReconcileResponse as k, type ResolvedJob as l, type ReconcileResult as m, type DispatchResponse as n, type CallbackStatus as o, type CronvelloHooks as p, type CronvelloJobConfig as q, type CronvelloJobConfigWithKey as r, type CronvelloJobContext as s, type CronvelloJobHandler as t, type CronvelloJobsInput as u, type CronvelloLogger as v, type CronvelloRunSource as w, type JobStatus as x, type Pagination as y, type ReconcileTaskChange as z };
565
+ export { type AccountTasksQuery as A, type ReconcileTaskChange as B, type CronvelloAppConfig as C, type DispatchRequest as D, type ExecutionMode as E, type FetchLike as F, type RegistryReconcileChange as G, type HttpMethod as H, type RegistryTaskInput as I, type JobCreateBody as J, type RunStatus as K, type RunType as L, type MeResponse as M, type SuccessCriteria as N, type DispatchHandler as O, type PublicJob as P, type RunNowResult as R, type SyncOptions as S, Transport as T, type UsageResponse as U, type JobUpdateBody as a, type PublicTask as b, type TaskCreateBody as c, type TasksPage as d, type TaskUpdateBody as e, type RunsQuery as f, type RunsPage as g, type AccountRunsQuery as h, type PublicRun as i, type RegistryReconcileRequest as j, type RegistryReconcileResponse as k, type ResolvedJob as l, type ReconcileResult as m, type DispatchResponse as n, type Urgency as o, type CallbackStatus as p, type CronvelloHooks as q, type CronvelloJobConfig as r, type CronvelloJobConfigWithKey as s, type CronvelloJobContext as t, type CronvelloJobHandler as u, type CronvelloJobsInput as v, type CronvelloLogger as w, type CronvelloRunSource as x, type JobStatus as y, type Pagination as z };
@@ -1,4 +1,4 @@
1
- import { O as DispatchHandler } from './dispatch-handler-BoadpsL9.cjs';
1
+ import { O as DispatchHandler } from './dispatch-handler-yHIFEnNG.cjs';
2
2
 
3
3
  /**
4
4
  * Express adapter. Zero dependency on `express` itself — the request/response are typed
package/dist/express.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { O as DispatchHandler } from './dispatch-handler-BoadpsL9.js';
1
+ import { O as DispatchHandler } from './dispatch-handler-yHIFEnNG.js';
2
2
 
3
3
  /**
4
4
  * Express adapter. Zero dependency on `express` itself — the request/response are typed
package/dist/index.cjs CHANGED
@@ -545,8 +545,13 @@ function createDispatcher(state) {
545
545
  if (req.method.toUpperCase() !== "POST") {
546
546
  return resp(405, { ok: false, error: "Method not allowed" });
547
547
  }
548
+ const secret = state.dispatchSecret;
549
+ if (!secret) {
550
+ log.error?.("[cronvello] dispatch refused: no `dispatchSecret` configured on this app");
551
+ return resp(500, { ok: false, error: "Dispatch is not configured" });
552
+ }
548
553
  const token = parseBearer(req.authorization);
549
- if (!token || !timingSafeEqual(token, state.dispatchSecret)) {
554
+ if (!token || !timingSafeEqual(token, secret)) {
550
555
  log.warn?.("[cronvello] dispatch rejected: bad or missing bearer token");
551
556
  return resp(401, { ok: false, error: "Unauthorized" });
552
557
  }
@@ -577,7 +582,7 @@ function createDispatcher(state) {
577
582
  const isAsync = callback !== null;
578
583
  const ctx = buildContext(key, job, req, body, isAsync, "dispatch");
579
584
  if (callback) {
580
- const work = runAndReportCallback(job, ctx, callback, state.dispatchSecret, log, invokeHandler);
585
+ const work = runAndReportCallback(job, ctx, callback, secret, log, invokeHandler);
581
586
  if (req.waitUntil) req.waitUntil(work);
582
587
  return resp(202, { ok: true, job: key, accepted: true });
583
588
  }
@@ -1189,6 +1194,7 @@ var LocalEngine = class {
1189
1194
  started = false;
1190
1195
  stopped = false;
1191
1196
  signalCleanup = null;
1197
+ startedAtMs = null;
1192
1198
  constructor(jobs, runner, options = {}) {
1193
1199
  this.clock = options.clock ?? realClock;
1194
1200
  if (options.onEvent) this.listeners.add(options.onEvent);
@@ -1237,7 +1243,8 @@ var LocalEngine = class {
1237
1243
  if (this.started) return this;
1238
1244
  this.started = true;
1239
1245
  this.stopped = false;
1240
- this.emit({ type: "engine-start", jobs: this.states.length, at: this.clock.now() });
1246
+ this.startedAtMs = this.clock.now();
1247
+ this.emit({ type: "engine-start", jobs: this.states.length, at: this.startedAtMs });
1241
1248
  for (const state of this.states) {
1242
1249
  if (state.isReboot) {
1243
1250
  this.launch(state);
@@ -1301,6 +1308,14 @@ var LocalEngine = class {
1301
1308
  get activeRuns() {
1302
1309
  return this.inFlight.size;
1303
1310
  }
1311
+ /** Epoch ms of the `start()` call, or null while the engine has never been started. */
1312
+ get startedAt() {
1313
+ return this.startedAtMs;
1314
+ }
1315
+ /** True between `start()` and `stop()`. */
1316
+ get running() {
1317
+ return this.started && !this.stopped;
1318
+ }
1304
1319
  // ── Scheduling ─────────────────────────────────────────────────────────────
1305
1320
  scheduleNext(state) {
1306
1321
  if (this.stopped) return;
@@ -1495,14 +1510,20 @@ function defineCronvello(config) {
1495
1510
  }
1496
1511
  const jobs = normalizeJobs(config.jobs, validate);
1497
1512
  const dispatchPath = normalizePath(config.dispatchPath ?? DEFAULT_DISPATCH_PATH);
1498
- const dispatchUrl = joinUrl(config.appUrl, dispatchPath);
1499
- const client = new CronvelloClient({
1500
- apiKey: config.apiKey,
1501
- baseUrl: config.baseUrl ?? CRONVELLO_DEFAULT_BASE_URL,
1502
- ...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {},
1503
- ...config.maxRetries !== void 0 ? { maxRetries: config.maxRetries } : {},
1504
- ...config.fetch ? { fetch: config.fetch } : {}
1505
- });
1513
+ let clientInstance;
1514
+ const getClient = () => {
1515
+ if (!clientInstance) {
1516
+ requireCloud(config, ["apiKey"], "Talking to the Cronvello API");
1517
+ clientInstance = new CronvelloClient({
1518
+ apiKey: config.apiKey,
1519
+ baseUrl: config.baseUrl ?? CRONVELLO_DEFAULT_BASE_URL,
1520
+ ...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {},
1521
+ ...config.maxRetries !== void 0 ? { maxRetries: config.maxRetries } : {},
1522
+ ...config.fetch ? { fetch: config.fetch } : {}
1523
+ });
1524
+ }
1525
+ return clientInstance;
1526
+ };
1506
1527
  const dispatcher = createDispatcher({
1507
1528
  jobs,
1508
1529
  dispatchSecret: config.dispatchSecret,
@@ -1511,17 +1532,29 @@ function defineCronvello(config) {
1511
1532
  ...config.maxBodyBytes !== void 0 ? { maxBodyBytes: config.maxBodyBytes } : {}
1512
1533
  });
1513
1534
  const app = {
1514
- client,
1535
+ get client() {
1536
+ return getClient();
1537
+ },
1515
1538
  jobs,
1516
1539
  appName: config.appName,
1517
1540
  dispatchPath,
1518
- dispatchUrl,
1541
+ get dispatchUrl() {
1542
+ requireCloud(config, ["appUrl"], "Building the dispatch URL");
1543
+ return joinUrl(config.appUrl, dispatchPath);
1544
+ },
1545
+ get isCloudConfigured() {
1546
+ return missingCloudFields(config, CLOUD_FIELDS).length === 0;
1547
+ },
1519
1548
  async sync(options) {
1549
+ requireCloud(config, CLOUD_FIELDS, "Syncing your jobs to Cronvello");
1550
+ const client = getClient();
1551
+ const dispatchUrl = joinUrl(config.appUrl, dispatchPath);
1552
+ const dispatchSecret = config.dispatchSecret;
1520
1553
  const opts = options ?? {};
1521
1554
  if (!opts.dryRun) {
1522
1555
  try {
1523
1556
  const res = await client.reconcileRegistry(
1524
- buildRegistryRequest(config, dispatchUrl, defaultTimeZone, [...jobs.values()], opts)
1557
+ buildRegistryRequest(config, dispatchSecret, dispatchUrl, defaultTimeZone, [...jobs.values()], opts)
1525
1558
  );
1526
1559
  return {
1527
1560
  jobId: res.job.id,
@@ -1549,7 +1582,7 @@ function defineCronvello(config) {
1549
1582
  client,
1550
1583
  appName: config.appName,
1551
1584
  dispatchUrl,
1552
- dispatchSecret: config.dispatchSecret,
1585
+ dispatchSecret,
1553
1586
  defaultTimeZone,
1554
1587
  jobs: [...jobs.values()]
1555
1588
  },
@@ -1560,6 +1593,7 @@ function defineCronvello(config) {
1560
1593
  if (!jobs.has(key)) {
1561
1594
  throw new CronvelloConfigError(`Unknown job '${key}'. Known: ${[...jobs.keys()].join(", ") || "(none)"}`);
1562
1595
  }
1596
+ const client = getClient();
1563
1597
  const containers = await client.jobs.list();
1564
1598
  const container = containers.find((j) => j.name === config.appName);
1565
1599
  if (!container) {
@@ -1574,8 +1608,16 @@ function defineCronvello(config) {
1574
1608
  },
1575
1609
  trigger: (key, payload) => dispatcher.runLocal(key, payload),
1576
1610
  handle: dispatcher.handle,
1577
- expressHandler: () => expressHandler(app),
1578
- nextHandler: () => nextHandler(app),
1611
+ // Mounting a dispatch route that could only ever answer "not configured" hides the real
1612
+ // mistake behind a runtime 500, so both adapters fail at mount time instead.
1613
+ expressHandler: () => {
1614
+ requireCloud(config, ["dispatchSecret"], "Mounting the dispatch handler");
1615
+ return expressHandler(app);
1616
+ },
1617
+ nextHandler: () => {
1618
+ requireCloud(config, ["dispatchSecret"], "Mounting the dispatch handler");
1619
+ return nextHandler(app);
1620
+ },
1579
1621
  dev(options) {
1580
1622
  const { autoStart = true, dashboard, ...engineOptions } = options ?? {};
1581
1623
  const engineJobs = buildEngineJobs([...jobs.values()], defaultTimeZone);
@@ -1625,7 +1667,9 @@ function buildEngineJobs(jobs, defaultTimeZone) {
1625
1667
  !appUrl && "CRONVELLO_APP_URL (or PUBLIC_URL)"
1626
1668
  ].filter(Boolean);
1627
1669
  if (missing.length) {
1628
- throw new CronvelloConfigError(`defineCronvello.fromEnv() is missing required env: ${missing.join(", ")}.`);
1670
+ throw new CronvelloConfigError(
1671
+ `defineCronvello.fromEnv() is missing required env: ${missing.join(", ")}. To run locally with no account, use defineCronvello({ appName, jobs }) instead \u2014 it needs none of these.`
1672
+ );
1629
1673
  }
1630
1674
  return defineCronvello2({
1631
1675
  ...config,
@@ -1637,17 +1681,32 @@ function buildEngineJobs(jobs, defaultTimeZone) {
1637
1681
  }
1638
1682
  defineCronvello2.fromEnv = fromEnv;
1639
1683
  })(defineCronvello || (defineCronvello = {}));
1684
+ var CLOUD_FIELDS = ["apiKey", "appUrl", "dispatchSecret"];
1685
+ var CLOUD_FIELD_HINT = {
1686
+ apiKey: "`apiKey` (crn_live_\u2026, from your Cronvello account)",
1687
+ appUrl: "`appUrl` (the public https URL of THIS app, where Cronvello delivers callbacks)",
1688
+ dispatchSecret: "`dispatchSecret` (a random 32-byte value: run `npx cronvello secret`)"
1689
+ };
1690
+ function missingCloudFields(config, fields) {
1691
+ return fields.filter((f) => !config[f]);
1692
+ }
1693
+ function requireCloud(config, fields, purpose) {
1694
+ const missing = missingCloudFields(config, fields);
1695
+ if (!missing.length) return;
1696
+ throw new CronvelloConfigError(
1697
+ `${purpose} needs config this app doesn't have: ${missing.map((f) => CLOUD_FIELD_HINT[f]).join(", ")}. Local runs (\`cronvello dev\`, \`trigger()\`) work without any of it.`
1698
+ );
1699
+ }
1640
1700
  function validateConfig(config) {
1641
1701
  if (!config) throw new CronvelloConfigError("defineCronvello requires a config object.");
1642
1702
  if (!config.appName || !config.appName.trim()) throw new CronvelloConfigError("`appName` is required.");
1643
- if (!config.appUrl || !/^https?:\/\//i.test(config.appUrl)) {
1703
+ if (!config.jobs) throw new CronvelloConfigError("`jobs` is required.");
1704
+ if (config.appUrl !== void 0 && !/^https?:\/\//i.test(config.appUrl)) {
1644
1705
  throw new CronvelloConfigError("`appUrl` must be an absolute http(s) URL (the public URL of THIS app).");
1645
1706
  }
1646
- if (!config.apiKey) throw new CronvelloConfigError("`apiKey` (crn_live_\u2026) is required.");
1647
- if (!config.dispatchSecret || config.dispatchSecret.length < 16) {
1648
- throw new CronvelloConfigError("`dispatchSecret` is required and must be at least 16 chars (use a random 32-byte value).");
1707
+ if (config.dispatchSecret !== void 0 && config.dispatchSecret.length < 16) {
1708
+ throw new CronvelloConfigError("`dispatchSecret` must be at least 16 chars (use a random 32-byte value).");
1649
1709
  }
1650
- if (!config.jobs) throw new CronvelloConfigError("`jobs` is required.");
1651
1710
  }
1652
1711
  function normalizeJobs(input, validate) {
1653
1712
  const map = /* @__PURE__ */ new Map();
@@ -1684,14 +1743,14 @@ function normalizePath(path) {
1684
1743
  function joinUrl(base, path) {
1685
1744
  return `${base.replace(/\/+$/, "")}${path}`;
1686
1745
  }
1687
- function buildRegistryRequest(config, dispatchUrl, defaultTimeZone, jobs, opts) {
1746
+ function buildRegistryRequest(config, dispatchSecret, dispatchUrl, defaultTimeZone, jobs, opts) {
1688
1747
  const tasks = jobs.map((j) => {
1689
1748
  const cfg = j.config;
1690
1749
  const task = {
1691
1750
  key: j.key,
1692
1751
  schedule: cfg.schedule,
1693
1752
  targetUrl: dispatchUrl,
1694
- targetToken: config.dispatchSecret,
1753
+ targetToken: dispatchSecret,
1695
1754
  method: "POST",
1696
1755
  timeZone: cfg.timeZone ?? defaultTimeZone,
1697
1756
  requestBody: JSON.stringify({ job: j.key, ...cfg.payload ?? {} })
@@ -1857,6 +1916,112 @@ function field(value, min, max, label) {
1857
1916
  return value;
1858
1917
  }
1859
1918
 
1919
+ // src/client/admin-client.ts
1920
+ var CronvelloAdminClient = class {
1921
+ transport;
1922
+ /** The resolved base URL in use. */
1923
+ baseUrl;
1924
+ /** Registration, status, key rotation and removal of external apps. */
1925
+ externalApps;
1926
+ constructor(options) {
1927
+ if (!options || !options.serviceKey) {
1928
+ throw new CronvelloConfigError(
1929
+ "CronvelloAdminClient requires a `serviceKey` (Cronvello's backend-to-backend key, not an account apiKey)."
1930
+ );
1931
+ }
1932
+ this.baseUrl = (options.baseUrl ?? CRONVELLO_DEFAULT_BASE_URL).replace(/\/+$/, "");
1933
+ this.transport = new Transport({
1934
+ baseUrl: this.baseUrl,
1935
+ apiKey: options.serviceKey,
1936
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
1937
+ ...options.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {},
1938
+ ...options.fetch ? { fetch: options.fetch } : {},
1939
+ ...options.onRequest ? { onRequest: options.onRequest } : {}
1940
+ });
1941
+ this.externalApps = new ExternalAppsResource(this.transport);
1942
+ }
1943
+ };
1944
+ var ExternalAppsResource = class {
1945
+ constructor(t) {
1946
+ this.t = t;
1947
+ }
1948
+ t;
1949
+ /**
1950
+ * Idempotent upsert, keyed on the string `appId`: creates when unknown, updates credentials
1951
+ * and URL when it already exists. Safe to retry and to re-run on every provisioning pass.
1952
+ *
1953
+ * When the server mints the token (`generateApiKey: true` on a *new* app) it comes back as
1954
+ * `generatedApiKey` — plaintext, exactly once. On an update it is `null`.
1955
+ */
1956
+ register(input) {
1957
+ assertRegisterInput(input);
1958
+ return this.t.request({ method: "POST", path: "/external-apps/service/register", body: input });
1959
+ }
1960
+ /** Registration status, last-sync info and job count for one app, by its string `appId`. */
1961
+ status(appId) {
1962
+ assertAppId(appId, "status");
1963
+ return this.t.request({ method: "GET", path: `/external-apps/service/status/${enc2(appId)}` });
1964
+ }
1965
+ /**
1966
+ * Mint a fresh per-app token, by string `appId`. The new token is returned once as `newApiKey`
1967
+ * and must be written into the app's environment — the previous one stops working.
1968
+ *
1969
+ * Use this for drift recovery when the current token is no longer known.
1970
+ */
1971
+ rotateKey(appId) {
1972
+ assertAppId(appId, "rotateKey");
1973
+ return this.t.request({ method: "POST", path: `/external-apps/service/rotate-key/${enc2(appId)}` });
1974
+ }
1975
+ /**
1976
+ * Delete a registration, cascading its jobs and tasks.
1977
+ *
1978
+ * ⚠ This one takes the **numeric** {@link ExternalApp.id}, not the string `appId` the other
1979
+ * three methods take — an asymmetry in the server's route contract. Read the id off a
1980
+ * `register()` result (or a prior lookup); passing a string `appId` here is rejected locally.
1981
+ */
1982
+ delete(id) {
1983
+ if (!Number.isInteger(id) || id <= 0) {
1984
+ throw new CronvelloConfigError(
1985
+ `externalApps.delete() takes the numeric app id (ExternalApp.id), not the string appId \u2014 received ${JSON.stringify(id)}.`
1986
+ );
1987
+ }
1988
+ return this.t.request({ method: "DELETE", path: `/external-apps/service/delete/${id}` });
1989
+ }
1990
+ };
1991
+ function assertRegisterInput(input) {
1992
+ if (!input || typeof input !== "object") {
1993
+ throw new CronvelloConfigError("externalApps.register() requires an input object.");
1994
+ }
1995
+ if (!input.appId || !input.appId.trim()) {
1996
+ throw new CronvelloConfigError("externalApps.register() requires a non-empty `appId`.");
1997
+ }
1998
+ if (!input.name || !input.name.trim()) {
1999
+ throw new CronvelloConfigError("externalApps.register() requires a non-empty `name`.");
2000
+ }
2001
+ if (!input.base_url && input.targetUrl === void 0) {
2002
+ throw new CronvelloConfigError("externalApps.register() requires either `base_url` or `targetUrl`.");
2003
+ }
2004
+ const authMethod = input.authMethod ?? (input.oauthClientId || input.oauthClientSecret ? "oauth" : "api_key");
2005
+ if (authMethod === "api_key" && !input.apiKey && !input.generateApiKey) {
2006
+ throw new CronvelloConfigError(
2007
+ "externalApps.register() with api_key auth requires either `apiKey` or `generateApiKey: true`."
2008
+ );
2009
+ }
2010
+ if (authMethod === "oauth" && (!input.oauthClientId || !input.oauthClientSecret)) {
2011
+ throw new CronvelloConfigError(
2012
+ "externalApps.register() with oauth auth requires both `oauthClientId` and `oauthClientSecret`."
2013
+ );
2014
+ }
2015
+ }
2016
+ function assertAppId(appId, method) {
2017
+ if (typeof appId !== "string" || !appId.trim()) {
2018
+ throw new CronvelloConfigError(`externalApps.${method}() requires a non-empty string appId.`);
2019
+ }
2020
+ }
2021
+ function enc2(segment) {
2022
+ return encodeURIComponent(segment);
2023
+ }
2024
+
1860
2025
  // src/index.ts
1861
2026
  function generateDispatchSecret(bytes = 32) {
1862
2027
  const buf = new Uint8Array(bytes);
@@ -1865,6 +2030,7 @@ function generateDispatchSecret(bytes = 32) {
1865
2030
  }
1866
2031
 
1867
2032
  exports.CRONVELLO_DEFAULT_BASE_URL = CRONVELLO_DEFAULT_BASE_URL;
2033
+ exports.CronvelloAdminClient = CronvelloAdminClient;
1868
2034
  exports.CronvelloApiError = CronvelloApiError;
1869
2035
  exports.CronvelloClient = CronvelloClient;
1870
2036
  exports.CronvelloConfigError = CronvelloConfigError;