@objectstack/plugin-reports 15.0.0 → 15.1.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/plugin-reports@15.0.0 build /home/runner/work/framework/framework/packages/plugins/plugin-reports
2
+ > @objectstack/plugin-reports@15.1.0 build /home/runner/work/framework/framework/packages/plugins/plugin-reports
3
3
  > tsup --config ../../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- ESM dist/index.mjs 19.42 KB
14
- ESM dist/index.mjs.map 39.48 KB
15
- ESM ⚡️ Build success in 99ms
16
- CJS dist/index.js 20.55 KB
17
- CJS dist/index.js.map 39.47 KB
18
- CJS ⚡️ Build success in 99ms
13
+ ESM dist/index.mjs 22.17 KB
14
+ ESM dist/index.mjs.map 46.00 KB
15
+ ESM ⚡️ Build success in 165ms
16
+ CJS dist/index.js 23.30 KB
17
+ CJS dist/index.js.map 46.00 KB
18
+ CJS ⚡️ Build success in 171ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 19210ms
21
- DTS dist/index.d.mts 5.60 KB
22
- DTS dist/index.d.ts 5.60 KB
20
+ DTS ⚡️ Build success in 19372ms
21
+ DTS dist/index.d.mts 7.17 KB
22
+ DTS dist/index.d.ts 7.17 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # @objectstack/plugin-reports
2
2
 
3
+ ## 15.1.0
4
+
5
+ ### Patch Changes
6
+
7
+ - f531a26: fix(security): close three execution-surface authz holes surfaced by the #2849 class sweep (#2980, #2981, #2982)
8
+
9
+ Three independent, confirmed-exploitable defects where an execution surface
10
+ ignored the caller's identity or fell open on a missing one. Each is fixed at
11
+ its own enforcement point; none change behaviour for correctly-scoped callers.
12
+
13
+ - **#2980 — reports IDOR + scheduled-report RLS bypass.** `ReportService`
14
+ discarded the caller's context and read/wrote `sys_saved_report` with a system
15
+ context, so any authenticated user could read, delete, or overwrite any saved
16
+ report by id (cross-owner / cross-tenant), and `listReports` enumerated all
17
+ owners. `getReport`/`deleteReport`/`saveReport`/`listReports` are now
18
+ owner-scoped (system read of the protection-locked metadata object, but
19
+ authorization enforced by owner match); create/overwrite can no longer spoof
20
+ ownership. Scheduled dispatch no longer runs `isSystem` (which emailed the
21
+ target object's entire table past the owner's RLS): it resolves the owner to a
22
+ real RLS-bearing context via a new `resolveOwnerContext` seam and **fails
23
+ closed** (skips + marks the schedule failed) when the owner can't be resolved,
24
+ rather than running elevated. Wiring that resolver is the reports-surface
25
+ consumer of ADR-0073's user-less identity resolution.
26
+
27
+ - **#2981 — knowledge/RAG retrieval fall-open.** `applyPermissionFilter` returned
28
+ every hit when the context was missing _or_ system. A missing identity is no
29
+ longer treated as a grant: object-backed hits fail closed (dropped, keeping
30
+ ACL-less file/http hits), and only an **explicit** system context passes
31
+ through. Closes the agent path where an omitted `ToolExecutionContext.actor`
32
+ yielded unfiltered semantic search over the whole corpus.
33
+
34
+ - **#2982 — bulk-write OWD gap.** `update({multi:true})` / `deleteMany` had no
35
+ single id to `canEdit`-gate, so owner scoping was skipped on private (and
36
+ public_read) objects. A new `SharingService.buildWriteFilter` (the edit-set
37
+ analogue of `buildReadFilter`) is AND-ed into the write AST for multi writes,
38
+ constraining them to rows the caller may edit — including the on-behalf-of
39
+ delegator intersection.
40
+
41
+ Tracked as the motivating evidence of ADR-0096 (execution-surface identity
42
+ admission); the mechanism that would prevent the class structurally is separate.
43
+
44
+ - Updated dependencies [f531a26]
45
+ - Updated dependencies [f531a26]
46
+ - Updated dependencies [f531a26]
47
+ - Updated dependencies [f531a26]
48
+ - Updated dependencies [f531a26]
49
+ - Updated dependencies [f531a26]
50
+ - Updated dependencies [3fe9df1]
51
+ - Updated dependencies [f531a26]
52
+ - Updated dependencies [f531a26]
53
+ - Updated dependencies [f531a26]
54
+ - Updated dependencies [f531a26]
55
+ - Updated dependencies [f531a26]
56
+ - Updated dependencies [f531a26]
57
+ - Updated dependencies [f531a26]
58
+ - Updated dependencies [f531a26]
59
+ - Updated dependencies [f531a26]
60
+ - Updated dependencies [f531a26]
61
+ - Updated dependencies [f531a26]
62
+ - Updated dependencies [f531a26]
63
+ - Updated dependencies [f531a26]
64
+ - Updated dependencies [f531a26]
65
+ - Updated dependencies [f531a26]
66
+ - Updated dependencies [4109153]
67
+ - Updated dependencies [f531a26]
68
+ - Updated dependencies [f531a26]
69
+ - Updated dependencies [f531a26]
70
+ - Updated dependencies [f531a26]
71
+ - Updated dependencies [f531a26]
72
+ - Updated dependencies [f531a26]
73
+ - Updated dependencies [f531a26]
74
+ - Updated dependencies [627f225]
75
+ - Updated dependencies [f531a26]
76
+ - Updated dependencies [f531a26]
77
+ - Updated dependencies [f531a26]
78
+ - @objectstack/spec@15.1.0
79
+ - @objectstack/platform-objects@15.1.0
80
+ - @objectstack/core@15.1.0
81
+
3
82
  ## 15.0.0
4
83
 
