@cosmicdrift/kumiko-dev-server 0.109.0 → 0.111.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.109.0",
3
+ "version": "0.111.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.109.0",
54
- "@cosmicdrift/kumiko-framework": "0.109.0",
53
+ "@cosmicdrift/kumiko-bundled-features": "0.111.0",
54
+ "@cosmicdrift/kumiko-framework": "0.111.0",
55
55
  "ts-morph": "^28.0.0"
56
56
  },
57
57
  "publishConfig": {
@@ -1,6 +1,7 @@
1
1
  import { seedAllConfigValues } from "@cosmicdrift/kumiko-bundled-features/config";
2
- import type { DbConnection, EncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
4
5
 
5
6
  // Single boot-seed entry-point. runDevApp + runProdApp both call this
6
7
  // from their post-stack hook, so the wiring lives in exactly one place
@@ -12,7 +13,7 @@ import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
12
13
  export async function applyBootSeeds(deps: {
13
14
  registry: Registry;
14
15
  db: DbConnection;
15
- encryption?: EncryptionProvider;
16
+ cipher?: EnvelopeCipher;
16
17
  }): Promise<void> {
17
- await seedAllConfigValues(deps.registry, deps.db, deps.encryption);
18
+ await seedAllConfigValues(deps.registry, deps.db, deps.cipher);
18
19
  }
@@ -0,0 +1,82 @@
1
+ // One place that turns env/options into the app-wide crypto objects:
2
+ // the MasterKeyProvider (KUMIKO_SECRETS_MASTER_KEY_V<n> keyring) and the
3
+ // EnvelopeCipher for `encrypted: true` config keys. runProdApp and
4
+ // runDevApp both resolve this once and thread it into
5
+ // buildBootExtraContext + applyBootSeeds so resolver, set-handler and
6
+ // seeds share the same cipher instance (and DEK cache).
7
+
8
+ import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
9
+ import {
10
+ createDekCache,
11
+ createEnvelopeCipher,
12
+ createEnvMasterKeyProvider,
13
+ type DekCache,
14
+ type EnvelopeCipher,
15
+ type MasterKeyProvider,
16
+ } from "@cosmicdrift/kumiko-framework/secrets";
17
+
18
+ // Keyring detection without throwing: only build the env provider when at
19
+ // least one versioned KEK is actually set. Covers (a) apps without any
20
+ // encryption — no KEK requirement, and (b) dev with an app-supplied
21
+ // provider in options.masterKey — env stays irrelevant.
22
+ const MASTER_KEK_VAR = /^KUMIKO_SECRETS_MASTER_KEY_V\d+$/;
23
+ export function envHasMasterKek(env: Record<string, string | undefined>): boolean {
24
+ return Object.entries(env).some(([k, v]) => MASTER_KEK_VAR.test(k) && !!v);
25
+ }
26
+
27
+ export type BootCrypto = {
28
+ readonly masterKeyProvider?: MasterKeyProvider;
29
+ // Cipher for encrypted config keys. Present exactly when a master key is
30
+ // available. Decrypts legacy CONFIG_ENCRYPTION_KEY values as fallback
31
+ // until the config re-encrypt job migrated them.
32
+ readonly configCipher?: EnvelopeCipher;
33
+ // Cipher for `encrypted: true` entity fields — same master key, but the
34
+ // legacy fallback reads the pre-envelope ENCRYPTION_KEY format. Identical
35
+ // to configCipher when no ENCRYPTION_KEY is set.
36
+ readonly entityFieldCipher?: EnvelopeCipher;
37
+ readonly dekCache: DekCache;
38
+ };
39
+
40
+ export function resolveBootCrypto(
41
+ envSource: Record<string, string | undefined>,
42
+ masterKeyOverride?: MasterKeyProvider,
43
+ ): BootCrypto {
44
+ const masterKeyProvider =
45
+ masterKeyOverride ??
46
+ (envHasMasterKek(envSource)
47
+ ? createEnvMasterKeyProvider({
48
+ // CURRENT_VERSION default "1" spiegelt secretsEnvSchema — ohne
49
+ // ihn wirft der raw-env-Provider, obwohl V1 gesetzt ist.
50
+ env: {
51
+ ...envSource,
52
+ KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION:
53
+ envSource["KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION"] ?? "1",
54
+ },
55
+ })
56
+ : undefined);
57
+
58
+ const dekCache = createDekCache();
59
+ const legacyConfigKey = envSource["CONFIG_ENCRYPTION_KEY"];
60
+ const configCipher = masterKeyProvider
61
+ ? createEnvelopeCipher(masterKeyProvider, {
62
+ dekCache,
63
+ ...(legacyConfigKey ? { legacy: createEncryptionProvider(legacyConfigKey) } : {}),
64
+ })
65
+ : undefined;
66
+
67
+ const legacyEntityKey = envSource["ENCRYPTION_KEY"];
68
+ const entityFieldCipher =
69
+ masterKeyProvider && legacyEntityKey
70
+ ? createEnvelopeCipher(masterKeyProvider, {
71
+ dekCache,
72
+ legacy: createEncryptionProvider(legacyEntityKey),
73
+ })
74
+ : configCipher;
75
+
76
+ return {
77
+ ...(masterKeyProvider && { masterKeyProvider }),
78
+ ...(configCipher && { configCipher }),
79
+ ...(entityFieldCipher && { entityFieldCipher }),
80
+ dekCache,
81
+ };
82
+ }
@@ -29,12 +29,15 @@ import {
29
29
  patScopesFromFeature,
30
30
  } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
31
31
  import {
32
+ bindAutoRevokeFromFeature,
32
33
  createSessionCallbacks,
34
+ SESSIONS_FEATURE,
33
35
  type SessionCallbacks,
34
36
  } from "@cosmicdrift/kumiko-bundled-features/sessions";
35
37
  import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
36
38
  import type { PatResolver, SessionMetadata } from "@cosmicdrift/kumiko-framework/api";
37
39
  import { createInMemoryLoginRateLimiter } from "@cosmicdrift/kumiko-framework/api";
40
+ import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
38
41
  import {
39
42
  collectWriteHandlerQns,
40
43
  createRegistry,
@@ -48,10 +51,11 @@ import {
48
51
  validateAppCustomScreenWriteQns,
49
52
  validateBoot,
50
53
  } from "@cosmicdrift/kumiko-framework/engine";
51
- import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
54
+ import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
52
55
  import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
53
56
  import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
54
57
  import { applyBootSeeds } from "./boot/apply-boot-seeds";
58
+ import { resolveBootCrypto } from "./boot/boot-crypto";
55
59
 
56
60
  import { watchAndRegenerate } from "./codegen";
57
61
  import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
@@ -309,8 +313,17 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
309
313
  // throwaway-Registry hier extrahiert nur die config-Keys, weil der
310
314
  // configResolver vor dem Server-Boot konstruiert wird (stack.registry
311
315
  // gibt's erst onAfterSetup) — createKumikoServer baut intern seine eigene.
316
+ const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
317
+ // App-wide cipher for `encrypted: true` entity fields (symmetrisch zu
318
+ // runProdApp) — executors resolve it lazily.
319
+ configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
312
320
  const cfgExtra = effectiveAuth
313
- ? mergeConfigResolverDefault(options.extraContext, createRegistry(features), envSource)
321
+ ? mergeConfigResolverDefault(
322
+ options.extraContext,
323
+ createRegistry(features),
324
+ envSource,
325
+ bootCrypto.configCipher,
326
+ )
314
327
  : options.extraContext;
315
328
  // Auto-wire textContent (immer) + secrets (feature-gated), symmetrisch zu
316
329
  // runProdApp. Anders als prod existiert die db hier erst im Factory-deps
@@ -325,7 +338,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
325
338
  registry: deps.registry,
326
339
  hasAuth: false,
327
340
  sseBroker: deps.sseBroker,
328
- ...(options.masterKey && { masterKey: options.masterKey }),
341
+ crypto: bootCrypto,
329
342
  });
330
343
  const base = typeof cfgExtra === "function" ? cfgExtra(deps) : (cfgExtra ?? {});
331
344
  return { ...boot, ...base };
@@ -463,6 +476,12 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
463
476
  db: stack.db,
464
477
  ...(expiresInMs !== undefined && { expiresInMs }),
465
478
  });
