@cosmicdrift/kumiko-dev-server 0.165.0 → 0.165.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/bin/kumiko-build.ts +6 -1
- package/bin/kumiko-schema-check.ts +7 -0
- package/package.json +4 -4
- package/src/__tests__/build-server-bundle.test.ts +131 -1
- 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 +44 -4
- package/src/build.ts +1 -0
- package/src/compose-stacks.ts +8 -9
- package/src/run-dev-app.ts +15 -7
- package/src/scaffold-app.ts +8 -0
package/bin/kumiko-build.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
discoverServerEntry,
|
|
25
25
|
formatBuildResult,
|
|
26
26
|
formatServerBuildResult,
|
|
27
|
+
readExtraRuntimeExternals,
|
|
27
28
|
} from "../src/build";
|
|
28
29
|
import { runCodegen } from "../src/codegen";
|
|
29
30
|
|
|
@@ -73,7 +74,11 @@ try {
|
|
|
73
74
|
}
|
|
74
75
|
if (hasServer) {
|
|
75
76
|
const t0 = performance.now();
|
|
76
|
-
const
|
|
77
|
+
const extraRuntimeExternals = readExtraRuntimeExternals(cwd);
|
|
78
|
+
const result = await buildServerBundle({
|
|
79
|
+
cwd,
|
|
80
|
+
...(extraRuntimeExternals.length > 0 && { extraRuntimeExternals }),
|
|
81
|
+
});
|
|
77
82
|
const ms = Math.round(performance.now() - t0);
|
|
78
83
|
// biome-ignore lint/suspicious/noConsole: CLI-Output, einziger Weg
|
|
79
84
|
console.log(formatServerBuildResult(result, ms));
|
|
@@ -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.
|
|
3
|
+
"version": "0.165.1",
|
|
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.
|
|
58
|
-
"@cosmicdrift/kumiko-framework": "0.165.
|
|
59
|
-
"@cosmicdrift/kumiko-server-runtime": "0.165.
|
|
57
|
+
"@cosmicdrift/kumiko-bundled-features": "0.165.1",
|
|
58
|
+
"@cosmicdrift/kumiko-framework": "0.165.1",
|
|
59
|
+
"@cosmicdrift/kumiko-server-runtime": "0.165.1",
|
|
60
60
|
"ts-morph": "^28.0.0"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { buildServerBundle } from "../build-server-bundle";
|
|
5
|
+
import { buildServerBundle, readExtraRuntimeExternals } from "../build-server-bundle";
|
|
6
6
|
|
|
7
7
|
// Baut ein Mini-App-Fixture (bin/main.ts + bin/kumiko.ts teilen ein Modul) und
|
|
8
8
|
// prüft das Variante-B-Verhalten: ein Bun.build-Call → server.js + kumiko.js als
|
|
@@ -95,4 +95,134 @@ 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
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("readExtraRuntimeExternals (package.json kumiko field, #1484)", () => {
|
|
129
|
+
test("reads string list from kumiko.extraRuntimeExternals", () => {
|
|
130
|
+
const dir = mkdtempSync(join(tmpdir(), "extra-ext-"));
|
|
131
|
+
try {
|
|
132
|
+
writeFileSync(
|
|
133
|
+
join(dir, "package.json"),
|
|
134
|
+
`${JSON.stringify({
|
|
135
|
+
name: "app",
|
|
136
|
+
kumiko: { extraRuntimeExternals: ["@napi-rs/canvas", "pdf-parse"] },
|
|
137
|
+
})}\n`,
|
|
138
|
+
);
|
|
139
|
+
expect(readExtraRuntimeExternals(dir)).toEqual(["@napi-rs/canvas", "pdf-parse"]);
|
|
140
|
+
} finally {
|
|
141
|
+
rmSync(dir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("missing / invalid shapes yield empty list", () => {
|
|
146
|
+
const dir = mkdtempSync(join(tmpdir(), "extra-ext-bad-"));
|
|
147
|
+
try {
|
|
148
|
+
writeFileSync(join(dir, "package.json"), `${JSON.stringify({ name: "app" })}\n`);
|
|
149
|
+
expect(readExtraRuntimeExternals(dir)).toEqual([]);
|
|
150
|
+
writeFileSync(
|
|
151
|
+
join(dir, "package.json"),
|
|
152
|
+
`${JSON.stringify({ name: "app", kumiko: { extraRuntimeExternals: "nope" } })}\n`,
|
|
153
|
+
);
|
|
154
|
+
expect(readExtraRuntimeExternals(dir)).toEqual([]);
|
|
155
|
+
writeFileSync(
|
|
156
|
+
join(dir, "package.json"),
|
|
157
|
+
`${JSON.stringify({
|
|
158
|
+
name: "app",
|
|
159
|
+
kumiko: { extraRuntimeExternals: ["ok", 1, "", " "] },
|
|
160
|
+
})}\n`,
|
|
161
|
+
);
|
|
162
|
+
expect(readExtraRuntimeExternals(dir)).toEqual(["ok"]);
|
|
163
|
+
} finally {
|
|
164
|
+
rmSync(dir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("buildServerBundle + package.json extras (#1484)", () => {
|
|
170
|
+
test("extraRuntimeExternals from options land in runtimeDeps with app pin", async () => {
|
|
171
|
+
const dir = makeFixture();
|
|
172
|
+
try {
|
|
173
|
+
writeFileSync(
|
|
174
|
+
join(dir, "package.json"),
|
|
175
|
+
`${JSON.stringify({
|
|
176
|
+
name: "fixture-app",
|
|
177
|
+
dependencies: { "@napi-rs/canvas": "0.1.65" },
|
|
178
|
+
kumiko: { extraRuntimeExternals: ["@napi-rs/canvas"] },
|
|
179
|
+
})}\n`,
|
|
180
|
+
);
|
|
181
|
+
const extras = readExtraRuntimeExternals(dir);
|
|
182
|
+
const result = await buildServerBundle({
|
|
183
|
+
cwd: dir,
|
|
184
|
+
outDir: join(dir, "dist-server"),
|
|
185
|
+
extraRuntimeExternals: extras,
|
|
186
|
+
});
|
|
187
|
+
expect(result.runtimeDeps["@napi-rs/canvas"]).toBe("0.1.65");
|
|
188
|
+
const distPkg = JSON.parse(readFileSync(join(dir, "dist-server/package.json"), "utf-8")) as {
|
|
189
|
+
dependencies: Record<string, string>;
|
|
190
|
+
};
|
|
191
|
+
expect(distPkg.dependencies["@napi-rs/canvas"]).toBe("0.1.65");
|
|
192
|
+
} finally {
|
|
193
|
+
rmSync(dir, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("app package.json does not override framework runtime pins", async () => {
|
|
198
|
+
const dir = makeFixture();
|
|
199
|
+
try {
|
|
200
|
+
const frameworkPkgDir = join(dir, "node_modules/@cosmicdrift/kumiko-framework");
|
|
201
|
+
mkdirSync(frameworkPkgDir, { recursive: true });
|
|
202
|
+
writeFileSync(
|
|
203
|
+
join(frameworkPkgDir, "package.json"),
|
|
204
|
+
`${JSON.stringify({
|
|
205
|
+
name: "@cosmicdrift/kumiko-framework",
|
|
206
|
+
dependencies: { meilisearch: "^0.58.0" },
|
|
207
|
+
})}\n`,
|
|
208
|
+
);
|
|
209
|
+
writeFileSync(
|
|
210
|
+
join(dir, "package.json"),
|
|
211
|
+
`${JSON.stringify({
|
|
212
|
+
name: "fixture-app",
|
|
213
|
+
dependencies: { meilisearch: "^0.1.0", "@napi-rs/canvas": "0.1.65" },
|
|
214
|
+
kumiko: { extraRuntimeExternals: ["@napi-rs/canvas"] },
|
|
215
|
+
})}\n`,
|
|
216
|
+
);
|
|
217
|
+
const result = await buildServerBundle({
|
|
218
|
+
cwd: dir,
|
|
219
|
+
outDir: join(dir, "dist-server"),
|
|
220
|
+
extraRuntimeExternals: readExtraRuntimeExternals(dir),
|
|
221
|
+
});
|
|
222
|
+
expect(result.runtimeDeps["meilisearch"]).toBe("^0.58.0");
|
|
223
|
+
expect(result.runtimeDeps["@napi-rs/canvas"]).toBe("0.1.65");
|
|
224
|
+
} finally {
|
|
225
|
+
rmSync(dir, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
98
228
|
});
|
|
@@ -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 });
|
|
@@ -224,6 +224,25 @@ function findRepoRoot(start: string): string | undefined {
|
|
|
224
224
|
return undefined;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/** Read `package.json#kumiko.extraRuntimeExternals` for kumiko-build (#1484).
|
|
228
|
+
* Invalid / missing shapes → empty list (CLI stays convention-driven). */
|
|
229
|
+
export function readExtraRuntimeExternals(cwd: string): readonly string[] {
|
|
230
|
+
const pkgJson = join(cwd, "package.json");
|
|
231
|
+
if (!existsSync(pkgJson)) return [];
|
|
232
|
+
let raw: string;
|
|
233
|
+
try {
|
|
234
|
+
raw = readFileSync(pkgJson, "utf-8");
|
|
235
|
+
} catch {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
const parsed = parseJsonSafe<{
|
|
239
|
+
kumiko?: { extraRuntimeExternals?: unknown };
|
|
240
|
+
}>(raw, {});
|
|
241
|
+
const list = parsed.kumiko?.extraRuntimeExternals;
|
|
242
|
+
if (!Array.isArray(list)) return [];
|
|
243
|
+
return list.filter((x): x is string => typeof x === "string" && x.trim().length > 0);
|
|
244
|
+
}
|
|
245
|
+
|
|
227
246
|
// Versionen für RUNTIME_EXTERNALS auflösen: erst aus den installierten
|
|
228
247
|
// node_modules/@cosmicdrift/*-Packages (relativ zu cwd — funktioniert für
|
|
229
248
|
// jede Consumer-App), dann als Zusatz aus packages/framework + bundled-
|
|
@@ -237,21 +256,42 @@ async function resolveRuntimeDepsVersions(
|
|
|
237
256
|
const out: Record<string, string> = {};
|
|
238
257
|
|
|
239
258
|
const pinSources = [
|
|
240
|
-
join(cwd, "node_modules/@cosmicdrift/kumiko-framework/package.json"),
|
|
241
|
-
join(cwd, "node_modules/@cosmicdrift/kumiko-bundled-features/package.json"),
|
|
242
259
|
...(repoRoot
|
|
243
260
|
? [
|
|
244
261
|
join(repoRoot, "packages/framework/package.json"),
|
|
245
262
|
join(repoRoot, "packages/bundled-features/package.json"),
|
|
246
263
|
]
|
|
247
264
|
: []),
|
|
265
|
+
// node_modules applied last among framework sources so installed versions
|
|
266
|
+
// win over the repo-root fallback (#1217).
|
|
267
|
+
join(cwd, "node_modules/@cosmicdrift/kumiko-framework/package.json"),
|
|
268
|
+
join(cwd, "node_modules/@cosmicdrift/kumiko-bundled-features/package.json"),
|
|
248
269
|
];
|
|
249
270
|
const allDeps: Record<string, string> = {};
|
|
250
271
|
for (const path of pinSources) {
|
|
251
272
|
if (!existsSync(path)) continue;
|
|
252
273
|
const raw = await readFile(path, "utf-8");
|
|
253
|
-
const parsed = parseJsonOrThrow<{
|
|
254
|
-
|
|
274
|
+
const parsed = parseJsonOrThrow<{
|
|
275
|
+
dependencies?: Record<string, string>;
|
|
276
|
+
optionalDependencies?: Record<string, string>;
|
|
277
|
+
}>(raw, path);
|
|
278
|
+
Object.assign(allDeps, parsed.dependencies ?? {}, parsed.optionalDependencies ?? {});
|
|
279
|
+
}
|
|
280
|
+
// App package.json fills gaps only — pins extraRuntimeExternals without
|
|
281
|
+
// overriding framework-native pins the app may also list (#1484 / review).
|
|
282
|
+
const appPkg = join(cwd, "package.json");
|
|
283
|
+
if (existsSync(appPkg)) {
|
|
284
|
+
const raw = await readFile(appPkg, "utf-8");
|
|
285
|
+
const parsed = parseJsonOrThrow<{
|
|
286
|
+
dependencies?: Record<string, string>;
|
|
287
|
+
optionalDependencies?: Record<string, string>;
|
|
288
|
+
}>(raw, appPkg);
|
|
289
|
+
for (const [name, ver] of Object.entries({
|
|
290
|
+
...(parsed.dependencies ?? {}),
|
|
291
|
+
...(parsed.optionalDependencies ?? {}),
|
|
292
|
+
})) {
|
|
293
|
+
if (allDeps[name] === undefined) allDeps[name] = ver;
|
|
294
|
+
}
|
|
255
295
|
}
|
|
256
296
|
for (const pkg of packages) {
|
|
257
297
|
out[pkg] = allDeps[pkg] ?? "*";
|
package/src/build.ts
CHANGED
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 {
|