@cosmicdrift/kumiko-bundled-features 0.56.0 → 0.56.1

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.56.0",
3
+ "version": "0.56.1",
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>",
@@ -1,6 +1,6 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { randomBytes } from "node:crypto";
3
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
3
+ import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
4
4
  import {
5
5
  createEncryptionProvider,
6
6
  type DbConnection,
@@ -370,6 +370,61 @@ describe("Settings-Hub system-scope write by a human SystemAdmin", () => {
370
370
  expect(await sysAccessor()("integration:config:system-secret")).toBe("sk_live_roundtrip");
371
371
  });
372
372
 
373
+ // Prod scenario: the value already EXISTS (Stripe screen with stored keys),
374
+ // SystemAdmin clicks Speichern → set.write takes the executor.update path,
375
+ // not create. That path was never covered → version-conflict surfaced only
376
+ // in prod. Three consecutive re-saves catch a stale-version regression.
377
+ test("re-saving an existing plain config key (update path) does not version-conflict", async () => {
378
+ for (const value of [false, true, false]) {
379
+ await stack.http.writeOk(
380
+ ConfigHandlers.set,
381
+ { key: "integration:config:billing-live", value, scope: "system" },
382
+ systemAdmin,
383
+ );
384
+ expect(await sysAccessor()("integration:config:billing-live")).toBe(value);
385
+ }
386
+ });
387
+
388
+ test("re-saving an existing secrets-backed key (update path) does not version-conflict", async () => {
389
+ for (const value of ["sk_live_v2", "sk_live_v3"]) {
390
+ await stack.http.writeOk(
391
+ ConfigHandlers.set,
392
+ { key: "integration:config:system-secret", value, scope: "system" },
393
+ systemAdmin,
394
+ );
395
+ expect(await sysAccessor()("integration:config:system-secret")).toBe(value);
396
+ }
397
+ });
398
+
399
+ test("save survives a projection/stream version desync (the prod cut-over symptom)", async () => {
400
+ // Reproduces admin.publicstatus.eu: a config value whose read-row version
401
+ // drifted from its event-stream version (migration wrote the row outside
402
+ // the event flow). With optimistic locking this version-conflicts on every
403
+ // save. The handler now skips the lock and appends at the real stream
404
+ // version, so the save succeeds AND the projection resyncs.
405
+ await stack.http.writeOk(
406
+ ConfigHandlers.set,
407
+ { key: "integration:config:billing-live", value: true, scope: "system" },
408
+ systemAdmin,
409
+ );
410
+ // Corrupt the projection version so it no longer matches the stream.
411
+ await asRawClient(db).unsafe(
412
+ "UPDATE read_config_values SET version = version + 5 WHERE key = 'integration:config:billing-live'",
413
+ );
414
+
415
+ // Two consecutive saves: the first proves the lock is bypassed, the second
416
+ // proves the projection actually resynced (a stale version wouldn't drift
417
+ // back into conflict).
418
+ for (const value of [false, true]) {
419
+ await stack.http.writeOk(
420
+ ConfigHandlers.set,
421
+ { key: "integration:config:billing-live", value, scope: "system" },
422
+ systemAdmin,
423
+ );
424
+ expect(await sysAccessor()("integration:config:billing-live")).toBe(value);
425
+ }
426
+ });
427
+
373
428
  test("a plain tenant Admin is denied the privileged key (not via system-only)", async () => {
374
429
  const error = await stack.http.writeErr(
375
430
  ConfigHandlers.set,
@@ -95,6 +95,14 @@ export const setWrite = defineWriteHandler({
95
95
  const existing = await findConfigRow(db, event.payload.key, tenantId, userId);
96
96
 
97
97
  if (existing) {
98
+ // skipOptimisticLock: config is single-writer operator state, not a
99
+ // collaboratively-edited aggregate — last-write-wins is the intended
100
+ // semantics. More importantly, the optimistic check compares the
101
+ // PROJECTION version (existing.version) against the event-stream
102
+ // version; if those drift (a migration/seed that wrote the read-row
103
+ // outside the normal event flow — e.g. the Stripe config cut-over),
104
+ // every save would version-conflict forever. Appending at the real
105
+ // stream version resyncs the projection and self-heals the drift.
98
106
  const result = await executor.update(
99
107
  {
100
108
  id: existing.id,
@@ -103,6 +111,7 @@ export const setWrite = defineWriteHandler({
103
111
  },
104
112
  event.user,
105
113
  db,
114
+ { skipOptimisticLock: true },
106
115
  );
107
116
  if (!result.isSuccess) return result;
108
117
  } else {
@@ -20,7 +20,7 @@ const WEBHOOK_SECRET_HANDLE: ConfigKeyHandle<"text"> = {
20
20
  type: "text",
21
21
  };
22
22
  const BILLING_LIVE_HANDLE: ConfigKeyHandle<"boolean"> = {
23
- name: "subscription-stripe:config:billingLive",
23
+ name: "subscription-stripe:config:billing-live",
24
24
  type: "boolean",
25
25
  };
26
26