@cosmicdrift/kumiko-dev-server 0.160.0 → 0.162.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.
|
|
3
|
+
"version": "0.162.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": "0.162.0",
|
|
58
|
+
"@cosmicdrift/kumiko-framework": "0.162.0",
|
|
59
|
+
"@cosmicdrift/kumiko-server-runtime": "0.162.0",
|
|
60
60
|
"ts-morph": "^28.0.0"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
3
2
|
import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
4
3
|
import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
4
|
import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
@@ -56,6 +55,7 @@ describe("composeStacks", () => {
|
|
|
56
55
|
"data-retention",
|
|
57
56
|
"compliance-profiles",
|
|
58
57
|
"tenant-lifecycle",
|
|
58
|
+
"auth-foundation",
|
|
59
59
|
"sessions",
|
|
60
60
|
]);
|
|
61
61
|
});
|
|
@@ -87,12 +87,13 @@ describe("composeStacks", () => {
|
|
|
87
87
|
]);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
test("composeIdentityStack defaults sessions
|
|
91
|
-
expect(stackFeatureNames(composeIdentityStack())).toEqual(["sessions"]);
|
|
90
|
+
test("composeIdentityStack defaults auth-foundation + sessions", () => {
|
|
91
|
+
expect(stackFeatureNames(composeIdentityStack())).toEqual(["auth-foundation", "sessions"]);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
test("composeIdentityStack mounts auth-mfa when mfa options given", () => {
|
|
95
95
|
expect(stackFeatureNames(composeIdentityStack({ mfa: TEST_MFA }))).toEqual([
|
|
96
|
+
"auth-foundation",
|
|
96
97
|
"sessions",
|
|
97
98
|
"auth-mfa",
|
|
98
99
|
]);
|
|
@@ -114,7 +115,6 @@ describe("composeStacks", () => {
|
|
|
114
115
|
validateBoot(
|
|
115
116
|
composeFeatures(
|
|
116
117
|
[
|
|
117
|
-
authFoundationFeature,
|
|
118
118
|
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
119
119
|
...composeIdentityStack(),
|
|
120
120
|
...composeGdprStack({ sessions: true }),
|
|
@@ -131,7 +131,6 @@ describe("composeStacks boots for real", () => {
|
|
|
131
131
|
test("studio-shaped combined stack passes validateBoot (not just name-list comparison)", () => {
|
|
132
132
|
const features = composeFeatures(
|
|
133
133
|
[
|
|
134
|
-
authFoundationFeature,
|
|
135
134
|
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
136
135
|
...composeOpsStack({ rateLimiting: true }),
|
|
137
136
|
...composePagesStack(),
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
seedAdmin,
|
|
13
13
|
seedUserWithPassword,
|
|
14
14
|
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
|
|
15
|
-
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
16
15
|
import {
|
|
17
16
|
AuthMfaHandlers,
|
|
18
17
|
base32Decode,
|
|
@@ -118,7 +117,6 @@ beforeAll(async () => {
|
|
|
118
117
|
|
|
119
118
|
const features = composeFeatures(
|
|
120
119
|
[
|
|
121
|
-
authFoundationFeature,
|
|
122
120
|
...identity,
|
|
123
121
|
...composeOpsStack({ delivery: true, audit: false, jobs: false }),
|
|
124
122
|
...composeRendererStack(),
|
package/src/compose-stacks.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// or tier maps (those stay in bin/server.ts / app run-config).
|
|
4
4
|
|
|
5
5
|
import { createAuditFeature } from "@cosmicdrift/kumiko-bundled-features/audit";
|
|
6
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
6
7
|
import {
|
|
7
8
|
type AuthMfaFeatureOptions,
|
|
8
9
|
createAuthMfaFeature,
|
|
@@ -76,10 +77,17 @@ export type OpsStackOptions = {
|
|
|
76
77
|
};
|
|
77
78
|
|
|
78
79
|
/** sessions (+ optional auth-mfa). config/user/tenant/auth-email-password stay
|
|
79
|
-
* on composeFeatures(includeBundled). Pass `mfa` options to mount TOTP.
|
|
80
|
+
* on composeFeatures(includeBundled). Pass `mfa` options to mount TOTP.
|
|
81
|
+
* When `sessions` is on (the default), auth-foundation is mounted alongside it —
|
|
82
|
+
* the framework's registry now hard-requires it (sessions.requires("auth-foundation")).
|
|
83
|
+
* Pass `providers` for the tokenVerifier(s) auth-foundation itself requires at least
|
|
84
|
+
* one of (e.g. createPersonalAccessTokensFeature({ scopes })) — PAT scopes are
|
|
85
|
+
* app-specific (which write-handlers an API token may call), so they stay caller-owned
|
|
86
|
+
* instead of a framework default. */
|
|
80
87
|
export type IdentityStackOptions = {
|
|
81
88
|
readonly sessions?: boolean;
|
|
82
89
|
readonly mfa?: AuthMfaFeatureOptions;
|
|
90
|
+
readonly providers?: readonly FeatureDefinition[];
|
|
83
91
|
};
|
|
84
92
|
|
|
85
93
|
export function stackFeatureNames(features: readonly FeatureDefinition[]): string[] {
|
|
@@ -131,7 +139,7 @@ export function composeGdprStack(options: GdprStackOptions = {}): FeatureDefinit
|
|
|
131
139
|
const out: FeatureDefinition[] =
|
|
132
140
|
order === "compliance-first" ? [compliance, retention] : [retention, compliance];
|
|
133
141
|
if (options.tenantLifecycle) out.push(createTenantLifecycleFeature());
|
|
134
|
-
if (options.sessions) out.push(createSessionsFeature());
|
|
142
|
+
if (options.sessions) out.push(authFoundationFeature, createSessionsFeature());
|
|
135
143
|
return out;
|
|
136
144
|
}
|
|
137
145
|
|
|
@@ -162,7 +170,10 @@ export function composeOpsStack(options: OpsStackOptions = {}): FeatureDefinitio
|
|
|
162
170
|
export function composeIdentityStack(options: IdentityStackOptions = {}): FeatureDefinition[] {
|
|
163
171
|
const sessions = options.sessions ?? true;
|
|
164
172
|
const out: FeatureDefinition[] = [];
|
|
165
|
-
if (sessions)
|
|
173
|
+
if (sessions) {
|
|
174
|
+
out.push(authFoundationFeature, createSessionsFeature());
|
|
175
|
+
if (options.providers) out.push(...options.providers);
|
|
176
|
+
}
|
|
166
177
|
if (options.mfa !== undefined) out.push(createAuthMfaFeature(options.mfa));
|
|
167
178
|
return out;
|
|
168
179
|
}
|
|
@@ -19,6 +19,7 @@ import { existsSync, mkdtempSync, statSync } from "node:fs";
|
|
|
19
19
|
import { readFile, watch } from "node:fs/promises";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { join, resolve } from "node:path";
|
|
22
|
+
import { resolveAnonymousAccessFromRegistry } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
22
23
|
import { type AuthRoutesConfig, generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
23
24
|
import { buildAppSchema, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
24
25
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -673,6 +674,7 @@ export async function createKumikoServer(
|
|
|
673
674
|
...(options.auth !== undefined && { authConfig: options.auth }),
|
|
674
675
|
...(options.extraContext !== undefined && { extraContext: options.extraContext }),
|
|
675
676
|
...(options.anonymousAccess !== undefined && { anonymousAccess: options.anonymousAccess }),
|
|
677
|
+
enrichAnonymousAccess: (base, deps) => resolveAnonymousAccessFromRegistry(base, deps),
|
|
676
678
|
...(options.files !== undefined && { files: options.files }),
|
|
677
679
|
...(options.effectiveFeatures !== undefined && {
|
|
678
680
|
effectiveFeatures: options.effectiveFeatures,
|
package/src/run-dev-app.ts
CHANGED
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
type SeedAdminOptions,
|
|
19
19
|
seedAdmin,
|
|
20
20
|
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
EXT_SESSION_STORE,
|
|
23
|
+
resolveSessionStore,
|
|
24
|
+
resolveTokenVerifier,
|
|
25
|
+
type SessionStore,
|
|
26
|
+
} from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
22
27
|
import {
|
|
23
28
|
AUTH_MFA_FEATURE,
|
|
24
29
|
AuthMfaHandlers,
|
|
@@ -34,9 +39,7 @@ import {
|
|
|
34
39
|
} from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
35
40
|
import {
|
|
36
41
|
bindAutoRevokeFromFeature,
|
|
37
|
-
createSessionCallbacks,
|
|
38
42
|
SESSIONS_FEATURE,
|
|
39
|
-
type SessionCallbacks,
|
|
40
43
|
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
41
44
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
42
45
|
import {
|
|
@@ -118,17 +121,6 @@ export type RunDevAppAuthOptions = {
|
|
|
118
121
|
/** Optional override of the login error → HTTP status map. Default
|
|
119
122
|
* maps invalidCredentials → 401, noMembership → 403. */
|
|
120
123
|
readonly loginErrorStatusMap?: Readonly<Record<string, number>>;
|
|
121
|
-
/** Opt-in: revocable server-side sessions. Caller MUSS
|
|
122
|
-
* `createSessionsFeature()` zu `features` adden — runDevApp wired
|
|
123
|
-
* hier nur die Auth-Callbacks (creator/revoker/checker) gegen
|
|
124
|
-
* stack.db (sidless JWTs werden dann abgelehnt).
|
|
125
|
-
*
|
|
126
|
-
* Standardverhalten ohne diese Option: stateless JWTs ohne sid,
|
|
127
|
-
* Logout ist client-side cookie-clear, Kartenhaus existing-Apps
|
|
128
|
-
* bleibt unangefasst. */
|
|
129
|
-
readonly sessions?: {
|
|
130
|
-
readonly expiresInMs?: number;
|
|
131
|
-
};
|
|
132
124
|
/** Auth-Mail-Convenience — symmetrisch zu RunProdAppAuthOptions.mail.
|
|
133
125
|
* Verdrahtet alle 4 Mail-Flows aus env-SMTP + Standard-Templates;
|
|
134
126
|
* hmacSecret = `JWT_SECRET`-env (Dev-Fallback wenn ungesetzt). Ohne
|
|
@@ -395,19 +387,15 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
395
387
|
return { ...boot, ...base };
|
|
396
388
|
};
|
|
397
389
|
|
|
398
|
-
// Sessions
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
let sessionCallbacks: SessionCallbacks | undefined;
|
|
406
|
-
const requireSessions = (): SessionCallbacks => {
|
|
407
|
-
if (!sessionCallbacks) {
|
|
408
|
-
throw new Error("[runDevApp] session-callbacks accessed before onAfterSetup");
|
|
390
|
+
// Sessions: late-bound holder — resolveSessionStore needs concrete db+
|
|
391
|
+
// registry (only after setupTestStack). Wired when sessions feature is
|
|
392
|
+
// mounted (#1372); no auth.sessions opt-in anymore.
|
|
393
|
+
let sessionStore: SessionStore | undefined;
|
|
394
|
+
const requireSessionStore = (): SessionStore => {
|
|
395
|
+
if (!sessionStore) {
|
|
396
|
+
throw new Error("[runDevApp] sessionStore accessed before onAfterSetup");
|
|
409
397
|
}
|
|
410
|
-
return
|
|
398
|
+
return sessionStore;
|
|
411
399
|
};
|
|
412
400
|
// Token-verifier opt-in: same late-bound holder pattern — resolveTokenVerifier
|
|
413
401
|
// needs the real db+registry (only concrete after setupTestStack). Wired
|
|
@@ -444,14 +432,19 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
444
432
|
}
|
|
445
433
|
: {};
|
|
446
434
|
|
|
435
|
+
// Parity with runProdApp (#1372 review): gate on sessionStore provider, not
|
|
436
|
+
// the sessions feature name — a custom provider must also wire auth callbacks.
|
|
437
|
+
const sessionStoreProviderMounted = features.some((f) =>
|
|
438
|
+
f.extensionUsages.some((u) => u.extensionName === EXT_SESSION_STORE),
|
|
439
|
+
);
|
|
447
440
|
const sessionAuthFragment =
|
|
448
|
-
effectiveAuth
|
|
441
|
+
effectiveAuth && sessionStoreProviderMounted
|
|
449
442
|
? {
|
|
450
443
|
sessionCreator: (user: SessionUser, meta: SessionMetadata) =>
|
|
451
|
-
|
|
452
|
-
sessionRevoker: (sid: string) =>
|
|
444
|
+
requireSessionStore().creator(user, meta),
|
|
445
|
+
sessionRevoker: (sid: string) => requireSessionStore().revoker(sid),
|
|
453
446
|
sessionChecker: (sid: string, userId: string) =>
|
|
454
|
-
|
|
447
|
+
requireSessionStore().checker(sid, userId),
|
|
455
448
|
}
|
|
456
449
|
: {};
|
|
457
450
|
|
|
@@ -494,7 +487,11 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
494
487
|
...sessionAuthFragment,
|
|
495
488
|
...patAuthFragment,
|
|
496
489
|
...tenantLifecycleAuthFragment,
|
|
497
|
-
...(mfaFeature && {
|
|
490
|
+
...(mfaFeature && {
|
|
491
|
+
mfaVerifyHandler: AuthMfaHandlers.verify,
|
|
492
|
+
mfaPreauthEnableStartHandler: AuthMfaHandlers.enableStartPreauth,
|
|
493
|
+
mfaPreauthConfirmHandler: AuthMfaHandlers.enableConfirmPreauth,
|
|
494
|
+
}),
|
|
498
495
|
...(effectiveAuth.passwordReset && {
|
|
499
496
|
passwordReset: {
|
|
500
497
|
requestHandler: AuthHandlers.requestPasswordReset,
|
|
@@ -540,26 +537,21 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
540
537
|
registry: stack.registry,
|
|
541
538
|
});
|
|
542
539
|
}
|
|
543
|
-
if (effectiveAuth
|
|
544
|
-
|
|
545
|
-
|
|
540
|
+
if (effectiveAuth && stack.registry.getExtensionUsages(EXT_SESSION_STORE).length > 0) {
|
|
541
|
+
// Secure-by-default (#1372): resolve sessionStore provider; bind
|
|
542
|
+
// password-change mass-revoke + MFA revokeAllOthers.
|
|
543
|
+
const store = await resolveSessionStore({
|
|
546
544
|
db: stack.db,
|
|
547
|
-
|
|
545
|
+
registry: stack.registry,
|
|
548
546
|
});
|
|
549
|
-
// Secure-by-default (symmetrisch zu runProdApp): Password-Change/
|
|
550
|
-
// -Reset mass-revoked die Sessions des Users ohne App-Opt-in.
|
|
551
547
|
const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
|
|
552
548
|
if (sessionsFeature) {
|
|
553
|
-
bindAutoRevokeFromFeature(sessionsFeature)?.(
|
|
549
|
+
bindAutoRevokeFromFeature(sessionsFeature)?.(store.massRevoker);
|
|
554
550
|
}
|
|
555
|
-
// MFA enable/disable/regenerate mass-revokes every OTHER live
|
|
556
|
-
// session (stolen-session defense) — only wired when auth-mfa is
|
|
557
|
-
// mounted.
|
|
558
551
|
if (mfaFeature) {
|
|
559
|
-
bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(
|
|
560
|
-
sessionCallbacks.sessionRevokeAllOthers,
|
|
561
|
-
);
|
|
552
|
+
bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(store.revokeAllOthers);
|
|
562
553
|
}
|
|
554
|
+
sessionStore = store;
|
|
563
555
|
}
|
|
564
556
|
if (patFeature) {
|
|
565
557
|
tokenVerifier = (rawToken) =>
|