@cosmicdrift/kumiko-dev-server 0.81.0 → 0.82.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-dev-server",
3
- "version": "0.81.0",
3
+ "version": "0.82.0",
4
4
  "description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -50,8 +50,8 @@
50
50
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
51
51
  },
52
52
  "dependencies": {
53
- "@cosmicdrift/kumiko-bundled-features": "0.81.0",
54
- "@cosmicdrift/kumiko-framework": "0.81.0",
53
+ "@cosmicdrift/kumiko-bundled-features": "0.82.0",
54
+ "@cosmicdrift/kumiko-framework": "0.82.0",
55
55
  "ts-morph": "^28.0.0"
56
56
  },
57
57
  "publishConfig": {
@@ -94,4 +94,38 @@ describe("runDevApp configResolver-default — ENV→app-override bridge", () =>
94
94
  }) as ExtraObj;
95
95
  expect(extra.configResolver).toBe(custom);
96
96
  });
97
+
98
+ // Regression: the GDPR export download threw errors.internal in prod because
99
+ // boot only wired `configResolver`, never `_configAccessorFactory` — so the
100
+ // dispatcher left ctx.config undefined and createFileProviderForTenant threw
101
+ // "ctx.config is missing". The boot now mints the factory.
102
+ test("boot wiring mints _configAccessorFactory so handlers get ctx.config", () => {
103
+ const extra = mergeConfigResolverDefault(undefined, stack.registry, {}) as ExtraObj & {
104
+ readonly _configAccessorFactory?: unknown;
105
+ };
106
+ expect(typeof extra._configAccessorFactory).toBe("function");
107
+ });
108
+
109
+ test("_configAccessorFactory uses the EFFECTIVE resolver (caller override wins over env-bridge)", async () => {
110
+ // money-horse pins `file-foundation:config:provider` = "s3-env" via an
111
+ // appOverride on its own configResolver; ctx.config MUST read that override,
112
+ // not the boot default — else the download resolves no provider.
113
+ const override = createConfigResolver({ appOverrides: new Map([[PAGE_SIZE, "99"]]) });
114
+ const extra = mergeConfigResolverDefault({ configResolver: override }, stack.registry, {
115
+ DEVCFG_PAGE_SIZE: "25", // env-bridge default would be 25 — the override must win
116
+ }) as ExtraObj & {
117
+ readonly _configAccessorFactory: (deps: {
118
+ readonly user: { readonly id: string; readonly tenantId: string };
119
+ readonly db: typeof stack.db;
120
+ }) => (key: string) => Promise<unknown>;
121
+ };
122
+ const accessor = extra._configAccessorFactory({
123
+ user: { id: TestUsers.systemAdmin.id, tenantId: TestUsers.systemAdmin.tenantId },
124
+ db: stack.db,
125
+ });
126
+ // appOverride values come through raw (string) — what matters is it's the
127
+ // override's "99", NOT the env-bridge default ("25"): proves the factory
128
+ // was built from the caller's resolver, exactly like money-horse's "s3-env".
129
+ expect(await accessor(PAGE_SIZE)).toBe("99");
130
+ });
97
131
  });
@@ -69,6 +69,7 @@ import type {
69
69
  PasswordResetSetup,
70
70
  SignupSetup,
71
71
  } from "./run-prod-app";
72
+ import { addConfigAccessorFactory } from "./run-prod-app";
72
73
 
