@cosmicdrift/kumiko-bundled-features 0.245.0 → 0.246.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,6 @@
1
1
  import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { parentRowIsVisible } from "../../shared";
3
4
  import { tagAssignmentAggregateId } from "../aggregate-id";
4
5
  import { DEFAULT_TAG_ACCESS } from "../constants";
5
6
  import { tagAssignmentExecutor, tagExecutor } from "../executor";
@@ -21,6 +22,11 @@ import { type AssignTagPayload, assignTagPayloadSchema } from "../schemas";
21
22
  // Referential integrity: there is no FK (event-sourced, no JOIN), so before a
22
23
  // first-time create we verify the tag exists in the catalog — a malformed call
23
24
  // with an unknown tagId would otherwise project a dangling assignment.
25
+ //
26
+ // Host reference: entityType/entityId are never trusted client input either —
27
+ // the aggregate-id is tenant-scoped, so this is not a cross-tenant hole, but
28
+ // without the check any dispatch-eligible tenant user could tag an object they
29
+ // are not allowed to read. See shared/parent-visibility.ts.
24
30
  export function createAssignTagHandler(access: AccessRule = DEFAULT_TAG_ACCESS): WriteHandlerDef {
25
31
  return {
26
32
  name: "assign-tag",
@@ -30,6 +36,24 @@ export function createAssignTagHandler(access: AccessRule = DEFAULT_TAG_ACCESS):
30
36
  "Attaches an existing catalog tag to one host entity addressed by its type and id, reporting success when the tag was already attached; use it to tag a record, not to create the tag.",
31
37
  handler: async (event, ctx) => {
32
38
  const payload = event.payload as AssignTagPayload; // @cast-boundary engine-payload
39
+ // entityType/entityId are client input: the caller must be able to see
40
+ // the host row through that entity's own read path (tenant scope plus its
41
+ // `access.read` ownership) before its assignment row may be written.
42
+ // Checked FIRST, ahead of every lookup below, so a denied caller can't tell
43
+ // an invisible parent apart from a missing assignment or an unknown tag —
44
+ // every path answers with the same NotFoundError.
45
+ if (
46
+ !(await parentRowIsVisible(
47
+ ctx.registry,
48
+ payload.entityType,
49
+ payload.entityId,
50
+ event.user,
51
+ ctx.db,
52
+ ))
53
+ ) {
54
+ return writeFailure(new NotFoundError(payload.entityType, payload.entityId));
55
+ }
56
+
33
57
  const id = tagAssignmentAggregateId(
34
58
  event.user.tenantId,
35
59
  payload.tagId,
@@ -1,4 +1,6 @@
1
1
  import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { parentRowIsVisible } from "../../shared";
2
4
  import { tagAssignmentAggregateId } from "../aggregate-id";
3
5
  import { DEFAULT_TAG_ACCESS } from "../constants";
4
6
  import { tagAssignmentExecutor } from "../executor";
@@ -7,6 +9,10 @@ import { type RemoveTagPayload, removeTagPayloadSchema } from "../schemas";
7
9
  // remove-tag — unlinks a tag from a host entity. Idempotent: removing an
8
10
  // assignment that doesn't exist is already the requested end state (not
9
11
  // assigned), so we pre-check and return success without a delete.
12
+ //
13
+ // Host reference: entityType/entityId are client input, so the caller must be
14
+ // able to see the host row before detaching anything from it — see
15
+ // shared/parent-visibility.ts.
10
16
  export function createRemoveTagHandler(access: AccessRule = DEFAULT_TAG_ACCESS): WriteHandlerDef {
11
17
  return {
12
18
  name: "remove-tag",
@@ -16,6 +22,24 @@ export function createRemoveTagHandler(access: AccessRule = DEFAULT_TAG_ACCESS):
16
22
  "Detaches one catalog tag from one host entity, reporting success when it was not attached; use it to untag a single record while the tag itself stays in the catalog.",
17
23
  handler: async (event, ctx) => {
18
24
  const payload = event.payload as RemoveTagPayload; // @cast-boundary engine-payload
25
+ // entityType/entityId are client input: the caller must be able to see
26
+ // the host row through that entity's own read path (tenant scope plus its
27
+ // `access.read` ownership) before its assignment row may be written.
28
+ // Checked FIRST, ahead of every lookup below, so a denied caller can't tell
29
+ // an invisible parent apart from a missing assignment or an unknown tag —
30
+ // every path answers with the same NotFoundError.
31
+ if (
32
+ !(await parentRowIsVisible(
33
+ ctx.registry,
34
+ payload.entityType,
35
+ payload.entityId,
36
+ event.user,
37
+ ctx.db,
38
+ ))
39
+ ) {
40
+ return writeFailure(new NotFoundError(payload.entityType, payload.entityId));
41
+ }
42
+
19
43
  const id = tagAssignmentAggregateId(
20
44
  event.user.tenantId,
21
45
  payload.tagId,