@cosmicdrift/kumiko-bundled-features 0.217.0 → 0.218.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-bundled-features",
3
- "version": "0.217.0",
3
+ "version": "0.218.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -126,12 +126,12 @@
126
126
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
127
127
  },
128
128
  "dependencies": {
129
- "@cosmicdrift/kumiko-dispatcher-live": "0.217.0",
130
- "@cosmicdrift/kumiko-framework": "0.217.0",
131
- "@cosmicdrift/kumiko-headless": "0.217.0",
132
- "@cosmicdrift/kumiko-renderer": "0.217.0",
133
- "@cosmicdrift/kumiko-renderer-web": "0.217.0",
134
- "@cosmicdrift/kumiko-types": "0.217.0",
129
+ "@cosmicdrift/kumiko-dispatcher-live": "0.218.0",
130
+ "@cosmicdrift/kumiko-framework": "0.218.0",
131
+ "@cosmicdrift/kumiko-headless": "0.218.0",
132
+ "@cosmicdrift/kumiko-renderer": "0.218.0",
133
+ "@cosmicdrift/kumiko-renderer-web": "0.218.0",
134
+ "@cosmicdrift/kumiko-types": "0.218.0",
135
135
  "@mollie/api-client": "^4.5.0",
136
136
  "@node-rs/argon2": "^2.0.2",
137
137
  "@types/mailparser": "^3.4.6",
@@ -160,7 +160,7 @@
160
160
  "devDependencies": {
161
161
  "@testing-library/user-event": "^14.6.1",
162
162
  "@types/qrcode": "^1.5.5",
163
- "@cosmicdrift/kumiko-locale-de": "0.217.0",
164
- "@cosmicdrift/kumiko-locale-es": "0.217.0"
163
+ "@cosmicdrift/kumiko-locale-de": "0.218.0",
164
+ "@cosmicdrift/kumiko-locale-es": "0.218.0"
165
165
  }
166
166
  }
