@cosmicdrift/kumiko-bundled-features 0.196.0 → 0.197.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.196.0",
3
+ "version": "0.197.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -125,12 +125,12 @@
125
125
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
126
126
  },
127
127
  "dependencies": {
128
- "@cosmicdrift/kumiko-dispatcher-live": "0.196.0",
129
- "@cosmicdrift/kumiko-framework": "0.196.0",
130
- "@cosmicdrift/kumiko-headless": "0.196.0",
131
- "@cosmicdrift/kumiko-renderer": "0.196.0",
132
- "@cosmicdrift/kumiko-renderer-web": "0.196.0",
133
- "@cosmicdrift/kumiko-types": "0.196.0",
128
+ "@cosmicdrift/kumiko-dispatcher-live": "0.197.0",
129
+ "@cosmicdrift/kumiko-framework": "0.197.0",
130
+ "@cosmicdrift/kumiko-headless": "0.197.0",
131
+ "@cosmicdrift/kumiko-renderer": "0.197.0",
132
+ "@cosmicdrift/kumiko-renderer-web": "0.197.0",
133
+ "@cosmicdrift/kumiko-types": "0.197.0",
134
134
  "@mollie/api-client": "^4.5.0",
135
135
  "@node-rs/argon2": "^2.0.2",
136
136
  "@types/mailparser": "^3.4.6",
@@ -0,0 +1,141 @@
1
+ // Proves #1989/2: DerivativePublicPredicateArgs carries `fieldName` and
2
+ // `variant` (not just entityId/tenantId) through to `isPublic`, so an app
3
+ // can opt individual variants out of public serving instead of every
4
+ // declared variant on an entityType being implicitly public once the
5
+ // entityType-level check passes.
6
+
7
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
8
+ import {
9
+ createEntity,
10
+ createImageField,
11
+ defineFeature,
12
+ EXT_DERIVATIVE_PUBLIC_PREDICATE,
13
+ EXT_DERIVATIVE_RENDERER,
14
+ } from "@cosmicdrift/kumiko-framework/engine";
15
+ import {
16
+ createFilesFeature,
17
+ createInMemoryFileProvider,
18
+ } from "@cosmicdrift/kumiko-framework/files";
19
+ import {
20
+ createTestUser,
21
+ setupTestStack,
22
+ type TestStack,
23
+ testTenantId,
24
+ } from "@cosmicdrift/kumiko-framework/stack";
25
+ import {
26
+ buildMultipartBody,
27
+ patchFileInstanceofForBunTest,
28
+ } from "@cosmicdrift/kumiko-framework/testing";
29
+ import type { DerivativeRendererPlugin } from "@cosmicdrift/kumiko-types/derivatives-types";
30
+ import { createConfigFeature } from "../../config";
31
+ import { fileFoundationFeature } from "../../file-foundation";
32
+ import { createFileDerivativesFeature } from "../feature";
33
+ import type { DerivativePublicPredicateArgs } from "../handlers/public-variant.query";
34
+
35
+ const VARIANT_BYTES = new Uint8Array([9, 9, 9]);
36
+ const fakeRender: DerivativeRendererPlugin["render"] = async () => VARIANT_BYTES;
37
+
38
+ const ENTITY_ID = "gadget-1";
39
+ const HOST = "gadgets.example.com";
40
+ const TENANT = testTenantId(7);
41
+
42
+ // Deliberately gates on `variant`, not just entityId — "premium" stays
43
+ // denied even for an entityId the entityType-level check would otherwise
44
+ // allow, proving a predicate can opt a specific variant out.
45
+ let receivedArgs: DerivativePublicPredicateArgs[] = [];
46
+ const gadgetEntity = createEntity({
47
+ table: "public_variant_predicate_gadgets",
48
+ fields: {
49
+ img: createImageField({
50
+ variants: {
51
+ thumb: { maxEdge: 160, format: "webp" },
52
+ premium: { maxEdge: 4096, format: "webp" },
53
+ },
54
+ }),
55
+ },
56
+ });
57
+
58
+ const gadgetPredicateFeature = defineFeature("publicvariantpredicateargstest", (r) => {
59
+ r.entity("gadget", gadgetEntity);
60
+ r.useExtension(EXT_DERIVATIVE_PUBLIC_PREDICATE, "gadget", {
61
+ isPublic: (args: DerivativePublicPredicateArgs) => {
62
+ receivedArgs.push(args);
63
+ return args.variant !== "premium";
64
+ },
65
+ });
66
+ r.useExtension(EXT_DERIVATIVE_RENDERER, "image/*", { render: fakeRender });
67
+ });
68
+
69
+ const user = createTestUser({ id: 1, tenantId: TENANT, roles: ["Admin"] });
70
+
71
+ let stack: TestStack;
72
+
73
+ beforeAll(async () => {
74
+ patchFileInstanceofForBunTest();
75
+ stack = await setupTestStack({
76
+ features: [
77
+ createConfigFeature(),
78
+ fileFoundationFeature,
79
+ createFilesFeature(),
80
+ createFileDerivativesFeature({
81
+ resolveApexTenant: (host) => (host === HOST ? TENANT : null),
82
+ }),
83
+ gadgetPredicateFeature,
84
+ ],
85
+ files: { storageProvider: createInMemoryFileProvider() },
86
+ });
87
+ });
88
+
89
+ afterAll(async () => {
90
+ await stack.cleanup();
91
+ });
92
+
93
+ beforeEach(async () => {
94
+ receivedArgs = [];
95
+ await stack.redis.flushNamespace();
96
+ });
97
+
98
+ async function uploadGadgetImage(): Promise<string> {
99
+ const token = await stack.jwt.sign(user);
100
+ const fd = new FormData();
101
+ fd.append("file", new File([Buffer.from([1, 2, 3])], "img.jpg", { type: "image/jpeg" }));
102
+ fd.append("entityType", "gadget");
103
+ fd.append("entityId", ENTITY_ID);
104
+ fd.append("fieldName", "img");
105
+ const { body, contentType } = await buildMultipartBody(fd);
106
+ const res = await stack.app.request("/api/files", {
107
+ method: "POST",
108
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
109
+ body,
110
+ });
111
+ expect(res.status).toBe(201);
112
+ const json = (await res.json()) as { id: string };
113
+ return json.id;
114
+ }
115
+
116
+ describe("file-derivatives :: isPublic receives fieldName + variant", () => {
117
+ test("isPublic is called with the FileRef's fieldName and the requested variant name", async () => {
118
+ const fileId = await uploadGadgetImage();
119
+
120
+ const res = await stack.app.request(`http://${HOST}/media/${fileId}/thumb`);
121
+
122
+ expect(res.status).toBe(200);
123
+ expect(receivedArgs).toHaveLength(1);
124
+ expect(receivedArgs[0]).toMatchObject({
125
+ entityId: ENTITY_ID,
126
+ fieldName: "img",
127
+ variant: "thumb",
128
+ });
129
+ });
130
+
131
+ test("a predicate that opts a specific variant out denies exactly that variant, not the whole entity", async () => {
132
+ const fileId = await uploadGadgetImage();
133
+
134
+ const thumbRes = await stack.app.request(`http://${HOST}/media/${fileId}/thumb`);
135
+ const premiumRes = await stack.app.request(`http://${HOST}/media/${fileId}/premium`);
136
+
137
+ expect(thumbRes.status).toBe(200);
138
+ expect(premiumRes.status).toBe(404);
139
+ expect(receivedArgs.map((a) => a.variant)).toEqual(["thumb", "premium"]);
140
+ });
141
+ });
@@ -0,0 +1,75 @@
1
+ // Proves #1989/3: publicVariantQuery's `fileRefId` is UUID-validated by the
2
+ // handler's own zod schema, not only by the httpRoute wrapper's
3
+ // FILE_REF_ID_RE pre-check. The generic `/api/query` dispatch path (reachable
4
+ // once resolveApexTenant is configured, see public-variant-query-gating.
5
+ // integration.test.ts for the "not configured" case) skips that httpRoute
6
+ // pre-check entirely and calls `handler.schema.safeParse` directly — before
7
+ // this fix, a non-UUID `fileRefId` sailed through the schema and hit
8
+ // fetchOne(), throwing a Postgres 22P02 (malformed uuid literal) that
9
+ // poisons the pooled Bun.SQL connection, an unauth DoS primitive.
10
+
11
+ import { describe, expect, test } from "bun:test";
12
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
13
+ import {
14
+ createFilesFeature,
15
+ createInMemoryFileProvider,
16
+ } from "@cosmicdrift/kumiko-framework/files";
17
+ import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-framework/stack";
18
+ import { createConfigFeature } from "../../config";
19
+ import { fileFoundationFeature } from "../../file-foundation";
20
+ import { createFileDerivativesFeature } from "../feature";
21
+ import { PUBLIC_VARIANT_QN } from "../handlers/public-variant.query";
22
+
23
+ describe("file-derivatives :: publicVariant query fileRefId validation", () => {
24
+ test("non-UUID fileRefId via /api/query → 400 validation_error, never reaches the DB", async () => {
25
+ const stack: TestStack = await setupTestStack({
26
+ features: [
27
+ createConfigFeature(),
28
+ fileFoundationFeature,
29
+ createFilesFeature(),
30
+ createFileDerivativesFeature({ resolveApexTenant: () => SYSTEM_TENANT_ID }),
31
+ ],
32
+ anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
33
+ files: { storageProvider: createInMemoryFileProvider() },
34
+ });
35
+
36
+ try {
37
+ const res = await stack.http.raw("POST", "/api/query", {
38
+ type: PUBLIC_VARIANT_QN,
39
+ payload: { fileRefId: "not-a-uuid; DROP TABLE file_refs;--", variant: "thumb" },
40
+ });
41
+
42
+ expect(res.status).toBe(400);
43
+ const body = (await res.json()) as { error?: { code?: string } };
44
+ expect(body.error?.code).toBe("validation_error");
45
+ } finally {
46
+ await stack.cleanup();
47
+ }
48
+ });
49
+
50
+ test("well-formed but unknown UUID fileRefId via /api/query → 200 with null (schema passes, DB lookup just finds nothing)", async () => {
51
+ const stack: TestStack = await setupTestStack({
52
+ features: [
53
+ createConfigFeature(),
54
+ fileFoundationFeature,
55
+ createFilesFeature(),
56
+ createFileDerivativesFeature({ resolveApexTenant: () => SYSTEM_TENANT_ID }),
57
+ ],
58
+ anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
59
+ files: { storageProvider: createInMemoryFileProvider() },
60
+ });
61
+
62
+ try {
63
+ const res = await stack.http.raw("POST", "/api/query", {
64
+ type: PUBLIC_VARIANT_QN,
65
+ payload: { fileRefId: "00000000-0000-4000-8000-000000000000", variant: "thumb" },
66
+ });
67
+
68
+ expect(res.status).toBe(200);
69
+ const body = (await res.json()) as { data: unknown };
70
+ expect(body.data).toBeNull();
71
+ } finally {
72
+ await stack.cleanup();
73
+ }
74
+ });
75
+ });
@@ -182,6 +182,16 @@ describe("GET /media/:fileRefId/:variant (anonymous, default-deny)", () => {
182
182
  expect(new Uint8Array(await res.arrayBuffer())).toEqual(VARIANT_BYTES);
183
183
  });
184
184
 
185
+ // setupTestStack doesn't wrap with the app-wide security-headers default, so a pass proves the route sets its own.
186
+ test("sets X-Content-Type-Options: nosniff on the response itself", async () => {
187
+ const fileId = await uploadFile(userA, { entityType: "widget", entityId: PUBLIC_WIDGET_ID });
188
+
189
+ const res = await stack.app.request(`http://${HOST_A}/media/${fileId}/thumb`);
190
+
191
+ expect(res.status).toBe(200);
192
+ expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
193
+ });
194
+
185
195
  test("a second call on the same (fileRefId, variant) hits the cache — renderer runs once", async () => {
186
196
  const fileId = await uploadFile(userA, { entityType: "widget", entityId: PUBLIC_WIDGET_ID });
187
197
 
@@ -78,7 +78,7 @@ export function createFileDerivativesFeature(opts: FileDerivativesOptions = {}):
78
78
 
79
79
  return defineFeature(FEATURE_NAME, (r) => {
80
80
  r.describe(
81
- "Declares the `derivativeRenderer` extension point. `ctx.derivatives.variant(fileRefId, spec, name)` derives a variant of a tracked FileRef the first time it's requested and reuses the stored result afterwards (derive-on-first-use, keyed by a hash of the spec). Mount at least one `derivatives-*` renderer feature alongside this one — without a registered renderer for the FileRef's MIME type, every `variant(...)` call throws. Also declares the `derivativePublicPredicate` extension point (`r.useExtension(EXT_DERIVATIVE_PUBLIC_PREDICATE, '<entityType>', { isPublic })`) and, when `createFileDerivativesFeature({resolveApexTenant})` is passed a host-resolver, mounts an anonymous `GET {basePath}/:fileRefId/:variant` route that serves any variant name the FileRef's field declared in its `variants` for a FileRef whose entityType has a registered predicate returning true — default-deny (404) otherwise, same as an unknown FileRef or an undeclared variant name.",
81
+ "Declares the `derivativeRenderer` extension point. `ctx.derivatives.variant(fileRefId, spec, name)` derives a variant of a tracked FileRef the first time it's requested and reuses the stored result afterwards (derive-on-first-use, keyed by a hash of the spec). Mount at least one `derivatives-*` renderer feature alongside this one — without a registered renderer for the FileRef's MIME type, every `variant(...)` call throws. Also declares the `derivativePublicPredicate` extension point (`r.useExtension(EXT_DERIVATIVE_PUBLIC_PREDICATE, '<entityType>', { isPublic })`) and, when `createFileDerivativesFeature({resolveApexTenant})` is passed a host-resolver, mounts an anonymous `GET {basePath}/:fileRefId/:variant` route that serves any variant name the FileRef's field declared in its `variants` for a FileRef whose entityType has a registered predicate returning true — default-deny (404) otherwise, same as an unknown FileRef or an undeclared variant name. The route's only rate-limit (`per: \"ip\"`) trusts the first `x-forwarded-for` hop — deployers must ensure their ingress overwrites rather than appends to that header, or the throttle is bypassable by rotating it.",
82
82
  );
83
83
  r.uiHints({
84
84
  displayLabel: "File Derivatives",
@@ -171,7 +171,12 @@ export function createFileDerivativesFeature(opts: FileDerivativesOptions = {}):
171
171
  body: bytes,
172
172
  etag,
173
173
  cache: { kind: "revalidate", maxAgeSeconds: 60 },
174
- headers: { "content-type": result.mimeType, vary: "Host" },
174
+ // Explicit here, not left to the app-wide security-headers default, so the route stays safe standalone.
175
+ headers: {
176
+ "content-type": result.mimeType,
177
+ vary: "Host",
178
+ "x-content-type-options": "nosniff",
179
+ },
175
180
  });
176
181
  },
177
182
  });
@@ -10,7 +10,12 @@
10
10
  // FileRef that doesn't exist (no existence-leak via distinct status codes).
11
11
  //
12
12
  // tenantId comes from `query.user.tenantId` ONLY (host-resolved by the
13
- // httpRoute wrapper's `resolveApexTenant`), never from the payload.
13
+ // httpRoute wrapper's `resolveApexTenant`), never from the payload — true
14
+ // for the httpRoute-wrapper path. Over the generic `/api/query` dispatch
15
+ // (only reachable when this feature is mounted WITH resolveApexTenant, see
16
+ // feature.ts), `ctx.user.tenantId` instead comes from that dispatcher's own
17
+ // `anonymousAccess` resolution — tenant provenance there depends on the
18
+ // consumer's `resolverTrust`/anonymousAccess setup, not the host.
14
19
 
15
20
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
16
21
  import { resolveFieldVariant } from "@cosmicdrift/kumiko-framework/derivatives";
@@ -29,9 +34,15 @@ type FileRefRow = {
29
34
  readonly fieldName: string | null;
30
35
  };
31
36
 
37
+ // A declared variant with no explicit gate in `isPublic` is implicitly
38
+ // public as soon as the entityType-level check returns `true` — `fieldName`
39
+ // and `variant` let a predicate opt individual fields/variants out (e.g. a
40
+ // cheap `thumb` served publicly while an expensive `full` stays gated).
32
41
  export type DerivativePublicPredicateArgs = {
33
42
  readonly entityId: string;
34
43
  readonly tenantId: TenantId;
44
+ readonly fieldName: string;
45
+ readonly variant: string;
35
46
  };
36
47
 
37
48
  export type DerivativePublicPredicatePlugin = {
@@ -58,7 +69,7 @@ export const PUBLIC_VARIANT_QN = "file-derivatives:query:public-variant";
58
69
  export const publicVariantQuery = defineQueryHandler({
59
70
  name: "public-variant",
60
71
  schema: z.object({
61
- fileRefId: z.string(),
72
+ fileRefId: z.string().uuid(),
62
73
  variant: z.string().min(1).max(64),
63
74
  }),
64
75
  access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
@@ -90,7 +101,10 @@ export const publicVariantQuery = defineQueryHandler({
90
101
  );
91
102
  }
92
103
 
93
- const isPublic = await usage.options.isPublic({ entityId, tenantId: ctx.user.tenantId }, ctx);
104
+ const isPublic = await usage.options.isPublic(
105
+ { entityId, tenantId: ctx.user.tenantId, fieldName, variant: query.payload.variant },
106
+ ctx,
107
+ );
94
108
  if (!isPublic) return null;
95
109
 
96
110
  // ctx.files/ctx.derivatives are optional on HandlerContext (only wired
@@ -1,3 +1,7 @@
1
1
  export type { FileDerivativesOptions, PublicVariantResolveApexTenant } from "./feature";
2
2
  export { createFileDerivativesFeature, fileDerivativesFeature } from "./feature";
3
+ export type {
4
+ DerivativePublicPredicateArgs,
5
+ DerivativePublicPredicatePlugin,
6
+ } from "./handlers/public-variant.query";
3
7
  export { card, full, hero, thumb } from "./presets";
@@ -35,6 +35,7 @@ export {
35
35
  reverseTransactionHandler,
36
36
  } from "./handlers/reverse-transaction.write";
37
37
  export {
38
+ findReversedIds,
38
39
  type LedgerTxRow,
39
40
  mergeScheduleActuals,
40
41
  type ProjectedPeriod,