c8y-nitro 0.7.1 → 0.8.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.
@@ -0,0 +1,179 @@
1
+ import { c as getTenantOptionsByCategory, f as upsertTenantOption, i as deleteTenantOption, n as createBasicAuthHeader, p as createC8yManifest, s as getCurrentApplicationSettings } from "./c8y-api-DscUs-Kd.mjs";
2
+ import { n as readBootstrapCredentialsFromEnv, r as syncBootstrappedMicroservice } from "./autoBootstrap-Drk7ltXK.mjs";
3
+ import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
4
+ import process from "node:process";
5
+ import { defineCommand } from "citty";
6
+ import { consola } from "consola";
7
+ import { createInterface } from "node:readline/promises";
8
+ //#region src/cli/commands/options.ts
9
+ var options_default = defineCommand({
10
+ meta: {
11
+ name: "options",
12
+ description: "Manage tenant options on the development tenant"
13
+ },
14
+ args: {},
15
+ async run() {
16
+ consola.info("Loading configuration...");
17
+ const { env, nitroConfig, configDir } = await loadC8yConfig();
18
+ consola.info("Validating environment variables...");
19
+ const envVars = validateBootstrapEnv(env);
20
+ consola.info("Loading manifest...");
21
+ const manifest = await createC8yManifest(configDir, nitroConfig);
22
+ const category = manifest.settingsCategory || manifest.contextPath || manifest.name;
23
+ if (!manifest.settings || manifest.settings.length === 0) throw new Error("No settings defined in manifest. Add settings to your c8y.manifest configuration.");
24
+ consola.success(`Using category: ${category}`);
25
+ const devAuthHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
26
+ let bootstrapCredentials;
27
+ try {
28
+ consola.info("Checking microservice on development tenant...");
29
+ bootstrapCredentials = await syncBootstrappedMicroservice({
30
+ rootDir: configDir,
31
+ baseUrl: envVars.C8Y_BASEURL,
32
+ developmentTenant: envVars.C8Y_DEVELOPMENT_TENANT,
33
+ authHeader: devAuthHeader,
34
+ manifest,
35
+ logger: consola,
36
+ env
37
+ });
38
+ } catch (error) {
39
+ consola.warn(`Could not sync microservice with the development tenant: ${error instanceof Error ? error.message : String(error)}`);
40
+ bootstrapCredentials = readBootstrapCredentialsFromEnv(env);
41
+ }
42
+ const bootstrapAuthHeader = bootstrapCredentials ? createBasicAuthHeader(bootstrapCredentials.tenant, bootstrapCredentials.name, bootstrapCredentials.password) : void 0;
43
+ if (!bootstrapAuthHeader) consola.warn("No bootstrap credentials available - reading with the development user. Encrypted credentials.* options cannot be decrypted.");
44
+ consola.info("Fetching current tenant options...");
45
+ const currentOptions = await getTenantOptionsByCategory(envVars.C8Y_BASEURL, category, devAuthHeader);
46
+ if (bootstrapAuthHeader) try {
47
+ const decryptedSettings = await getCurrentApplicationSettings(envVars.C8Y_BASEURL, bootstrapAuthHeader);
48
+ for (const [key, value] of Object.entries(decryptedSettings)) if (currentOptions[key] !== void 0 || key.startsWith("credentials.")) currentOptions[key] = value;
49
+ } catch (error) {
50
+ consola.warn(`Could not read decrypted settings with the bootstrap user: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ const availableKeys = manifest.settings.map((s) => s.key);
53
+ const setManifestKeys = availableKeys.filter((k) => currentOptions[k] !== void 0);
54
+ consola.success(`Found ${Object.keys(currentOptions).length} options set on tenant (${setManifestKeys.length} of ${availableKeys.length} manifest settings)`);
55
+ const ctx = {
56
+ baseUrl: envVars.C8Y_BASEURL,
57
+ category,
58
+ devAuthHeader,
59
+ bootstrapAuthHeader,
60
+ availableKeys,
61
+ currentOptions
62
+ };
63
+ const action = await consola.prompt("What do you want to do?", {
64
+ type: "select",
65
+ options: [
66
+ {
67
+ label: "Read option value",
68
+ value: "read"
69
+ },
70
+ {
71
+ label: "Update/Create option",
72
+ value: "update"
73
+ },
74
+ {
75
+ label: "Delete option(s)",
76
+ value: "delete"
77
+ }
78
+ ]
79
+ });
80
+ if (action === "read") await handleRead(ctx);
81
+ else if (action === "update") await handleUpdate(ctx);
82
+ else if (action === "delete") await handleDelete(ctx);
83
+ }
84
+ });
85
+ /**
86
+ * Handle reading options: prints all options at once (decrypted where
87
+ * possible), then - after the user confirms with Enter - rewrites the output
88
+ * in place with `credentials.*` values masked, so secrets don't linger in the
89
+ * terminal scrollback.
90
+ * @param ctx - The shared options context
91
+ */
92
+ async function handleRead(ctx) {
93
+ const { bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
94
+ if (!bootstrapAuthHeader && availableKeys.some((k) => k.startsWith("credentials."))) consola.warn("Encrypted credentials.* options cannot be read in decrypted form without bootstrap credentials. Run `npx c8y-nitro bootstrap` first.");
95
+ const keys = [.../* @__PURE__ */ new Set([...availableKeys, ...Object.keys(currentOptions)])];
96
+ const padWidth = Math.max(...keys.map((k) => k.length));
97
+ const renderLine = (key, maskCredentials) => {
98
+ const value = currentOptions[key];
99
+ const shownValue = value === void 0 ? "(not set)" : maskCredentials && key.startsWith("credentials.") ? "****" : value;
100
+ const suffix = availableKeys.includes(key) ? "" : " (not in manifest)";
101
+ return ` ${key.padEnd(padWidth)} = ${shownValue}${suffix}`;
102
+ };
103
+ const revealedLines = keys.map((key) => renderLine(key, false));
104
+ process.stdout.write(`${revealedLines.join("\n")}\n`);
105
+ const rl = createInterface({
106
+ input: process.stdin,
107
+ output: process.stdout
108
+ });
109
+ await rl.question("Press <Enter> to hide credential values...");
110
+ rl.close();
111
+ if (process.stdout.isTTY) {
112
+ const columns = process.stdout.columns || 80;
113
+ const rowsFor = (line) => Math.max(1, Math.ceil(line.length / columns));
114
+ const rowsToErase = revealedLines.reduce((rows, line) => rows + rowsFor(line), 1);
115
+ process.stdout.write(`\x1B[${rowsToErase}A\x1B[0J`);
116
+ process.stdout.write(`${keys.map((key) => renderLine(key, true)).join("\n")}\n`);
117
+ }
118
+ }
119
+ /**
120
+ * Handle updating options (with loop for multiple updates)
121
+ * @param ctx - The shared options context
122
+ */
123
+ async function handleUpdate(ctx) {
124
+ const { baseUrl, category, devAuthHeader, bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
125
+ let continueUpdating = true;
126
+ while (continueUpdating) {
127
+ const key = await consola.prompt("Select option to update:", {
128
+ type: "select",
129
+ options: availableKeys.map((k) => ({
130
+ label: currentOptions[k] !== void 0 ? `${k} (current: ${currentOptions[k]})` : k.startsWith("credentials.") && !bootstrapAuthHeader ? `${k} (current: unknown)` : `${k} (not set)`,
131
+ value: k
132
+ })),
133
+ cancel: "reject"
134
+ });
135
+ const currentValue = currentOptions[key];
136
+ const newValue = await consola.prompt("Enter new value:", {
137
+ type: "text",
138
+ default: currentValue,
139
+ cancel: "reject"
140
+ });
141
+ consola.info(`Updating option: ${key}`);
142
+ await upsertTenantOption(baseUrl, category, key, newValue, devAuthHeader);
143
+ currentOptions[key] = newValue;
144
+ consola.success(`Option '${key}' updated successfully`);
145
+ if (!await consola.prompt("Update another option?", {
146
+ type: "confirm",
147
+ initial: false,
148
+ cancel: "reject"
149
+ })) continueUpdating = false;
150
+ }
151
+ }
152
+ /**
153
+ * Handle deleting multiple options
154
+ * @param ctx - The shared options context
155
+ */
156
+ async function handleDelete(ctx) {
157
+ const { baseUrl, category, devAuthHeader, bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
158
+ const allKeys = bootstrapAuthHeader ? availableKeys.filter((k) => currentOptions[k] !== void 0) : [...availableKeys.filter((k) => !k.startsWith("credentials.") && currentOptions[k] !== void 0), ...availableKeys.filter((k) => k.startsWith("credentials."))];
159
+ if (allKeys.length === 0) {
160
+ consola.warn("No options are currently set");
161
+ return;
162
+ }
163
+ const keysToDelete = await consola.prompt("Select option(s) to delete:", {
164
+ type: "multiselect",
165
+ options: allKeys,
166
+ required: true,
167
+ cancel: "reject"
168
+ });
169
+ consola.info(`Deleting ${keysToDelete.length} option(s)...`);
170
+ for (const key of keysToDelete) {
171
+ consola.info(`Deleting option: ${key}`);
172
+ await deleteTenantOption(baseUrl, category, key, devAuthHeader);
173
+ delete currentOptions[key];
174
+ consola.success(`✓ Deleted: ${key}`);
175
+ }
176
+ consola.success("Delete operation completed");
177
+ }
178
+ //#endregion
179
+ export { options_default as default };
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "c8y-nitro";
3
- var version = "0.7.1";
3
+ var version = "0.8.0";
4
4
  var description = "Lightning fast Cumulocity IoT microservice development powered by Nitro";
5
5
  //#endregion
6
6
  export { name as n, version as r, description as t };
@@ -1,4 +1,4 @@
1
- import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-BFjK6AYb.mjs";
1
+ import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-DscUs-Kd.mjs";
2
2
  import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { consola } from "consola";
@@ -1,5 +1,5 @@
1
1
  import { t as createError } from "../../logging-BUYT8Z2V.mjs";
2
- import { t as useDeployedTenantClient } from "../../client-DxmbyZju.mjs";
2
+ import { t as useDeployedTenantClient } from "../../client-D0Tdq7H1.mjs";
3
3
  import { c8yTenantOptionKeys } from "c8y-nitro/runtime";
4
4
  import { defineCachedFunction } from "nitro/cache";
5
5
  import { useRuntimeConfig } from "nitro/runtime-config";
@@ -1,4 +1,4 @@
1
- import { n as getUserTenantDomain } from "../../client-DxmbyZju.mjs";
1
+ import { r as tryGetUserTenantDomainFast } from "../../client-D0Tdq7H1.mjs";
2
2
  import { c8yManifest } from "c8y-nitro/runtime";
3
3
  import { useRuntimeConfig } from "nitro/runtime-config";
4
4
  import { getRequestURL } from "nitro/h3";
@@ -14,13 +14,13 @@ function joinURLSegments(origin, ...segments) {
14
14
  async function resolveServer(event) {
15
15
  const baseURL = useRuntimeConfig().app?.baseURL;
16
16
  const contextPath = c8yManifest.contextPath ?? c8yManifest.name;
17
- if (!import.meta.dev) try {
18
- const domain = await getUserTenantDomain(event);
17
+ if (!import.meta.dev) {
18
+ const domain = await tryGetUserTenantDomainFast(event);
19
19
  if (domain) return {
20
20
  url: joinURLSegments(`https://${domain}`, "service", contextPath, baseURL),
21
21
  description: "Cumulocity microservice endpoint"
22
22
  };
23
- } catch {}
23
+ }
24
24
  const requestUrl = getRequestURL(event, {
25
25
  xForwardedHost: true,
26
26
  xForwardedProto: true
@@ -43,8 +43,9 @@ async function resolveServer(event) {
43
43
  var openapi_default = defineMiddleware(async (event, next) => {
44
44
  const runtimeConfig = useRuntimeConfig();
45
45
  const specRoute = runtimeConfig.nitro?.openAPI?.route || "/_openapi.json";
46
- const baseURL = (runtimeConfig.app?.baseURL ?? "/").replace(/\/+$/, "");
47
- if (event.url.pathname !== `${baseURL}${specRoute}`) return next();
46
+ const specPath = `${(runtimeConfig.app?.baseURL ?? "/").replace(/\/+$/, "")}${specRoute}`;
47
+ const rewrittenSpecPath = `/service/${c8yManifest.contextPath ?? c8yManifest.name}${specPath}`;
48
+ if (event.url.pathname !== specPath && event.url.pathname !== rewrittenSpecPath) return next();
48
49
  const body = await next();
49
50
  if (!isOpenAPIDocument(body)) return body;
50
51
  const excludedPrefixes = runtimeConfig.c8yOpenApiExcludeRoutes ?? [];
@@ -0,0 +1,38 @@
1
+ import { n as buildPublicRequestUrl, r as tryGetUserTenantDomainFast } from "../../client-D0Tdq7H1.mjs";
2
+ import { c8yManifest } from "c8y-nitro/runtime";
3
+ import { defineMiddleware } from "nitro";
4
+ //#region src/module/runtime/handlers/rewriteRequestUrl.ts
5
+ /**
6
+ * Rewrites the incoming request URL to the public tenant endpoint.
7
+ *
8
+ * When deployed, the platform proxy forwards requests with the internal
9
+ * cluster host (e.g. `service-scope-t123.<cluster>.svc.cluster.local`) and the
10
+ * `/service/<contextPath>` prefix stripped. Any handler that derives absolute
11
+ * URLs from the request (`event.url`, `getRequestURL(event)`) therefore leaks
12
+ * internal cluster URLs into responses. This middleware swaps `event.url` for
13
+ * the public one (`https://<tenant domain>/service/<contextPath>/...`) before
14
+ * route handlers run, so request-derived URLs come out publicly reachable.
15
+ *
16
+ * The tenant domain is resolved from the requesting user's credentials and
17
+ * memoized in-process, so the per-request overhead after the first resolution
18
+ * is a hash and a Map lookup. Unauthenticated requests (probes, preflights)
19
+ * are passed through untouched.
20
+ *
21
+ * Registered after the scanned runtime middlewares (see registerRuntime) so
22
+ * they still observe the original URL, and before the OpenAPI transform,
23
+ * which reuses the tenant domain resolved here from its cache.
24
+ */
25
+ var rewriteRequestUrl_default = defineMiddleware(async (event, next) => {
26
+ if (import.meta.dev) return next();
27
+ const domain = await tryGetUserTenantDomainFast(event);
28
+ if (domain && event.url.host !== domain) {
29
+ event.url = buildPublicRequestUrl(domain, c8yManifest.contextPath ?? c8yManifest.name, event.url);
30
+ try {
31
+ event.req.headers.set("x-forwarded-host", domain);
32
+ event.req.headers.set("x-forwarded-proto", "https");
33
+ } catch {}
34
+ }
35
+ return next();
36
+ });
37
+ //#endregion
38
+ export { rewriteRequestUrl_default as default };
package/dist/utils.d.mts CHANGED
@@ -502,110 +502,264 @@ declare function useLogger<T extends object = Record<string, unknown>>(eventOrRe
502
502
  */
503
503
  declare const createError: typeof createError$1;
504
504
  //#endregion
505
- //#region src/utils/schedule.d.ts
506
- type ScheduledTaskPayload = TaskPayload;
507
- type ScheduledTaskContext = TaskContext;
505
+ //#region src/utils/tasks.d.ts
508
506
  /**
509
- * A one-shot schedule definition for `scheduleTask()`.\
510
- * Numbers are treated as seconds from now, strings are parsed as human-readable durations
511
- * such as `"10 minutes"`, and dates are used as exact run times.
507
+ * Concurrency policy for a single job.
508
+ * - `'single'`: at most one run of the job is in flight at a time. A tick that
509
+ * fires while the previous run is still going is skipped, and a manual
510
+ * {@link TaskRegistry.triggerJob} while a run is active joins the in-flight run.
511
+ * - `'parallel'`: every tick (or trigger) starts a fresh run, even if a previous
512
+ * run has not finished. Runs of the same job may overlap.
512
513
  */
513
- type ScheduledTaskInput = Date | number | string;
514
+ type JobConcurrency = 'single' | 'parallel';
515
+ type TaskRegistryPayload = TaskPayload;
516
+ type TaskRegistryContext = TaskContext;
514
517
  /**
515
- * Options for scheduling a Nitro task to run once in the future.\
516
- * `payload` and `context` are forwarded to the task handler's `run()` method.
518
+ * The event passed to a task handler on every execution.
517
519
  */
518
- interface ScheduleTaskOptions {
520
+ interface TaskEvent<TPayload = TaskRegistryPayload> {
519
521
  /**
520
- * Payload forwarded to the task handler's `TaskEvent.payload`.
522
+ * Name of the task (function) being executed.
521
523
  */
522
- payload?: ScheduledTaskPayload;
524
+ task: string;
525
+ /**
526
+ * Name of the job that triggered this run, when the run originates from a scheduled or triggered job.
527
+ */
528
+ job?: string;
529
+ /**
530
+ * Data bound when the job was scheduled (or passed to `run()`/`triggerJob()`).
531
+ */
532
+ payload: TPayload;
533
+ /**
534
+ * Arbitrary context bound alongside the payload.
535
+ */
536
+ context: TaskRegistryContext;
537
+ }
538
+ /**
539
+ * A task handler function registered via {@link TaskRegistry.createTask}.
540
+ */
541
+ type TaskHandler<TPayload = TaskRegistryPayload, TResult = unknown> = (event: TaskEvent<TPayload>) => TResult | Promise<TResult>;
542
+ /**
543
+ * When a job should run.
544
+ * - `{ cron }`: recurring on a cron expression. 5- or 6-field (with seconds) is
545
+ * supported. Evaluated in UTC by default, or in the job's `timezone` if set.
546
+ * - `{ in }`: one-shot, `in` seconds from now.
547
+ * - `{ at }`: one-shot, at an exact instant given as a `Date` or ISO 8601 string,
548
+ * taken literally (never re-projected to another timezone). Times in the past
549
+ * fire almost immediately.
550
+ */
551
+ type JobSchedule = {
552
+ cron: string;
553
+ } | {
554
+ in: number;
555
+ } | {
556
+ at: Date | string;
557
+ };
558
+ /**
559
+ * Options for scheduling a job via {@link TaskRegistry.scheduleJob}.
560
+ */
561
+ interface ScheduleJobOptions<TTask extends string = string, TPayload = TaskRegistryPayload, TSchedule extends JobSchedule = JobSchedule> {
562
+ /**
563
+ * Unique, caller-chosen job name. This is the handle used by `cancelJob()`, `getJob()` and `triggerJob()`.
564
+ */
565
+ name: string;
566
+ /**
567
+ * Name of a task registered via `createTask()` that this job runs.
568
+ */
569
+ task: TTask;
570
+ /**
571
+ * When the job should run.
572
+ */
573
+ schedule: TSchedule;
574
+ /**
575
+ * Data forwarded to the handler's `event.payload`.\
576
+ * Prefer stable identifiers (e.g. a config id) over live object references so the
577
+ * handler can resolve current values at run time and the schedule stays serializable.
578
+ */
579
+ payload?: TPayload;
523
580
  /**
524
- * Context forwarded to the task handler's `TaskEvent.context`.
581
+ * Data forwarded to the handler's `event.context`.
525
582
  */
526
- context?: ScheduledTaskContext;
583
+ context?: TaskRegistryContext;
527
584
  /**
528
- * When the task should run. Numbers are seconds from now.
585
+ * Concurrency policy for this job. Defaults to `'single'`.
529
586
  */
530
- schedule: ScheduledTaskInput;
587
+ concurrency?: JobConcurrency;
588
+ /**
589
+ * Recurring only: the IANA timezone the cron expression is evaluated in
590
+ * (e.g. `'Europe/Berlin'`). Defaults to `'UTC'` so the schedule is deterministic
591
+ * regardless of the container's local time. croner performs the alignment — no
592
+ * conversion is done by `c8y-nitro`. Ignored for one-shot schedules.
593
+ */
594
+ timezone?: string;
595
+ /**
596
+ * Recurring only: also run once immediately on registration, in addition to the cron. Defaults to `false`.
597
+ */
598
+ immediate?: boolean;
599
+ /**
600
+ * Recurring only: stop after this many scheduled runs.
601
+ */
602
+ maxRuns?: number;
603
+ /**
604
+ * Overwrite an existing job with the same name instead of throwing. Defaults to `false`.
605
+ */
606
+ replace?: boolean;
531
607
  }
532
608
  /**
533
- * Public information about a pending scheduled task.\
534
- * Returned by `scheduleTask()` and by `listScheduledTasks()`.
609
+ * Fields common to every job, regardless of kind.
535
610
  */
536
- interface ScheduledTaskInfo {
611
+ interface BaseJobInfo {
537
612
  /**
538
- * Stable UUID used to list or cancel the scheduled task.
613
+ * The job's unique name.
539
614
  */
540
- id: string;
615
+ name: string;
541
616
  /**
542
- * Nitro task name that will be passed to `runTask()`.
617
+ * The task (function) the job runs.
543
618
  */
544
619
  task: string;
545
620
  /**
546
- * Exact execution time as an ISO date string.
621
+ * The job's concurrency policy.
622
+ */
623
+ concurrency: JobConcurrency;
624
+ /**
625
+ * Next scheduled execution as an ISO datetime string, or `null` if nothing is pending.
626
+ */
627
+ nextRun: string | null;
628
+ /**
629
+ * Whether a run of this job is currently in flight.
547
630
  */
548
- runAt: string;
631
+ running: boolean;
549
632
  }
550
633
  /**
551
- * Schedules a Nitro task to run once in the future.\
552
- * Resolves and calls the task handler directly from Nitro's virtual task registry\
553
- * Numbers are treated as seconds, strings are parsed as human-readable durations, and dates are used as exact run times.
554
- *
555
- * @param taskName - The Nitro task name to run (from `tasks/*.ts`)
556
- * @param options - Task payload, context, and the schedule time
557
- * @returns Information about the scheduled task
558
- *
559
- * @example
560
- * // Run a task in 30 seconds:
561
- * const scheduled = await scheduleTask('emails:send', {
562
- * payload: { messageId: 'abc123' },
563
- * schedule: 30,
564
- * })
565
- *
566
- * @example
567
- * // Run a task using a human-readable duration:
568
- * await scheduleTask('reports:generate', {
569
- * payload: { reportId: 'report-1' },
570
- * schedule: '1 hour',
571
- * })
572
- *
573
- * @example
574
- * // Run a task at an exact time:
575
- * await scheduleTask('cleanup:tenant', {
576
- * payload: { tenant: 't12345' },
577
- * schedule: new Date('2026-05-01T12:00:00Z'),
578
- * })
634
+ * Information about a one-shot job (scheduled with `{ in }` or `{ at }`).
635
+ */
636
+ interface OnceJobInfo extends BaseJobInfo {
637
+ kind: 'once';
638
+ /**
639
+ * Always `null` for one-shot jobs.
640
+ */
641
+ cron: null;
642
+ }
643
+ /**
644
+ * Information about a recurring job (scheduled with `{ cron }`).
579
645
  */
580
- declare function scheduleTask(taskName: string, options: ScheduleTaskOptions): Promise<ScheduledTaskInfo>;
646
+ interface RecurringJobInfo extends BaseJobInfo {
647
+ kind: 'recurring';
648
+ /**
649
+ * The cron expression the job runs on.
650
+ */
651
+ cron: string;
652
+ /**
653
+ * The IANA timezone the cron expression is evaluated in.
654
+ */
655
+ timezone: string;
656
+ /**
657
+ * A few upcoming executions as ISO datetime strings.
658
+ */
659
+ nextRuns: string[];
660
+ }
581
661
  /**
582
- * Lists all tasks that are currently scheduled and have not started yet.\
583
- * The returned object is keyed by the scheduled task UUID for easy lookup and cancellation.
584
- *
585
- * @returns Object mapping scheduled task IDs to their public task information
586
- *
587
- * @example
588
- * const tasks = await listScheduledTasks()
589
- * for (const [id, task] of Object.entries(tasks)) {
590
- * console.log(id, task.task, task.runAt)
591
- * }
662
+ * Public information about a registered job. A discriminated union on `kind` —
663
+ * returned by `scheduleJob()`, `getJob()` and `listJobs()`.
592
664
  */
593
- declare function listScheduledTasks(): Promise<Record<string, ScheduledTaskInfo>>;
665
+ type JobInfo = OnceJobInfo | RecurringJobInfo;
594
666
  /**
595
- * Cancels a scheduled task before it starts running.\
596
- * Once the underlying Nitro task has started, it cannot be cancelled with this utility.
667
+ * Maps a {@link JobSchedule} to the {@link JobInfo} variant it produces.
668
+ */
669
+ type JobInfoFor<TSchedule extends JobSchedule> = TSchedule extends {
670
+ cron: string;
671
+ } ? RecurringJobInfo : OnceJobInfo;
672
+ /**
673
+ * Describes the payload and result type of a single registered task.
674
+ */
675
+ interface TaskShape<TPayload = TaskRegistryPayload, TResult = unknown> {
676
+ payload: TPayload;
677
+ result: TResult;
678
+ }
679
+ /**
680
+ * A map of task name to its {@link TaskShape}, accumulated by the builder.
681
+ */
682
+ type TaskMap = Record<string, TaskShape>;
683
+ /**
684
+ * A type-charged registry of tasks (functions) and the jobs that schedule them.
597
685
  *
598
- * @param id - The scheduled task UUID returned by `scheduleTask()` or `listScheduledTasks()`
599
- * @returns `true` when a pending task was cancelled, otherwise `false`
686
+ * Each `createTask()` call widens the registry's type with the new task name, so
687
+ * `run()`, `scheduleJob()` and `triggerJob()` autocomplete known task names and
688
+ * reject typos at compile time.
689
+ */
690
+ interface TaskRegistry<TTasks extends TaskMap = {}> {
691
+ /**
692
+ * Registers a task (function) under a compile-time-known name.\
693
+ * Passing a name that is already registered is a type error (and throws at runtime).
694
+ *
695
+ * @param name - Unique task name
696
+ * @param handler - The function to run. Annotate its event to type the payload,
697
+ * e.g. `(e: TaskEvent<{ configId: string }>) => ...`.
698
+ */
699
+ createTask: <TName extends string, TPayload extends TaskRegistryPayload = TaskRegistryPayload, TResult = unknown>(name: TName extends keyof TTasks ? never : TName, handler: TaskHandler<TPayload, TResult>) => TaskRegistry<TTasks & Record<TName, TaskShape<TPayload, TResult>>>;
700
+ /**
701
+ * Runs a task immediately and ad-hoc, without registering a job. Always runs
702
+ * fresh (no deduplication). Use a job with `'single'` concurrency when you need
703
+ * overlap protection.
704
+ */
705
+ run: <TName extends keyof TTasks & string>(task: TName, options?: {
706
+ payload?: TTasks[TName]['payload'];
707
+ context?: TaskRegistryContext;
708
+ }) => Promise<TTasks[TName]['result']>;
709
+ /**
710
+ * Schedules a named job that runs a task once or repeatedly. The return type is
711
+ * inferred from the schedule: `{ cron }` yields a {@link RecurringJobInfo},
712
+ * `{ in }`/`{ at }` yield a {@link OnceJobInfo}.
713
+ */
714
+ scheduleJob: <TName extends keyof TTasks & string, TSchedule extends JobSchedule>(options: ScheduleJobOptions<TName, TTasks[TName]['payload'], TSchedule>) => JobInfoFor<TSchedule>;
715
+ /**
716
+ * Runs an already-registered job immediately, respecting its concurrency policy.
717
+ * For a `'single'` job with a run in flight this returns the in-flight promise.
718
+ */
719
+ triggerJob: <TResult = unknown>(name: string, options?: {
720
+ payload?: TaskRegistryPayload;
721
+ context?: TaskRegistryContext;
722
+ }) => Promise<TResult>;
723
+ /**
724
+ * Cancels a scheduled job by name.
725
+ * @returns `true` when a job was found and cancelled, otherwise `false`.
726
+ */
727
+ cancelJob: (name: string) => boolean;
728
+ /**
729
+ * Lists all currently registered jobs.
730
+ */
731
+ listJobs: () => JobInfo[];
732
+ /**
733
+ * Returns information about a single job, or `undefined` if it does not exist.
734
+ */
735
+ getJob: (name: string) => JobInfo | undefined;
736
+ /**
737
+ * Returns `true` when a task with the given name is registered.
738
+ */
739
+ hasTask: (name: string) => boolean;
740
+ }
741
+ /**
742
+ * Creates a new, empty task registry.
600
743
  *
601
744
  * @example
602
- * const scheduled = await scheduleTask('emails:send', {
603
- * payload: { messageId: 'abc123' },
604
- * schedule: '10 minutes',
605
- * })
745
+ * import { c8yTasks } from 'c8y-nitro/utils'
746
+ *
747
+ * export const tasks = c8yTasks()
748
+ * .createTask('sync', async ({ payload }) => {
749
+ * const config = await loadConfig(payload.configId) // resolve live state here
750
+ * await sync(config)
751
+ * })
752
+ *
753
+ * // one per config, each an independent job
754
+ * tasks.scheduleJob({ name: 'sync-abc', task: 'sync', payload: { configId: 'abc' }, schedule: { cron: '*\/5 * * * *' } })
755
+ * tasks.scheduleJob({ name: 'sync-def', task: 'sync', payload: { configId: 'def' }, schedule: { cron: '*\/5 * * * *' }, concurrency: 'parallel' })
606
756
  *
607
- * const cancelled = await cancelScheduledTask(scheduled.id)
757
+ * @remarks
758
+ * The registry is a plain runtime singleton — export it from one module and import
759
+ * it wherever you schedule or cancel jobs. Recurring jobs only arm when the module
760
+ * that schedules them executes, so register them from a Nitro plugin (or another
761
+ * startup path) if they must run from boot.
608
762
  */
609
- declare function cancelScheduledTask(id: string): Promise<boolean>;
763
+ declare function c8yTasks(): TaskRegistry;
610
764
  //#endregion
611
- export { ForeignTenantOptionCategory, ForeignTenantOptionKeyInput, ScheduleTaskOptions, ScheduledTaskContext, ScheduledTaskInfo, ScheduledTaskInput, ScheduledTaskPayload, SetEditableOptions, TenantOption, TenantOptionCategory, TenantOptionKeyInput, cancelScheduledTask, createError, createLogger, hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, listScheduledTasks, scheduleTask, useDeployedTenantClient, useDeployedTenantCredentials, useLogger, useSubscribedTenantClients, useSubscribedTenantCredentials, useTenantOption, useTenantOptions, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };
765
+ export { BaseJobInfo, ForeignTenantOptionCategory, ForeignTenantOptionKeyInput, JobConcurrency, JobInfo, JobInfoFor, JobSchedule, OnceJobInfo, RecurringJobInfo, ScheduleJobOptions, SetEditableOptions, TaskEvent, TaskHandler, TaskMap, TaskRegistry, TaskRegistryContext, TaskRegistryPayload, TaskShape, TenantOption, TenantOptionCategory, TenantOptionKeyInput, c8yTasks, createError, createLogger, hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, useDeployedTenantClient, useDeployedTenantCredentials, useLogger, useSubscribedTenantClients, useSubscribedTenantCredentials, useTenantOption, useTenantOptions, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };