@cosmicdrift/kumiko-dev-server 0.165.0 → 2.0.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.
@@ -68,6 +68,7 @@ async function readMountedFeatures(runConfigPath: string): Promise<Set<string>>
68
68
  const mod = (await import(runConfigPath)) as {
69
69
  APP_FEATURES?: ReadonlyArray<{ name: string }>;
70
70
  HAS_AUTH?: boolean;
71
+ HAS_SIGNUP?: boolean;
71
72
  };
72
73
  if (!mod.APP_FEATURES) {
73
74
  throw new Error(
@@ -85,6 +86,12 @@ async function readMountedFeatures(runConfigPath: string): Promise<Set<string>>
85
86
  if (mod.HAS_AUTH ?? true) {
86
87
  for (const name of implicitAuthModeFeatureNames()) set.add(name);
87
88
  }
89
+ // auth-self-registration is only prepended when authOptions.signup is set
90
+ // (composeFeatures) — opt-in via HAS_SIGNUP so apps without signup don't
91
+ // get a stale-registry false positive (#1521).
92
+ if (mod.HAS_SIGNUP === true) {
93
+ set.add("auth-self-registration");
94
+ }
88
95
  return set;
89
96
  }
90
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.165.0",
3
+ "version": "2.0.0",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Not shipped into production node_modules — see @cosmicdrift/kumiko-server-runtime for the prod boot path.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -54,9 +54,9 @@
54
54
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
55
55
  },
56
56
  "dependencies": {
57
- "@cosmicdrift/kumiko-bundled-features": "0.165.0",
58
- "@cosmicdrift/kumiko-framework": "0.165.0",
59
- "@cosmicdrift/kumiko-server-runtime": "0.165.0",
57
+ "@cosmicdrift/kumiko-bundled-features": "2.0.0",
58
+ "@cosmicdrift/kumiko-framework": "2.0.0",
59
+ "@cosmicdrift/kumiko-server-runtime": "2.0.0",
60
60
  "ts-morph": "^28.0.0"
61
61
  },
62
62
  "publishConfig": {
@@ -95,4 +95,32 @@ describe("buildServerBundle (multi-entry + splitting)", () => {
95
95
  rmSync(dir, { recursive: true, force: true });
96
96
  }
97
97
  });
98
+
99
+ test("prefers node_modules/@cosmicdrift/* over packages/* when both declare a version", async () => {
100
+ // Framework-monorepo checkout: both the installed node_modules package
101
+ // (symlinked in real life, diverging here) and packages/framework exist.
102
+ // node_modules must win — it reflects what actually gets shipped/installed
103
+ // for the consumer, packages/framework is only a repo-root fallback.
104
+ const dir = makeFixture();
105
+ try {
106
+ const frameworkPkgDir = join(dir, "node_modules/@cosmicdrift/kumiko-framework");
107
+ mkdirSync(frameworkPkgDir, { recursive: true });
108
+ writeFileSync(
109
+ join(frameworkPkgDir, "package.json"),
110
+ `${JSON.stringify({ name: "@cosmicdrift/kumiko-framework", dependencies: { meilisearch: "^0.58.0" } })}\n`,
111
+ );
112
+
113
+ mkdirSync(join(dir, "packages/framework"), { recursive: true });
114
+ writeFileSync(
115
+ join(dir, "packages/framework/package.json"),
116
+ `${JSON.stringify({ name: "@cosmicdrift/kumiko-framework", dependencies: { meilisearch: "^0.30.0" } })}\n`,
117
+ );
118
+
119
+ const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server") });
120
+
121
+ expect(result.runtimeDeps["meilisearch"]).toBe("^0.58.0");
122
+ } finally {
123
+ rmSync(dir, { recursive: true, force: true });
124
+ }
125
+ });
98
126
  });
@@ -144,16 +144,14 @@ describe("composeStacks boots for real", () => {
144
144
  expect(() => validateBoot(features)).not.toThrow();
145
145
  });
146
146
 
