@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.
- package/bin/kumiko-schema-check.ts +7 -0
- package/package.json +4 -4
- package/src/__tests__/build-server-bundle.test.ts +28 -0
- package/src/__tests__/compose-stacks.test.ts +5 -7
- package/src/__tests__/create-kumiko-server.integration.test.ts +7 -0
- package/src/__tests__/scaffold-app.test.ts +21 -0
- package/src/build-server-bundle.ts +4 -2
- package/src/compose-stacks.ts +8 -9
- package/src/run-dev-app.ts +15 -7
- package/src/scaffold-app.ts +8 -0
|
@@ -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.
|
|
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.
|
|
58
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
59
|
-
"@cosmicdrift/kumiko-server-runtime": "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
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
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(
|
|
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) {
|
package/src/compose-stacks.ts
CHANGED
|
@@ -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)
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
-
*
|
|
89
|
-
* (e.g. createPersonalAccessTokensFeature
|
|
90
|
-
* (
|
|
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;
|
package/src/run-dev-app.ts
CHANGED
|
@@ -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
|
|
135
|
+
/** Email-verification flow. Symmetric to passwordReset. */
|
|
136
136
|
readonly emailVerification?: EmailVerificationSetup;
|
|
137
|
-
/** Self-
|
|
137
|
+
/** Self-signup flow (magic link). Symmetric to RunProdAppAuthOptions. */
|
|
138
138
|
readonly signup?: SignupSetup;
|
|
139
|
-
/** Tenant-
|
|
139
|
+
/** Tenant-invite flow (magic link). Symmetric. */
|
|
140
140
|
readonly invite?: InviteSetup;
|
|
141
|
-
/** Account-unlock flow (#1266). Symmetric
|
|
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
|
|
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
|
|
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
|
|
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,
|
package/src/scaffold-app.ts
CHANGED
|
@@ -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 {
|