@cosmicdrift/kumiko-bundled-features 0.390.0 → 0.391.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.
Files changed (107) hide show
  1. package/dist/audit/event-classification.d.ts +1 -0
  2. package/dist/audit/event-classification.js +4 -0
  3. package/dist/audit/handlers/audit-events.d.ts +3 -0
  4. package/dist/audit/handlers/audit-events.js +9 -2
  5. package/dist/audit/handlers/details.query.js +3 -4
  6. package/dist/audit/handlers/list.query.js +1 -0
  7. package/dist/audit/handlers/resolve-audit-tenant.d.ts +1 -0
  8. package/dist/audit/handlers/resolve-audit-tenant.js +6 -2
  9. package/dist/auth-mfa/i18n.js +1 -0
  10. package/dist/auth-mfa/screens.js +3 -0
  11. package/dist/file-foundation/feature.js +4 -0
  12. package/dist/file-foundation/i18n.d.ts +20 -0
  13. package/dist/file-foundation/i18n.js +12 -0
  14. package/dist/file-foundation/index.d.ts +1 -0
  15. package/dist/file-foundation/index.js +1 -0
  16. package/dist/file-foundation/probe.d.ts +2 -0
  17. package/dist/file-foundation/probe.js +53 -0
  18. package/dist/{user-data-rights/lib → file-foundation}/storage-provider-resolver.js +2 -2
  19. package/dist/inbound-mail-foundation/feature.js +2 -0
  20. package/dist/inbound-mail-foundation/i18n.js +2 -0
  21. package/dist/inbound-mail-foundation/probe-keys.d.ts +28 -0
  22. package/dist/inbound-mail-foundation/probe-keys.js +16 -0
  23. package/dist/inbound-mail-foundation/probe.d.ts +2 -0
  24. package/dist/inbound-mail-foundation/probe.js +89 -0
  25. package/dist/inbound-mail-foundation/types.d.ts +3 -0
  26. package/dist/inbound-provider-imap/feature.js +3 -14
  27. package/dist/inbound-provider-imap/imap-client.d.ts +1 -0
  28. package/dist/inbound-provider-imap/imap-client.js +15 -0
  29. package/dist/mail-foundation/feature.js +2 -0
  30. package/dist/mail-foundation/i18n.d.ts +38 -0
  31. package/dist/mail-foundation/i18n.js +26 -0
  32. package/dist/mail-foundation/index.d.ts +1 -0
  33. package/dist/mail-foundation/index.js +1 -0
  34. package/dist/{user-data-rights/lib → mail-foundation}/mail-transport-resolver.js +2 -2
  35. package/dist/mail-foundation/probe.d.ts +2 -0
  36. package/dist/mail-foundation/probe.js +66 -0
  37. package/dist/readiness/built-in-probes.d.ts +11 -0
  38. package/dist/readiness/built-in-probes.js +55 -0
  39. package/dist/readiness/collect-probes.d.ts +9 -0
  40. package/dist/readiness/collect-probes.js +30 -0
  41. package/dist/readiness/constants.d.ts +19 -0
  42. package/dist/readiness/constants.js +21 -0
  43. package/dist/readiness/execute-probes.d.ts +3 -0
  44. package/dist/readiness/execute-probes.js +80 -0
  45. package/dist/readiness/feature.d.ts +14 -1
  46. package/dist/readiness/feature.js +66 -3
  47. package/dist/readiness/handlers/latest-results.query.d.ts +5 -0
  48. package/dist/readiness/handlers/latest-results.query.js +13 -0
  49. package/dist/readiness/handlers/latest-summary.query.d.ts +39 -0
  50. package/dist/readiness/handlers/latest-summary.query.js +49 -0
  51. package/dist/readiness/handlers/run-detail.query.d.ts +18 -0
  52. package/dist/readiness/handlers/run-detail.query.js +30 -0
  53. package/dist/readiness/handlers/run-list.query.d.ts +24 -0
  54. package/dist/readiness/handlers/run-list.query.js +33 -0
  55. package/dist/readiness/handlers/run-probes.write.d.ts +7 -0
  56. package/dist/readiness/handlers/run-probes.write.js +68 -0
  57. package/dist/readiness/handlers/run-results.query.d.ts +7 -0
  58. package/dist/readiness/handlers/run-results.query.js +17 -0
  59. package/dist/readiness/handlers/run-tone.d.ts +2 -0
  60. package/dist/readiness/handlers/run-tone.js +5 -0
  61. package/dist/readiness/i18n.d.ts +5 -0
  62. package/dist/readiness/i18n.js +53 -0
  63. package/dist/readiness/index.d.ts +1 -1
  64. package/dist/readiness/index.js +1 -1
  65. package/dist/readiness/probe-boot-check.d.ts +2 -0
  66. package/dist/readiness/probe-boot-check.js +39 -0
  67. package/dist/readiness/probe-context.d.ts +5 -0
  68. package/dist/readiness/probe-context.js +43 -0
  69. package/dist/readiness/run-probes-job.d.ts +2 -0
  70. package/dist/readiness/run-probes-job.js +82 -0
  71. package/dist/readiness/run-view.d.ts +25 -0
  72. package/dist/readiness/run-view.js +13 -0
  73. package/dist/readiness/schema/probe-run.d.ts +343 -0
  74. package/dist/readiness/schema/probe-run.js +51 -0
  75. package/dist/readiness/screens.d.ts +31 -0
  76. package/dist/readiness/screens.js +171 -0
  77. package/dist/readiness/visible-results.d.ts +52 -0
  78. package/dist/readiness/visible-results.js +69 -0
  79. package/dist/tenant/membership-stream-history.d.ts +15 -0
  80. package/dist/tenant/membership-stream-history.js +162 -0
  81. package/dist/tenant/membership-table.d.ts +1 -0
  82. package/dist/tenant/membership-table.js +2 -3
  83. package/dist/tenant/seeding.js +9 -12
  84. package/dist/tenant/stream-tenant-backfill.d.ts +2 -0
  85. package/dist/tenant/stream-tenant-backfill.js +34 -1
  86. package/dist/user-data-rights/feature.js +2 -2
  87. package/dist/user-data-rights/handlers/download-by-job.query.js +1 -1
  88. package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
  89. package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +1 -1
  90. package/dist/user-profile/feature.d.ts +4 -0
  91. package/dist/user-profile/feature.js +17 -1
  92. package/dist/user-profile/web/constants.d.ts +3 -0
  93. package/dist/user-profile/web/constants.js +4 -0
  94. package/dist/user-profile/web/i18n.js +1 -0
  95. package/kumiko-es-tables.json +4 -0
  96. package/package.json +13 -13
  97. package/src/audit/changes.json +6 -0
  98. package/src/auth-mfa/changes.json +6 -0
  99. package/src/file-foundation/changes.json +6 -0
  100. package/src/inbound-mail-foundation/changes.json +6 -0
  101. package/src/inbound-provider-imap/changes.json +6 -0
  102. package/src/mail-foundation/changes.json +6 -0
  103. package/src/readiness/changes.json +7 -0
  104. package/src/tenant/changes.json +12 -0
  105. package/src/user-profile/changes.json +6 -0
  106. /package/dist/{user-data-rights/lib → file-foundation}/storage-provider-resolver.d.ts +0 -0
  107. /package/dist/{user-data-rights/lib → mail-foundation}/mail-transport-resolver.d.ts +0 -0