147
- test("money-horse-shaped composeGdprStack({sessions:true}) without a tokenVerifier provider fails boot", () => {
148
- // Regression pin for the open design question this stack shape raised:
149
- // composeGdprStack({sessions:true}) mounts auth-foundation, which
150
- // hard-requires at least one tokenVerifier provider. Without composing
151
- // one in (money-horse/solon's real run-config passes none), boot must
152
- // fail loudly instead of silently shipping an unauthenticatable app.
147
+ test("money-horse-shaped composeGdprStack({sessions:true}) without a tokenVerifier provider passes boot", () => {
148
+ // Session-only stacks (#1570): composeGdprStack({sessions:true}) mounts
149
+ // auth-foundation + sessions (sessionStore). Cookie auth does not need a
150
+ // tokenVerifier; money-horse/solon run-configs pass none and must boot.
153
151
  const features = composeFeatures([...composeGdprStack({ sessions: true })], {
154
152
  includeBundled: true,
155
153
  });
156
- expect(() => validateBoot(features)).toThrow(/no tokenVerifier providers registered/i);
154
+ expect(() => validateBoot(features)).not.toThrow();
157
155
  });
158
156
 
159
157
  test("composeGdprStack({sessions:true, providers:[...]}) passes boot with a tokenVerifier mounted", () => {
@@ -385,6 +385,13 @@ describe("createKumikoServer — real Bun.build (buildClient)", () => {
385
385
  const body = await res.text();
386
386
  expect(body.length).toBeGreaterThan(0);
387
387
  expect(body).toMatch(/ping/);
388
+
389
+ // Stop the watcher before teardown rmSync deletes clientEntry out from
390
+ // under it — a bare "client.tsx" delete event classifies as "restart"
391
+ // (classifyChange only special-cases endsWith("/client.tsx")) and
392
+ // process.exit(75) kills the whole bun test runner mid-suite.
393
+ await handle.stop();
394
+ handle = undefined;
388
395
  } finally {
389
396
  rmSync(tmpDir, { recursive: true, force: true });
390
397
  }
@@ -186,6 +186,27 @@ describe("scaffoldApp", () => {
186
186
  expect(readme).toContain("- `delivery`");
187
187
  });
188
188
 
189
+ test("callExpression/export-callability mismatch throws instead of silently mis-instantiating", async () => {
190
+ const dest = join(tmp, "my-shop");
191
+ await expect(
192
+ scaffoldApp({
193
+ name: "my-shop",
194
+ destination: dest,
195
+ features: [
196
+ {
197
+ name: "delivery",
198
+ importPath: "@cosmicdrift/kumiko-bundled-features/delivery",
199
+ exportName: "createDeliveryFeature",
200
+ // createDeliveryFeature is a factory function, but this claims it's
201
+ // a plain object export (no trailing "()") — must not silently push
202
+ // the function itself as a FeatureDefinition.
203
+ callExpression: "createDeliveryFeature",
204
+ },
205
+ ],
206
+ }),
207
+ ).rejects.toThrow(/is.*callable but callExpression/);
208
+ });
209
+
189
210
  test("bin/main.ts contains runProdApp + auth.admin stub + staticDir", async () => {
190
211
  const dest = join(tmp, "my-shop");
191
212
  await scaffoldApp({ name: "my-shop", destination: dest });
@@ -237,14 +237,16 @@ async function resolveRuntimeDepsVersions(
237
237
  const out: Record<string, string> = {};
238
238
 
239
239
  const pinSources = [
240
- join(cwd, "node_modules/@cosmicdrift/kumiko-framework/package.json"),
241
- join(cwd, "node_modules/@cosmicdrift/kumiko-bundled-features/package.json"),
242
240
  ...(repoRoot
243
241
  ? [
244
242
  join(repoRoot, "packages/framework/package.json"),
245
243
  join(repoRoot, "packages/bundled-features/package.json"),
246
244
  ]
247
245
  : []),
246
+ // node_modules applied last so its versions win over the repo-root
247
+ // fallback — see the function comment above (#1217 bug class).
248
+ join(cwd, "node_modules/@cosmicdrift/kumiko-framework/package.json"),
249
+ join(cwd, "node_modules/@cosmicdrift/kumiko-bundled-features/package.json"),
248
250
  ];
249
251
  const allDeps: Record<string, string> = {};
250
252
  for (const path of pinSources) {
@@ -59,10 +59,10 @@ export type GdprStackOptions = {
59
59
  readonly order?: GdprStackOrder;
60
60
  readonly sessions?: boolean;
61
61
  readonly tenantLifecycle?: boolean;
62
- /** tokenVerifier provider(s) for auth-foundation when `sessions` is on.
63
- * Without at least one, auth-foundation's boot check throws ("no
64
- * tokenVerifier providers registered"). Omit only if the app mounts a
65
- * provider itself elsewhere. */
62
+ /** Optional tokenVerifier provider(s) (e.g. PAT) when `sessions` is on.
63
+ * Session-only stacks need none sessions registers sessionStore, which
64
+ * satisfies auth-foundation's boot check (#1570). Pass providers when the
65
+ * app also wants Bearer/PAT auth. */
66
66
  readonly providers?: readonly FeatureDefinition[];
67
67
  };
68
68
 
@@ -84,11 +84,10 @@ export type OpsStackOptions = {
84
84
  /** sessions (+ optional auth-mfa). config/user/tenant/auth-email-password stay
85
85
  * on composeFeatures(includeBundled). Pass `mfa` options to mount TOTP.
86
86
  * When `sessions` is on (the default), auth-foundation is mounted alongside it —
87
- * the framework's registry now hard-requires it (sessions.requires("auth-foundation")),
88
- * which in turn needs at least one tokenVerifier provider mounted by the caller
89
- * (e.g. createPersonalAccessTokensFeature({ scopes })) — PAT scopes are app-specific
90
- * (which write-handlers an API token may call), so they stay caller-owned instead of
91
- * a framework default. */
87
+ * the framework's registry now hard-requires it (sessions.requires("auth-foundation")).
88
+ * Session-only is enough for boot (#1570); optional Bearer/PAT still needs a
89
+ * tokenVerifier provider from the caller (e.g. createPersonalAccessTokensFeature
90
+ * ({ scopes })) PAT scopes are app-specific, so they stay caller-owned. */
92
91
  export type IdentityStackOptions = {
93
92
  readonly sessions?: boolean;
94
93
  readonly mfa?: AuthMfaFeatureOptions;
@@ -132,23 +132,28 @@ export type RunDevAppAuthOptions = {
132
132
  * der request/confirm-Handler im auth-email-password-Feature wird
133
133
  * registriert. Symmetrisch zu RunProdAppAuthOptions.passwordReset. */
134
134
  readonly passwordReset?: PasswordResetSetup;
135
- /** Email-verification flow. Symmetric zu passwordReset. */
135
+ /** Email-verification flow. Symmetric to passwordReset. */
136
136
  readonly emailVerification?: EmailVerificationSetup;
137
- /** Self-Signup flow (Magic-Link). Symmetric zu RunProdAppAuthOptions. */
137
+ /** Self-signup flow (magic link). Symmetric to RunProdAppAuthOptions. */
138
138
  readonly signup?: SignupSetup;
139
- /** Tenant-Invite flow (Magic-Link). Symmetric. */
139
+ /** Tenant-invite flow (magic link). Symmetric. */
140
140
  readonly invite?: InviteSetup;
141
- /** Account-unlock flow (#1266). Symmetric zu RunProdAppAuthOptions. */
141
+ /** Account-unlock flow (#1266). Symmetric to RunProdAppAuthOptions. */
142
142
  readonly accountUnlock?: AccountUnlockSetup;
143
143
  /** Domain attribute for both auth cookies (see
144
- * AuthRoutesConfig.cookieDomain). Symmetric zu RunProdAppAuthOptions. */
144
+ * AuthRoutesConfig.cookieDomain). Symmetric to RunProdAppAuthOptions. */
145
145
  readonly cookieDomain?: string;
146
146
  /** Server-side Origin allowlist for the CSRF guard (see
147
- * AuthRoutesConfig.allowedOrigins). Symmetric zu RunProdAppAuthOptions —
147
+ * AuthRoutesConfig.allowedOrigins). Symmetric to RunProdAppAuthOptions —
148
148
  * required once `cookieDomain` is set. */
149
149
  readonly allowedOrigins?: readonly string[];
150
- /** Opt out of the Origin guard. Symmetric zu RunProdAppAuthOptions. */
150
+ /** Opt out of the Origin guard. Symmetric to RunProdAppAuthOptions. */
151
151
  readonly unsafeSkipOriginCheck?: boolean;
152
+ /** Number of trusted reverse-proxy hops for client-IP derivation (see
153
+ * AuthRoutesConfig.trustedProxyHops, kumiko-framework#1539). Symmetric to
154
+ * RunProdAppAuthOptions — dev usually runs unproxied, so this is normally
155
+ * left unset (default 0). */
156
+ readonly trustedProxyHops?: number;
152
157
  };
153
158
 
154
159
  /** Hook for app-specific seeding (demo data, fixtures). Runs after the
@@ -484,6 +489,9 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
484
489
  ...(effectiveAuth.unsafeSkipOriginCheck !== undefined && {
485
490
  unsafeSkipOriginCheck: effectiveAuth.unsafeSkipOriginCheck,
486
491
  }),
492
+ ...(effectiveAuth.trustedProxyHops !== undefined && {
493
+ trustedProxyHops: effectiveAuth.trustedProxyHops,
494
+ }),
487
495
  ...sessionAuthFragment,
488
496
  ...patAuthFragment,
489
497
  ...tenantLifecycleAuthFragment,
@@ -852,6 +852,14 @@ async function instantiateScaffoldFeatures(
852
852
  `scaffoldApp: ${entry.importPath} missing export ${entry.exportName} for ${entry.callExpression}`,
853
853
  );
854
854
  }
855
+ const looksCallable = entry.callExpression.endsWith(")");
856
+ if (looksCallable !== (typeof exp === "function")) {
857
+ throw new Error(
858
+ `scaffoldApp: ${entry.importPath} export ${entry.exportName} is ${
859
+ typeof exp === "function" ? "" : "not "
860
+ }callable but callExpression "${entry.callExpression}" says otherwise`,
861
+ );
862
+ }
855
863
  if (typeof exp === "function") {
856
864
  instances.push((exp as (...args: unknown[]) => FeatureDefinition)(...(entry.callArgs ?? [])));
857
865
  } else {