@cosmicdrift/kumiko-bundled-features 0.156.2 → 0.156.3

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.156.2",
3
+ "version": "0.156.3",
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>",
@@ -116,11 +116,11 @@
116
116
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
117
117
  },
118
118
  "dependencies": {
119
- "@cosmicdrift/kumiko-dispatcher-live": "0.156.2",
120
- "@cosmicdrift/kumiko-framework": "0.156.2",
121
- "@cosmicdrift/kumiko-headless": "0.156.2",
122
- "@cosmicdrift/kumiko-renderer": "0.156.2",
123
- "@cosmicdrift/kumiko-renderer-web": "0.156.2",
119
+ "@cosmicdrift/kumiko-dispatcher-live": "0.156.3",
120
+ "@cosmicdrift/kumiko-framework": "0.156.3",
121
+ "@cosmicdrift/kumiko-headless": "0.156.3",
122
+ "@cosmicdrift/kumiko-renderer": "0.156.3",
123
+ "@cosmicdrift/kumiko-renderer-web": "0.156.3",
124
124
  "@mollie/api-client": "^4.5.0",
125
125
  "imapflow": "^1.3.3",
126
126
  "mailparser": "^3.9.8",
@@ -139,6 +139,7 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
139
139
  // now. Deliberately NOT re-checking strictEmailVerification here:
140
140
  // MfaVerifyOptions has no such flag, and email verification can't
141
141
  // regress between login and verify within the 10-minute window.
142
+ if (!userRow) return invalidChallengeToken();
142
143
  if (
143
144
  userRow?.status === USER_STATUS.Restricted ||
144
145
  userRow?.status === USER_STATUS.DeletionRequested ||
@@ -0,0 +1,15 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
3
+ import { capCounterEntity } from "../entity";
4
+
5
+ function pgTypeOf(table: unknown, dbName: string): string | undefined {
6
+ const cols = (table as { columns?: ReadonlyArray<{ name: string; pgType?: string }> }).columns;
7
+ return cols?.find((c) => c.name === dbName)?.pgType;
8
+ }
9
+
10
+ describe("capCounterEntity — DDL (#1205 regression)", () => {
11
+ test("value column is bigint, not double precision", () => {
12
+ const table = buildEntityTable("cap-counter", capCounterEntity);
13
+ expect(pgTypeOf(table, "value")).toBe("bigint");
14
+ });
15
+ });
@@ -1,6 +1,6 @@
1
1
  import {
2
+ createBigIntField,
2
3
  createEntity,
3
- createNumberField,
4
4
  createTextField,
5
5
  createTimestampField,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
@@ -41,7 +41,7 @@ export const capCounterEntity = createEntity({
41
41
  table: "read_cap_counters",
42
42
  fields: {
43
43
  capName: createTextField({ required: true, maxLength: 100, sortable: true, searchable: true }),
44
- value: createNumberField({ required: true, default: 0 }),
44
+ value: createBigIntField({ required: true, default: 0 }),
45
45
  periodStart: createTimestampField({ required: true }),
46
46
  lastSoftWarnedAt: createTimestampField(),
47
47
  },
@@ -164,5 +164,9 @@ export const seenMessageEntity = createEntity({
164
164
  // Plain EntityTableMeta (kein branded EntityTable) — unmanaged Direct-
165
165
  // Write-Stores, Handler schreiben via ctx.db (siehe user-session.ts-
166
166
  // Rationale).
167
- export const syncCursorTable = buildEntityTableMeta("mail-sync-cursor", syncCursorEntity);
168
- export const seenMessageTable = buildEntityTableMeta("mail-seen-message", seenMessageEntity);
167
+ export const syncCursorTable = buildEntityTableMeta("mail-sync-cursor", syncCursorEntity, {
168
+ source: "unmanaged",
169
+ });
170
+ export const seenMessageTable = buildEntityTableMeta("mail-seen-message", seenMessageEntity, {
171
+ source: "unmanaged",
172
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
3
+ import { jobRunEntity } from "../job-run-table";
4
+
5
+ function pgTypeOf(table: unknown, dbName: string): string | undefined {
6
+ const cols = (table as { columns?: ReadonlyArray<{ name: string; pgType?: string }> }).columns;
7
+ return cols?.find((c) => c.name === dbName)?.pgType;
8
+ }
9
+
10
+ describe("jobRunEntity — DDL (#1205 regression)", () => {
11
+ test("duration column is integer, not double precision", () => {
12
+ const table = buildEntityTable("read_job_runs", jobRunEntity);
13
+ expect(pgTypeOf(table, "duration")).toBe("integer");
14
+ });
15
+ });
@@ -48,7 +48,7 @@ export const jobRunEntity = createEntity({
48
48
  attempt: createNumberField({ required: true, default: 1, integer: true }),
49
49
  startedAt: createTimestampField({ required: true }),
50
50
  finishedAt: createTimestampField(),
51
- duration: createNumberField(),
51
+ duration: createNumberField({ integer: true }),
52
52
  triggeredById: createTextField(),
53
53
  },
54
54
  });
@@ -53,7 +53,7 @@ export function createPersonalAccessTokensFeature(
53
53
  // Direct-write store like read_user_sessions: create/revoke write it, the
54
54
  // resolver point-reads it. r.entity would make it a rebuildable projection
55
55
  // whose replay (no token events) would wipe every live token (#498/#494).
56
- r.rawTable(buildEntityTableMeta("api-token", apiTokenEntity), {
56
+ r.rawTable(buildEntityTableMeta("api-token", apiTokenEntity, { source: "unmanaged" }), {
57
57
  reason: "read_side.api_tokens_direct_write",
58
58
  // create.write encrypts `name` via encryptForDirectWrite (#820).
59
59
  piiEncryptedOnWrite: true,
@@ -58,4 +58,6 @@ export const apiTokenEntity = createEntity({
58
58
  // buildEntityTableMeta (not buildEntityTable): this is a direct-write store, so
59
59
  // the table must be a WritableTable (post ES-write-brand #742) — same as
60
60
  // sessions' userSessionTable. buildEntityTable is branded executor-only.
61
- export const apiTokenTable = buildEntityTableMeta("api-token", apiTokenEntity);
61
+ export const apiTokenTable = buildEntityTableMeta("api-token", apiTokenEntity, {
62
+ source: "unmanaged",
63
+ });
@@ -175,8 +175,16 @@ describe("rotate-job circuit-breaker", () => {
175
175
  // Happy-path counterpart to the failure tests above: a working provider
176
176
  // that actually rewraps. Run last — it's the only test that mutates the
177
177
  // shared 20-row fixture, so the earlier "stays at 20" assertions must
178
- // see it before this runs.
178
+ // see it before this runs. Enforced below, not just documented: a
179
+ // precondition check fails loud (not silently-wrong) if a test inserted
180
+ // between this and the circuit-breaker tests already touched the
181
+ // fixture — rotateJob's batch scan has no per-row exclusion/ordering
182
+ // to fall back on, so isolating this test onto a second tenant doesn't
183
+ // help (rotator would just exhaust maxFailures on the other tenant's
184
+ // still-V1 rows before ever reaching its own).
179
185
  test("successful rotation rewraps the DEK, bumps kekVersion, and preserves plaintext", async () => {
186
+ expect(await countV1Rows()).toBe(20);
187
+
180
188
  const rotator = createEnvMasterKeyProvider({
181
189
  env: {
182
190
  KUMIKO_SECRETS_MASTER_KEY_V1: SEED_V1_KEY,
@@ -185,6 +193,17 @@ describe("rotate-job circuit-breaker", () => {
185
193
  },
186
194
  });
187
195
 
196
+ // Snapshot pre-rotation ciphertext — rotate.job.ts's header comment
197
+ // claims "the ciphertext itself never changes, only the DEK wrapper".
198
+ // Assert that invariant below instead of only checking kekVersion/
199
+ // plaintext, which a rewrap-that-re-encrypts would also satisfy.
200
+ const preRotation = await selectMany<{ key: string; envelope: StoredEnvelope }>(
201
+ stack.db,
202
+ tenantSecretsTable,
203
+ { tenantId: admin.tenantId },
204
+ );
205
+ const ciphertextByKey = new Map(preRotation.map((row) => [row.key, row.envelope.ciphertext]));
206
+
188
207
  await rotateJob({ batchSize: 10, maxFailures: 5 }, jobCtx(rotator));
189
208
 
190
209
  expect(await countV1Rows()).toBe(0);
@@ -209,6 +228,9 @@ describe("rotate-job circuit-breaker", () => {
209
228
  expect(rows).toHaveLength(20);
210
229
  for (const row of rows) {
211
230
  expect(row.kekVersion).toBe(2);
231
+ const expectedCiphertext = ciphertextByKey.get(row.key);
232
+ expect(expectedCiphertext).toBeDefined();
233
+ expect(row.envelope.ciphertext).toBe(expectedCiphertext as string);
212
234
  const expectedIndex = row.key.split("-").at(-1);
213
235
  const plaintext = await decryptValue(decodeStoredEnvelope(row.envelope), verifier);
214
236
  expect(plaintext).toBe(`secret-${expectedIndex}`);
@@ -96,7 +96,7 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
96
96
  // (#498/#494). r.rawTable keeps the migration DDL but opts the
97
97
  // table out of implicit rebuild, like jobs/channel-in-app/feature-toggles
98
98
  // which are direct-write stores too.
99
- r.rawTable(buildEntityTableMeta("user-session", userSessionEntity), {
99
+ r.rawTable(buildEntityTableMeta("user-session", userSessionEntity, { source: "unmanaged" }), {
100
100
  reason: "read_side.user_sessions_direct_write",
101
101
  // sessionCreator encrypts ip/userAgent via encryptForDirectWrite (#820).
102
102
  piiEncryptedOnWrite: true,
@@ -74,4 +74,6 @@ export const userSessionEntity = createEntity({
74
74
  // handlers write it directly via ctx.db; the meta carries no executor-only
75
75
  // brand so those writes stay legal. See feature.ts for the rebuild-exclusion
76
76
  // rationale (#494/#498).
77
- export const userSessionTable = buildEntityTableMeta("user-session", userSessionEntity);
77
+ export const userSessionTable = buildEntityTableMeta("user-session", userSessionEntity, {
78
+ source: "unmanaged",
79
+ });