@@ -5,8 +5,8 @@
5
5
  // the HTTP dispatcher), so the resolver builds a per-tenant accessor from it.
6
6
  // Without that bridge createTransportForTenant throws "ctx.config is missing"
7
7
  // in the worker lane (the prod-bug class the file-provider resolver already fixed).
8
- import { createTransportForTenant } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";
9
- import { createConfigAccessor } from "../../config/index.js";
8
+ import { createConfigAccessor } from "../config/index.js";
9
+ import { createTransportForTenant } from "./feature.js";
10
10
  export function makeTenantMailTransportResolver(ctx) {
11
11
  return async (tenantId) => {
12
12
  if (!ctx.configResolver) {
@@ -0,0 +1,2 @@
1
+ import { type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare function registerMailProbe(r: FeatureRegistrar<"mail-foundation">): void;
@@ -0,0 +1,66 @@
1
+ import { createSystemUser, ProbeFailure, SETTINGS_HUB_FEATURE, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { UnconfiguredError } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { HostResolutionError } from "@cosmicdrift/kumiko-framework/http";
4
+ import { createConfigAccessor } from "../config/index.js";
5
+ import { tenantMailTransport } from "./feature.js";
6
+ import { MAIL_PROBE_KEYS } from "./i18n.js";
7
+ const PROBE_SUBJECT = "Kumiko system check";
8
+ const PROBE_TEXT = "This message was sent by the system check. No action is needed.";
9
+ const MAIL_SETTINGS_SCREEN_QN = `${SETTINGS_HUB_FEATURE}:screen:mail-foundation-tenant`;
10
+ // The raw error stays in the server log: it can name hosts, users or the recipient.
11
+ function probeFailure(ctx, error, key) {
12
+ ctx.obs.log.warn("mail probe failed", {
13
+ error: error instanceof Error ? error.message : String(error),
14
+ });
15
+ return new ProbeFailure({ key });
16
+ }
17
+ function transportFailureKey(error) {
18
+ if (error instanceof HostResolutionError)
19
+ return MAIL_PROBE_KEYS.hostNotFound;
20
+ return error instanceof UnconfiguredError
21
+ ? MAIL_PROBE_KEYS.setupIncomplete
22
+ : MAIL_PROBE_KEYS.setupFailed;
23
+ }
24
+ // The mail goes to whoever started the check; the address never appears in the stored result.
25
+ export function registerMailProbe(r) {
26
+ r.probe({
27
+ name: "send-test-mail",
28
+ scope: "tenant",
29
+ cost: "free",
30
+ area: MAIL_PROBE_KEYS.area,
31
+ label: MAIL_PROBE_KEYS.label,
32
+ fixScreen: MAIL_SETTINGS_SCREEN_QN,
33
+ run: async (ctx) => {
34
+ const { configResolver, secrets } = ctx.services;
35
+ if (!configResolver) {
36
+ return { status: "skipped", message: { key: MAIL_PROBE_KEYS.notConfigured } };
37
+ }
38
+ const config = createConfigAccessor(ctx.registry, configResolver, ctx.tenantId, createSystemUser(ctx.tenantId).id, ctx.db);
39
+ const transport = await tenantMailTransport({
40
+ tenantId: ctx.tenantId,
41
+ config,
42
+ registry: ctx.registry,
43
+ secrets,
44
+ }).catch((error) => {
45
+ throw probeFailure(ctx, error, transportFailureKey(error));
46
+ });
47
+ if (transport === null) {
48
+ return { status: "skipped", message: { key: MAIL_PROBE_KEYS.notConfigured } };
49
+ }
50
+ if (ctx.triggeredByEmail === undefined) {
51
+ return { status: "skipped", message: { key: MAIL_PROBE_KEYS.noRecipient } };
52
+ }
53
+ await transport
54
+ .send({
55
+ to: ctx.triggeredByEmail,
56
+ subject: PROBE_SUBJECT,
57
+ text: PROBE_TEXT,
58
+ html: `<p>${PROBE_TEXT}</p>`,
59
+ })
60
+ .catch((error) => {
61
+ throw probeFailure(ctx, error, MAIL_PROBE_KEYS.sendRejected);
62
+ });
63
+ return { status: "ok", message: { key: MAIL_PROBE_KEYS.accepted } };
64
+ },
65
+ });
66
+ }
@@ -0,0 +1,11 @@
1
+ import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare const BuiltInProbeKeys: {
3
+ readonly areaInfrastructure: "readiness.area.infrastructure";
4
+ readonly databaseLabel: "readiness.probe.database.label";
5
+ readonly databaseOk: "readiness.probe.database.ok";
6
+ readonly cacheLabel: "readiness.probe.cache.label";
7
+ readonly cacheOk: "readiness.probe.cache.ok";
8
+ readonly searchLabel: "readiness.probe.search.label";
9
+ readonly searchOk: "readiness.probe.search.ok";
10
+ };
11
+ export declare function registerBuiltInProbes(r: FeatureRegistrar<"readiness">): void;
@@ -0,0 +1,55 @@
1
+ import { ProbeTextKeys } from "./constants.js";
2
+ import { probeRunsTable } from "./schema/probe-run.js";
3
+ export const BuiltInProbeKeys = {
4
+ areaInfrastructure: "readiness.area.infrastructure",
5
+ databaseLabel: "readiness.probe.database.label",
6
+ databaseOk: "readiness.probe.database.ok",
7
+ cacheLabel: "readiness.probe.cache.label",
8
+ cacheOk: "readiness.probe.cache.ok",
9
+ searchLabel: "readiness.probe.search.label",
10
+ searchOk: "readiness.probe.search.ok",
11
+ };
12
+ export function registerBuiltInProbes(r) {
13
+ r.probe({
14
+ name: "database",
15
+ scope: "system",
16
+ cost: "free",
17
+ area: BuiltInProbeKeys.areaInfrastructure,
18
+ label: BuiltInProbeKeys.databaseLabel,
19
+ run: async (ctx) => {
20
+ await ctx.db.fetchOne(probeRunsTable, { id: ctx.probeRunId });
21
+ return { status: "ok", message: { key: BuiltInProbeKeys.databaseOk } };
22
+ },
23
+ });
24
+ r.probe({
25
+ name: "cache",
26
+ scope: "system",
27
+ cost: "free",
28
+ area: BuiltInProbeKeys.areaInfrastructure,
29
+ label: BuiltInProbeKeys.cacheLabel,
30
+ run: async (ctx) => {
31
+ const { redis } = ctx.services;
32
+ if (!redis)
33
+ return { status: "skipped", message: { key: ProbeTextKeys.notConfigured } };
34
+ const reply = await redis.ping();
35
+ if (reply !== "PONG")
36
+ throw new Error(`unexpected PING reply: ${reply}`);
37
+ return { status: "ok", message: { key: BuiltInProbeKeys.cacheOk } };
38
+ },
39
+ });
40
+ r.probe({
41
+ name: "search",
42
+ scope: "system",
43
+ cost: "free",
44
+ area: BuiltInProbeKeys.areaInfrastructure,
45
+ label: BuiltInProbeKeys.searchLabel,
46
+ run: async (ctx) => {
47
+ const { searchAdapter } = ctx.services;
48
+ if (!searchAdapter) {
49
+ return { status: "skipped", message: { key: ProbeTextKeys.notConfigured } };
50
+ }
51
+ await searchAdapter.search(ctx.tenantId, "readiness-probe", { limit: 1 });
52
+ return { status: "ok", message: { key: BuiltInProbeKeys.searchOk } };
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,9 @@
1
+ import type { ProbeDefinition, Registry } from "@cosmicdrift/kumiko-framework/engine";
2
+ export type CollectProbesOptions = {
3
+ readonly includeSystemProbes: boolean;
4
+ readonly enabledFeatures: ReadonlySet<string> | undefined;
5
+ };
6
+ export declare function collectProbes(registry: Registry, options: CollectProbesOptions): readonly ProbeDefinition[];
7
+ export declare function hasPaidProbes(registry: Registry, options: CollectProbesOptions): boolean;
8
+ export declare function probeTimeoutMs(probe: ProbeDefinition): number;
9
+ export declare function staleRunAfterMs(registry: Registry): number;
@@ -0,0 +1,30 @@
1
+ import { DEFAULT_PROBE_TIMEOUT_MS, MIN_STALE_RUN_AFTER_MS, STALE_RUN_TIMEOUT_FACTOR, } from "./constants.js";
2
+ export function collectProbes(registry, options) {
3
+ const collected = [];
4
+ for (const feature of registry.features.values()) {
5
+ if (options.enabledFeatures !== undefined && !options.enabledFeatures.has(feature.name)) {
6
+ continue;
7
+ }
8
+ for (const probe of Object.values(feature.probes ?? {})) {
9
+ if (probe.scope === "system" && !options.includeSystemProbes)
10
+ continue;
11
+ collected.push(probe);
12
+ }
13
+ }
14
+ return collected;
15
+ }
16
+ export function hasPaidProbes(registry, options) {
17
+ return collectProbes(registry, options).some((probe) => probe.cost === "paid");
18
+ }
19
+ export function probeTimeoutMs(probe) {
20
+ return probe.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
21
+ }
22
+ export function staleRunAfterMs(registry) {
23
+ let longestTimeoutMs = 0;
24
+ for (const feature of registry.features.values()) {
25
+ for (const probe of Object.values(feature.probes ?? {})) {
26
+ longestTimeoutMs = Math.max(longestTimeoutMs, probeTimeoutMs(probe));
27
+ }
28
+ }
29
+ return Math.max(MIN_STALE_RUN_AFTER_MS, longestTimeoutMs * STALE_RUN_TIMEOUT_FACTOR);
30
+ }
@@ -1,4 +1,23 @@
1
1
  export declare const READINESS_FEATURE: "readiness";
2
2
  export declare const ReadinessQueries: {
3
3
  readonly status: "readiness:query:status";
4
+ readonly latestResults: "readiness:query:probe-run:latest-results";
5
+ readonly latestSummary: "readiness:query:probe-run:latest-summary";
6
+ readonly runList: "readiness:query:probe-run:list";
7
+ readonly runDetail: "readiness:query:probe-run:detail";
8
+ readonly runResults: "readiness:query:probe-run:results";
9
+ };
10
+ export declare const ReadinessWrites: {
11
+ readonly runProbes: "readiness:write:run-probes";
12
+ };
13
+ export declare const DEFAULT_PROBE_TIMEOUT_MS = 10000;
14
+ export declare const MIN_STALE_RUN_AFTER_MS: number;
15
+ export declare const STALE_RUN_TIMEOUT_FACTOR = 3;
16
+ export declare const ProbeTextKeys: {
17
+ readonly paidNotRequested: "readiness.probe.paid-not-requested";
18
+ readonly noTargets: "readiness.probe.no-targets";
19
+ readonly timeout: "readiness.probe.timeout";
20
+ readonly failed: "readiness.probe.failed";
21
+ readonly runCrashed: "readiness.probe.run-crashed";
22
+ readonly notConfigured: "readiness.probe.not-configured";
4
23
  };
@@ -3,4 +3,25 @@ export const READINESS_FEATURE = "readiness";
3
3
  // Qualified query handler names (QN format: scope:type:name)
4
4
  export const ReadinessQueries = {
5
5
  status: "readiness:query:status",
6
+ latestResults: "readiness:query:probe-run:latest-results",
7
+ latestSummary: "readiness:query:probe-run:latest-summary",
8
+ runList: "readiness:query:probe-run:list",
9
+ runDetail: "readiness:query:probe-run:detail",
10
+ runResults: "readiness:query:probe-run:results",
11
+ };
12
+ export const ReadinessWrites = {
13
+ runProbes: "readiness:write:run-probes",
14
+ };
15
+ export const DEFAULT_PROBE_TIMEOUT_MS = 10_000;
16
+ // An active run older than this is treated as dead (worker crash) and may be replaced.
17
+ export const MIN_STALE_RUN_AFTER_MS = 10 * 60_000;
18
+ export const STALE_RUN_TIMEOUT_FACTOR = 3;
19
+ // Translation keys of the runner's own result texts.
20
+ export const ProbeTextKeys = {
21
+ paidNotRequested: "readiness.probe.paid-not-requested",
22
+ noTargets: "readiness.probe.no-targets",
23
+ timeout: "readiness.probe.timeout",
24
+ failed: "readiness.probe.failed",
25
+ runCrashed: "readiness.probe.run-crashed",
26
+ notConfigured: "readiness.probe.not-configured",
6
27
  };
@@ -0,0 +1,3 @@
1
+ import { type ProbeContext, type ProbeDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { ProbeResultRow } from "./schema/probe-run.ts";
3
+ export declare function executeProbes(ctx: ProbeContext, probes: readonly ProbeDefinition[], includePaid: boolean): Promise<readonly ProbeResultRow[]>;
@@ -0,0 +1,80 @@
1
+ import { runWithTimeout } from "@cosmicdrift/kumiko-framework/api";
2
+ import { ProbeFailure, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { probeTimeoutMs } from "./collect-probes.js";
4
+ import { ProbeTextKeys } from "./constants.js";
5
+ function baseRow(probe, target) {
6
+ return {
7
+ probe: probe.qualifiedName,
8
+ scope: probe.scope,
9
+ area: probe.area,
10
+ label: target ? target.label : { key: probe.label },
11
+ ...(target && { targetId: target.id }),
12
+ ...(target?.ownerUserId !== undefined && { ownerUserId: target.ownerUserId }),
13
+ ...(probe.fixScreen !== undefined && { fixScreen: probe.fixScreen }),
14
+ };
15
+ }
16
+ function rowOf(probe, target, outcome) {
17
+ return { ...baseRow(probe, target), ...outcome };
18
+ }
19
+ // Raw error messages can carry secrets or addresses, so only ProbeFailure texts reach the run.
20
+ function failureText(ctx, probe, failure) {
21
+ if (failure.timedOut) {
22
+ return { key: ProbeTextKeys.timeout, params: { seconds: probeTimeoutMs(probe) / 1000 } };
23
+ }
24
+ if (failure.error instanceof ProbeFailure)
25
+ return failure.error.text;
26
+ ctx.obs.log.warn("readiness probe failed", {
27
+ probe: probe.qualifiedName,
28
+ error: failure.error instanceof Error ? failure.error.message : String(failure.error),
29
+ });
30
+ return { key: ProbeTextKeys.failed };
31
+ }
32
+ async function runOne(ctx, probe, target) {
33
+ const result = await runWithTimeout(() => probe.run(ctx, target), probeTimeoutMs(probe));
34
+ if (result.ok) {
35
+ // A skipped probe did not run, so its row shows no duration like the runner's own skips.
36
+ return rowOf(probe, target, {
37
+ status: result.value.status,
38
+ durationMs: result.value.status === "skipped" ? null : result.latencyMs,
39
+ message: result.value.message,
40
+ });
41
+ }
42
+ return rowOf(probe, target, {
43
+ status: "failed",
44
+ durationMs: result.latencyMs,
45
+ message: failureText(ctx, probe, result),
46
+ });
47
+ }
48
+ function skippedRow(probe, messageKey) {
49
+ return rowOf(probe, undefined, {
50
+ status: "skipped",
51
+ durationMs: null,
52
+ message: { key: messageKey },
53
+ });
54
+ }
55
+ async function runProbe(ctx, probe, includePaid) {
56
+ if (probe.cost === "paid" && !includePaid) {
57
+ return [skippedRow(probe, ProbeTextKeys.paidNotRequested)];
58
+ }
59
+ const { targets } = probe;
60
+ if (targets === undefined)
61
+ return [await runOne(ctx, probe, undefined)];
62
+ const resolved = await runWithTimeout(() => targets(ctx), probeTimeoutMs(probe));
63
+ if (!resolved.ok) {
64
+ return [
65
+ rowOf(probe, undefined, {
66
+ status: "failed",
67
+ durationMs: resolved.latencyMs,
68
+ message: failureText(ctx, probe, resolved),
69
+ }),
70
+ ];
71
+ }
72
+ const ownTargets = resolved.value.filter((target) => target.ownerUserId === undefined || target.ownerUserId === ctx.triggeredByUserId);
73
+ if (ownTargets.length === 0)
74
+ return [skippedRow(probe, ProbeTextKeys.noTargets)];
75
+ return Promise.all(ownTargets.map((target) => runOne(ctx, probe, target)));
76
+ }
77
+ export async function executeProbes(ctx, probes, includePaid) {
78
+ const perProbe = await Promise.all(probes.map((probe) => runProbe(ctx, probe, includePaid)));
79
+ return perProbe.flat();
80
+ }
@@ -1,7 +1,20 @@
1
- export declare function createReadinessFeature(): import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
1
+ export interface ReadinessNavOptions {
2
+ readonly order?: number;
3
+ readonly parent?: string;
4
+ readonly workspaces?: readonly string[];
5
+ }
6
+ export interface ReadinessFeatureOptions {
7
+ readonly nav?: ReadinessNavOptions;
8
+ }
9
+ export declare function createReadinessFeature(options?: ReadinessFeatureOptions): import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
2
10
  readonly exports: {
3
11
  queries: {
4
12
  status: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
13
+ latestResults: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
14
+ latestSummary: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
15
+ runList: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
16
+ runDetail: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
17
+ runResults: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
5
18
  };
6
19
  };
7
20
  };
@@ -8,10 +8,25 @@
8
8
  // query may roll up missing config + missing required secrets + a single
9
9
  // `ready` boolean — the settings-checklist call for admin UIs.
10
10
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
11
+ import { registerBuiltInProbes } from "./built-in-probes.js";
12
+ import { ReadinessWrites } from "./constants.js";
13
+ import { latestResultsQuery } from "./handlers/latest-results.query.js";
14
+ import { latestSummaryQuery } from "./handlers/latest-summary.query.js";
15
+ import { runDetailQuery } from "./handlers/run-detail.query.js";
16
+ import { runListQuery } from "./handlers/run-list.query.js";
17
+ import { runProbesWrite } from "./handlers/run-probes.write.js";
18
+ import { runResultsQuery } from "./handlers/run-results.query.js";
11
19
  import { statusQuery } from "./handlers/status.query.js";
12
- export function createReadinessFeature() {
20
+ import { READINESS_I18N } from "./i18n.js";
21
+ import { validateProbeTranslations } from "./probe-boot-check.js";
22
+ import { runProbesJob } from "./run-probes-job.js";
23
+ import { probeRunEntity } from "./schema/probe-run.js";
24
+ import { SYSTEM_CHECK_NAV_LABEL, SYSTEM_CHECK_SCREEN_ID, SYSTEM_CHECK_SCREEN_QN, systemCheckRunScreen, systemCheckScreen, } from "./screens.js";
25
+ const DEFAULT_NAV_ORDER = 90;
26
+ export function createReadinessFeature(options = {}) {
27
+ const { nav } = options;
13
28
  return defineFeature("readiness", (r) => {
14
- r.describe("One-call tenant-onboarding probe: `readiness:query:status` rolls up every config key and secret declared `required: true` across all mounted features and reports which still lack a usable value for the calling tenant, plus a single `ready` boolean. Provider-features under an `r.extensionSelector`-declared extension point count only while their provider is the selected one — a tenant on the inmemory mail transport is not blocked by unset SMTP keys. Mount it (together with `config` and `secrets`) when an admin UI needs a settings checklist before the first mail-send or file-write; the per-concern lists stay available via `config:query:readiness` and `secrets:query:list`.");
29
+ r.describe("One-call tenant-onboarding probe: `readiness:query:status` rolls up every config key and secret declared `required: true` across all mounted features and reports which still lack a usable value for the calling tenant, plus a single `ready` boolean. Provider-features under an `r.extensionSelector`-declared extension point count only while their provider is the selected one — a tenant on the inmemory mail transport is not blocked by unset SMTP keys. Mount it (together with `config` and `secrets`) when an admin UI needs a settings checklist before the first mail-send or file-write; the per-concern lists stay available via `config:query:readiness` and `secrets:query:list`. `readiness:write:run-probes` runs the live probes features register with `r.probe` (mail, files, mailboxes, PDF service and more) as a job; the system-check screen shows the latest run and the history once `nav` is passed.");
15
30
  r.uiHints({
16
31
  displayLabel: "Readiness · Onboarding Probe",
17
32
  category: "operations",
@@ -19,9 +34,57 @@ export function createReadinessFeature() {
19
34
  });
20
35
  r.requires({ features: ["config"] });
21
36
  r.requires({ features: ["secrets"] });
37
+ r.optionalRequires("user");
38
+ r.adoptsBundledLists();
39
+ r.translations({ keys: READINESS_I18N });
40
+ r.entity({ name: "probe-run", ...probeRunEntity });
41
+ r.writeHandler(runProbesWrite);
42
+ r.job({
43
+ name: "run-probes",
44
+ trigger: { on: ReadinessWrites.runProbes },
45
+ runIn: "worker",
46
+ retries: 0,
47
+ escapeHatch: {
48
+ reason: "looks up the run starter's address via the system-only user auth lookup",
49
+ grants: ["systemIdentity"],
50
+ },
51
+ // Inline so the feature-AST reader can parse the job; the logic lives in run-probes-job.ts.
52
+ handler: (payload, ctx) => runProbesJob(payload, ctx),
53
+ });
54
+ registerBuiltInProbes(r);
55
+ r.bootCheck(({ features }) => validateProbeTranslations(features));
56
+ r.screen(systemCheckScreen(nav === undefined));
57
+ r.screen(systemCheckRunScreen);
58
+ if (nav !== undefined) {
59
+ // Explicit property assignments: the feature-AST parser reads neither `??`, shorthand
60
+ // properties nor a conditional spread, and the boot validator treats `parent: undefined`
61
+ // as a top-level entry.
62
+ const { order: navOrder = DEFAULT_NAV_ORDER, parent: navParent, workspaces: navWorkspaces, } = nav;
63
+ r.nav({
64
+ id: SYSTEM_CHECK_SCREEN_ID,
65
+ label: SYSTEM_CHECK_NAV_LABEL,
66
+ icon: "shield-check",
67
+ screen: SYSTEM_CHECK_SCREEN_QN,
68
+ order: navOrder,
69
+ parent: navParent,
70
+ workspaces: navWorkspaces,
71
+ });
72
+ }
22
73
  const queries = {
23
74
  status: r.queryHandler(statusQuery),
75
+ latestResults: r.queryHandler(latestResultsQuery),
76
+ latestSummary: r.queryHandler(latestSummaryQuery),
77
+ runList: r.queryHandler(runListQuery),
78
+ runDetail: r.queryHandler(runDetailQuery),
79
+ runResults: r.queryHandler(runResultsQuery),
24
80
  };
25
81
  return { queries };
26
- }, { dedupeOptions: {} });
82
+ }, {
83
+ dedupeOptions: {
84
+ hasNav: nav !== undefined,
85
+ navOrder: nav?.order ?? DEFAULT_NAV_ORDER,
86
+ navParent: nav?.parent ?? "",
87
+ navWorkspaces: nav?.workspaces?.join(",") ?? "",
88
+ },
89
+ });
27
90
  }
@@ -0,0 +1,5 @@
1
+ import * as z from "zod";
2
+ export declare const latestResultsQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"probe-run:latest-results", z.ZodObject<{}, z.core.$strip>, {
3
+ rows: readonly import("../visible-results.ts").VisibleProbeResult[];
4
+ nextCursor: null;
5
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -0,0 +1,13 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import { fetchLatestRun, viewRun } from "../run-view.js";
4
+ export const latestResultsQuery = defineQueryHandler({
5
+ name: "probe-run:latest-results",
6
+ schema: z.object({}),
7
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
8
+ description: "Returns the per-probe results of the tenant's latest system-check run (finished or still running) that the caller may see: system probes only for SystemAdmin, personal mailboxes only for their owner. Empty rows while the run is still working or when no run exists.",
9
+ handler: async (query, ctx) => {
10
+ const latest = await fetchLatestRun(ctx.db);
11
+ return { rows: latest ? viewRun(latest, query.user).results : [], nextCursor: null };
12
+ },
13
+ });
@@ -0,0 +1,39 @@
1
+ import * as z from "zod";
2
+ export declare const SUMMARY_TEXT_KEY = "readiness.summary.ok-of-total";
3
+ export declare const SUMMARY_RUNNING_TEXT_KEY = "readiness.summary.running";
4
+ export declare const SUMMARY_NO_RUN_TEXT_KEY = "readiness.summary.no-run";
5
+ export declare const latestSummaryQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"probe-run:latest-summary", z.ZodObject<{}, z.core.$strip>, {
6
+ okCount: number;
7
+ failedCount: number;
8
+ skippedCount: number;
9
+ totalCount: number;
10
+ id: string;
11
+ runId: null;
12
+ hasPaidProbes: boolean;
13
+ text: {
14
+ i18nKey: string;
15
+ i18nParams?: undefined;
16
+ };
17
+ } | {
18
+ okCount: number;
19
+ failedCount: number;
20
+ skippedCount: number;
21
+ totalCount: number;
22
+ id: string;
23
+ runId: string;
24
+ hasPaidProbes: boolean;
25
+ requestedAt: string;
26
+ status: string;
27
+ outcome: import("../visible-results.ts").RunOutcome;
28
+ tone: "default" | "negative" | "positive";
29
+ text: {
30
+ i18nKey: string;
31
+ i18nParams?: undefined;
32
+ } | {
33
+ i18nKey: string;
34
+ i18nParams: {
35
+ ok: number;
36
+ total: number;
37
+ };
38
+ };
39
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -0,0 +1,49 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import { hasPaidProbes } from "../collect-probes.js";
4
+ import { fetchLatestRun, viewRun } from "../run-view.js";
5
+ import { countResults } from "../visible-results.js";
6
+ import { toneOf } from "./run-tone.js";
7
+ export const SUMMARY_TEXT_KEY = "readiness.summary.ok-of-total";
8
+ export const SUMMARY_RUNNING_TEXT_KEY = "readiness.summary.running";
9
+ export const SUMMARY_NO_RUN_TEXT_KEY = "readiness.summary.no-run";
10
+ export const latestSummaryQuery = defineQueryHandler({
11
+ name: "probe-run:latest-summary",
12
+ schema: z.object({}),
13
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
14
+ description: "Returns the one-line summary of the tenant's latest system-check run (when it started, how many of the checks the caller may see passed, a tone and a ready-made translatable text) and whether the caller's check has paid probes. Zero counts and runId null when no run exists yet.",
15
+ handler: async (query, ctx) => {
16
+ const paid = hasPaidProbes(ctx.registry, {
17
+ includeSystemProbes: query.user.roles.includes("SystemAdmin"),
18
+ enabledFeatures: ctx.services.effectiveFeatures?.(query.user.tenantId),
19
+ });
20
+ const latest = await fetchLatestRun(ctx.db);
21
+ if (!latest) {
22
+ return {
23
+ // The system-check screen's record id only gates its sections; their queries ignore it.
24
+ id: query.user.tenantId,
25
+ runId: null,
26
+ hasPaidProbes: paid,
27
+ ...countResults([]),
28
+ text: { i18nKey: SUMMARY_NO_RUN_TEXT_KEY },
29
+ };
30
+ }
31
+ const { counts, outcome } = viewRun(latest, query.user);
32
+ return {
33
+ id: latest.id,
34
+ runId: latest.id,
35
+ hasPaidProbes: paid,
36
+ requestedAt: latest.requestedAt.toString(),
37
+ status: latest.status,
38
+ ...counts,
39
+ outcome,
40
+ tone: toneOf(outcome),
41
+ text: outcome === "running"
42
+ ? { i18nKey: SUMMARY_RUNNING_TEXT_KEY }
43
+ : {
44
+ i18nKey: SUMMARY_TEXT_KEY,
45
+ i18nParams: { ok: counts.okCount, total: counts.totalCount },
46
+ },
47
+ };
48
+ },
49
+ });
@@ -0,0 +1,18 @@
1
+ import * as z from "zod";
2
+ export declare const runDetailQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"probe-run:detail", z.ZodObject<{
3
+ runId: z.ZodUUID;
4
+ }, z.core.$strip>, {
5
+ okCount: number;
6
+ failedCount: number;
7
+ skippedCount: number;
8
+ totalCount: number;
9
+ id: string;
10
+ status: string;
11
+ requestedAt: string;
12
+ startedAt: string | null;
13
+ finishedAt: string | null;
14
+ triggeredByUserId: string;
15
+ durationMs: number | null;
16
+ outcome: import("../visible-results.ts").RunOutcome;
17
+ tone: "default" | "negative" | "positive";
18
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -0,0 +1,30 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
3
+ import * as z from "zod";
4
+ import { viewRun } from "../run-view.js";
5
+ import { probeRunsTable } from "../schema/probe-run.js";
6
+ import { toneOf } from "./run-tone.js";
7
+ export const runDetailQuery = defineQueryHandler({
8
+ name: "probe-run:detail",
9
+ schema: z.object({ runId: z.uuid() }),
10
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
11
+ description: "Returns the header of one system-check run of the caller's tenant: status, timing, starter and the outcome counts of the results the caller may see.",
12
+ handler: async (query, ctx) => {
13
+ const run = await ctx.db.fetchOne(probeRunsTable, { id: query.payload.runId });
14
+ if (!run)
15
+ throw new NotFoundError("probe-run", query.payload.runId);
16
+ const { counts, outcome } = viewRun(run, query.user);
17
+ return {
18
+ id: run.id,
19
+ status: run.status,
20
+ requestedAt: run.requestedAt.toString(),
21
+ startedAt: run.startedAt?.toString() ?? null,
22
+ finishedAt: run.finishedAt?.toString() ?? null,
23
+ triggeredByUserId: run.triggeredByUserId,
24
+ durationMs: run.durationMs,
25
+ ...counts,
26
+ outcome,
27
+ tone: toneOf(outcome),
28
+ };
29
+ },
30
+ });
@@ -0,0 +1,24 @@
1
+ export declare const runListQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"probe-run:list", import("zod").ZodObject<{
2
+ limit: import("zod").ZodDefault<import("zod").ZodNumber>;
3
+ offset: import("zod").ZodOptional<import("zod").ZodNumber>;
4
+ search: import("zod").ZodOptional<import("zod").ZodString>;
5
+ sort: import("zod").ZodOptional<import("zod").ZodEnum<{
6
+ requestedAt: "requestedAt";
7
+ }>>;
8
+ sortDirection: import("zod").ZodOptional<import("zod").ZodEnum<{
9
+ asc: "asc";
10
+ desc: "desc";
11
+ }>>;
12
+ totalCount: import("zod").ZodOptional<import("zod").ZodBoolean>;
13
+ cursor: import("zod").ZodOptional<import("zod").ZodNever>;
14
+ }, import("zod/v4/core").$strip>, import("@cosmicdrift/kumiko-types/cursor-types").CursorResult<{
15
+ id: string;
16
+ requestedAt: string;
17
+ triggeredByUserId: string;
18
+ triggeredByName: string;
19
+ status: string;
20
+ okCount: number;
21
+ failedCount: number;
22
+ durationMs: number | null;
23
+ outcome: import("../visible-results.ts").RunOutcome;
24
+ }>, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;