@objectstack/plugin-reports 17.0.0-rc.5 → 17.0.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,413 @@
1
1
  # @objectstack/plugin-reports
2
2
 
3
+ ## 17.0.0-rc.6
4
+
5
+ ### Patch Changes
6
+
7
+ - f40c5b4: refactor(plugin-approvals,plugin-reports): enforcement implementations annotate the full `ExecutionContext` (#7135)
8
+
9
+ The services half of #7070, mirroring what PR #7140 did for
10
+ `plugin-sharing` / `plugin-audit`. #6523 converged 36 contract signatures onto
11
+ the complete `resolveAuthzContext` envelope, applying the #6206 ruling —
12
+ enforcement adjudicates on the whole envelope, never a per-site subset. The
13
+ implementations behind those contracts still annotated their own parameters
14
+ with the six-field shape the contracts used to name, so nothing they could
15
+ _read_ had widened.
16
+
17
+ `ApprovalService`, the approval flow-node provider and `ReportService` now
18
+ declare `ExecutionContext` on all 43 of those positions, and the casts the
19
+ narrow annotation forced are gone:
20
+
21
+ - `isOverrideActor()` read the derived `posture` (ADR-0095) through an
22
+ unchecked `(context as any)`. That gate decides whether a platform or tenant
23
+ admin may release a STUCK approval — one routed to an unstaffed position, the
24
+ only in-product recovery from a permanently locked record — so an erasure sat
25
+ directly on an enforcement input: a mistyped rung would have compiled and
26
+ silently denied every override. It is a declared read now.
27
+ - Both services' `SYSTEM_CTX` is typed as the envelope and passed as itself,
28
+ retiring the `SYSTEM_CTX as unknown as …` double casts at the three sites
29
+ that hand it to a contract method.
30
+ - The `(context as any).userId` / `.tenantId` reads in `ApprovalService` now
31
+ read declared fields.
32
+ - `OwnerContextResolver` returns the envelope, which is what a scheduled report
33
+ actually resolves for its owner (#2849 / #2980).
34
+
35
+ **No runtime behaviour changes.** The values were always complete — this
36
+ family's damage was type-side — so every gate answers exactly what it answered
37
+ before. Method parameters only WIDEN what they accept, so no caller is
38
+ affected, and no public export changes shape.
39
+
40
+ Casts deliberately kept, and now documented where they sit: `organizationId`
41
+ is not a field of the envelope at all — that spelling has its own history
42
+ (#5858 / `check:org-identifier`) and was held out of this change by #7070. In
43
+ `approval-node.ts` the single remaining assertion exists only because the
44
+ literal names that key; it was reduced from `as unknown as …` to a single
45
+ `as ExecutionContext`, which still requires the literal to be comparable to
46
+ the envelope.
47
+
48
+ Because a re-narrowed annotation would compile, ship and pass every test in
49
+ these packages, the convergence is pinned by a new compile-time module per
50
+ package, `exec-context-annotation.pin.ts`: it hands each parameter a fresh
51
+ literal naming envelope-only fields (`posture`, `accessible_org_ids`,
52
+ `org_user_ids`), which TypeScript's excess-property check rejects the moment a
53
+ parameter narrows back, plus negative cases so a parameter erased to `any`
54
+ cannot pass either.
55
+
56
+ The exported `SharingExecutionContext` type itself is NOT removed here: it is
57
+ defined in `packages/spec`, which is single-owner, so its retirement is a
58
+ separate follow-up.
59
+
60
+ - d0d5205: refactor(core,plugin-audit,service-storage,plugin-reports): give the `__` operation-private-key convention a single owner (#7284)
61
+
62
+ `withoutOperationPrivateKeys` — the rule that a consumer forwarding a caller's
63
+ execution envelope to a question about a DIFFERENT object must first drop the
64
+ `__`-prefixed keys plugin-security stamped for the operation in flight — had been
65
+ hand-copied into three packages: `plugin-audit`'s comment access hooks (#7141),
66
+ `service-storage`'s attachment access hooks (#7145) and `plugin-reports`' report
67
+ service (#7204). Each carried its own `OPERATION_PRIVATE_KEY_PREFIX` and its own
68
+ doc block, and the prose had already diverged while the code still agreed — the
69
+ shape that makes a later divergence in behaviour hard to notice.
70
+
71
+ The helper now lives once, in `@objectstack/core`
72
+ (`security/operation-private-keys.ts`), exported from the package root. Core is
73
+ the only candidate all three consumers already depend on: `plugin-security` is
74
+ the producer of the convention and the most honest owner, but none of the three
75
+ depends on it and a string-prefix filter does not justify three new dependency
76
+ edges onto a plugin; `@objectstack/spec` is fenced off by Prime Directive #2. The
77
+ new home sits beside `assemble-execution-context.ts`, which owns the other end of
78
+ the same lifecycle — that file is where an `ExecutionContext` is built at a
79
+ transport entry point, this one is where it is stripped back down before being
80
+ forwarded.
81
+
82
+ The full reasoning moved with the code rather than being thinned: which keys the
83
+ middleware stamps and why each is a widening input, why they are dropped by
84
+ PREFIX and never by a name list, and why the fresh copy is load-bearing in both
85
+ directions. Each consumer keeps only its own local half — which object _its_
86
+ gates actually ask about — and points at the shared home.
87
+
88
+ No behaviour change: the three copies were byte-equivalent, and all three
89
+ packages' suites pass unchanged. Two new pins at the home cover it — the rule's
90
+ own behaviour, which no package-level test had ever asserted directly, and a
91
+ repository-shape pin that turns red if a fourth file declares its own copy.
92
+
93
+ - cb466aa: Reports read with the caller's whole execution envelope, so a `group`-posture report no longer under-reports
94
+
95
+ `executeReport` rebuilt a five-field projection of the caller's `ExecutionContext`
96
+ (`userId` / `tenantId` / `positions` / `permissions` / `isSystem`) before handing it to
97
+ the engine read that produces the report — while the method's own comment promised
98
+ "reports execute with the caller's identity".
99
+
100
+ **Before.** `accessible_org_ids` was not in that projection, and the engine reads it by
101
+ name (`buildDriverOptions`, ADR-0105 D2 / #3623) to widen the driver's native tenant
102
+ scope to the caller's whole membership set under the `group` tenancy posture. Absent, the
103
+ drivers fall back to active-org equality — "fail toward isolation". So the identical query
104
+ returned the membership union in an interactive list view and collapsed to the active org
105
+ inside a **saved or scheduled** report: silently short rows, no error, nothing in the
106
+ output saying so. Measured end-to-end on a real kernel + SQL driver: three rows across two
107
+ member orgs came back as three interactively and two in the report, and a scheduled CSV
108
+ digest emailed the owner the same two. `timezone` went the same way, so a read-time
109
+ formula field resolved its calendar day in UTC instead of the caller's business timezone;
110
+ `posture`, `org_user_ids`, `systemPermissions` and `onBehalfOf` were dropped too.
111
+
112
+ **After.** The read receives the caller's envelope whole (the #6206 ruling — enforcement
113
+ adjudicates on the whole `resolveAuthzContext` envelope, never a per-site subset), minus
114
+ the `__`-prefixed keys plugin-security stamps for the operation in flight, and as a fresh
115
+ object so a callee's stamp cannot write back into the caller's request context. The same
116
+ shape `plugin-audit` (#7141) and `service-storage` (#7145) landed. Direction is unchanged
117
+ outside `group`: the `isolated` posture, a deployment with no posture provider, and a
118
+ `group` caller with an empty accessible set all still read at active-org equality.
119
+
120
+ - 2c2a212: fix(reports): owner-gate the saved-report schedule routes (#2980)
121
+
122
+ The report read/run/delete routes are owner-isolated (a caller may only touch a
123
+ report they own, denied as `REPORT_NOT_FOUND` to avoid leaking that the id
124
+ exists), but the two schedule routes bypassed that gate: `unscheduleReport` and
125
+ `listSchedules` took the caller `context` as `_context` and never consulted it,
126
+ querying under the system context (RLS-bypassing). Any authenticated caller
127
+ could therefore delete another owner's report schedule — a cross-owner
128
+ destructive write — or list another owner's schedules (leaking recipient
129
+ addresses and cron), by supplying an id.
130
+
131
+ Both now resolve the schedule's parent report and require the caller to own it,
132
+ mirroring the sibling routes:
133
+
134
+ - **`unscheduleReport`** loads the schedule, then its report, and deletes only
135
+ when `canAccessReport` holds; a cross-owner attempt throws `REPORT_NOT_FOUND`
136
+ (mapped to `404` by the REST layer, deny-as-404 anti-enumeration), while a
137
+ genuinely-absent schedule stays idempotent. `scheduleReport` (create) was
138
+ already gated via `getReport`, so only the delete/list doors were open.
139
+ - **`listSchedules`** returns an empty list to any non-system caller who cannot
140
+ access the report it is scoped to — the same non-leaking posture as
141
+ `listReports`. The scheduler's system context still sees every schedule.
142
+
143
+ No authoring-surface or metadata change; existing owner-path behavior is
144
+ unchanged.
145
+
146
+ - dadd1ad: refactor(spec,plugin-sharing): retire the exported `SharingExecutionContext` type (#7218)
147
+
148
+ <!-- adr-0087: registered sharing-execution-context-retired -->
149
+
150
+ **BREAKING — public surface removal.** `SharingExecutionContext` is deleted from
151
+ `@objectstack/spec` (`contracts/sharing-service`) and from
152
+ `@objectstack/plugin-sharing`, which re-exported it. Both `api-surface/` and
153
+ `export-origins/` snapshots are regenerated accordingly.
154
+
155
+ This is the deferred deletion recorded when #7070 split the convergence in two.
156
+ #6523 / PR #7068 converged 36 contract signatures onto the full
157
+ `resolveAuthzContext` envelope (`ExecutionContext`), applying the #6206 ruling —
158
+ enforcement adjudicates on the whole envelope, never a per-site subset. The
159
+ consumer halves then re-annotated the implementations: PR #7140 (identity:
160
+ `plugin-sharing`, `plugin-audit`) and PR #7206 (services: `plugin-approvals`,
161
+ `plugin-reports`). Both landed with the type still exported, because it is
162
+ DEFINED in `packages/spec` and that package's retirement is the spec seat's to
163
+ make. Nothing declares it any more, so it goes.
164
+
165
+ **Migration.** Anyone who imported `SharingExecutionContext` from either package
166
+ should import `ExecutionContext` from `@objectstack/spec` instead — the type the
167
+ contracts have declared since #7068. The old shape was six optional fields, all
168
+ of which exist on the envelope with the same names and types, so a value that
169
+ satisfied the retired type already satisfies `ExecutionContext`; only the
170
+ spelling of the annotation changes.
171
+
172
+ **No runtime behaviour changes.** The type was erased at compile time and no
173
+ signature's accepted shape moved: the contracts already took the wide envelope.
174
+
175
+ **What the retirement did NOT remove — the reason to read the pins.** Deleting
176
+ the type does not make re-narrowing a compile error. Structural subtyping still
177
+ accepts a six-field context where the envelope is expected, so the boundary is
178
+ held by the declared parameter type plus the pins, exactly as before. The three
179
+ `exec-context-annotation.pin.ts` files (`plugin-sharing`, `plugin-approvals`,
180
+ `plugin-reports`) told their failure story as "the parameter narrows back to
181
+ `SharingExecutionContext`", which a deletion would have quietly hollowed out.
182
+ Each now keeps the retired six-field shape as a local, non-exported SPECIMEN
183
+ type and refutes every enforcement parameter against it by type identity, so a
184
+ re-narrowing under ANY name is red — alongside the fresh-literal
185
+ excess-property checks they already carried. `sharing-service.test.ts` in
186
+ `packages/spec` is re-anchored the same way, and its "twin unchanged in shape"
187
+ case becomes a "twin stays retired" case. The narrative the retired type's doc
188
+ block carried (the measured `(context as any).posture` specimen, and why tsc
189
+ cannot police this) moves to the module doc of `contracts/sharing-service`,
190
+ which the contracts and pins now point at.
191
+
192
+ - 2465133: fix(plugins): sweep the service-lookup erasures out of the plugin composition roots, and fix the two alias-only HTTP reads it exposed (#4251 B5)
193
+
194
+ Batch B5 of the #4251 sweep: the seven remaining `packages/plugins/*` composition
195
+ roots. 35 lookup sites that had been erased to `any` now carry the slot's
196
+ contract, so the compiler checks what each plugin actually calls on the service
197
+ it resolved. The ratchet drops 143 sites / 32 files to 108 / 25.
198
+
199
+ **Two real defects, both of the shape this sweep exists to find.** Approvals'
200
+ actionable-link pages (ADR-0043) and sharing's public share-link REST routes each
201
+ read the HTTP server under `http-server` _only_ — the deprecated alias. The
202
+ ledger records `http.server` as canonical and as the only name present on every
203
+ provider path: `runtime.ts`'s `config.server` path registers no alias at all. On
204
+ that path both lookups threw, the surrounding `catch` swallowed it, and the
205
+ routes silently never mounted — approval e-mail action links 404'd and the
206
+ share-link surface was absent, with nothing in the log to say so. Both reads are
207
+ now canonical-first with the alias as fallback, each name in its own `try`
208
+ because `getService` throws on an empty slot (so `a() ?? b()` inside one `try`
209
+ never reaches `b` — the same correction #4393 made in metadata and
210
+ cloud-connection).
211
+
212
+ Typing choices follow the batch method: pure data-plane consumers take the
213
+ narrow contract (`IDataEngine` in reports), consumers that bind hook or
214
+ middleware seams take the engine seen whole (`IObjectQLEngine` in approvals,
215
+ sharing and pinyin-search), and slots with no contract get a **named** local
216
+ surface rather than `any` — plugin-email's `MailSettingsSurface`, and the
217
+ surfaces the consuming packages already declared (`ApprovalMessagingSurface`,
218
+ `SharingSecurityProbe`, `ReportEmail`). A named surface that omits a member
219
+ still makes the compiler name every call site; `any` says nothing.
220
+
221
+ No behaviour change beyond the two alias reads. No contract changes.
222
+
223
+ - Updated dependencies [3d5c090]
224
+ - Updated dependencies [e5bd768]
225
+ - Updated dependencies [e027b3e]
226
+ - Updated dependencies [c2429b0]
227
+ - Updated dependencies [445a0c2]
228
+ - Updated dependencies [f6609e6]
229
+ - Updated dependencies [a70358a]
230
+ - Updated dependencies [97e7e3c]
231
+ - Updated dependencies [8828b9e]
232
+ - Updated dependencies [53068c1]
233
+ - Updated dependencies [ee58392]
234
+ - Updated dependencies [f16e54e]
235
+ - Updated dependencies [06be54e]
236
+ - Updated dependencies [259459d]
237
+ - Updated dependencies [3f7f14e]
238
+ - Updated dependencies [6968885]
239
+ - Updated dependencies [eaed61f]
240
+ - Updated dependencies [debe2f6]
241
+ - Updated dependencies [97b0798]
242
+ - Updated dependencies [5fa04fb]
243
+ - Updated dependencies [43a7a8d]
244
+ - Updated dependencies [73f69dc]
245
+ - Updated dependencies [04c56aa]
246
+ - Updated dependencies [b3efeb7]
247
+ - Updated dependencies [ddd075a]
248
+ - Updated dependencies [88154be]
249
+ - Updated dependencies [e8dc61e]
250
+ - Updated dependencies [2f3e793]
251
+ - Updated dependencies [d8e8d9c]
252
+ - Updated dependencies [94e749b]
253
+ - Updated dependencies [ea1d916]
254
+ - Updated dependencies [ae31a19]
255
+ - Updated dependencies [e0f300b]
256
+ - Updated dependencies [62b6a2f]
257
+ - Updated dependencies [5b4780b]
258
+ - Updated dependencies [a933452]
259
+ - Updated dependencies [8140915]
260
+ - Updated dependencies [7b48cf9]
261
+ - Updated dependencies [b5404f4]
262
+ - Updated dependencies [f764691]
263
+ - Updated dependencies [e120a5a]
264
+ - Updated dependencies [e650d67]
265
+ - Updated dependencies [04476e7]
266
+ - Updated dependencies [79228cd]
267
+ - Updated dependencies [b3363e9]
268
+ - Updated dependencies [2ef1807]
269
+ - Updated dependencies [d03fe25]
270
+ - Updated dependencies [2672f85]
271
+ - Updated dependencies [11066f6]
272
+ - Updated dependencies [916af17]
273
+ - Updated dependencies [84c86fb]
274
+ - Updated dependencies [2a2a9fb]
275
+ - Updated dependencies [a2e157c]
276
+ - Updated dependencies [95c4227]
277
+ - Updated dependencies [2a61116]
278
+ - Updated dependencies [d4df105]
279
+ - Updated dependencies [e2798fa]
280
+ - Updated dependencies [0fd8556]
281
+ - Updated dependencies [74155c7]
282
+ - Updated dependencies [6908830]
283
+ - Updated dependencies [8b06bba]
284
+ - Updated dependencies [4c54037]
285
+ - Updated dependencies [0f7157b]
286
+ - Updated dependencies [d9bef45]
287
+ - Updated dependencies [f549a0d]
288
+ - Updated dependencies [82da264]
289
+ - Updated dependencies [f586f1a]
290
+ - Updated dependencies [9b9b70f]
291
+ - Updated dependencies [f5a9bc2]
292
+ - Updated dependencies [881a3cc]
293
+ - Updated dependencies [ad6317b]
294
+ - Updated dependencies [8a88885]
295
+ - Updated dependencies [5f7669e]
296
+ - Updated dependencies [becbe53]
297
+ - Updated dependencies [b127c8b]
298
+ - Updated dependencies [a80302a]
299
+ - Updated dependencies [474f131]
300
+ - Updated dependencies [050cd82]
301
+ - Updated dependencies [4d552af]
302
+ - Updated dependencies [44d677c]
303
+ - Updated dependencies [c32944d]
304
+ - Updated dependencies [1dd780f]
305
+ - Updated dependencies [c8d6f6e]
306
+ - Updated dependencies [92a67f2]
307
+ - Updated dependencies [9136327]
308
+ - Updated dependencies [bf0ae99]
309
+ - Updated dependencies [cb3b6cd]
310
+ - Updated dependencies [73b7234]
311
+ - Updated dependencies [d2b97c3]
312
+ - Updated dependencies [59b794f]
313
+ - Updated dependencies [fc3a36a]
314
+ - Updated dependencies [69787f0]
315
+ - Updated dependencies [5d022a1]
316
+ - Updated dependencies [042b9ee]
317
+ - Updated dependencies [f549a0d]
318
+ - Updated dependencies [a36db28]
319
+ - Updated dependencies [3f8817a]
320
+ - Updated dependencies [a2443e3]
321
+ - Updated dependencies [e1554b1]
322
+ - Updated dependencies [4856789]
323
+ - Updated dependencies [c3f4916]
324
+ - Updated dependencies [33e0385]
325
+ - Updated dependencies [2205363]
326
+ - Updated dependencies [09fe58d]
327
+ - Updated dependencies [d0a5ceb]
328
+ - Updated dependencies [e18a162]
329
+ - Updated dependencies [d6d1a50]
330
+ - Updated dependencies [d127ff0]
331
+ - Updated dependencies [9b86cf6]
332
+ - Updated dependencies [8825a06]
333
+ - Updated dependencies [5087ac6]
334
+ - Updated dependencies [2d1ddf0]
335
+ - Updated dependencies [354b00f]
336
+ - Updated dependencies [3de535b]
337
+ - Updated dependencies [fe2e15a]
338
+ - Updated dependencies [c6b6bb4]
339
+ - Updated dependencies [59c544d]
340
+ - Updated dependencies [2f59da0]
341
+ - Updated dependencies [8ad609c]
342
+ - Updated dependencies [bbee302]
343
+ - Updated dependencies [08863dd]
344
+ - Updated dependencies [56664f5]
345
+ - Updated dependencies [31cbe90]
346
+ - Updated dependencies [90bbf25]
347
+ - Updated dependencies [eb91eba]
348
+ - Updated dependencies [42da73d]
349
+ - Updated dependencies [643b7c7]
350
+ - Updated dependencies [d0d5205]
351
+ - Updated dependencies [1a15893]
352
+ - Updated dependencies [b70e534]
353
+ - Updated dependencies [2233a85]
354
+ - Updated dependencies [62dd69a]
355
+ - Updated dependencies [e15e679]
356
+ - Updated dependencies [2ab1257]
357
+ - Updated dependencies [4cc4fb7]
358
+ - Updated dependencies [28d1eb7]
359
+ - Updated dependencies [2c26040]
360
+ - Updated dependencies [f758cec]
361
+ - Updated dependencies [78f0be8]
362
+ - Updated dependencies [35f7fb4]
363
+ - Updated dependencies [a5302c7]
364
+ - Updated dependencies [7084313]
365
+ - Updated dependencies [0e043d8]
366
+ - Updated dependencies [dadd1ad]
367
+ - Updated dependencies [2f2e63c]
368
+ - Updated dependencies [486d526]
369
+ - Updated dependencies [89d7b35]
370
+ - Updated dependencies [85ec26d]
371
+ - Updated dependencies [f6476fc]
372
+ - Updated dependencies [4ac12ef]
373
+ - Updated dependencies [b88f5e8]
374
+ - Updated dependencies [42cc219]
375
+ - Updated dependencies [d42a92f]
376
+ - Updated dependencies [51d74ad]
377
+ - Updated dependencies [d7e0b42]
378
+ - Updated dependencies [3510e4a]
379
+ - Updated dependencies [aa4b90d]
380
+ - Updated dependencies [54299ca]
381
+ - Updated dependencies [dc61def]
382
+ - Updated dependencies [251e888]
383
+ - Updated dependencies [183b4c4]
384
+ - Updated dependencies [2fdb36e]
385
+ - Updated dependencies [e787608]
386
+ - Updated dependencies [20526f5]
387
+ - Updated dependencies [c5eef1d]
388
+ - Updated dependencies [e0f300b]
389
+ - Updated dependencies [761a0ba]
390
+ - Updated dependencies [61282f9]
391
+ - Updated dependencies [be87153]
392
+ - Updated dependencies [60f0dd8]
393
+ - Updated dependencies [a87c5cd]
394
+ - Updated dependencies [a47f338]
395
+ - Updated dependencies [2598216]
396
+ - Updated dependencies [2c7e62d]
397
+ - Updated dependencies [eb7613c]
398
+ - Updated dependencies [ecc9110]
399
+ - Updated dependencies [f7bd4e2]
400
+ - Updated dependencies [361bd5b]
401
+ - Updated dependencies [1818998]
402
+ - Updated dependencies [09ee21c]
403
+ - Updated dependencies [f549a0d]
404
+ - Updated dependencies [3fc2e48]
405
+ - Updated dependencies [e8f435c]
406
+ - Updated dependencies [41610f6]
407
+ - @objectstack/spec@17.0.0-rc.6
408
+ - @objectstack/platform-objects@17.0.0-rc.6
409
+ - @objectstack/core@17.0.0-rc.6
410
+
3
411
  ## 17.0.0-rc.5
4
412
 
5
413
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { SysReportSchedule, SysSavedReport } from '@objectstack/platform-objects/audit';
2
- import { IReportService, SharingExecutionContext, SaveReportInput, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
2
+ import { IReportService, 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
+ import { ExecutionContext } from '@objectstack/spec/kernel';
4
5
  import { Plugin, PluginContext } from '@objectstack/core';
5
6
 
6
7
  /**
@@ -40,6 +41,26 @@ interface ReportClock {
40
41
  now(): Date;
41
42
  }
42
43
  declare function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string;
44
+ /**
45
+ * Why this module strips the operation-private keys before forwarding an
46
+ * envelope — the LOCAL half of the argument.
47
+ *
48
+ * A report read asks about `report.object_name`, which is not necessarily the
49
+ * object the caller's envelope last carried a `__`-prefixed depth for: a REST
50
+ * request that touched another object before reaching `/reports/:id/run` hands
51
+ * over an envelope plugin-security's middleware has already written into, and
52
+ * that middleware only OVERWRITES `__readScope` when it resolves permission sets
53
+ * for the new object (`if (permissionSets.length > 0)`). A stale depth therefore
54
+ * survives into a question it was never resolved for.
55
+ *
56
+ * [#7204] The general rule — which keys those are, why they are dropped by
57
+ * PREFIX rather than by a name list, and why the copy is load-bearing in both
58
+ * directions — is `withoutOperationPrivateKeys` in `@objectstack/core`. It was
59
+ * hand-copied into this file, `plugin-audit`'s comment kit and
60
+ * `service-storage`'s attachment kit before #7284 gave it one owner; ⛔ import
61
+ * it, never re-derive it locally (`operation-private-keys.pin.test.ts` catches
62
+ * the fourth copy).
63
+ */
43
64
  /**
44
65
  * Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing
45
66
  * `ExecutionContext` so a **scheduled** report executes under the owner's
@@ -50,7 +71,7 @@ declare function renderReport(rows: any[], format: ReportFormat, fields?: string
50
71
  * resolver is the reports-surface consumer of ADR-0073's user-less identity
51
72
  * resolution.
52
73
  */
53
- type OwnerContextResolver = (ownerId: string) => Promise<SharingExecutionContext | null>;
74
+ type OwnerContextResolver = (ownerId: string) => Promise<ExecutionContext | null>;
54
75
  interface ReportServiceOptions {
55
76
  engine: ReportEngine;
56
77
  email?: ReportEmail;
@@ -125,21 +146,23 @@ declare class ReportService implements IReportService {
125
146
  private canAccessReport;
126
147
  /** Raw metadata read of a saved report by id (no authz — callers gate). */
127
148
  private loadReportRow;
128
- saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport>;
149
+ /** Raw metadata read of a report schedule by id (no authz callers gate). */
150
+ private loadScheduleRow;
151
+ saveReport(input: SaveReportInput, context: ExecutionContext): Promise<SavedReport>;
129
152
  listReports(filter: {
130
153
  object?: string;
131
154
  ownerId?: string;
132
- } | undefined, context: SharingExecutionContext): Promise<SavedReport[]>;
133
- getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null>;
134
- deleteReport(reportId: string, context: SharingExecutionContext): Promise<void>;
135
- run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult>;
136
- runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult>;
155
+ } | undefined, context: ExecutionContext): Promise<SavedReport[]>;
156
+ getReport(reportId: string, context: ExecutionContext): Promise<SavedReport | null>;
157
+ deleteReport(reportId: string, context: ExecutionContext): Promise<void>;
158
+ run(reportId: string, context: ExecutionContext): Promise<ReportRunResult>;
159
+ runAdHoc(input: SaveReportInput, context: ExecutionContext): Promise<ReportRunResult>;
137
160
  private executeReport;
138
- scheduleReport(input: ScheduleReportInput, context: SharingExecutionContext): Promise<ReportSchedule>;
139
- unscheduleReport(scheduleId: string, _context: SharingExecutionContext): Promise<void>;
161
+ scheduleReport(input: ScheduleReportInput, context: ExecutionContext): Promise<ReportSchedule>;
162
+ unscheduleReport(scheduleId: string, context: ExecutionContext): Promise<void>;
140
163
  listSchedules(filter: {
141
164
  reportId?: string;
142
- } | undefined, _context: SharingExecutionContext): Promise<ReportSchedule[]>;
165
+ } | undefined, context: ExecutionContext): Promise<ReportSchedule[]>;
143
166
  dispatchDue(now?: Date): Promise<{
144
167
  fired: number;
145
168
  failed: number;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { SysReportSchedule, SysSavedReport } from '@objectstack/platform-objects/audit';
2
- import { IReportService, SharingExecutionContext, SaveReportInput, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
2
+ import { IReportService, 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
+ import { ExecutionContext } from '@objectstack/spec/kernel';
4
5
  import { Plugin, PluginContext } from '@objectstack/core';
5
6
 
6
7
  /**
@@ -40,6 +41,26 @@ interface ReportClock {
40
41
  now(): Date;
41
42
  }
42
43
  declare function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string;
44
+ /**
45
+ * Why this module strips the operation-private keys before forwarding an
46
+ * envelope — the LOCAL half of the argument.
47
+ *
48
+ * A report read asks about `report.object_name`, which is not necessarily the
49
+ * object the caller's envelope last carried a `__`-prefixed depth for: a REST
50
+ * request that touched another object before reaching `/reports/:id/run` hands
51
+ * over an envelope plugin-security's middleware has already written into, and
52
+ * that middleware only OVERWRITES `__readScope` when it resolves permission sets
53
+ * for the new object (`if (permissionSets.length > 0)`). A stale depth therefore
54
+ * survives into a question it was never resolved for.
55
+ *
56
+ * [#7204] The general rule — which keys those are, why they are dropped by
57
+ * PREFIX rather than by a name list, and why the copy is load-bearing in both
58
+ * directions — is `withoutOperationPrivateKeys` in `@objectstack/core`. It was
59
+ * hand-copied into this file, `plugin-audit`'s comment kit and
60
+ * `service-storage`'s attachment kit before #7284 gave it one owner; ⛔ import
61
+ * it, never re-derive it locally (`operation-private-keys.pin.test.ts` catches
62
+ * the fourth copy).
63
+ */
43
64
  /**
44
65
  * Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing
45
66
  * `ExecutionContext` so a **scheduled** report executes under the owner's
@@ -50,7 +71,7 @@ declare function renderReport(rows: any[], format: ReportFormat, fields?: string
50
71
  * resolver is the reports-surface consumer of ADR-0073's user-less identity
51
72
  * resolution.
52
73
  */
53
- type OwnerContextResolver = (ownerId: string) => Promise<SharingExecutionContext | null>;
74
+ type OwnerContextResolver = (ownerId: string) => Promise<ExecutionContext | null>;
54
75
  interface ReportServiceOptions {
55
76
  engine: ReportEngine;
56
77
  email?: ReportEmail;
@@ -125,21 +146,23 @@ declare class ReportService implements IReportService {
125
146
  private canAccessReport;
126
147
  /** Raw metadata read of a saved report by id (no authz — callers gate). */
127
148
  private loadReportRow;
128
- saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport>;
149
+ /** Raw metadata read of a report schedule by id (no authz callers gate). */
150
+ private loadScheduleRow;
151
+ saveReport(input: SaveReportInput, context: ExecutionContext): Promise<SavedReport>;
129
152
  listReports(filter: {
130
153
  object?: string;
131
154
  ownerId?: string;
132
- } | undefined, context: SharingExecutionContext): Promise<SavedReport[]>;
133
- getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null>;
134
- deleteReport(reportId: string, context: SharingExecutionContext): Promise<void>;
135
- run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult>;
136
- runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult>;
155
+ } | undefined, context: ExecutionContext): Promise<SavedReport[]>;
156
+ getReport(reportId: string, context: ExecutionContext): Promise<SavedReport | null>;
157
+ deleteReport(reportId: string, context: ExecutionContext): Promise<void>;
158
+ run(reportId: string, context: ExecutionContext): Promise<ReportRunResult>;
159
+ runAdHoc(input: SaveReportInput, context: ExecutionContext): Promise<ReportRunResult>;
137
160
  private executeReport;
138
- scheduleReport(input: ScheduleReportInput, context: SharingExecutionContext): Promise<ReportSchedule>;
139
- unscheduleReport(scheduleId: string, _context: SharingExecutionContext): Promise<void>;
161
+ scheduleReport(input: ScheduleReportInput, context: ExecutionContext): Promise<ReportSchedule>;
162
+ unscheduleReport(scheduleId: string, context: ExecutionContext): Promise<void>;
140
163
  listSchedules(filter: {
141
164
  reportId?: string;
142
- } | undefined, _context: SharingExecutionContext): Promise<ReportSchedule[]>;
165
+ } | undefined, context: ExecutionContext): Promise<ReportSchedule[]>;
143
166
  dispatchDue(now?: Date): Promise<{
144
167
  fired: number;
145
168
  failed: number;
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ module.exports = __toCommonJS(index_exports);
30
30
  var import_audit2 = require("@objectstack/platform-objects/audit");
31
31
 
32
32
  // src/report-service.ts
33
+ var import_core = require("@objectstack/core");
33
34
  var import_croner = require("croner");
34
35
  var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
35
36
  var DEFAULT_FORMAT = "csv";
@@ -219,6 +220,15 @@ var ReportService = class {
219
220
  });
220
221
  return Array.isArray(rows) && rows[0] ? rows[0] : null;
221
222
  }
223
+ /** Raw metadata read of a report schedule by id (no authz — callers gate). */
224
+ async loadScheduleRow(scheduleId) {
225
+ const rows = await this.engine.find("sys_report_schedule", {
226
+ where: { id: scheduleId },
227
+ limit: 1,
228
+ context: SYSTEM_CTX
229
+ });
230
+ return Array.isArray(rows) && rows[0] ? rows[0] : null;
231
+ }
222
232
  // ── Report CRUD ────────────────────────────────────────────────
223
233
  async saveReport(input, context) {
224
234
  if (!input.name) throw new Error("VALIDATION_FAILED: name is required");
@@ -326,9 +336,35 @@ var ReportService = class {
326
336
  // Reports execute with the caller's identity so sharing rules
327
337
  // (if installed) apply. Falls back to system bypass only when
328
338
  // the report definition was created by a system writer.
339
+ //
340
+ // [#7204] The WHOLE envelope, not a rebuilt subset of it — the #6206
341
+ // ruling (#6523): a read that adjudicates on the caller's identity
342
+ // adjudicates on the whole `resolveAuthzContext` envelope. The
343
+ // five-field projection this replaced (`userId` / `tenantId` /
344
+ // `positions` / `permissions` / `isSystem`) was doing two jobs, and
345
+ // only one of them was correct:
346
+ //
347
+ // - dropping the middleware-private keys — CORRECT, and preserved
348
+ // above by {@link withoutOperationPrivateKeys};
349
+ // - dropping the PRINCIPAL fields — the defect. `accessible_org_ids`
350
+ // (ADR-0105 D2) is the one that changes rows: `buildDriverOptions`
351
+ // reads it BY NAME to widen the driver's native tenant scope to the
352
+ // caller's membership union under the `group` posture, and an absent
353
+ // set makes drivers "fall back to equality: fail toward isolation".
354
+ // So the same query returned the union in an interactive list view
355
+ // and collapsed to active-org equality inside a saved or scheduled
356
+ // report — silently short rows, no error. `timezone` is read two
357
+ // lines up in the same engine method (`hasTz`) and again by
358
+ // `applyFormulaPlan` for read-time formula fields, and `posture`,
359
+ // `org_user_ids`, `systemPermissions` and `onBehalfOf` went the same
360
+ // way; they are forwarded now for the same reason — the envelope is
361
+ // the contract's unit.
362
+ //
363
+ // The three defaults below are byte-for-byte what the projection
364
+ // produced for an envelope that omits them, and are kept so this change
365
+ // adds fields without changing any that were already there.
329
366
  context: {
330
- userId: context.userId,
331
- tenantId: context.tenantId,
367
+ ...(0, import_core.withoutOperationPrivateKeys)(context),
332
368
  positions: context.positions ?? [],
333
369
  permissions: context.permissions ?? [],
334
370
  isSystem: context.isSystem ?? false
@@ -401,11 +437,21 @@ var ReportService = class {
401
437
  await this.engine.insert("sys_report_schedule", row, { context: SYSTEM_CTX });
402
438
  return rowFromSchedule(row);
403
439
  }
404
- async unscheduleReport(scheduleId, _context) {
440
+ async unscheduleReport(scheduleId, context) {
405
441
  if (!scheduleId) throw new Error("VALIDATION_FAILED: scheduleId is required");
442
+ const schedule = await this.loadScheduleRow(scheduleId);
443
+ if (!schedule) return;
444
+ const report = await this.loadReportRow(schedule.report_id);
445
+ if (!this.canAccessReport(report, context)) {
446
+ throw new Error(`REPORT_NOT_FOUND: ${scheduleId}`);
447
+ }
406
448
  await this.engine.delete("sys_report_schedule", { where: { id: scheduleId }, context: SYSTEM_CTX });
407
449
  }
408
- async listSchedules(filter, _context) {
450
+ async listSchedules(filter, context) {
451
+ if (!context?.isSystem) {
452
+ if (!filter?.reportId) return [];
453
+ if (!await this.getReport(filter.reportId, context)) return [];
454
+ }
409
455
  const f = {};
410
456
  if (filter?.reportId) f.report_id = filter.reportId;
411
457
  const rows = await this.engine.find("sys_report_schedule", {