@@ -275,7 +275,7 @@ describe("POST /api/auth/signup-confirm", () => {
275
275
  expect(body.token).toBeTruthy();
276
276
  expect(body.user?.id).toBeTruthy();
277
277
  expect(body.user?.tenantId).toBeTruthy();
278
- expect(body.user?.roles).toContain("Admin");
278
+ expect(body.user?.roles).toContain("TenantAdmin");
279
279
  expect(body.tenantKey).toBeTruthy();
280
280
 
281
281
  // Cookies gesetzt (auth + csrf)
@@ -299,7 +299,7 @@ describe("POST /api/auth/signup-confirm", () => {
299
299
  expect(memberships).toHaveLength(1);
300
300
  const rolesRaw = memberships[0]?.["roles"];
301
301
  if (typeof rolesRaw === "string") {
302
- expect(JSON.parse(rolesRaw) as string[]).toContain("Admin");
302
+ expect(JSON.parse(rolesRaw) as string[]).toContain("TenantAdmin");
303
303
  }
304
304
 
305
305
  // #1463 regression: seedTenant fires the tenant entity's postSave
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.218.0",
4
+ "type": "improvement",
5
+ "title": "Self-signup provisions TenantAdmin on the new tenant",
6
+ "detail": "INITIAL_SIGNUP_ROLES is TenantAdmin (was Admin) so the first user on a new tenant can invite peers and manage memberships without elevation dead-ends."
7
+ },
2
8
  {
3
9
  "version": "0.165.1",
4
10
  "type": "breaking",
@@ -10,8 +16,8 @@
10
16
  "version": "0.167.1",
11
17
  "type": "improvement",
12
18
  "title": "accountLockout is reachable through runProdApp/runDevApp (fw#1627).",
13
- "detail": "accountLockout was an option on createAuthEmailPasswordFeature, but both wrappers mount auth-email-password themselves and their auth options only knew accountUnlock — an app on either entrypoint had no way to turn brute-force protection on. RunProdAppAuthOptions and RunDevAppAuthOptions now carry accountLockout and composeFeatures passes it through.",
14
- "migration": "Opt-in, nothing breaks without it. Two things to know before relying on it: without ctx.redis the lockout is silently skipped in login.write.ts — no error, no warning, no protection — so treat Redis as a deploy invariant, and prove the lockout with a behavioural test instead of assuming the mount is enough."
19
+ "detail": "accountLockout was an option on createAuthEmailPasswordFeature, but both wrappers mount auth-email-password themselves and their auth options only knew accountUnlock \u2014 an app on either entrypoint had no way to turn brute-force protection on. RunProdAppAuthOptions and RunDevAppAuthOptions now carry accountLockout and composeFeatures passes it through.",
20
+ "migration": "Opt-in, nothing breaks without it. Two things to know before relying on it: without ctx.redis the lockout is silently skipped in login.write.ts \u2014 no error, no warning, no protection \u2014 so treat Redis as a deploy invariant, and prove the lockout with a behavioural test instead of assuming the mount is enough."
15
21
  },
16
22
  {
17
23
  "version": "0.162.0",
@@ -22,19 +28,19 @@
22
28
  {
23
29
  "version": "0.154.1",
24
30
  "type": "fix",
25
- "title": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled…",
26
- "detail": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled-features/auth-email-password/web`. Every other auth screen (ForgotPasswordScreen, SignupScreen, ResetPasswordScreen, …) was already exported from the barrel with its props type; `LoginScreen` was missed. `makeAuthGate`'s second parameter is typed `LoginScreenProps`, so consumers passing a typed `loginScreenProps` override (e.g. `@cosmicdriftgamestudio/kumiko-designer`) couldn't import the type at all. - @cosmicdrift/kumiko-framework@0.154.1 - @cosmicdrift/kumiko-dispatcher-live@0.154.1 - @cosmicdrift/kumiko-headless@0.154.1 - @cosmicdrift/kumiko-renderer@0.154.1 - @cosmicdrift/kumiko-renderer-web@0.154.1"
31
+ "title": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled\u2026",
32
+ "detail": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled-features/auth-email-password/web`. Every other auth screen (ForgotPasswordScreen, SignupScreen, ResetPasswordScreen, \u2026) was already exported from the barrel with its props type; `LoginScreen` was missed. `makeAuthGate`'s second parameter is typed `LoginScreenProps`, so consumers passing a typed `loginScreenProps` override (e.g. `@cosmicdriftgamestudio/kumiko-designer`) couldn't import the type at all. - @cosmicdrift/kumiko-framework@0.154.1 - @cosmicdrift/kumiko-dispatcher-live@0.154.1 - @cosmicdrift/kumiko-headless@0.154.1 - @cosmicdrift/kumiko-renderer@0.154.1 - @cosmicdrift/kumiko-renderer-web@0.154.1"
27
33
  },
28
34
  {
29
35
  "version": "0.151.0",
30
36
  "type": "improvement",
31
- "title": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly — u…",
32
- "detail": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly — use the new `createLoginRoute({ loginScreenProps, mfaVerifyScreen, unaffected (they already build on the same logic internally now). Why: a raw `<LoginScreen />` render has no MFA-challenge handling unless the caller remembers to hand-wire `onMfaChallenge` + swap in a verify screen itself — exactly how kumiko-framework#266's login-time MFA step went missing in a real app's standalone apex/marketing login route (it renders outside `emailPasswordClient`'s own gate, which already handled this correctly). `createLoginRoute` is the one place this logic lives now, for both the gated and standalone cases — there's no lower-level piece left to misuse. The `apex-surface-auth` recipe is updated to match."
37
+ "title": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly \u2014 u\u2026",
38
+ "detail": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly \u2014 use the new `createLoginRoute({ loginScreenProps, mfaVerifyScreen, unaffected (they already build on the same logic internally now). Why: a raw `<LoginScreen />` render has no MFA-challenge handling unless the caller remembers to hand-wire `onMfaChallenge` + swap in a verify screen itself \u2014 exactly how kumiko-framework#266's login-time MFA step went missing in a real app's standalone apex/marketing login route (it renders outside `emailPasswordClient`'s own gate, which already handled this correctly). `createLoginRoute` is the one place this logic lives now, for both the gated and standalone cases \u2014 there's no lower-level piece left to misuse. The `apex-surface-auth` recipe is updated to match."
33
39
  },
34
40
  {
35
41
  "version": "0.131.0",
36
42
  "type": "improvement",
37
- "title": "App-Mounting 2.0 Säule A: Mid-Level-Widget-Kit in renderer-web (StatCard, MiniStat, Secti…",
38
- "detail": "App-Mounting 2.0 Säule A: Mid-Level-Widget-Kit in renderer-web (StatCard, MiniStat, SectionCard, StatusBadge, ProgressBar, CollapsibleSection, DetailList, ModeSwitch, StatusBarChart, TimeseriesChart, EmptyState/LoadingState/ErrorState, QueryTable) + Status-Farb-Tokens (--color-status-\\*). Neue Hooks useMutation + useDisclosure. Neues Core-Primitive Link (default/button/muted), Button-Variant \"link\", Text-Variant \"muted\"; auth-email-password nutzt sie (authButtonClass/authMutedLinkClass entfernt)."
43
+ "title": "App-Mounting 2.0 S\u00e4ule A: Mid-Level-Widget-Kit in renderer-web (StatCard, MiniStat, Secti\u2026",
44
+ "detail": "App-Mounting 2.0 S\u00e4ule A: Mid-Level-Widget-Kit in renderer-web (StatCard, MiniStat, SectionCard, StatusBadge, ProgressBar, CollapsibleSection, DetailList, ModeSwitch, StatusBarChart, TimeseriesChart, EmptyState/LoadingState/ErrorState, QueryTable) + Status-Farb-Tokens (--color-status-\\*). Neue Hooks useMutation + useDisclosure. Neues Core-Primitive Link (default/button/muted), Button-Variant \"link\", Text-Variant \"muted\"; auth-email-password nutzt sie (authButtonClass/authMutedLinkClass entfernt)."
39
45
  }
40
46
  ]
@@ -1,7 +1,7 @@
1
1
  // Magic-Link-Signup, Step 2 (confirm).
2
2
  //
3
3
  // Token aus URL + Password vom User → wir lösen den Token in Redis ein
4
- // und legen Tenant + User + Admin-Membership atomar an. emailVerified
4
+ // und legen Tenant + User + TenantAdmin membership atomically. emailVerified
5
5
  // wird sofort auf true gesetzt — der Magic-Link IST der Beweis.
6
6
  //
7
7
  // Pipeline:
@@ -141,7 +141,7 @@ export function createSignupConfirmHandler() {
141
141
 
142
142
  // SessionUser für JWT-Mint. Roles aus INITIAL_SIGNUP_ROLES
143
143
  // damit DB-write (provisionSignupAccount) und Session-claim
144
- // dieselbe Quelle teilen — sonst hätten zwei Stellen "Admin"
144
+ // dieselbe Quelle teilen — sonst hätten zwei Stellen "TenantAdmin"
145
145
  // hardcoded und ein Refactor würde role-mismatch zwischen DB
146
146
  // und JWT erzeugen.
147
147
  const session: SessionUser = {
@@ -103,12 +103,12 @@ export async function seedUserWithPassword(
103
103
  * minten (Account-Takeover, #365). `seedUser` selbst bleibt idempotent
104
104
  * add-only (für Bootstrap/Tests); die create-only-Garantie sitzt hier.
105
105
  * tenantKey-Kollisionen löst der Caller via generateUniqueName. */
106
- /** Default-Roles für den Self-Signup-Admin. Geteilt zwischen
106
+ /** Default membership roles for self-signup tenant owners. Shared between
107
107
  * provisionSignupAccount (DB-write) und signup-confirm-handler
108
108
  * (SessionUser-Konstruktion für JWT-Mint) — sonst hätten zwei
109
- * Stellen unabhängig "Admin" hardcoded und würden bei einem
109
+ * Stellen unabhängig "TenantAdmin" hardcoded und würden bei einem
110
110
  * Refactor zu role-mismatch zwischen DB und Session leiden. */
111
- export const INITIAL_SIGNUP_ROLES = ["Admin"] as const;
111
+ export const INITIAL_SIGNUP_ROLES = ["TenantAdmin"] as const;
112
112
 
113
113
  export type ProvisionSignupAccountOptions = {
114
114
  readonly email: string;
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.218.0",
4
+ "type": "fix",
5
+ "title": "user-list: system-scope search; membership roles on tenants; emailVerified sort",
6
+ "detail": "systemScope entityList search uses SYSTEM_TENANT_ID (was session tenant → Meili miss/500). Tenants column shows membership roles (Offlot Demo (TenantAdmin)). emailVerified sortable; email also searchable for roster lookup. Global roles stay SystemAdmin-only — TenantAdmin/Admin live on membership (Team → Members)."
7
+ },
2
8
  {
3
9
  "version": "0.217.0",
4
10
  "type": "improvement",
@@ -5,6 +5,7 @@ import {
5
5
  defineEntityListHandler,
6
6
  type HandlerContext,
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
+ import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
8
9
  // kumiko-lint-ignore cross-feature-import SystemAdmin user-list joins memberships for tenants column
9
10
  import { tenantMembershipsTable, tenantTable } from "../../tenant";
10
11
  import { userEntity } from "../schema/user";
@@ -13,7 +14,7 @@ const baseList = defineEntityListHandler("user", userEntity, {
13
14
  access: { roles: access.systemAdmin },
14
15
  });
15
16
 
16
- type MembershipRow = { userId: unknown; tenantId: unknown };
17
+ type MembershipRow = { userId: unknown; tenantId: unknown; roles?: unknown };
17
18
  type TenantRow = { id: unknown; name?: unknown; key?: unknown };
18
19
 
19
20
  function isMissingRelation(err: unknown): boolean {
@@ -30,7 +31,6 @@ async function loadMemberships(
30
31
  userId: { in: [...userIds] },
31
32
  });
32
33
  } catch (err) {
33
- // User-only test stacks (and mid-migration DBs) may lack membership tables.
34
34
  if (isMissingRelation(err)) return null;
35
35
  throw err;
36
36
  }
@@ -46,6 +46,12 @@ function tenantLabelById(tenants: readonly TenantRow[]): Map<string, string> {
46
46
  return map;
47
47
  }
48
48
 
49
+ /** "Offlot Demo (TenantAdmin)" — membership roles live here, not on user.roles. */
50
+ function membershipLabel(tenantLabel: string, membershipRoles: readonly string[]): string {
51
+ if (membershipRoles.length === 0) return tenantLabel;
52
+ return `${tenantLabel} (${membershipRoles.join("+")})`;
53
+ }
54
+
49
55
  function labelsByUserId(
50
56
  memberships: readonly MembershipRow[],
51
57
  labelByTenantId: Map<string, string>,
@@ -55,7 +61,8 @@ function labelsByUserId(
55
61
  const userId = String(m.userId ?? "");
56
62
  const tenantId = String(m.tenantId ?? "");
57
63
  if (userId === "" || tenantId === "") continue;
58
- const label = labelByTenantId.get(tenantId) ?? tenantId;
64
+ const tenantLabel = labelByTenantId.get(tenantId) ?? tenantId;
65
+ const label = membershipLabel(tenantLabel, parseRoles(m.roles ?? null));
59
66
  const list = map.get(userId) ?? [];
60
67
  list.push(label);
61
68
  map.set(userId, list);
package/src/user/i18n.ts CHANGED
@@ -5,8 +5,14 @@ export const USER_I18N: Readonly<Record<string, LocalizedString>> = {
5
5
  "screen:user-edit.title": { en: "Edit user" },
6
6
  "user:entity:user:field:email": { en: "Email" },
7
7
  "user:entity:user:field:displayName": { en: "Display name" },
8
- "user:entity:user:field:roles": { en: "Roles" },
8
+ "user:entity:user:field:roles": { en: "Platform roles" },
9
9
  "user:entity:user:field:tenants": { en: "Tenants" },
10
+ "user:entity:user:field:tenants:description": {
11
+ en: "Membership tenants with roles, e.g. Offlot Demo (TenantAdmin). Edit under Team → Members.",
12
+ },
13
+ "user:entity:user:field:roles:description": {
14
+ en: "Global platform roles only (SystemAdmin). TenantAdmin/Admin are membership roles under Team → Members.",
15
+ },
10
16
  "user:entity:user:field:roles:option:SystemAdmin": { en: "SystemAdmin" },
11
17
  "user:entity:user:field:status": { en: "Status" },
12
18
  "user:entity:user:field:emailVerified": { en: "Email verified" },
@@ -72,6 +72,8 @@ export const userEntity = createEntity({
72
72
  maxLength: 320,
73
73
  personal: "self",
74
74
  find: "exact",
75
+ // Roster search needs email hits; find:"exact" alone is blind-index only.
76
+ searchable: true,
75
77
  access: { write: access.privileged },
76
78
  }),
77
79
 
@@ -112,6 +114,7 @@ export const userEntity = createEntity({
112
114
  // refuse a session while this is false (strict mode).
113
115
  emailVerified: createBooleanField({
114
116
  default: false,
117
+ sortable: true,
115
118
  access: { write: access.privileged },
116
119
  }),
117
120
 
@@ -27,7 +27,8 @@ export const userListScreen: EntityListScreenDefinition = {
27
27
  },
28
28
  ],
29
29
  defaultSort: { field: "status", dir: "asc" },
30
- // ponytail: screen searchable but email is encrypted PII — list search uses non-encrypted columns only.
30
+ // Search uses SYSTEM_TENANT_ID for systemScope lists (see event-store-executor-read).
31
+ // Apps must boot a user search index on that tenant (offlot: bootWaitlistSearch).
31
32
  searchable: true,
32
33
  access: { roles: ["SystemAdmin"] },
33
34
  };