5
84
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { SysReportSchedule, SysSavedReport } from '@objectstack/platform-objects/audit';
2
- import { IReportService, SaveReportInput, SharingExecutionContext, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
2
+ import { IReportService, SharingExecutionContext, SaveReportInput, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
3
3
  export { IReportService, ReportFormat, ReportQuery, ReportRunResult, ReportSchedule, SaveReportInput, SavedReport, ScheduleReportInput } from '@objectstack/spec/contracts';
4
4
  import { Plugin, PluginContext } from '@objectstack/core';
5
5
 
@@ -40,6 +40,17 @@ interface ReportClock {
40
40
  now(): Date;
41
41
  }
42
42
  declare function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string;
43
+ /**
44
+ * Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing
45
+ * `ExecutionContext` so a **scheduled** report executes under the owner's
46
+ * authority — the same rows the owner would see interactively — instead of
47
+ * bypassing RLS with a system context. Returns `null` when the owner cannot
48
+ * be resolved (unknown/disabled user), in which case the scheduler fails the
49
+ * run closed rather than running elevated (#2849 / #2980). Supplying this
50
+ * resolver is the reports-surface consumer of ADR-0073's user-less identity
51
+ * resolution.
52
+ */
53
+ type OwnerContextResolver = (ownerId: string) => Promise<SharingExecutionContext | null>;
43
54
  interface ReportServiceOptions {
44
55
  engine: ReportEngine;
45
56
  email?: ReportEmail;
@@ -51,6 +62,12 @@ interface ReportServiceOptions {
51
62
  };
52
63
  /** Cap rows per report to protect both DB and email size. */
53
64
  maxRows?: number;
65
+ /**
66
+ * Resolves a report owner into an RLS-bearing context for scheduled runs
67
+ * (see {@link OwnerContextResolver}). When omitted, scheduled reports fail
68
+ * closed instead of running with RLS bypassed (#2980).
69
+ */
70
+ resolveOwnerContext?: OwnerContextResolver;
54
71
  }
55
72
  declare class ReportService implements IReportService {
56
73
  private readonly engine;
@@ -58,14 +75,27 @@ declare class ReportService implements IReportService {
58
75
  private readonly clock;
59
76
  private readonly logger;
60
77
  private readonly maxRows;
78
+ private readonly resolveOwnerContext?;
61
79
  constructor(opts: ReportServiceOptions);
80
+ /**
81
+ * Authorization for a saved-report row. `sys_saved_report` is a
82
+ * protection-locked system object, so its rows are *read* with
83
+ * `SYSTEM_CTX`; the caller's right to see/mutate a specific report is
84
+ * enforced HERE, by owner match, not by the metadata read's own RLS —
85
+ * otherwise any authenticated caller could read/delete/overwrite any
86
+ * report by id (#2980). An explicit elevated context (`isSystem`) — the
87
+ * scheduler / server tooling — sees everything.
88
+ */
89
+ private canAccessReport;
90
+ /** Raw metadata read of a saved report by id (no authz — callers gate). */
91
+ private loadReportRow;
62
92
  saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport>;
63
93
  listReports(filter: {
64
94
  object?: string;
65
95
  ownerId?: string;
66
- } | undefined, _context: SharingExecutionContext): Promise<SavedReport[]>;
67
- getReport(reportId: string, _context: SharingExecutionContext): Promise<SavedReport | null>;
68
- deleteReport(reportId: string, _context: SharingExecutionContext): Promise<void>;
96
+ } | undefined, context: SharingExecutionContext): Promise<SavedReport[]>;
97
+ getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null>;
98
+ deleteReport(reportId: string, context: SharingExecutionContext): Promise<void>;
69
99
  run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult>;
70
100
  runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult>;
71
101
  private executeReport;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { SysReportSchedule, SysSavedReport } from '@objectstack/platform-objects/audit';
2
- import { IReportService, SaveReportInput, SharingExecutionContext, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
2
+ import { IReportService, SharingExecutionContext, SaveReportInput, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
3
3
  export { IReportService, ReportFormat, ReportQuery, ReportRunResult, ReportSchedule, SaveReportInput, SavedReport, ScheduleReportInput } from '@objectstack/spec/contracts';
4
4
  import { Plugin, PluginContext } from '@objectstack/core';
5
5
 
@@ -40,6 +40,17 @@ interface ReportClock {
40
40
  now(): Date;
41
41
  }
42
42
  declare function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string;
43
+ /**
44
+ * Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing
45
+ * `ExecutionContext` so a **scheduled** report executes under the owner's
46
+ * authority — the same rows the owner would see interactively — instead of
47
+ * bypassing RLS with a system context. Returns `null` when the owner cannot
48
+ * be resolved (unknown/disabled user), in which case the scheduler fails the
49
+ * run closed rather than running elevated (#2849 / #2980). Supplying this
50
+ * resolver is the reports-surface consumer of ADR-0073's user-less identity
51
+ * resolution.
52
+ */
53
+ type OwnerContextResolver = (ownerId: string) => Promise<SharingExecutionContext | null>;
43
54
  interface ReportServiceOptions {
44
55
  engine: ReportEngine;
45
56
  email?: ReportEmail;
@@ -51,6 +62,12 @@ interface ReportServiceOptions {
51
62
  };
52
63
  /** Cap rows per report to protect both DB and email size. */
53
64
  maxRows?: number;
65
+ /**
66
+ * Resolves a report owner into an RLS-bearing context for scheduled runs
67
+ * (see {@link OwnerContextResolver}). When omitted, scheduled reports fail
68
+ * closed instead of running with RLS bypassed (#2980).
69
+ */
70
+ resolveOwnerContext?: OwnerContextResolver;
54
71
  }
55
72
  declare class ReportService implements IReportService {
56
73
  private readonly engine;
@@ -58,14 +75,27 @@ declare class ReportService implements IReportService {
58
75
  private readonly clock;
59
76
  private readonly logger;
60
77
  private readonly maxRows;
78
+ private readonly resolveOwnerContext?;
61
79
  constructor(opts: ReportServiceOptions);
80
+ /**
81
+ * Authorization for a saved-report row. `sys_saved_report` is a
82
+ * protection-locked system object, so its rows are *read* with
83
+ * `SYSTEM_CTX`; the caller's right to see/mutate a specific report is
84
+ * enforced HERE, by owner match, not by the metadata read's own RLS —
85
+ * otherwise any authenticated caller could read/delete/overwrite any
86
+ * report by id (#2980). An explicit elevated context (`isSystem`) — the
87
+ * scheduler / server tooling — sees everything.
88
+ */
89
+ private canAccessReport;
90
+ /** Raw metadata read of a saved report by id (no authz — callers gate). */
91
+ private loadReportRow;
62
92
  saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport>;
63
93
  listReports(filter: {
64
94
  object?: string;
65
95
  ownerId?: string;
66
- } | undefined, _context: SharingExecutionContext): Promise<SavedReport[]>;
67
- getReport(reportId: string, _context: SharingExecutionContext): Promise<SavedReport | null>;
68
- deleteReport(reportId: string, _context: SharingExecutionContext): Promise<void>;
96
+ } | undefined, context: SharingExecutionContext): Promise<SavedReport[]>;
97
+ getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null>;
98
+ deleteReport(reportId: string, context: SharingExecutionContext): Promise<void>;
69
99
  run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult>;
70
100
  runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult>;
71
101
  private executeReport;
package/dist/index.js CHANGED
@@ -154,6 +154,32 @@ var ReportService = class {
154
154
  this.clock = opts.clock ?? { now: () => /* @__PURE__ */ new Date() };
155
155
  this.logger = opts.logger ?? {};
156
156
  this.maxRows = Math.max(1, opts.maxRows ?? 5e3);
157
+ this.resolveOwnerContext = opts.resolveOwnerContext;
158
+ }
159
+ // ── Access control ─────────────────────────────────────────────
160
+ /**
161
+ * Authorization for a saved-report row. `sys_saved_report` is a
162
+ * protection-locked system object, so its rows are *read* with
163
+ * `SYSTEM_CTX`; the caller's right to see/mutate a specific report is
164
+ * enforced HERE, by owner match, not by the metadata read's own RLS —
165
+ * otherwise any authenticated caller could read/delete/overwrite any
166
+ * report by id (#2980). An explicit elevated context (`isSystem`) — the
167
+ * scheduler / server tooling — sees everything.
168
+ */
169
+ canAccessReport(row, context) {
170
+ if (!row) return false;
171
+ if (context?.isSystem) return true;
172
+ const userId = context?.userId;
173
+ return !!userId && row.owner_id === userId;
174
+ }
175
+ /** Raw metadata read of a saved report by id (no authz — callers gate). */
176
+ async loadReportRow(reportId) {
177
+ const rows = await this.engine.find("sys_saved_report", {
178
+ filter: { id: reportId },
179
+ limit: 1,
180
+ context: SYSTEM_CTX
181
+ });
182
+ return Array.isArray(rows) && rows[0] ? rows[0] : null;
157
183
  }
158
184
  // ── Report CRUD ────────────────────────────────────────────────
159
185
  async saveReport(input, context) {
@@ -161,24 +187,25 @@ var ReportService = class {
161
187
  if (!input.object) throw new Error("VALIDATION_FAILED: object is required");
162
188
  if (!input.query) throw new Error("VALIDATION_FAILED: query is required");
163
189
  const now = this.clock.now().toISOString();
190
+ const ownerId = context.isSystem ? input.ownerId ?? context.userId ?? null : context.userId ?? null;
164
191
  const payload = {
165
192
  name: input.name,
166
193
  description: input.description ?? null,
167
194
  object_name: input.object,
168
195
  query_json: JSON.stringify(input.query ?? {}),
169
196
  format: input.format ?? DEFAULT_FORMAT,
170
- owner_id: input.ownerId ?? context.userId ?? null,
197
+ owner_id: ownerId,
171
198
  updated_at: now
172
199
  };
173
200
  if (input.id) {
174
- const existing = await this.engine.find("sys_saved_report", {
175
- filter: { id: input.id },
176
- limit: 1,
177
- context: SYSTEM_CTX
178
- });
179
- if (Array.isArray(existing) && existing[0]) {
201
+ const existing = await this.loadReportRow(input.id);
202
+ if (existing) {
203
+ if (!this.canAccessReport(existing, context)) {
204
+ throw new Error(`REPORT_NOT_FOUND: ${input.id}`);
205
+ }
206
+ if (!context.isSystem) payload.owner_id = existing.owner_id ?? payload.owner_id;
180
207
  await this.engine.update("sys_saved_report", { id: input.id, ...payload }, { context: SYSTEM_CTX });
181
- return rowFromSaved({ ...existing[0], ...payload, id: input.id });
208
+ return rowFromSaved({ ...existing, ...payload, id: input.id });
182
209
  }
183
210
  }
184
211
  const id = input.id ?? uid("rpt");
@@ -186,10 +213,16 @@ var ReportService = class {
186
213
  await this.engine.insert("sys_saved_report", row, { context: SYSTEM_CTX });
187
214
  return rowFromSaved(row);
188
215
  }
189
- async listReports(filter, _context) {
216
+ async listReports(filter, context) {
190
217
  const f = {};
191
218
  if (filter?.object) f.object_name = filter.object;
192
- if (filter?.ownerId) f.owner_id = filter.ownerId;
219
+ if (context?.isSystem) {
220
+ if (filter?.ownerId) f.owner_id = filter.ownerId;
221
+ } else {
222
+ if (!context?.userId) return [];
223
+ if (filter?.ownerId && filter.ownerId !== context.userId) return [];
224
+ f.owner_id = context.userId;
225
+ }
193
226
  const rows = await this.engine.find("sys_saved_report", {
194
227
  filter: f,
195
228
  limit: 500,
@@ -198,16 +231,18 @@ var ReportService = class {
198
231
  });
199
232
  return Array.isArray(rows) ? rows.map(rowFromSaved) : [];
200
233
  }
201
- async getReport(reportId, _context) {
202
- const rows = await this.engine.find("sys_saved_report", {
203
- filter: { id: reportId },
204
- limit: 1,
205
- context: SYSTEM_CTX
206
- });
207
- return Array.isArray(rows) && rows[0] ? rowFromSaved(rows[0]) : null;
234
+ async getReport(reportId, context) {
235
+ const row = await this.loadReportRow(reportId);
236
+ if (!this.canAccessReport(row, context)) return null;
237
+ return rowFromSaved(row);
208
238
  }
209
- async deleteReport(reportId, _context) {
239
+ async deleteReport(reportId, context) {
210
240
  if (!reportId) throw new Error("VALIDATION_FAILED: reportId is required");
241
+ const row = await this.loadReportRow(reportId);
242
+ if (!row) return;
243
+ if (!this.canAccessReport(row, context)) {
244
+ throw new Error(`REPORT_NOT_FOUND: ${reportId}`);
245
+ }
211
246
  const schedules = await this.engine.find("sys_report_schedule", {
212
247
  filter: { report_id: reportId },
213
248
  limit: 500,
@@ -353,8 +388,8 @@ var ReportService = class {
353
388
  let fired = 0, failed = 0, skipped = 0;
354
389
  for (const schedule of list) {
355
390
  try {
356
- const report = await this.getReport(schedule.report_id, { isSystem: true });
357
- if (!report) {
391
+ const row = await this.loadReportRow(schedule.report_id);
392
+ if (!row) {
358
393
  skipped++;
359
394
  await this.markSchedule(schedule.id, {
360
395
  last_status: "skipped",
@@ -362,8 +397,22 @@ var ReportService = class {
362
397
  });
363
398
  continue;
364
399
  }
400
+ const report = rowFromSaved(row);
401
+ const ownerId = report.owner_id;
402
+ const runContext = ownerId && this.resolveOwnerContext ? await this.resolveOwnerContext(ownerId).catch((err) => {
403
+ this.logger.warn?.("ReportService.dispatchDue: owner context resolution failed", err);
404
+ return null;
405
+ }) : null;
406
+ if (!runContext) {
407
+ failed++;
408
+ await this.markSchedule(schedule.id, {
409
+ last_status: "failed",
410
+ last_error: ownerId ? `owner '${ownerId}' context unavailable \u2014 refusing to run scheduled report with RLS bypassed (#2849/#2980)` : "report has no owner \u2014 refusing to run scheduled report with RLS bypassed (#2849/#2980)"
411
+ });
412
+ continue;
413
+ }
365
414
  const fmt = schedule.format ?? "html_table";
366
- const result = await this.executeReport({ ...report, format: fmt }, { isSystem: true }, false);
415
+ const result = await this.executeReport({ ...report, format: fmt }, runContext, false);
367
416
  const recipients = schedule.recipients.split(",").map((s) => s.trim()).filter(Boolean);
368
417
  const subject = renderSubject(schedule.subject_template, {
369
418
  name: schedule.name ?? report.name,
@@ -510,7 +559,14 @@ var ReportsServicePlugin = class {
510
559
  engine,
511
560
  email,
512
561
  logger: ctx.logger,
513
- maxRows: this.options.maxRows
562
+ maxRows: this.options.maxRows,
563
+ // Scheduled reports run under the owner's resolved RLS context, not a
564
+ // system bypass (#2980). No owner-context resolver is wired yet — that
565
+ // is the reports-surface consumer of ADR-0073's user-less identity
566
+ // resolution (M2) — so until it lands, scheduled runs FAIL CLOSED
567
+ // (skipped + marked failed) rather than exfiltrate. Interactive runs
568
+ // (run/runAdHoc) are unaffected: they carry the caller's context.
569
+ resolveOwnerContext: void 0
514
570
  });
515
571
  ctx.registerService("reports", this.service);
516
572
  if (this.options.disableDispatcher) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/report-service.ts","../src/reports-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-reports\n *\n * Saved reports + scheduled email digests for ObjectStack.\n * Persists `sys_saved_report` definitions and `sys_report_schedule`\n * rows, then drives a dispatcher that runs due schedules and emails\n * the rendered output via the configured `email` service.\n */\n\nexport { SysSavedReport, SysReportSchedule } from '@objectstack/platform-objects/audit';\nexport {\n ReportService,\n renderReport,\n type ReportEngine,\n type ReportEmail,\n type ReportClock,\n type ReportServiceOptions,\n} from './report-service.js';\nexport {\n ReportsServicePlugin,\n type ReportsPluginOptions,\n} from './reports-plugin.js';\nexport type {\n IReportService,\n SavedReport,\n ReportSchedule,\n ReportQuery,\n ReportRunResult,\n ReportFormat,\n SaveReportInput,\n ScheduleReportInput,\n} from '@objectstack/spec/contracts';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IReportService,\n SavedReport,\n ReportSchedule,\n ReportQuery,\n ReportRunResult,\n ReportFormat,\n SaveReportInput,\n ScheduleReportInput,\n SharingExecutionContext,\n} from '@objectstack/spec/contracts';\nimport { Cron } from 'croner';\n\n/**\n * Narrow engine surface — keeps the service testable without booting\n * a real ObjectQL kernel.\n */\nexport interface ReportEngine {\n find(object: string, options?: any): Promise<any[]>;\n findOne?(object: string, options?: any): Promise<any>;\n insert(object: string, data: any, options?: any): Promise<any>;\n update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;\n delete(object: string, options?: any): Promise<any>;\n}\n\n/**\n * Minimum email surface — implementations may pass the full\n * `IEmailService` instance straight through.\n */\nexport interface ReportEmail {\n send(input: {\n to: string | string[];\n subject: string;\n text?: string;\n html?: string;\n attachments?: Array<{ filename: string; content: string; contentType?: string }>;\n relatedObject?: string;\n relatedId?: string;\n }): Promise<{ status: 'sent' | 'queued' | 'failed' }>;\n}\n\n/** Stamped only in tests / specialised callers to make `now` deterministic. */\nexport interface ReportClock { now(): Date }\n\nconst SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nconst DEFAULT_FORMAT: ReportFormat = 'csv';\nconst DEFAULT_INTERVAL_MIN = 1440;\nconst DEFAULT_LIMIT = 1000;\n\nfunction uid(prefix: string): string {\n const g: any = globalThis as any;\n if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;\n return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction parseQuery(raw: unknown): ReportQuery {\n if (!raw) return {};\n if (typeof raw === 'string') {\n try { return JSON.parse(raw) as ReportQuery; }\n catch { return {}; }\n }\n if (typeof raw === 'object') return raw as ReportQuery;\n return {};\n}\n\nfunction rowFromSaved(row: any): SavedReport {\n return {\n id: String(row.id),\n name: String(row.name ?? ''),\n description: row.description ?? undefined,\n object_name: String(row.object_name ?? ''),\n query: parseQuery(row.query_json),\n format: (row.format as ReportFormat) ?? DEFAULT_FORMAT,\n owner_id: row.owner_id ?? undefined,\n last_run_at: row.last_run_at ?? undefined,\n last_row_count: row.last_row_count ?? undefined,\n created_at: row.created_at ?? undefined,\n updated_at: row.updated_at ?? undefined,\n };\n}\n\nfunction rowFromSchedule(row: any): ReportSchedule {\n return {\n id: String(row.id),\n report_id: String(row.report_id),\n name: row.name ?? undefined,\n interval_minutes: row.interval_minutes ?? undefined,\n cron_expression: row.cron_expression ?? undefined,\n timezone: row.timezone ?? undefined,\n active: row.active !== false,\n recipients: String(row.recipients ?? ''),\n format: row.format ?? undefined,\n subject_template: row.subject_template ?? undefined,\n owner_id: row.owner_id ?? undefined,\n next_run_at: row.next_run_at ?? undefined,\n last_sent_at: row.last_sent_at ?? undefined,\n last_status: row.last_status ?? undefined,\n last_error: row.last_error ?? undefined,\n };\n}\n\n// ─── Rendering ─────────────────────────────────────────────────────\n\nfunction escapeCsvCell(v: unknown): string {\n if (v == null) return '';\n const s = typeof v === 'string' ? v : (typeof v === 'object' ? JSON.stringify(v) : String(v));\n if (/[\",\\r\\n]/.test(s)) return `\"${s.replace(/\"/g, '\"\"')}\"`;\n return s;\n}\n\nfunction pickFields(rows: any[], explicit?: string[]): string[] {\n if (explicit && explicit.length > 0) return explicit;\n const seen = new Set<string>();\n for (const r of rows.slice(0, 50)) {\n if (r && typeof r === 'object') for (const k of Object.keys(r)) seen.add(k);\n }\n return Array.from(seen);\n}\n\nfunction renderCsv(rows: any[], fields?: string[]): string {\n const cols = pickFields(rows, fields);\n const head = cols.join(',');\n const body = rows.map(r => cols.map(c => escapeCsvCell(r?.[c])).join(',')).join('\\r\\n');\n return body.length > 0 ? `${head}\\r\\n${body}` : head;\n}\n\nfunction renderJson(rows: any[]): string {\n return JSON.stringify(rows, null, 2);\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, c => ({\n '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;',\n } as Record<string, string>)[c]);\n}\n\nfunction renderHtmlTable(rows: any[], fields?: string[]): string {\n const cols = pickFields(rows, fields);\n const th = cols.map(c => `<th style=\"text-align:left;padding:4px 8px;border-bottom:1px solid #ccc;\">${escapeHtml(c)}</th>`).join('');\n const trs = rows.map(r => {\n const tds = cols.map(c => {\n const v = r?.[c];\n const s = v == null ? '' : (typeof v === 'string' ? v : (typeof v === 'object' ? JSON.stringify(v) : String(v)));\n return `<td style=\"padding:4px 8px;border-bottom:1px solid #eee;\">${escapeHtml(s)}</td>`;\n }).join('');\n return `<tr>${tds}</tr>`;\n }).join('');\n return `<table style=\"border-collapse:collapse;font-family:system-ui,Arial,sans-serif;font-size:13px;\">`\n + `<thead><tr>${th}</tr></thead><tbody>${trs}</tbody></table>`;\n}\n\nexport function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string {\n switch (format) {\n case 'json': return renderJson(rows);\n case 'html_table': return renderHtmlTable(rows, fields);\n case 'csv':\n default: return renderCsv(rows, fields);\n }\n}\n\n// ─── Subject templating (minimal {{var}}) ─────────────────────────\n\nfunction renderSubject(template: string | undefined, vars: Record<string, string>): string {\n const tpl = template ?? '{{name}} — {{date}}';\n return tpl.replace(/\\{\\{\\s*(\\w+)\\s*\\}\\}/g, (_m, k) => vars[String(k)] ?? '');\n}\n\n// ─── Service ──────────────────────────────────────────────────────\n\nexport interface ReportServiceOptions {\n engine: ReportEngine;\n email?: ReportEmail;\n clock?: ReportClock;\n logger?: { info?: (msg: any, ...rest: any[]) => void; warn?: (msg: any, ...rest: any[]) => void; error?: (msg: any, ...rest: any[]) => void };\n /** Cap rows per report to protect both DB and email size. */\n maxRows?: number;\n}\n\nexport class ReportService implements IReportService {\n private readonly engine: ReportEngine;\n private readonly email?: ReportEmail;\n private readonly clock: ReportClock;\n private readonly logger: NonNullable<ReportServiceOptions['logger']>;\n private readonly maxRows: number;\n\n constructor(opts: ReportServiceOptions) {\n this.engine = opts.engine;\n this.email = opts.email;\n this.clock = opts.clock ?? { now: () => new Date() };\n this.logger = opts.logger ?? {};\n this.maxRows = Math.max(1, opts.maxRows ?? 5000);\n }\n\n // ── Report CRUD ────────────────────────────────────────────────\n\n async saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport> {\n if (!input.name) throw new Error('VALIDATION_FAILED: name is required');\n if (!input.object) throw new Error('VALIDATION_FAILED: object is required');\n if (!input.query) throw new Error('VALIDATION_FAILED: query is required');\n\n const now = this.clock.now().toISOString();\n const payload: any = {\n name: input.name,\n description: input.description ?? null,\n object_name: input.object,\n query_json: JSON.stringify(input.query ?? {}),\n format: input.format ?? DEFAULT_FORMAT,\n owner_id: input.ownerId ?? context.userId ?? null,\n updated_at: now,\n };\n\n if (input.id) {\n const existing = await this.engine.find('sys_saved_report', {\n filter: { id: input.id }, limit: 1, context: SYSTEM_CTX,\n });\n if (Array.isArray(existing) && existing[0]) {\n await this.engine.update('sys_saved_report', { id: input.id, ...payload }, { context: SYSTEM_CTX });\n return rowFromSaved({ ...existing[0], ...payload, id: input.id });\n }\n }\n\n const id = input.id ?? uid('rpt');\n const row = { id, ...payload, created_at: now };\n await this.engine.insert('sys_saved_report', row, { context: SYSTEM_CTX });\n return rowFromSaved(row);\n }\n\n async listReports(\n filter: { object?: string; ownerId?: string } | undefined,\n _context: SharingExecutionContext,\n ): Promise<SavedReport[]> {\n const f: any = {};\n if (filter?.object) f.object_name = filter.object;\n if (filter?.ownerId) f.owner_id = filter.ownerId;\n const rows = await this.engine.find('sys_saved_report', {\n filter: f, limit: 500, orderBy: [{ field: 'updated_at', order: 'desc' }], context: SYSTEM_CTX,\n });\n return Array.isArray(rows) ? rows.map(rowFromSaved) : [];\n }\n\n async getReport(reportId: string, _context: SharingExecutionContext): Promise<SavedReport | null> {\n const rows = await this.engine.find('sys_saved_report', {\n filter: { id: reportId }, limit: 1, context: SYSTEM_CTX,\n });\n return Array.isArray(rows) && rows[0] ? rowFromSaved(rows[0]) : null;\n }\n\n async deleteReport(reportId: string, _context: SharingExecutionContext): Promise<void> {\n if (!reportId) throw new Error('VALIDATION_FAILED: reportId is required');\n // Cascade — drop attached schedules first.\n const schedules = await this.engine.find('sys_report_schedule', {\n filter: { report_id: reportId }, limit: 500, context: SYSTEM_CTX,\n });\n for (const s of (schedules ?? [])) {\n await this.engine.delete('sys_report_schedule', { where: { id: (s as any).id }, context: SYSTEM_CTX });\n }\n await this.engine.delete('sys_saved_report', { where: { id: reportId }, context: SYSTEM_CTX });\n }\n\n // ── Execution ───────────────────────────────────────────────────\n\n async run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult> {\n const report = await this.getReport(reportId, context);\n if (!report) throw new Error(`REPORT_NOT_FOUND: ${reportId}`);\n return this.executeReport(report, context);\n }\n\n async runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult> {\n if (!input.object) throw new Error('VALIDATION_FAILED: object is required');\n if (!input.query) throw new Error('VALIDATION_FAILED: query is required');\n const adhoc: SavedReport = {\n id: '__adhoc__',\n name: input.name ?? 'Ad-hoc report',\n object_name: input.object,\n query: input.query,\n format: input.format ?? DEFAULT_FORMAT,\n };\n return this.executeReport(adhoc, context, /* stamp */ false);\n }\n\n private async executeReport(\n report: SavedReport,\n context: SharingExecutionContext,\n stamp = true,\n ): Promise<ReportRunResult> {\n const q = report.query ?? {};\n const limit = Math.min(q.limit ?? DEFAULT_LIMIT, this.maxRows);\n const rows = await this.engine.find(report.object_name, {\n filter: q.filter,\n fields: q.fields,\n orderBy: q.orderBy,\n limit,\n // Reports execute with the caller's identity so sharing rules\n // (if installed) apply. Falls back to system bypass only when\n // the report definition was created by a system writer.\n context: {\n userId: context.userId,\n tenantId: context.tenantId,\n positions: context.positions ?? [],\n permissions: context.permissions ?? [],\n isSystem: context.isSystem ?? false,\n },\n });\n const list = Array.isArray(rows) ? rows : [];\n const body = renderReport(list, report.format, q.fields);\n const ranAt = this.clock.now().toISOString();\n\n if (stamp && report.id !== '__adhoc__') {\n try {\n await this.engine.update('sys_saved_report', {\n id: report.id,\n last_run_at: ranAt,\n last_row_count: list.length,\n updated_at: ranAt,\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger.warn?.('ReportService: failed to stamp last_run_at', err);\n }\n }\n\n return {\n reportId: report.id,\n rowCount: list.length,\n format: report.format,\n body,\n rows: list,\n ranAt,\n };\n }\n\n // ── Schedules ──────────────────────────────────────────────────\n\n async scheduleReport(input: ScheduleReportInput, context: SharingExecutionContext): Promise<ReportSchedule> {\n if (!input.reportId) throw new Error('VALIDATION_FAILED: reportId is required');\n if (!input.recipients || input.recipients.length === 0) {\n throw new Error('VALIDATION_FAILED: recipients must be a non-empty array');\n }\n const report = await this.getReport(input.reportId, context);\n if (!report) throw new Error(`REPORT_NOT_FOUND: ${input.reportId}`);\n\n const now = this.clock.now();\n const interval = input.intervalMinutes ?? DEFAULT_INTERVAL_MIN;\n const cron = input.cronExpression?.trim() || null;\n if (cron) {\n // Validate eagerly so an author gets a clear error at schedule time\n // instead of a schedule that silently falls back to interval on sweep.\n try {\n new Cron(cron, { timezone: input.timezone || 'UTC' });\n } catch (err) {\n throw new Error(`VALIDATION_FAILED: invalid cron_expression '${cron}': ${(err as Error).message}`);\n }\n }\n const nextRun = this.nextRunAt(\n { cron_expression: cron, interval_minutes: interval, timezone: input.timezone ?? 'UTC' },\n now,\n ).toISOString();\n const id = uid('rsch');\n const row: any = {\n id,\n report_id: input.reportId,\n name: input.name ?? null,\n interval_minutes: interval,\n cron_expression: cron,\n timezone: input.timezone ?? 'UTC',\n active: input.active !== false,\n recipients: input.recipients.join(','),\n format: input.format ?? 'html_table',\n subject_template: input.subjectTemplate ?? null,\n owner_id: input.ownerId ?? context.userId ?? null,\n next_run_at: nextRun,\n created_at: now.toISOString(),\n updated_at: now.toISOString(),\n };\n await this.engine.insert('sys_report_schedule', row, { context: SYSTEM_CTX });\n return rowFromSchedule(row);\n }\n\n async unscheduleReport(scheduleId: string, _context: SharingExecutionContext): Promise<void> {\n if (!scheduleId) throw new Error('VALIDATION_FAILED: scheduleId is required');\n await this.engine.delete('sys_report_schedule', { where: { id: scheduleId }, context: SYSTEM_CTX });\n }\n\n async listSchedules(\n filter: { reportId?: string } | undefined,\n _context: SharingExecutionContext,\n ): Promise<ReportSchedule[]> {\n const f: any = {};\n if (filter?.reportId) f.report_id = filter.reportId;\n const rows = await this.engine.find('sys_report_schedule', {\n filter: f, limit: 500, orderBy: [{ field: 'next_run_at', order: 'asc' }], context: SYSTEM_CTX,\n });\n return Array.isArray(rows) ? rows.map(rowFromSchedule) : [];\n }\n\n // ── Dispatcher ─────────────────────────────────────────────────\n\n async dispatchDue(now?: Date): Promise<{ fired: number; failed: number; skipped: number }> {\n const ts = (now ?? this.clock.now()).toISOString();\n const due = await this.engine.find('sys_report_schedule', {\n filter: { active: true },\n limit: 200,\n context: SYSTEM_CTX,\n });\n const list = (Array.isArray(due) ? due : []).map(rowFromSchedule)\n .filter(s => !s.next_run_at || s.next_run_at <= ts);\n\n let fired = 0, failed = 0, skipped = 0;\n for (const schedule of list) {\n try {\n const report = await this.getReport(schedule.report_id, { isSystem: true });\n if (!report) {\n skipped++;\n await this.markSchedule(schedule.id, {\n last_status: 'skipped',\n last_error: `report ${schedule.report_id} missing`,\n });\n continue;\n }\n // Force the schedule's own format so the recipient gets what\n // the admin configured (CSV attachment vs inline HTML table).\n const fmt: ReportFormat = (schedule.format ?? 'html_table') as ReportFormat;\n const result = await this.executeReport({ ...report, format: fmt }, { isSystem: true }, false);\n\n const recipients = schedule.recipients.split(',').map(s => s.trim()).filter(Boolean);\n const subject = renderSubject(schedule.subject_template, {\n name: schedule.name ?? report.name,\n date: ts.slice(0, 10),\n rows: String(result.rowCount),\n });\n\n if (this.email && recipients.length > 0) {\n if (fmt === 'csv') {\n await this.email.send({\n to: recipients,\n subject,\n text: `Attached: ${result.rowCount} row(s).`,\n attachments: [{\n // Keep unicode letters (CJK schedule names) — only strip\n // filesystem-hostile characters, else 周报 becomes `__`.\n filename: `${(schedule.name ?? report.name).replace(/[^\\p{L}\\p{N}._-]+/gu, '_').replace(/^_+|_+$/g, '') || 'report'}-${ts.slice(0, 10)}.csv`,\n content: result.body,\n contentType: 'text/csv',\n }],\n relatedObject: 'sys_report_schedule',\n relatedId: schedule.id,\n });\n } else {\n await this.email.send({\n to: recipients,\n subject,\n html: `<p>${escapeHtml(report.name)} — ${result.rowCount} row(s)</p>${result.body}`,\n text: `${report.name} — ${result.rowCount} row(s)`,\n relatedObject: 'sys_report_schedule',\n relatedId: schedule.id,\n });\n }\n } else if (!this.email) {\n this.logger.warn?.('ReportService.dispatchDue: no email service — schedule fired but mail not sent');\n }\n\n await this.advanceSchedule(schedule, ts);\n fired++;\n } catch (err: any) {\n failed++;\n await this.markSchedule(schedule.id, {\n last_status: 'failed',\n last_error: String(err?.message ?? err ?? 'unknown').slice(0, 500),\n });\n this.logger.error?.('ReportService.dispatchDue: schedule failed', err);\n }\n }\n return { fired, failed, skipped };\n }\n\n /**\n * Compute the next fire time for a schedule. A `cron_expression` wins over\n * `interval_minutes` (the documented `sys_report_schedule` contract) and is\n * evaluated in the schedule's `timezone` (default UTC) via croner — the same\n * library the job scheduler uses. Falls back to `from + interval_minutes` for\n * interval schedules, and also if a cron expression is invalid or has no\n * future occurrence (logged; never throws into the sweep). `from` is the\n * reference instant (the injected clock), so `today()`-style boundaries honor\n * the test clock.\n */\n private nextRunAt(\n schedule: { cron_expression?: string | null; interval_minutes?: number | null; timezone?: string | null },\n from: Date,\n ): Date {\n const cron = (schedule.cron_expression ?? '').trim();\n if (cron) {\n try {\n const next = new Cron(cron, { timezone: schedule.timezone || 'UTC' }).nextRun(from);\n if (next) return next;\n this.logger.warn?.(`ReportService: cron '${cron}' has no next occurrence; falling back to interval`);\n } catch (err) {\n this.logger.warn?.(`ReportService: invalid cron '${cron}'; falling back to interval`, err);\n }\n }\n const interval = schedule.interval_minutes ?? DEFAULT_INTERVAL_MIN;\n return new Date(from.getTime() + interval * 60_000);\n }\n\n private async advanceSchedule(schedule: ReportSchedule, ranAt: string): Promise<void> {\n const nextRun = this.nextRunAt(schedule, this.clock.now()).toISOString();\n await this.engine.update('sys_report_schedule', {\n id: schedule.id,\n next_run_at: nextRun,\n last_sent_at: ranAt,\n last_status: 'ok',\n last_error: null,\n updated_at: ranAt,\n }, { context: SYSTEM_CTX });\n }\n\n private async markSchedule(id: string, patch: Record<string, unknown>): Promise<void> {\n try {\n await this.engine.update('sys_report_schedule', {\n id, ...patch, updated_at: this.clock.now().toISOString(),\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger.warn?.('ReportService: failed to mark schedule', err);\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport {\n SysSavedReport,\n SysReportSchedule,\n} from '@objectstack/platform-objects/audit';\nimport { ReportService, type ReportEngine, type ReportEmail } from './report-service.js';\n\nexport interface ReportsPluginOptions {\n /**\n * How often the dispatcher should poll `sys_report_schedule` for\n * due rows. Defaults to 60 seconds — short enough to honour\n * minute-grained schedules without flooding the DB.\n */\n dispatchIntervalMs?: number;\n /** Cap rows per report. Mirrors ReportServiceOptions.maxRows. */\n maxRows?: number;\n /** Disable the dispatcher tick entirely. */\n disableDispatcher?: boolean;\n}\n\n/**\n * ReportsServicePlugin — registers `sys_saved_report` /\n * `sys_report_schedule`, the `reports` service, and the dispatcher\n * loop that emails due schedules.\n *\n * The dispatcher uses `IJobService.schedule` when one is registered;\n * otherwise it falls back to a plain `setInterval` so single-kernel\n * deployments work without `service-job`.\n *\n * @example\n * ```ts\n * import { ReportsServicePlugin } from '@objectstack/plugin-reports';\n *\n * kernel.use(new ReportsServicePlugin({ dispatchIntervalMs: 60_000 }));\n * ```\n */\nexport class ReportsServicePlugin implements Plugin {\n name = 'com.objectstack.service.reports';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: ReportsPluginOptions;\n private service?: ReportService;\n private intervalHandle?: ReturnType<typeof setInterval>;\n private jobName?: string;\n private jobService?: any;\n\n constructor(options: ReportsPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.reports',\n name: 'Reports Service',\n version: '1.0.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysSavedReport, SysReportSchedule],\n });\n ctx.logger.info('ReportsServicePlugin: schemas registered');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.hook('kernel:ready', async () => {\n let engine: any = null;\n try { engine = ctx.getService<any>('objectql'); }\n catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }\n if (!engine) {\n ctx.logger.warn('ReportsServicePlugin: no ObjectQL engine — service NOT registered');\n return;\n }\n\n let email: ReportEmail | undefined;\n try { email = ctx.getService<any>('email'); } catch { /* email is optional */ }\n if (!email) {\n ctx.logger.warn('ReportsServicePlugin: no email service — schedules will fire without delivery');\n }\n\n this.service = new ReportService({\n engine: engine as ReportEngine,\n email,\n logger: ctx.logger,\n maxRows: this.options.maxRows,\n });\n ctx.registerService('reports', this.service);\n\n if (this.options.disableDispatcher) {\n ctx.logger.info('ReportsServicePlugin: dispatcher disabled (disableDispatcher=true)');\n return;\n }\n\n const intervalMs = Math.max(5_000, this.options.dispatchIntervalMs ?? 60_000);\n\n // Prefer the platform job service when available — it lets ops\n // see report dispatch alongside every other scheduled job.\n try {\n const job = ctx.getService<any>('job');\n if (job && typeof job.schedule === 'function') {\n this.jobService = job;\n this.jobName = 'reports.dispatch';\n await job.schedule(this.jobName, { type: 'interval', intervalMs }, async () => {\n try { await this.service?.dispatchDue(); }\n catch (err) { ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err as any); }\n });\n ctx.logger.info('ReportsServicePlugin: dispatcher registered with job service', { intervalMs });\n return;\n }\n } catch { /* fall through to setInterval */ }\n\n this.intervalHandle = setInterval(() => {\n this.service?.dispatchDue().catch(err => {\n ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err);\n });\n }, intervalMs);\n // Don't keep Node alive purely for the dispatcher — common\n // mistake in tests / serverless. unref is a no-op in some\n // runtimes which is fine.\n (this.intervalHandle as any)?.unref?.();\n ctx.logger.info('ReportsServicePlugin: dispatcher registered (setInterval fallback)', { intervalMs });\n });\n }\n\n async stop(ctx: PluginContext): Promise<void> {\n if (this.intervalHandle) clearInterval(this.intervalHandle);\n this.intervalHandle = undefined;\n if (this.jobService && this.jobName && typeof this.jobService.cancel === 'function') {\n try { await this.jobService.cancel(this.jobName); }\n catch (err) { ctx.logger.warn('ReportsServicePlugin: failed to cancel job', err as any); }\n }\n }\n}\n"],"mappings":"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","names":["import_audit"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/report-service.ts","../src/reports-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-reports\n *\n * Saved reports + scheduled email digests for ObjectStack.\n * Persists `sys_saved_report` definitions and `sys_report_schedule`\n * rows, then drives a dispatcher that runs due schedules and emails\n * the rendered output via the configured `email` service.\n */\n\nexport { SysSavedReport, SysReportSchedule } from '@objectstack/platform-objects/audit';\nexport {\n ReportService,\n renderReport,\n type ReportEngine,\n type ReportEmail,\n type ReportClock,\n type ReportServiceOptions,\n} from './report-service.js';\nexport {\n ReportsServicePlugin,\n type ReportsPluginOptions,\n} from './reports-plugin.js';\nexport type {\n IReportService,\n SavedReport,\n ReportSchedule,\n ReportQuery,\n ReportRunResult,\n ReportFormat,\n SaveReportInput,\n ScheduleReportInput,\n} from '@objectstack/spec/contracts';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IReportService,\n SavedReport,\n ReportSchedule,\n ReportQuery,\n ReportRunResult,\n ReportFormat,\n SaveReportInput,\n ScheduleReportInput,\n SharingExecutionContext,\n} from '@objectstack/spec/contracts';\nimport { Cron } from 'croner';\n\n/**\n * Narrow engine surface — keeps the service testable without booting\n * a real ObjectQL kernel.\n */\nexport interface ReportEngine {\n find(object: string, options?: any): Promise<any[]>;\n findOne?(object: string, options?: any): Promise<any>;\n insert(object: string, data: any, options?: any): Promise<any>;\n update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;\n delete(object: string, options?: any): Promise<any>;\n}\n\n/**\n * Minimum email surface — implementations may pass the full\n * `IEmailService` instance straight through.\n */\nexport interface ReportEmail {\n send(input: {\n to: string | string[];\n subject: string;\n text?: string;\n html?: string;\n attachments?: Array<{ filename: string; content: string; contentType?: string }>;\n relatedObject?: string;\n relatedId?: string;\n }): Promise<{ status: 'sent' | 'queued' | 'failed' }>;\n}\n\n/** Stamped only in tests / specialised callers to make `now` deterministic. */\nexport interface ReportClock { now(): Date }\n\nconst SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nconst DEFAULT_FORMAT: ReportFormat = 'csv';\nconst DEFAULT_INTERVAL_MIN = 1440;\nconst DEFAULT_LIMIT = 1000;\n\nfunction uid(prefix: string): string {\n const g: any = globalThis as any;\n if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;\n return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction parseQuery(raw: unknown): ReportQuery {\n if (!raw) return {};\n if (typeof raw === 'string') {\n try { return JSON.parse(raw) as ReportQuery; }\n catch { return {}; }\n }\n if (typeof raw === 'object') return raw as ReportQuery;\n return {};\n}\n\nfunction rowFromSaved(row: any): SavedReport {\n return {\n id: String(row.id),\n name: String(row.name ?? ''),\n description: row.description ?? undefined,\n object_name: String(row.object_name ?? ''),\n query: parseQuery(row.query_json),\n format: (row.format as ReportFormat) ?? DEFAULT_FORMAT,\n owner_id: row.owner_id ?? undefined,\n last_run_at: row.last_run_at ?? undefined,\n last_row_count: row.last_row_count ?? undefined,\n created_at: row.created_at ?? undefined,\n updated_at: row.updated_at ?? undefined,\n };\n}\n\nfunction rowFromSchedule(row: any): ReportSchedule {\n return {\n id: String(row.id),\n report_id: String(row.report_id),\n name: row.name ?? undefined,\n interval_minutes: row.interval_minutes ?? undefined,\n cron_expression: row.cron_expression ?? undefined,\n timezone: row.timezone ?? undefined,\n active: row.active !== false,\n recipients: String(row.recipients ?? ''),\n format: row.format ?? undefined,\n subject_template: row.subject_template ?? undefined,\n owner_id: row.owner_id ?? undefined,\n next_run_at: row.next_run_at ?? undefined,\n last_sent_at: row.last_sent_at ?? undefined,\n last_status: row.last_status ?? undefined,\n last_error: row.last_error ?? undefined,\n };\n}\n\n// ─── Rendering ─────────────────────────────────────────────────────\n\nfunction escapeCsvCell(v: unknown): string {\n if (v == null) return '';\n const s = typeof v === 'string' ? v : (typeof v === 'object' ? JSON.stringify(v) : String(v));\n if (/[\",\\r\\n]/.test(s)) return `\"${s.replace(/\"/g, '\"\"')}\"`;\n return s;\n}\n\nfunction pickFields(rows: any[], explicit?: string[]): string[] {\n if (explicit && explicit.length > 0) return explicit;\n const seen = new Set<string>();\n for (const r of rows.slice(0, 50)) {\n if (r && typeof r === 'object') for (const k of Object.keys(r)) seen.add(k);\n }\n return Array.from(seen);\n}\n\nfunction renderCsv(rows: any[], fields?: string[]): string {\n const cols = pickFields(rows, fields);\n const head = cols.join(',');\n const body = rows.map(r => cols.map(c => escapeCsvCell(r?.[c])).join(',')).join('\\r\\n');\n return body.length > 0 ? `${head}\\r\\n${body}` : head;\n}\n\nfunction renderJson(rows: any[]): string {\n return JSON.stringify(rows, null, 2);\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, c => ({\n '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;',\n } as Record<string, string>)[c]);\n}\n\nfunction renderHtmlTable(rows: any[], fields?: string[]): string {\n const cols = pickFields(rows, fields);\n const th = cols.map(c => `<th style=\"text-align:left;padding:4px 8px;border-bottom:1px solid #ccc;\">${escapeHtml(c)}</th>`).join('');\n const trs = rows.map(r => {\n const tds = cols.map(c => {\n const v = r?.[c];\n const s = v == null ? '' : (typeof v === 'string' ? v : (typeof v === 'object' ? JSON.stringify(v) : String(v)));\n return `<td style=\"padding:4px 8px;border-bottom:1px solid #eee;\">${escapeHtml(s)}</td>`;\n }).join('');\n return `<tr>${tds}</tr>`;\n }).join('');\n return `<table style=\"border-collapse:collapse;font-family:system-ui,Arial,sans-serif;font-size:13px;\">`\n + `<thead><tr>${th}</tr></thead><tbody>${trs}</tbody></table>`;\n}\n\nexport function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string {\n switch (format) {\n case 'json': return renderJson(rows);\n case 'html_table': return renderHtmlTable(rows, fields);\n case 'csv':\n default: return renderCsv(rows, fields);\n }\n}\n\n// ─── Subject templating (minimal {{var}}) ─────────────────────────\n\nfunction renderSubject(template: string | undefined, vars: Record<string, string>): string {\n const tpl = template ?? '{{name}} — {{date}}';\n return tpl.replace(/\\{\\{\\s*(\\w+)\\s*\\}\\}/g, (_m, k) => vars[String(k)] ?? '');\n}\n\n// ─── Service ──────────────────────────────────────────────────────\n\n/**\n * Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing\n * `ExecutionContext` so a **scheduled** report executes under the owner's\n * authority — the same rows the owner would see interactively — instead of\n * bypassing RLS with a system context. Returns `null` when the owner cannot\n * be resolved (unknown/disabled user), in which case the scheduler fails the\n * run closed rather than running elevated (#2849 / #2980). Supplying this\n * resolver is the reports-surface consumer of ADR-0073's user-less identity\n * resolution.\n */\nexport type OwnerContextResolver = (\n ownerId: string,\n) => Promise<SharingExecutionContext | null>;\n\nexport interface ReportServiceOptions {\n engine: ReportEngine;\n email?: ReportEmail;\n clock?: ReportClock;\n logger?: { info?: (msg: any, ...rest: any[]) => void; warn?: (msg: any, ...rest: any[]) => void; error?: (msg: any, ...rest: any[]) => void };\n /** Cap rows per report to protect both DB and email size. */\n maxRows?: number;\n /**\n * Resolves a report owner into an RLS-bearing context for scheduled runs\n * (see {@link OwnerContextResolver}). When omitted, scheduled reports fail\n * closed instead of running with RLS bypassed (#2980).\n */\n resolveOwnerContext?: OwnerContextResolver;\n}\n\nexport class ReportService implements IReportService {\n private readonly engine: ReportEngine;\n private readonly email?: ReportEmail;\n private readonly clock: ReportClock;\n private readonly logger: NonNullable<ReportServiceOptions['logger']>;\n private readonly maxRows: number;\n private readonly resolveOwnerContext?: OwnerContextResolver;\n\n constructor(opts: ReportServiceOptions) {\n this.engine = opts.engine;\n this.email = opts.email;\n this.clock = opts.clock ?? { now: () => new Date() };\n this.logger = opts.logger ?? {};\n this.maxRows = Math.max(1, opts.maxRows ?? 5000);\n this.resolveOwnerContext = opts.resolveOwnerContext;\n }\n\n // ── Access control ─────────────────────────────────────────────\n\n /**\n * Authorization for a saved-report row. `sys_saved_report` is a\n * protection-locked system object, so its rows are *read* with\n * `SYSTEM_CTX`; the caller's right to see/mutate a specific report is\n * enforced HERE, by owner match, not by the metadata read's own RLS —\n * otherwise any authenticated caller could read/delete/overwrite any\n * report by id (#2980). An explicit elevated context (`isSystem`) — the\n * scheduler / server tooling — sees everything.\n */\n private canAccessReport(row: { owner_id?: unknown } | null | undefined, context: SharingExecutionContext | undefined): boolean {\n if (!row) return false;\n if (context?.isSystem) return true;\n const userId = context?.userId;\n return !!userId && row.owner_id === userId;\n }\n\n /** Raw metadata read of a saved report by id (no authz — callers gate). */\n private async loadReportRow(reportId: string): Promise<any | null> {\n const rows = await this.engine.find('sys_saved_report', {\n filter: { id: reportId }, limit: 1, context: SYSTEM_CTX,\n });\n return Array.isArray(rows) && rows[0] ? rows[0] : null;\n }\n\n // ── Report CRUD ────────────────────────────────────────────────\n\n async saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport> {\n if (!input.name) throw new Error('VALIDATION_FAILED: name is required');\n if (!input.object) throw new Error('VALIDATION_FAILED: object is required');\n if (!input.query) throw new Error('VALIDATION_FAILED: query is required');\n\n const now = this.clock.now().toISOString();\n // A non-system caller always owns what they create — a caller-supplied\n // ownerId cannot assign the report to someone else (#2980). Only an\n // explicit elevated context (server tooling / import) may set it.\n const ownerId = context.isSystem ? (input.ownerId ?? context.userId ?? null) : (context.userId ?? null);\n const payload: any = {\n name: input.name,\n description: input.description ?? null,\n object_name: input.object,\n query_json: JSON.stringify(input.query ?? {}),\n format: input.format ?? DEFAULT_FORMAT,\n owner_id: ownerId,\n updated_at: now,\n };\n\n if (input.id) {\n const existing = await this.loadReportRow(input.id);\n if (existing) {\n // An update to an existing report is a mutation — a caller may only\n // overwrite a report they own (#2980). Not-found for others so the\n // response doesn't leak that the id exists.\n if (!this.canAccessReport(existing, context)) {\n throw new Error(`REPORT_NOT_FOUND: ${input.id}`);\n }\n // Never let a non-system caller reassign ownership away from the row.\n if (!context.isSystem) payload.owner_id = existing.owner_id ?? payload.owner_id;\n await this.engine.update('sys_saved_report', { id: input.id, ...payload }, { context: SYSTEM_CTX });\n return rowFromSaved({ ...existing, ...payload, id: input.id });\n }\n }\n\n const id = input.id ?? uid('rpt');\n const row = { id, ...payload, created_at: now };\n await this.engine.insert('sys_saved_report', row, { context: SYSTEM_CTX });\n return rowFromSaved(row);\n }\n\n async listReports(\n filter: { object?: string; ownerId?: string } | undefined,\n context: SharingExecutionContext,\n ): Promise<SavedReport[]> {\n const f: any = {};\n if (filter?.object) f.object_name = filter.object;\n // Owner scoping (#2980): a non-system caller sees ONLY their own reports —\n // a caller-supplied ownerId can never widen past their own id. A caller\n // with no identity sees nothing (fail closed). System/tooling sees all,\n // honouring an explicit ownerId narrow.\n if (context?.isSystem) {\n if (filter?.ownerId) f.owner_id = filter.ownerId;\n } else {\n if (!context?.userId) return [];\n if (filter?.ownerId && filter.ownerId !== context.userId) return [];\n f.owner_id = context.userId;\n }\n const rows = await this.engine.find('sys_saved_report', {\n filter: f, limit: 500, orderBy: [{ field: 'updated_at', order: 'desc' }], context: SYSTEM_CTX,\n });\n return Array.isArray(rows) ? rows.map(rowFromSaved) : [];\n }\n\n async getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null> {\n const row = await this.loadReportRow(reportId);\n // Unauthorized reads are indistinguishable from a genuine miss (#2980).\n if (!this.canAccessReport(row, context)) return null;\n return rowFromSaved(row);\n }\n\n async deleteReport(reportId: string, context: SharingExecutionContext): Promise<void> {\n if (!reportId) throw new Error('VALIDATION_FAILED: reportId is required');\n const row = await this.loadReportRow(reportId);\n if (!row) return; // idempotent — nothing to drop\n // A caller may only delete a report they own (#2980); others get a\n // not-found so the delete neither fires nor reveals the report's existence.\n if (!this.canAccessReport(row, context)) {\n throw new Error(`REPORT_NOT_FOUND: ${reportId}`);\n }\n // Cascade — drop attached schedules first.\n const schedules = await this.engine.find('sys_report_schedule', {\n filter: { report_id: reportId }, limit: 500, context: SYSTEM_CTX,\n });\n for (const s of (schedules ?? [])) {\n await this.engine.delete('sys_report_schedule', { where: { id: (s as any).id }, context: SYSTEM_CTX });\n }\n await this.engine.delete('sys_saved_report', { where: { id: reportId }, context: SYSTEM_CTX });\n }\n\n // ── Execution ───────────────────────────────────────────────────\n\n async run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult> {\n const report = await this.getReport(reportId, context);\n if (!report) throw new Error(`REPORT_NOT_FOUND: ${reportId}`);\n return this.executeReport(report, context);\n }\n\n async runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult> {\n if (!input.object) throw new Error('VALIDATION_FAILED: object is required');\n if (!input.query) throw new Error('VALIDATION_FAILED: query is required');\n const adhoc: SavedReport = {\n id: '__adhoc__',\n name: input.name ?? 'Ad-hoc report',\n object_name: input.object,\n query: input.query,\n format: input.format ?? DEFAULT_FORMAT,\n };\n return this.executeReport(adhoc, context, /* stamp */ false);\n }\n\n private async executeReport(\n report: SavedReport,\n context: SharingExecutionContext,\n stamp = true,\n ): Promise<ReportRunResult> {\n const q = report.query ?? {};\n const limit = Math.min(q.limit ?? DEFAULT_LIMIT, this.maxRows);\n const rows = await this.engine.find(report.object_name, {\n filter: q.filter,\n fields: q.fields,\n orderBy: q.orderBy,\n limit,\n // Reports execute with the caller's identity so sharing rules\n // (if installed) apply. Falls back to system bypass only when\n // the report definition was created by a system writer.\n context: {\n userId: context.userId,\n tenantId: context.tenantId,\n positions: context.positions ?? [],\n permissions: context.permissions ?? [],\n isSystem: context.isSystem ?? false,\n },\n });\n const list = Array.isArray(rows) ? rows : [];\n const body = renderReport(list, report.format, q.fields);\n const ranAt = this.clock.now().toISOString();\n\n if (stamp && report.id !== '__adhoc__') {\n try {\n await this.engine.update('sys_saved_report', {\n id: report.id,\n last_run_at: ranAt,\n last_row_count: list.length,\n updated_at: ranAt,\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger.warn?.('ReportService: failed to stamp last_run_at', err);\n }\n }\n\n return {\n reportId: report.id,\n rowCount: list.length,\n format: report.format,\n body,\n rows: list,\n ranAt,\n };\n }\n\n // ── Schedules ──────────────────────────────────────────────────\n\n async scheduleReport(input: ScheduleReportInput, context: SharingExecutionContext): Promise<ReportSchedule> {\n if (!input.reportId) throw new Error('VALIDATION_FAILED: reportId is required');\n if (!input.recipients || input.recipients.length === 0) {\n throw new Error('VALIDATION_FAILED: recipients must be a non-empty array');\n }\n const report = await this.getReport(input.reportId, context);\n if (!report) throw new Error(`REPORT_NOT_FOUND: ${input.reportId}`);\n\n const now = this.clock.now();\n const interval = input.intervalMinutes ?? DEFAULT_INTERVAL_MIN;\n const cron = input.cronExpression?.trim() || null;\n if (cron) {\n // Validate eagerly so an author gets a clear error at schedule time\n // instead of a schedule that silently falls back to interval on sweep.\n try {\n new Cron(cron, { timezone: input.timezone || 'UTC' });\n } catch (err) {\n throw new Error(`VALIDATION_FAILED: invalid cron_expression '${cron}': ${(err as Error).message}`);\n }\n }\n const nextRun = this.nextRunAt(\n { cron_expression: cron, interval_minutes: interval, timezone: input.timezone ?? 'UTC' },\n now,\n ).toISOString();\n const id = uid('rsch');\n const row: any = {\n id,\n report_id: input.reportId,\n name: input.name ?? null,\n interval_minutes: interval,\n cron_expression: cron,\n timezone: input.timezone ?? 'UTC',\n active: input.active !== false,\n recipients: input.recipients.join(','),\n format: input.format ?? 'html_table',\n subject_template: input.subjectTemplate ?? null,\n owner_id: input.ownerId ?? context.userId ?? null,\n next_run_at: nextRun,\n created_at: now.toISOString(),\n updated_at: now.toISOString(),\n };\n await this.engine.insert('sys_report_schedule', row, { context: SYSTEM_CTX });\n return rowFromSchedule(row);\n }\n\n async unscheduleReport(scheduleId: string, _context: SharingExecutionContext): Promise<void> {\n if (!scheduleId) throw new Error('VALIDATION_FAILED: scheduleId is required');\n await this.engine.delete('sys_report_schedule', { where: { id: scheduleId }, context: SYSTEM_CTX });\n }\n\n async listSchedules(\n filter: { reportId?: string } | undefined,\n _context: SharingExecutionContext,\n ): Promise<ReportSchedule[]> {\n const f: any = {};\n if (filter?.reportId) f.report_id = filter.reportId;\n const rows = await this.engine.find('sys_report_schedule', {\n filter: f, limit: 500, orderBy: [{ field: 'next_run_at', order: 'asc' }], context: SYSTEM_CTX,\n });\n return Array.isArray(rows) ? rows.map(rowFromSchedule) : [];\n }\n\n // ── Dispatcher ─────────────────────────────────────────────────\n\n async dispatchDue(now?: Date): Promise<{ fired: number; failed: number; skipped: number }> {\n const ts = (now ?? this.clock.now()).toISOString();\n const due = await this.engine.find('sys_report_schedule', {\n filter: { active: true },\n limit: 200,\n context: SYSTEM_CTX,\n });\n const list = (Array.isArray(due) ? due : []).map(rowFromSchedule)\n .filter(s => !s.next_run_at || s.next_run_at <= ts);\n\n let fired = 0, failed = 0, skipped = 0;\n for (const schedule of list) {\n try {\n const row = await this.loadReportRow(schedule.report_id);\n if (!row) {\n skipped++;\n await this.markSchedule(schedule.id, {\n last_status: 'skipped',\n last_error: `report ${schedule.report_id} missing`,\n });\n continue;\n }\n const report = rowFromSaved(row);\n\n // Run the report under the OWNER's authority, not system (#2980).\n // A scheduled run must not read rows the report's owner cannot see —\n // that was a silent RLS bypass (a member's scheduled report emailed\n // the target object's entire table). Resolve the owner to a real\n // RLS-bearing context; if we can't (no resolver wired, or unknown/\n // disabled owner), FAIL CLOSED rather than run elevated.\n const ownerId = report.owner_id;\n const runContext = ownerId && this.resolveOwnerContext\n ? await this.resolveOwnerContext(ownerId).catch((err) => {\n this.logger.warn?.('ReportService.dispatchDue: owner context resolution failed', err);\n return null;\n })\n : null;\n if (!runContext) {\n failed++;\n await this.markSchedule(schedule.id, {\n last_status: 'failed',\n last_error: ownerId\n ? `owner '${ownerId}' context unavailable — refusing to run scheduled report with RLS bypassed (#2849/#2980)`\n : 'report has no owner — refusing to run scheduled report with RLS bypassed (#2849/#2980)',\n });\n continue;\n }\n\n // Force the schedule's own format so the recipient gets what\n // the admin configured (CSV attachment vs inline HTML table).\n const fmt: ReportFormat = (schedule.format ?? 'html_table') as ReportFormat;\n const result = await this.executeReport({ ...report, format: fmt }, runContext, false);\n\n const recipients = schedule.recipients.split(',').map(s => s.trim()).filter(Boolean);\n const subject = renderSubject(schedule.subject_template, {\n name: schedule.name ?? report.name,\n date: ts.slice(0, 10),\n rows: String(result.rowCount),\n });\n\n if (this.email && recipients.length > 0) {\n if (fmt === 'csv') {\n await this.email.send({\n to: recipients,\n subject,\n text: `Attached: ${result.rowCount} row(s).`,\n attachments: [{\n // Keep unicode letters (CJK schedule names) — only strip\n // filesystem-hostile characters, else 周报 becomes `__`.\n filename: `${(schedule.name ?? report.name).replace(/[^\\p{L}\\p{N}._-]+/gu, '_').replace(/^_+|_+$/g, '') || 'report'}-${ts.slice(0, 10)}.csv`,\n content: result.body,\n contentType: 'text/csv',\n }],\n relatedObject: 'sys_report_schedule',\n relatedId: schedule.id,\n });\n } else {\n await this.email.send({\n to: recipients,\n subject,\n html: `<p>${escapeHtml(report.name)} — ${result.rowCount} row(s)</p>${result.body}`,\n text: `${report.name} — ${result.rowCount} row(s)`,\n relatedObject: 'sys_report_schedule',\n relatedId: schedule.id,\n });\n }\n } else if (!this.email) {\n this.logger.warn?.('ReportService.dispatchDue: no email service — schedule fired but mail not sent');\n }\n\n await this.advanceSchedule(schedule, ts);\n fired++;\n } catch (err: any) {\n failed++;\n await this.markSchedule(schedule.id, {\n last_status: 'failed',\n last_error: String(err?.message ?? err ?? 'unknown').slice(0, 500),\n });\n this.logger.error?.('ReportService.dispatchDue: schedule failed', err);\n }\n }\n return { fired, failed, skipped };\n }\n\n /**\n * Compute the next fire time for a schedule. A `cron_expression` wins over\n * `interval_minutes` (the documented `sys_report_schedule` contract) and is\n * evaluated in the schedule's `timezone` (default UTC) via croner — the same\n * library the job scheduler uses. Falls back to `from + interval_minutes` for\n * interval schedules, and also if a cron expression is invalid or has no\n * future occurrence (logged; never throws into the sweep). `from` is the\n * reference instant (the injected clock), so `today()`-style boundaries honor\n * the test clock.\n */\n private nextRunAt(\n schedule: { cron_expression?: string | null; interval_minutes?: number | null; timezone?: string | null },\n from: Date,\n ): Date {\n const cron = (schedule.cron_expression ?? '').trim();\n if (cron) {\n try {\n const next = new Cron(cron, { timezone: schedule.timezone || 'UTC' }).nextRun(from);\n if (next) return next;\n this.logger.warn?.(`ReportService: cron '${cron}' has no next occurrence; falling back to interval`);\n } catch (err) {\n this.logger.warn?.(`ReportService: invalid cron '${cron}'; falling back to interval`, err);\n }\n }\n const interval = schedule.interval_minutes ?? DEFAULT_INTERVAL_MIN;\n return new Date(from.getTime() + interval * 60_000);\n }\n\n private async advanceSchedule(schedule: ReportSchedule, ranAt: string): Promise<void> {\n const nextRun = this.nextRunAt(schedule, this.clock.now()).toISOString();\n await this.engine.update('sys_report_schedule', {\n id: schedule.id,\n next_run_at: nextRun,\n last_sent_at: ranAt,\n last_status: 'ok',\n last_error: null,\n updated_at: ranAt,\n }, { context: SYSTEM_CTX });\n }\n\n private async markSchedule(id: string, patch: Record<string, unknown>): Promise<void> {\n try {\n await this.engine.update('sys_report_schedule', {\n id, ...patch, updated_at: this.clock.now().toISOString(),\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger.warn?.('ReportService: failed to mark schedule', err);\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport {\n SysSavedReport,\n SysReportSchedule,\n} from '@objectstack/platform-objects/audit';\nimport { ReportService, type ReportEngine, type ReportEmail } from './report-service.js';\n\nexport interface ReportsPluginOptions {\n /**\n * How often the dispatcher should poll `sys_report_schedule` for\n * due rows. Defaults to 60 seconds — short enough to honour\n * minute-grained schedules without flooding the DB.\n */\n dispatchIntervalMs?: number;\n /** Cap rows per report. Mirrors ReportServiceOptions.maxRows. */\n maxRows?: number;\n /** Disable the dispatcher tick entirely. */\n disableDispatcher?: boolean;\n}\n\n/**\n * ReportsServicePlugin — registers `sys_saved_report` /\n * `sys_report_schedule`, the `reports` service, and the dispatcher\n * loop that emails due schedules.\n *\n * The dispatcher uses `IJobService.schedule` when one is registered;\n * otherwise it falls back to a plain `setInterval` so single-kernel\n * deployments work without `service-job`.\n *\n * @example\n * ```ts\n * import { ReportsServicePlugin } from '@objectstack/plugin-reports';\n *\n * kernel.use(new ReportsServicePlugin({ dispatchIntervalMs: 60_000 }));\n * ```\n */\nexport class ReportsServicePlugin implements Plugin {\n name = 'com.objectstack.service.reports';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: ReportsPluginOptions;\n private service?: ReportService;\n private intervalHandle?: ReturnType<typeof setInterval>;\n private jobName?: string;\n private jobService?: any;\n\n constructor(options: ReportsPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.reports',\n name: 'Reports Service',\n version: '1.0.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysSavedReport, SysReportSchedule],\n });\n ctx.logger.info('ReportsServicePlugin: schemas registered');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.hook('kernel:ready', async () => {\n let engine: any = null;\n try { engine = ctx.getService<any>('objectql'); }\n catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }\n if (!engine) {\n ctx.logger.warn('ReportsServicePlugin: no ObjectQL engine — service NOT registered');\n return;\n }\n\n let email: ReportEmail | undefined;\n try { email = ctx.getService<any>('email'); } catch { /* email is optional */ }\n if (!email) {\n ctx.logger.warn('ReportsServicePlugin: no email service — schedules will fire without delivery');\n }\n\n this.service = new ReportService({\n engine: engine as ReportEngine,\n email,\n logger: ctx.logger,\n maxRows: this.options.maxRows,\n // Scheduled reports run under the owner's resolved RLS context, not a\n // system bypass (#2980). No owner-context resolver is wired yet — that\n // is the reports-surface consumer of ADR-0073's user-less identity\n // resolution (M2) — so until it lands, scheduled runs FAIL CLOSED\n // (skipped + marked failed) rather than exfiltrate. Interactive runs\n // (run/runAdHoc) are unaffected: they carry the caller's context.\n resolveOwnerContext: undefined,\n });\n ctx.registerService('reports', this.service);\n\n if (this.options.disableDispatcher) {\n ctx.logger.info('ReportsServicePlugin: dispatcher disabled (disableDispatcher=true)');\n return;\n }\n\n const intervalMs = Math.max(5_000, this.options.dispatchIntervalMs ?? 60_000);\n\n // Prefer the platform job service when available — it lets ops\n // see report dispatch alongside every other scheduled job.\n try {\n const job = ctx.getService<any>('job');\n if (job && typeof job.schedule === 'function') {\n this.jobService = job;\n this.jobName = 'reports.dispatch';\n await job.schedule(this.jobName, { type: 'interval', intervalMs }, async () => {\n try { await this.service?.dispatchDue(); }\n catch (err) { ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err as any); }\n });\n ctx.logger.info('ReportsServicePlugin: dispatcher registered with job service', { intervalMs });\n return;\n }\n } catch { /* fall through to setInterval */ }\n\n this.intervalHandle = setInterval(() => {\n this.service?.dispatchDue().catch(err => {\n ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err);\n });\n }, intervalMs);\n // Don't keep Node alive purely for the dispatcher — common\n // mistake in tests / serverless. unref is a no-op in some\n // runtimes which is fine.\n (this.intervalHandle as any)?.unref?.();\n ctx.logger.info('ReportsServicePlugin: dispatcher registered (setInterval fallback)', { intervalMs });\n });\n }\n\n async stop(ctx: PluginContext): Promise<void> {\n if (this.intervalHandle) clearInterval(this.intervalHandle);\n this.intervalHandle = undefined;\n if (this.jobService && this.jobName && typeof this.jobService.cancel === 'function') {\n try { await this.jobService.cancel(this.jobName); }\n catch (err) { ctx.logger.warn('ReportsServicePlugin: failed to cancel job', err as any); }\n }\n }\n}\n"],"mappings":"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","names":["import_audit"]}