@cosmicdrift/kumiko-bundled-features 0.281.0 → 0.283.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.281.0",
3
+ "version": "0.283.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>",
@@ -129,12 +129,12 @@
129
129
  "./workflow-runner": "./src/workflow-runner/index.ts"
130
130
  },
131
131
  "dependencies": {
132
- "@cosmicdrift/kumiko-dispatcher-live": "0.281.0",
133
- "@cosmicdrift/kumiko-framework": "0.281.0",
134
- "@cosmicdrift/kumiko-headless": "0.281.0",
135
- "@cosmicdrift/kumiko-renderer": "0.281.0",
136
- "@cosmicdrift/kumiko-renderer-web": "0.281.0",
137
- "@cosmicdrift/kumiko-types": "0.281.0",
132
+ "@cosmicdrift/kumiko-dispatcher-live": "0.283.0",
133
+ "@cosmicdrift/kumiko-framework": "0.283.0",
134
+ "@cosmicdrift/kumiko-headless": "0.283.0",
135
+ "@cosmicdrift/kumiko-renderer": "0.283.0",
136
+ "@cosmicdrift/kumiko-renderer-web": "0.283.0",
137
+ "@cosmicdrift/kumiko-types": "0.283.0",
138
138
  "@mollie/api-client": "^4.5.0",
139
139
  "@node-rs/argon2": "^2.0.2",
140
140
  "@types/mailparser": "^3.4.6",
@@ -163,7 +163,7 @@
163
163
  ],
164
164
  "devDependencies": {
165
165
  "@testing-library/user-event": "^14.6.1",
166
- "@cosmicdrift/kumiko-locale-de": "0.281.0",
167
- "@cosmicdrift/kumiko-locale-es": "0.281.0"
166
+ "@cosmicdrift/kumiko-locale-de": "0.283.0",
167
+ "@cosmicdrift/kumiko-locale-es": "0.283.0"
168
168
  }
169
169
  }