73
74
  export type RunDevAppAuthOptions = {
74
75
  /** Admin user to seed at boot. Idempotent — re-runs in persistent-DB
@@ -451,9 +452,13 @@ export function mergeConfigResolverDefault(
451
452
  appOverrides: buildEnvConfigOverrides(registry, envSource),
452
453
  }),
453
454
  };
454
- if (ctx === undefined) return defaults;
455
+ // ctx.config wird per-Request aus _configAccessorFactory geminted (siehe
456
+ // addConfigAccessorFactory) — sonst bleibt ctx.config undefined und Handler
457
+ // die es lesen (createFileProviderForTenant) werfen. Aus dem EFFEKTIVEN
458
+ // Resolver (Caller-Override gewinnt) gebaut, symmetrisch zu runProdApp.
459
+ if (ctx === undefined) return addConfigAccessorFactory(defaults, registry);
455
460
  if (typeof ctx === "function") {
456
- return (deps) => ({ ...defaults, ...ctx(deps) });
461
+ return (deps) => addConfigAccessorFactory({ ...defaults, ...ctx(deps) }, registry);
457
462
  }
458
- return { ...defaults, ...ctx };
463
+ return addConfigAccessorFactory({ ...defaults, ...ctx }, registry);
459
464
  }
@@ -38,6 +38,7 @@ import {
38
38
  } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
39
39
  import {
40
40
  buildEnvConfigOverrides,
41
+ createConfigAccessorFactory,
41
42
  createConfigResolver,
42
43
  } from "@cosmicdrift/kumiko-bundled-features/config";
43
44
  import { createSessionCallbacks } from "@cosmicdrift/kumiko-bundled-features/sessions";
@@ -47,11 +48,13 @@ import { createSseBroker, type SseBroker } from "@cosmicdrift/kumiko-framework/a
47
48
  import { createDbConnection, type DbRunner } from "@cosmicdrift/kumiko-framework/db";
48
49
  import {
49
50
  buildAppSchema,
51
+ type ConfigResolver,
50
52
  collectWriteHandlerQns,
51
53
  createRegistry,
52
54
  type EffectiveFeaturesResolver,
53
55
  type FeatureDefinition,
54
56
  findTierResolverUsage,
57
+ type Registry,
55
58
  type TenantId,
56
59
  type TierResolverPlugin,
57
60
  validateAppCustomScreenWriteQns,
@@ -517,6 +520,25 @@ export type ProdAppHandle = {
517
520
  readonly stop: () => Promise<void>;
518
521
  };
519
522
 
523
+ // Mint `ctx.config` per request: the dispatcher only builds a per-user
524
+ // ConfigAccessor when `_configAccessorFactory` is on the AppContext
525
+ // (pipeline/dispatcher.ts). Without it `ctx.config` stays undefined and any
526
+ // handler reading it — e.g. createFileProviderForTenant for the GDPR export
527
+ // download — throws "ctx.config is missing". Built from the EFFECTIVE resolver
528
+ // so an app-supplied configResolver override (its appOverrides) is the one
529
+ // ctx.config reads. Shared with runDevApp (mergeConfigResolverDefault) for
530
+ // dev/prod parity.
531
+ export function addConfigAccessorFactory<T extends { readonly configResolver?: ConfigResolver }>(
532
+ resolved: T,
533
+ registry: Registry,
534
+ ): T {
535
+ if (!resolved.configResolver) return resolved;
536
+ return {
537
+ ...resolved,
538
+ _configAccessorFactory: createConfigAccessorFactory(registry, resolved.configResolver),
539
+ };
540
+ }
541
+
520
542
  export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHandle> {
521
543
  // 0. Env-Schema validation + dry-run modes. Runs FIRST so:
522
544
  // - operators can introspect env-requirements without a real boot
@@ -688,14 +710,17 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
688
710
  typeof options.extraContext === "function"
689
711
  ? options.extraContext(deps)
690
712
  : (options.extraContext ?? {});
691
- const extraContext = options.auth
692
- ? {
693
- configResolver: createConfigResolver({
694
- appOverrides: buildEnvConfigOverrides(registry, envSource),
695
- }),
696
- ...resolvedExtraContext,
697
- }
698
- : resolvedExtraContext;
713
+ const extraContext = addConfigAccessorFactory(
714
+ options.auth
715
+ ? {
716
+ configResolver: createConfigResolver({
717
+ appOverrides: buildEnvConfigOverrides(registry, envSource),
718
+ }),
719
+ ...resolvedExtraContext,
720
+ }
721
+ : resolvedExtraContext,
722
+ registry,
723
+ );
699
724
  const resolvedAnonymousAccess =
700
725
  typeof options.anonymousAccess === "function"
701
726
  ? options.anonymousAccess(deps)