479
+ // Secure-by-default (symmetrisch zu runProdApp): Password-Change/
480
+ // -Reset mass-revoked die Sessions des Users ohne App-Opt-in.
481
+ const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
482
+ if (sessionsFeature) {
483
+ bindAutoRevokeFromFeature(sessionsFeature)?.(sessionCallbacks.sessionMassRevoker);
484
+ }
466
485
  }
467
486
  if (patFeature) {
468
487
  patResolver = createPatResolver({ db: stack.db, scopes: patScopesFromFeature(patFeature) });
@@ -474,7 +493,11 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
474
493
  // Runs before user-supplied seed callbacks so those can read /
475
494
  // override the deploy-defaults. The helper indirection is what
476
495
  // config-seed-boot.integration.ts pins — keep it as a single call.
477
- await applyBootSeeds({ registry: stack.registry, db: stack.db });
496
+ await applyBootSeeds({
497
+ registry: stack.registry,
498
+ db: stack.db,
499
+ ...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
500
+ });
478
501
  for (const seed of options.seeds ?? []) {
479
502
  await seed(stack);
480
503
  }
@@ -510,10 +533,12 @@ export function mergeConfigResolverDefault(
510
533
  ctx: CreateKumikoServerOptions["extraContext"],
511
534
  registry: Registry,
512
535
  envSource: Record<string, string | undefined>,
536
+ cipher?: EnvelopeCipher,
513
537
  ): CreateKumikoServerOptions["extraContext"] {
514
538
  const defaults = {
515
539
  configResolver: createConfigResolver({
516
540
  appOverrides: buildEnvConfigOverrides(registry, envSource),
541
+ ...(cipher && { cipher }),
517
542
  }),
518
543
  };
519
544
  // ctx.config wird per-Request aus _configAccessorFactory geminted (siehe
@@ -61,6 +61,7 @@ import {
61
61
  SECRETS_FEATURE_NAME,
62
62
  } from "@cosmicdrift/kumiko-bundled-features/secrets";
63
63
  import {
64
+ bindAutoRevokeFromFeature,
64
65
  createSessionCallbacks,
65
66
  SESSIONS_FEATURE,
66
67
  } from "@cosmicdrift/kumiko-bundled-features/sessions";
@@ -77,6 +78,7 @@ import {
77
78
  type SseBroker,
78
79
  } from "@cosmicdrift/kumiko-framework/api";
79
80
  import {
81
+ configureEntityFieldEncryption,
80
82
  createDbConnection,
81
83
  type DbConnection,
82
84
  type DbRunner,
@@ -122,13 +124,11 @@ import {
122
124
  createEventDedup,
123
125
  createIdempotencyGuard,
124
126
  } from "@cosmicdrift/kumiko-framework/pipeline";
125
- import {
126
- createEnvMasterKeyProvider,
127
- type MasterKeyProvider,
128
- } from "@cosmicdrift/kumiko-framework/secrets";
127
+ import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
129
128
  import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
130
129
  import Redis from "ioredis";
131
130
  import { applyBootSeeds } from "./boot/apply-boot-seeds";
131
+ import { type BootCrypto, resolveBootCrypto } from "./boot/boot-crypto";
132
132
  import { ASSETS_DIR } from "./build-prod-bundle";
133
133
  import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
134
134
  import { type ExtraRoutesSystemDeps, makeDispatchSystemWrite } from "./extra-routes-deps";
@@ -596,18 +596,13 @@ export function addConfigAccessorFactory<T extends { readonly configResolver?: C
596
596
  // nur einen db-gebundenen Accessor). secrets wird nur auto-verdrahtet wenn
597
597
  // das secrets-Feature gemountet ist UND ein KEK tatsächlich verfügbar ist
598
598
  // (masterKey-Override ODER env-KEK present) — sonst skip, damit der eager
599
- // createEnvMasterKeyProvider nicht wirft. Das deckt zwei Fälle: (a) App ohne
600
- // secrets kein KEK-Zwang; (b) dev mit App-eigenem DEV-KEK in extraContext
601
- // (kein env-KEK) → kein Boot-Crash, die App-explizite secrets-Wiring gewinnt.
599
+ // KEK-Detection + Provider/Cipher-Aufbau leben in boot/boot-crypto.ts
600
+ // (envHasMasterKek, resolveBootCrypto) gemeinsam mit runDevApp.
602
601
  // Prod-Misconfig (secrets gemountet, kein KEK) fängt schon secretsEnvSchema
603
602
  // beim Boot; fehlt der env-Schema-Pfad, wirft requireSecretsContext beim
604
603
  // ersten ctx.secrets-Zugriff mit Wiring-Hinweis. configResolver nur im
605
604
  // Auth-Mode. Exportiert + pure für Unit-Tests; der merge mit App-Werten
606
605
  // passiert beim Caller (App gewinnt).
607
- const MASTER_KEK_VAR = /^KUMIKO_SECRETS_MASTER_KEY_V\d+$/;
608
- function envHasMasterKek(env: Record<string, string | undefined>): boolean {
609
- return Object.entries(env).some(([k, v]) => MASTER_KEK_VAR.test(k) && !!v);
610
- }
611
606
 
612
607
  // Prod/dev parity for ctx.notify: without this `_notifyFactory` is only wired
613
608
  // in tests (createDeliveryTestContext), so ctx.notify is undefined at runtime
@@ -634,12 +629,16 @@ export function buildBootExtraContext(opts: {
634
629
  readonly envSource: Record<string, string | undefined>;
635
630
  readonly registry: Registry;
636
631
  readonly hasAuth: boolean;
632
+ // Resolved once per boot via resolveBootCrypto — shared by secrets,
633
+ // config-resolver, config-set-handler and boot-seeds. Absent (tests
634
+ // that don't care about encryption) ⇒ resolved from envSource here.
635
+ readonly crypto?: BootCrypto;
637
636
  readonly masterKey?: MasterKeyProvider;
638
637
  readonly sseBroker?: SseBroker;
639
638
  }): Record<string, unknown> {
639
+ const crypto = opts.crypto ?? resolveBootCrypto(opts.envSource, opts.masterKey);
640
640
  const hasSecretsFeature = opts.features.some((f) => f.name === SECRETS_FEATURE_NAME);
641
- const wireSecrets =
642
- hasSecretsFeature && (opts.masterKey !== undefined || envHasMasterKek(opts.envSource));
641
+ const wireSecrets = hasSecretsFeature && crypto.masterKeyProvider !== undefined;
643
642
  const hasDeliveryFeature = opts.features.some((f) => f.name === DELIVERY_FEATURE);
644
643
  return {
645
644
  textContent: createTextContentApi(opts.db),
@@ -650,25 +649,25 @@ export function buildBootExtraContext(opts: {
650
649
  ...(opts.sseBroker && { sseBroker: opts.sseBroker }),
651
650
  }),
652
651
  }),
653
- ...(wireSecrets && {
654
- secrets: createSecretsContext({
655
- db: opts.db,
656
- masterKeyProvider:
657
- opts.masterKey ??
658
- createEnvMasterKeyProvider({
659
- // CURRENT_VERSION default "1" spiegelt secretsEnvSchema — ohne
660
- // ihn wirft der raw-env-Provider, obwohl V1 gesetzt ist.
661
- env: {
662
- ...opts.envSource,
663
- KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION:
664
- opts.envSource["KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION"] ?? "1",
665
- },
666
- }),
652
+ // Top-level provider so feature jobs (secrets rotate, config reencrypt)
653
+ // reach it via ctx — previously only test-stack wired it.
654
+ ...(crypto.masterKeyProvider && { masterKeyProvider: crypto.masterKeyProvider }),
655
+ // Encrypt/decrypt partner for `encrypted: true` config keys. Wired
656
+ // whenever a master key exists — NOT gated on the secrets feature,
657
+ // config encryption must work without mounting ctx.secrets.
658
+ ...(crypto.configCipher && { configEncryption: crypto.configCipher }),
659
+ ...(wireSecrets &&
660
+ crypto.masterKeyProvider && {
661
+ secrets: createSecretsContext({
662
+ db: opts.db,
663
+ masterKeyProvider: crypto.masterKeyProvider,
664
+ dekCache: crypto.dekCache,
665
+ }),
667
666
  }),
668
- }),
669
667
  ...(opts.hasAuth && {
670
668
  configResolver: createConfigResolver({
671
669
  appOverrides: buildEnvConfigOverrides(opts.registry, opts.envSource),
670
+ ...(crypto.configCipher && { cipher: crypto.configCipher }),
672
671
  }),
673
672
  }),
674
673
  };
@@ -919,6 +918,10 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
919
918
 
920
919
  // Framework-Default-Provider zuerst, App-Werte (resolvedExtraContext)
921
920
  // gewinnen immer (z.B. money-horse's eigener configResolver).
921
+ const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
922
+ // App-wide cipher for `encrypted: true` entity fields — executors resolve
923
+ // it lazily, entities without encrypted fields never touch it.
924
+ configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
922
925
  const autoExtraContext = buildBootExtraContext({
923
926
  db,
924
927
  features,
@@ -926,7 +929,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
926
929
  registry,
927
930
  hasAuth: !!effectiveAuth,
928
931
  sseBroker,
929
- ...(options.masterKey && { masterKey: options.masterKey }),
932
+ crypto: bootCrypto,
930
933
  });
931
934
  const extraContext = addConfigAccessorFactory(
932
935
  { ...autoExtraContext, ...resolvedExtraContext },
@@ -942,19 +945,20 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
942
945
  // sessionStrictMode=true: Prod-Sessions sollen nicht stillschweigend
943
946
  // von einem JWT-ohne-sid umgangen werden können. sessionMassRevoker
944
947
  // (4. callback aus createSessionCallbacks) ist nicht Teil der
945
- // AuthRoutesConfig-Surface — der wird vom sessions-Feature selbst über
946
- // die `autoRevokeOnPasswordChange`-Option konsumiert, nicht über die
947
- // auth-routes.
948
+ // AuthRoutesConfig-Surface — der geht via bindAutoRevokeFromFeature ans
949
+ // sessions-Feature (Password-Change/-Reset revoked alle Sessions), nicht
950
+ // über die auth-routes.
948
951
  // Secure-by-default: if the sessions feature is mounted, server-side revocation +
949
- // sessionStrictMode are wired automatically; `auth.sessions` only overrides the config,
950
- // and `auth.sessions: false` is the explicit opt-out (back to stateless JWTs).
951
- const sessionsFeatureMounted = features.some((f) => f.name === SESSIONS_FEATURE);
952
+ // sessionStrictMode + auto-revoke-on-password-change are wired automatically;
953
+ // `auth.sessions` only overrides the config, and `auth.sessions: false` is the
954
+ // explicit opt-out (back to stateless JWTs).
955
+ const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
952
956
  const sessionAuthFragment = shouldWireProdSessions(
953
957
  Boolean(effectiveAuth),
954
- sessionsFeatureMounted,
958
+ sessionsFeature !== undefined,
955
959
  effectiveAuth?.sessions,
956
960
  )
957
- ? buildProdSessionAuth(db, resolveProdSessionsConfig(effectiveAuth?.sessions))
961
+ ? buildProdSessionAuth(db, resolveProdSessionsConfig(effectiveAuth?.sessions), sessionsFeature)
958
962
  : undefined;
959
963
 
960
964
  // PAT opt-in: if the personal-access-tokens feature is mounted, wire its
@@ -1104,7 +1108,11 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
1104
1108
  if (effectiveAuth) {
1105
1109
  await seedAdmin(db, effectiveAuth.admin);
1106
1110
  }
1107
- await applyBootSeeds({ registry, db });
1111
+ await applyBootSeeds({
1112
+ registry,
1113
+ db,
1114
+ ...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
1115
+ });
1108
1116
  for (const seed of options.seeds ?? []) {
1109
1117
  await seed({ db });
1110
1118
  }
@@ -1492,6 +1500,7 @@ export function staticCachePolicy(pathname: string): CachePolicy {
1492
1500
  function buildProdSessionAuth(
1493
1501
  db: import("@cosmicdrift/kumiko-framework/db").DbConnection,
1494
1502
  opts: ProdSessionsConfig,
1503
+ sessionsFeature: import("@cosmicdrift/kumiko-framework/engine").FeatureDefinition | undefined,
1495
1504
  ): {
1496
1505
  readonly sessionCreator: ReturnType<typeof createSessionCallbacks>["sessionCreator"];
1497
1506
  readonly sessionRevoker: ReturnType<typeof createSessionCallbacks>["sessionRevoker"];
@@ -1502,6 +1511,11 @@ function buildProdSessionAuth(
1502
1511
  db,
1503
1512
  ...(opts.expiresInMs !== undefined && { expiresInMs: opts.expiresInMs }),
1504
1513
  });
1514
+ // Secure-by-default: password-change/-reset mass-revokes the user's live
1515
+ // sessions without the app opting in via autoRevokeOnPasswordChange.
1516
+ if (sessionsFeature) {
1517
+ bindAutoRevokeFromFeature(sessionsFeature)?.(cbs.sessionMassRevoker);
1518
+ }
1505
1519
  return {
1506
1520
  sessionCreator: cbs.sessionCreator,
1507
1521
  sessionRevoker: cbs.sessionRevoker,