@@ -0,0 +1,111 @@
1
+ // fw#2971 — withUnsafeRawGrant was un-exported, so an app-owned CapSpec.usage()
2
+ // provider's db.unsafeRaw() call was rejected even though caps:usage itself
3
+ // runs with r.systemScope(). caps-usage.query.ts now declares its own
4
+ // escapeHatch so those providers keep working. This proves the grant
5
+ // actually reaches a cap provider doing a real raw-SQL SUM aggregate (the
6
+ // shape a typed TenantDb helper like count() can't express) end to end
7
+ // through the real caps:usage HTTP handler.
8
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9
+ import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
10
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
11
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/db";
12
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
13
+ import {
14
+ createTestUser,
15
+ setupTestStack,
16
+ type TestStack,
17
+ testTenantId,
18
+ unsafeCreateEntityTable,
19
+ } from "@cosmicdrift/kumiko-framework/stack";
20
+ import { billingFoundationFeature } from "../../billing-foundation";
21
+ import {
22
+ createComplianceProfilesFeature,
23
+ tenantComplianceProfileEntity,
24
+ } from "../../compliance-profiles";
25
+ import { createConfigFeature } from "../../config";
26
+ import { createTenantFeature } from "../../tenant/feature";
27
+ import { tenantEntity } from "../../tenant/schema/tenant";
28
+ import { seedTenant } from "../../tenant/seeding";
29
+ import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
30
+ import { TierEngineHandlers, tierAssignmentEntity, tierEngineFeature } from "../../tier-engine";
31
+ import { CapOverviewQueries } from "../constants";
32
+ import { createCapOverviewFeature } from "../feature";
33
+ import type { CapSpec } from "../types";
34
+
35
+ const RAW_USAGE_REASON =
36
+ "test: cap provider sums cap_overview_raw_usage_probe via raw SQL (fw#2971 regression)";
37
+ const TENANT_A = testTenantId(9101);
38
+
39
+ const rawSqlSumCap: CapSpec = {
40
+ id: "raw-sql-sum",
41
+ label: "test.cap.rawSqlSum",
42
+ limit: () => 1000,
43
+ usage: async (db, tenantId) => {
44
+ // @cast-boundary db-operator — unsafeRaw's DbRunner narrows to
45
+ // DbConnection|DbTx; this call site only ever runs outside a tx.
46
+ const raw = asRawClient(db.unsafeRaw(RAW_USAGE_REASON) as DbConnection);
47
+ const rows = await raw.unsafe<{ total: number }>(
48
+ "SELECT COALESCE(SUM(amount), 0)::int AS total FROM cap_overview_raw_usage_probe WHERE tenant_id = $1",
49
+ [tenantId],
50
+ );
51
+ return rows[0]?.total ?? 0;
52
+ },
53
+ };
54
+
55
+ let stack: TestStack;
56
+ let db: DbConnection;
57
+
58
+ beforeAll(async () => {
59
+ stack = await setupTestStack({
60
+ features: [
61
+ createConfigFeature(),
62
+ createTenantFeature(),
63
+ createComplianceProfilesFeature(),
64
+ createTenantLifecycleFeature(),
65
+ billingFoundationFeature,
66
+ tierEngineFeature,
67
+ createCapOverviewFeature({ caps: [rawSqlSumCap] }),
68
+ ],
69
+ });
70
+ db = stack.db;
71
+ await createEventsTable(db);
72
+ await unsafeCreateEntityTable(db, tenantEntity);
73
+ await unsafeCreateEntityTable(db, tierAssignmentEntity);
74
+ await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
75
+ configurePiiSubjectKms(new InMemoryKmsAdapter());
76
+
77
+ await asRawClient(db).unsafe(`
78
+ CREATE TABLE IF NOT EXISTS cap_overview_raw_usage_probe (
79
+ tenant_id text NOT NULL,
80
+ amount int NOT NULL
81
+ )
82
+ `);
83
+ await asRawClient(db).unsafe(
84
+ "INSERT INTO cap_overview_raw_usage_probe (tenant_id, amount) VALUES ($1, $2), ($1, $3)",
85
+ [TENANT_A, 30, 12],
86
+ );
87
+
88
+ await seedTenant(db, { id: TENANT_A, key: `cap-overview-raw-${TENANT_A}`, name: "Tenant Raw" });
89
+ const ownerA = createTestUser({
90
+ id: 91011,
91
+ tenantId: TENANT_A,
92
+ roles: ["TenantAdmin", "SystemAdmin"],
93
+ });
94
+ await stack.http.writeOk(TierEngineHandlers.create, { tier: "pro" }, ownerA);
95
+ });
96
+
97
+ afterAll(async () => {
98
+ await asRawClient(db).unsafe("DROP TABLE IF EXISTS cap_overview_raw_usage_probe");
99
+ await stack.cleanup();
100
+ });
101
+
102
+ describe("caps:usage's own escapeHatch grants a cap provider raw SQL", () => {
103
+ test("a cap provider's db.unsafeRaw() SUM aggregate runs successfully through caps:usage", async () => {
104
+ const memberA = createTestUser({ id: 91012, tenantId: TENANT_A, roles: ["User"] });
105
+ const result = await stack.http.queryOk<{
106
+ rows: readonly { id: string; used: number | null }[];
107
+ }>(CapOverviewQueries.capsUsage, {}, memberA);
108
+ const row = result.rows.find((r) => r.id === rawSqlSumCap.id);
109
+ expect(row?.used).toBe(42);
110
+ });
111
+ });
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.282.0",
4
+ "type": "fix",
5
+ "title": "caps:usage declares escapeHatch again so raw-SQL cap providers work",
6
+ "detail": "fw#2971 un-exported withUnsafeRawGrant, the mechanism app-owned CapSpec.usage() providers used (via caps:usage's system-mode db) to run raw SQL for aggregates TenantDb has no typed helper for (a SUM, unlike the COUNT(*)-only TenantDb.count() added in fw#2854). caps:usage's own defineQueryHandler(...) call now carries `escapeHatch: { reason: \"...\" }`, restoring the grant through the same mechanism every other handler-owned unsafeRaw use goes through. Cap providers that only need COUNT(*) should migrate to TenantDb.count(table, where) instead — it needs no escapeHatch at all."
7
+ },
2
8
  {
3
9
  "version": "0.233.0",
4
10
  "type": "improvement",
@@ -23,6 +23,12 @@ export function createCapsUsageQuery(caps: readonly CapSpec[]): QueryHandlerDefi
23
23
  "Returns the calling user's own tenant's configured caps with used amount, tier limit and utilisation fraction; use it to answer how close a tenant is to its quota (only SystemAdmin may target another tenant via tenantId).",
24
24
  schema: z.object({ tenantId: z.string().min(1).optional() }),
25
25
  access: { roles: MY_CAPS_ACCESS_ROLES },
26
+ // App-owned CapSpec.usage() providers get this handler's db; a SUM has
27
+ // no TenantDb helper (count() is COUNT(*)-only), so they need raw SQL.
28
+ escapeHatch: {
29
+ reason:
30
+ "cap-overview:caps:usage — grants app-owned CapSpec.usage() providers raw SQL for aggregates TenantDb has no typed helper for (e.g. a SUM)",
31
+ },
26
32
  handler: async (query, ctx) => {
27
33
  if (!ctx.systemDb) {
28
34
  throw new InternalError({