@cosmicdrift/kumiko-bundled-features 0.383.0 → 0.384.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.
Files changed (126) hide show
  1. package/dist/audit/event-classification.d.ts +8 -0
  2. package/dist/audit/event-classification.js +69 -0
  3. package/dist/audit/feature.js +3 -3
  4. package/dist/audit/handlers/audit-events.d.ts +25 -3
  5. package/dist/audit/handlers/audit-events.js +41 -7
  6. package/dist/audit/handlers/details-all-tenants.query.d.ts +3 -0
  7. package/dist/audit/handlers/details-all-tenants.query.js +2 -5
  8. package/dist/audit/handlers/details.query.d.ts +3 -0
  9. package/dist/audit/handlers/details.query.js +5 -3
  10. package/dist/audit/handlers/list-all-tenants.query.d.ts +9 -2
  11. package/dist/audit/handlers/list-all-tenants.query.js +10 -8
  12. package/dist/audit/handlers/list.query.d.ts +8 -0
  13. package/dist/audit/handlers/list.query.js +12 -4
  14. package/dist/audit/screens.d.ts +37 -0
  15. package/dist/audit/screens.js +26 -4
  16. package/dist/audit/web/audit-event-cell.d.ts +4 -0
  17. package/dist/audit/web/audit-event-cell.js +20 -0
  18. package/dist/audit/web/audit-event-labels.d.ts +7 -0
  19. package/dist/audit/web/audit-event-labels.js +23 -0
  20. package/dist/audit/web/client-plugin.js +6 -1
  21. package/dist/audit/web/constants.d.ts +8 -0
  22. package/dist/audit/web/constants.js +7 -0
  23. package/dist/audit/web/i18n.js +48 -1
  24. package/dist/auth-email-password/feature.js +3 -2
  25. package/dist/auth-email-password/handlers/login-options.query.d.ts +5 -0
  26. package/dist/auth-email-password/handlers/login-options.query.js +17 -0
  27. package/dist/auth-email-password/index.d.ts +1 -1
  28. package/dist/auth-email-password/index.js +1 -1
  29. package/dist/auth-email-password/{auth-paths.d.ts → web/auth-paths.d.ts} +5 -3
  30. package/dist/auth-email-password/{auth-paths.js → web/auth-paths.js} +5 -3
  31. package/dist/auth-email-password/web/client-plugin.d.ts +5 -0
  32. package/dist/auth-email-password/web/client-plugin.js +2 -0
  33. package/dist/auth-email-password/web/constants.d.ts +1 -0
  34. package/dist/auth-email-password/web/constants.js +1 -0
  35. package/dist/auth-email-password/web/default-auth-pages-gate.d.ts +4 -0
  36. package/dist/auth-email-password/web/default-auth-pages-gate.js +28 -0
  37. package/dist/auth-email-password/web/login-screen.d.ts +13 -18
  38. package/dist/auth-email-password/web/login-screen.js +31 -19
  39. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -126
  40. package/dist/billing-foundation/entities.d.ts +8 -168
  41. package/dist/billing-foundation/projection.d.ts +8 -168
  42. package/dist/cap-counter/entity.d.ts +1 -21
  43. package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -28
  44. package/dist/config/table.d.ts +6 -126
  45. package/dist/custom-fields/entity.d.ts +4 -84
  46. package/dist/data-retention/schema/tenant-retention-override.d.ts +6 -112
  47. package/dist/delivery/tables.d.ts +6 -126
  48. package/dist/document-ingest-foundation/entity.d.ts +6 -112
  49. package/dist/folders/entity.d.ts +8 -168
  50. package/dist/form-draft/entity.d.ts +2 -42
  51. package/dist/inbound-mail-foundation/entities.d.ts +25 -525
  52. package/dist/inbound-mail-foundation/projection.d.ts +20 -420
  53. package/dist/jobs/feature.js +22 -117
  54. package/dist/jobs/handlers/consumer.write.d.ts +37 -0
  55. package/dist/jobs/handlers/consumer.write.js +59 -0
  56. package/dist/jobs/handlers/consumers.query.d.ts +52 -0
  57. package/dist/jobs/handlers/consumers.query.js +114 -0
  58. package/dist/jobs/job-run-table.d.ts +6 -126
  59. package/dist/jobs/screens.d.ts +6 -0
  60. package/dist/jobs/screens.js +241 -0
  61. package/dist/jobs/web/constants.d.ts +20 -0
  62. package/dist/jobs/web/constants.js +21 -0
  63. package/dist/jobs/web/i18n.js +41 -0
  64. package/dist/ledger/entity.d.ts +13 -273
  65. package/dist/managed-pages/table.d.ts +12 -252
  66. package/dist/notes-history/entity.d.ts +14 -280
  67. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -126
  68. package/dist/secrets/i18n.d.ts +0 -3
  69. package/dist/secrets/i18n.js +0 -3
  70. package/dist/secrets/table.d.ts +1 -21
  71. package/dist/sessions/constants.d.ts +2 -1
  72. package/dist/sessions/constants.js +2 -1
  73. package/dist/sessions/handlers/detail.query.d.ts +5 -0
  74. package/dist/sessions/handlers/detail.query.js +7 -3
  75. package/dist/sessions/handlers/list.query.d.ts +5 -0
  76. package/dist/sessions/handlers/list.query.js +16 -10
  77. package/dist/sessions/handlers/mine.query.d.ts +5 -0
  78. package/dist/sessions/handlers/mine.query.js +15 -9
  79. package/dist/sessions/schema/user-session.d.ts +4 -84
  80. package/dist/sessions/screens.js +22 -8
  81. package/dist/sessions/web/client-plugin.d.ts +6 -0
  82. package/dist/sessions/web/client-plugin.js +11 -0
  83. package/dist/sessions/web/constants.d.ts +3 -0
  84. package/dist/sessions/web/constants.js +3 -0
  85. package/dist/sessions/web/default-translations.d.ts +2 -0
  86. package/dist/sessions/web/default-translations.js +3 -0
  87. package/dist/sessions/web/i18n.js +5 -1
  88. package/dist/sessions/web/index.d.ts +1 -0
  89. package/dist/sessions/web/index.js +1 -0
  90. package/dist/sessions/web/session-device-cell.d.ts +3 -0
  91. package/dist/sessions/web/session-device-cell.js +16 -0
  92. package/dist/sessions/web/user-agent.d.ts +26 -0
  93. package/dist/sessions/web/user-agent.js +58 -0
  94. package/dist/tags/entity.d.ts +9 -189
  95. package/dist/template-resolver/feature.d.ts +1 -0
  96. package/dist/template-resolver/feature.js +2 -0
  97. package/dist/template-resolver/handlers/overview.query.d.ts +5 -0
  98. package/dist/template-resolver/handlers/overview.query.js +19 -2
  99. package/dist/template-resolver/handlers/tenant-override-counts.query.d.ts +17 -0
  100. package/dist/template-resolver/handlers/tenant-override-counts.query.js +35 -0
  101. package/dist/template-resolver/handlers/tenant-override-selection.d.ts +12 -0
  102. package/dist/template-resolver/handlers/tenant-override-selection.js +34 -0
  103. package/dist/template-resolver/handlers/tenant-overrides.query.js +3 -29
  104. package/dist/template-resolver/table.d.ts +20 -350
  105. package/dist/template-resolver/user-content-table.d.ts +12 -238
  106. package/dist/template-resolver/web/qualified-names.d.ts +1 -0
  107. package/dist/template-resolver/web/qualified-names.js +1 -0
  108. package/dist/tenant/invitation-table.d.ts +6 -126
  109. package/dist/tenant/membership-table.d.ts +4 -84
  110. package/dist/tenant/schema/tenant.d.ts +6 -126
  111. package/dist/tier-engine/entity.d.ts +1 -21
  112. package/dist/tier-engine/tier-resolver.d.ts +1 -21
  113. package/dist/user/schema/user.d.ts +12 -252
  114. package/dist/user-data-rights/feature.js +33 -33
  115. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -294
  116. package/dist/user-data-rights/schema/download-token.d.ts +8 -168
  117. package/dist/user-data-rights/schema/export-job.d.ts +8 -154
  118. package/dist/user-data-rights/web/i18n.js +4 -1
  119. package/dist/waitlist/entity.d.ts +16 -322
  120. package/package.json +17 -9
  121. package/src/audit/changes.json +6 -0
  122. package/src/auth-email-password/changes.json +6 -0
  123. package/src/jobs/changes.json +6 -0
  124. package/src/sessions/changes.json +6 -0
  125. package/src/template-resolver/changes.json +6 -0
  126. package/src/user-data-rights/changes.json +18 -0
@@ -1,140 +1,20 @@
1
1
  export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
2
- readonly userId: {
3
- readonly type: "text";
4
- readonly description?: string;
5
- readonly maxLength?: number;
6
- readonly minLength?: number;
7
- readonly autoComplete?: string;
8
- readonly required?: boolean;
9
- readonly searchable?: boolean;
10
- readonly sortable?: boolean;
11
- readonly filterable?: boolean;
12
- readonly encrypted?: boolean;
13
- readonly sensitive?: boolean;
14
- readonly writeOnly?: boolean;
15
- readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
- readonly default?: string;
18
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
- readonly multiline?: boolean | {
20
- readonly rows?: number;
21
- };
22
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
2
+ readonly userId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
23
3
  required: true;
24
4
  };
25
- readonly tenantId: {
26
- readonly type: "text";
27
- readonly description?: string;
28
- readonly maxLength?: number;
29
- readonly minLength?: number;
30
- readonly autoComplete?: string;
31
- readonly required?: boolean;
32
- readonly searchable?: boolean;
33
- readonly sortable?: boolean;
34
- readonly filterable?: boolean;
35
- readonly encrypted?: boolean;
36
- readonly sensitive?: boolean;
37
- readonly writeOnly?: boolean;
38
- readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
40
- readonly default?: string;
41
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
- readonly multiline?: boolean | {
43
- readonly rows?: number;
44
- };
45
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
5
+ readonly tenantId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
46
6
  required: true;
47
7
  };
48
- readonly name: {
49
- readonly type: "text";
50
- readonly description?: string;
51
- readonly maxLength?: number;
52
- readonly minLength?: number;
53
- readonly autoComplete?: string;
54
- readonly required?: boolean;
55
- readonly searchable?: boolean;
56
- readonly sortable?: boolean;
57
- readonly filterable?: boolean;
58
- readonly encrypted?: boolean;
59
- readonly sensitive?: boolean;
60
- readonly writeOnly?: boolean;
61
- readonly readAsInstruction?: boolean;
62
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
63
- readonly default?: string;
64
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
65
- readonly multiline?: boolean | {
66
- readonly rows?: number;
67
- };
68
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
8
+ readonly name: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
69
9
  required: true;
70
10
  };
71
- readonly tokenHash: {
72
- readonly type: "text";
73
- readonly description?: string;
74
- readonly maxLength?: number;
75
- readonly minLength?: number;
76
- readonly autoComplete?: string;
77
- readonly required?: boolean;
78
- readonly searchable?: boolean;
79
- readonly sortable?: boolean;
80
- readonly filterable?: boolean;
81
- readonly encrypted?: boolean;
82
- readonly sensitive?: boolean;
83
- readonly writeOnly?: boolean;
84
- readonly readAsInstruction?: boolean;
85
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
86
- readonly default?: string;
87
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
88
- readonly multiline?: boolean | {
89
- readonly rows?: number;
90
- };
91
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
11
+ readonly tokenHash: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
92
12
  required: true;
93
13
  };
94
- readonly prefix: {
95
- readonly type: "text";
96
- readonly description?: string;
97
- readonly maxLength?: number;
98
- readonly minLength?: number;
99
- readonly autoComplete?: string;
100
- readonly required?: boolean;
101
- readonly searchable?: boolean;
102
- readonly sortable?: boolean;
103
- readonly filterable?: boolean;
104
- readonly encrypted?: boolean;
105
- readonly sensitive?: boolean;
106
- readonly writeOnly?: boolean;
107
- readonly readAsInstruction?: boolean;
108
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
109
- readonly default?: string;
110
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
111
- readonly multiline?: boolean | {
112
- readonly rows?: number;
113
- };
114
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
14
+ readonly prefix: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
115
15
  required: true;
116
16
  };
117
- readonly scopes: {
118
- readonly type: "text";
119
- readonly description?: string;
120
- readonly maxLength?: number;
121
- readonly minLength?: number;
122
- readonly autoComplete?: string;
123
- readonly required?: boolean;
124
- readonly searchable?: boolean;
125
- readonly sortable?: boolean;
126
- readonly filterable?: boolean;
127
- readonly encrypted?: boolean;
128
- readonly sensitive?: boolean;
129
- readonly writeOnly?: boolean;
130
- readonly readAsInstruction?: boolean;
131
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
132
- readonly default?: string;
133
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
134
- readonly multiline?: boolean | {
135
- readonly rows?: number;
136
- };
137
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
17
+ readonly scopes: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
138
18
  required: true;
139
19
  };
140
20
  readonly createdAt: {
@@ -1,14 +1,11 @@
1
1
  export declare const SECRETS_I18N: {
2
2
  readonly "secrets.errors.unknownKey": {
3
3
  readonly en: "Unknown secret key.";
4
- readonly de: "Unbekannter Geheimnis-Schlüssel.";
5
4
  };
6
5
  readonly "secrets.errors.writeDenied": {
7
6
  readonly en: "You are not allowed to change this secret.";
8
- readonly de: "Dieses Geheimnis darf nicht geändert werden.";
9
7
  };
10
8
  readonly "secrets.errors.invalidValue": {
11
9
  readonly en: "This value is not valid for this secret.";
12
- readonly de: "Dieser Wert ist für dieses Geheimnis nicht gültig.";
13
10
  };
14
11
  };
@@ -2,14 +2,11 @@ import { SECRETS_ERROR_KEYS } from "./web/constants.js";
2
2
  export const SECRETS_I18N = {
3
3
  [SECRETS_ERROR_KEYS.unknownKey]: {
4
4
  en: "Unknown secret key.",
5
- de: "Unbekannter Geheimnis-Schlüssel.",
6
5
  },
7
6
  [SECRETS_ERROR_KEYS.writeDenied]: {
8
7
  en: "You are not allowed to change this secret.",
9
- de: "Dieses Geheimnis darf nicht geändert werden.",
10
8
  },
11
9
  [SECRETS_ERROR_KEYS.invalidValue]: {
12
10
  en: "This value is not valid for this secret.",
13
- de: "Dieser Wert ist für dieses Geheimnis nicht gültig.",
14
11
  },
15
12
  };
@@ -4,27 +4,7 @@ export type StoredMetadata = {
4
4
  readonly hint?: string;
5
5
  };
6
6
  export declare const tenantSecretEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
7
- readonly key: {
8
- readonly type: "text";
9
- readonly description?: string;
10
- readonly maxLength?: number;
11
- readonly minLength?: number;
12
- readonly autoComplete?: string;
13
- readonly required?: boolean;
14
- readonly searchable?: boolean;
15
- readonly sortable?: boolean;
16
- readonly filterable?: boolean;
17
- readonly encrypted?: boolean;
18
- readonly sensitive?: boolean;
19
- readonly writeOnly?: boolean;
20
- readonly readAsInstruction?: boolean;
21
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
22
- readonly default?: string;
23
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
24
- readonly multiline?: boolean | {
25
- readonly rows?: number;
26
- };
27
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
7
+ readonly key: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
28
8
  required: true;
29
9
  };
30
10
  readonly kekVersion: {
@@ -1,4 +1,5 @@
1
- export declare const SESSIONS_FEATURE: "sessions";
1
+ import { SESSIONS_FEATURE } from "./web/constants.ts";
2
+ export { SESSIONS_FEATURE };
2
3
  export declare const USER_SESSION_ENTITY_NAME: "user-session";
3
4
  export declare const SessionHandlers: {
4
5
  readonly revoke: "sessions:write:user-session:revoke";
@@ -1,4 +1,5 @@
1
- export const SESSIONS_FEATURE = "sessions";
1
+ import { SESSIONS_FEATURE } from "./web/constants.js";
2
+ export { SESSIONS_FEATURE };
2
3
  export const USER_SESSION_ENTITY_NAME = "user-session";
3
4
  // Qualified write handler names — entity prefix is "userSession", snake_case
4
5
  // "user_session" on the wire.
@@ -9,4 +9,9 @@ export declare const createDetailQuery: (adminRoles?: readonly string[]) => impo
9
9
  revokedAt: unknown;
10
10
  ip: string | null;
11
11
  userAgent: string | null;
12
+ device: {
13
+ browser: "Chrome" | "Edge" | "Firefox" | "Opera" | "Safari" | "Samsung Internet" | null;
14
+ browserMajor: number | null;
15
+ os: "Android" | "ChromeOS" | "Linux" | "Windows" | "iOS" | "macOS" | null;
16
+ } | null;
12
17
  } | null, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -3,6 +3,7 @@ import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine
3
3
  import * as z from "zod";
4
4
  import { decryptStoredPii } from "../../shared/index.js";
5
5
  import { userSessionTable } from "../schema/user-session.js";
6
+ import { parsedDeviceSchema, parseUserAgent } from "../web/user-agent.js";
6
7
  // Admin single-session inspector — mirrors list.query's decrypt handling for
7
8
  // the one-row case. ctx.db (TenantDb) applies tenant-scoping automatically.
8
9
  const sessionRowSchema = z.object({
@@ -13,6 +14,7 @@ const sessionRowSchema = z.object({
13
14
  revokedAt: z.unknown(),
14
15
  ip: z.string().nullable(),
15
16
  userAgent: z.string().nullable(),
17
+ device: parsedDeviceSchema,
16
18
  });
17
19
  export const createDetailQuery = (adminRoles = access.admin) => defineQueryHandler({
18
20
  name: "user-session:detail",
@@ -26,6 +28,9 @@ export const createDetailQuery = (adminRoles = access.admin) => defineQueryHandl
26
28
  const row = await fetchOne(ctx.db, userSessionTable, { id: query.payload.id });
27
29
  if (!row)
28
30
  return null;
31
+ const userAgent = row.userAgent
32
+ ? await decryptStoredPii(row.userAgent, "userAgent", "sessions:detail")
33
+ : row.userAgent;
29
34
  return {
30
35
  id: row.id,
31
36
  userId: row.userId,
@@ -33,9 +38,8 @@ export const createDetailQuery = (adminRoles = access.admin) => defineQueryHandl
33
38
  expiresAt: row.expiresAt,
34
39
  revokedAt: row.revokedAt,
35
40
  ip: row.ip ? await decryptStoredPii(row.ip, "ip", "sessions:detail") : row.ip,
36
- userAgent: row.userAgent
37
- ? await decryptStoredPii(row.userAgent, "userAgent", "sessions:detail")
38
- : row.userAgent,
41
+ userAgent,
42
+ device: parseUserAgent(userAgent),
39
43
  };
40
44
  },
41
45
  });
@@ -14,4 +14,9 @@ export declare const createListQuery: (adminRoles?: readonly string[]) => import
14
14
  revokedAt: unknown;
15
15
  ip: string | null;
16
16
  userAgent: string | null;
17
+ device: {
18
+ browser: "Chrome" | "Edge" | "Firefox" | "Opera" | "Safari" | "Samsung Internet" | null;
19
+ browserMajor: number | null;
20
+ os: "Android" | "ChromeOS" | "Linux" | "Windows" | "iOS" | "macOS" | null;
21
+ } | null;
17
22
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -4,6 +4,7 @@ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
4
4
  import * as z from "zod";
5
5
  import { decryptStoredPii } from "../../shared/index.js";
6
6
  import { userSessionTable } from "../schema/user-session.js";
7
+ import { parsedDeviceSchema, parseUserAgent } from "../web/user-agent.js";
7
8
  // `sort` arrives raw from the client's query string. selectMany's orderBy
8
9
  // has no column-existence check — an unrecognised field just gets
9
10
  // snake_cased and quoted as-is (db/query.ts columnOf) — so this
@@ -36,6 +37,7 @@ export const createListQuery = (adminRoles = access.admin) => definePagedQueryHa
36
37
  revokedAt: z.unknown(),
37
38
  ip: z.string().nullable(),
38
39
  userAgent: z.string().nullable(),
40
+ device: parsedDeviceSchema,
39
41
  })),
40
42
  nextCursor: z.string().nullable(),
41
43
  }),
@@ -56,17 +58,21 @@ export const createListQuery = (adminRoles = access.admin) => definePagedQueryHa
56
58
  ],
57
59
  ...(query.payload.limit !== undefined && { limit: query.payload.limit }),
58
60
  });
59
- const decryptedRows = await Promise.all(rows.map(async (r) => ({
60
- id: r.id,
61
- userId: r.userId,
62
- createdAt: r.createdAt,
63
- expiresAt: r.expiresAt,
64
- revokedAt: r.revokedAt,
65
- ip: r.ip ? await decryptStoredPii(r.ip, "ip", "sessions:list") : r.ip,
66
- userAgent: r.userAgent
61
+ const decryptedRows = await Promise.all(rows.map(async (r) => {
62
+ const userAgent = r.userAgent
67
63
  ? await decryptStoredPii(r.userAgent, "userAgent", "sessions:list")
68
- : r.userAgent,
69
- })));
64
+ : r.userAgent;
65
+ return {
66
+ id: r.id,
67
+ userId: r.userId,
68
+ createdAt: r.createdAt,
69
+ expiresAt: r.expiresAt,
70
+ revokedAt: r.revokedAt,
71
+ ip: r.ip ? await decryptStoredPii(r.ip, "ip", "sessions:list") : r.ip,
72
+ userAgent,
73
+ device: parseUserAgent(userAgent),
74
+ };
75
+ }));
70
76
  return { rows: decryptedRows, nextCursor: null };
71
77
  },
72
78
  });
@@ -5,5 +5,10 @@ export declare const mineQuery: import("@cosmicdrift/kumiko-framework/engine").P
5
5
  expiresAt: unknown;
6
6
  ip: string | null;
7
7
  userAgent: string | null;
8
+ device: {
9
+ browser: "Chrome" | "Edge" | "Firefox" | "Opera" | "Safari" | "Samsung Internet" | null;
10
+ browserMajor: number | null;
11
+ os: "Android" | "ChromeOS" | "Linux" | "Windows" | "iOS" | "macOS" | null;
12
+ } | null;
8
13
  current: boolean;
9
14
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -3,6 +3,7 @@ import { definePagedQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import * as z from "zod";
4
4
  import { decryptStoredPii } from "../../shared/index.js";
5
5
  import { userSessionTable } from "../schema/user-session.js";
6
+ import { parsedDeviceSchema, parseUserAgent } from "../web/user-agent.js";
6
7
  // "My live sessions" — the backing data for a devices/sessions UI. Returns
7
8
  // ONLY the current user's own, currently-live sessions, ordered by most-
8
9
  // recently-used first. Revoked rows excluded (revokedAt IS NULL). Paged
@@ -25,6 +26,7 @@ export const mineQuery = definePagedQueryHandler({
25
26
  expiresAt: z.unknown(),
26
27
  ip: z.string().nullable(),
27
28
  userAgent: z.string().nullable(),
29
+ device: parsedDeviceSchema,
28
30
  current: z.boolean(),
29
31
  })),
30
32
  nextCursor: z.string().nullable(),
@@ -34,16 +36,20 @@ export const mineQuery = definePagedQueryHandler({
34
36
  orderBy: { col: "createdAt", direction: "desc" },
35
37
  });
36
38
  const currentSid = query.user.sid;
37
- const decryptedRows = await Promise.all(rows.map(async (r) => ({
38
- id: r.id,
39
- createdAt: r.createdAt,
40
- expiresAt: r.expiresAt,
41
- ip: r.ip ? await decryptStoredPii(r.ip, "ip", "sessions:mine") : r.ip,
42
- userAgent: r.userAgent
39
+ const decryptedRows = await Promise.all(rows.map(async (r) => {
40
+ const userAgent = r.userAgent
43
41
  ? await decryptStoredPii(r.userAgent, "userAgent", "sessions:mine")
44
- : r.userAgent,
45
- current: currentSid === r.id,
46
- })));
42
+ : r.userAgent;
43
+ return {
44
+ id: r.id,
45
+ createdAt: r.createdAt,
46
+ expiresAt: r.expiresAt,
47
+ ip: r.ip ? await decryptStoredPii(r.ip, "ip", "sessions:mine") : r.ip,
48
+ userAgent,
49
+ device: parseUserAgent(userAgent),
50
+ current: currentSid === r.id,
51
+ };
52
+ }));
47
53
  return { rows: decryptedRows, nextCursor: null };
48
54
  },
49
55
  });
@@ -1,48 +1,8 @@
1
1
  export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
2
- readonly userId: {
3
- readonly type: "text";
4
- readonly description?: string;
5
- readonly maxLength?: number;
6
- readonly minLength?: number;
7
- readonly autoComplete?: string;
8
- readonly required?: boolean;
9
- readonly searchable?: boolean;
10
- readonly sortable?: boolean;
11
- readonly filterable?: boolean;
12
- readonly encrypted?: boolean;
13
- readonly sensitive?: boolean;
14
- readonly writeOnly?: boolean;
15
- readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
- readonly default?: string;
18
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
- readonly multiline?: boolean | {
20
- readonly rows?: number;
21
- };
22
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
2
+ readonly userId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
23
3
  required: true;
24
4
  };
25
- readonly tenantId: {
26
- readonly type: "text";
27
- readonly description?: string;
28
- readonly maxLength?: number;
29
- readonly minLength?: number;
30
- readonly autoComplete?: string;
31
- readonly required?: boolean;
32
- readonly searchable?: boolean;
33
- readonly sortable?: boolean;
34
- readonly filterable?: boolean;
35
- readonly encrypted?: boolean;
36
- readonly sensitive?: boolean;
37
- readonly writeOnly?: boolean;
38
- readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
40
- readonly default?: string;
41
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
- readonly multiline?: boolean | {
43
- readonly rows?: number;
44
- };
45
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
5
+ readonly tenantId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
46
6
  required: true;
47
7
  };
48
8
  readonly createdAt: {
@@ -105,50 +65,10 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
105
65
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
106
66
  required: false;
107
67
  };
108
- readonly ip: {
109
- readonly type: "text";
110
- readonly description?: string;
111
- readonly maxLength?: number;
112
- readonly minLength?: number;
113
- readonly autoComplete?: string;
114
- readonly required?: boolean;
115
- readonly searchable?: boolean;
116
- readonly sortable?: boolean;
117
- readonly filterable?: boolean;
118
- readonly encrypted?: boolean;
119
- readonly sensitive?: boolean;
120
- readonly writeOnly?: boolean;
121
- readonly readAsInstruction?: boolean;
122
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
123
- readonly default?: string;
124
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
125
- readonly multiline?: boolean | {
126
- readonly rows?: number;
127
- };
128
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
68
+ readonly ip: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
129
69
  required: false;
130
70
  };
131
- readonly userAgent: {
132
- readonly type: "text";
133
- readonly description?: string;
134
- readonly maxLength?: number;
135
- readonly minLength?: number;
136
- readonly autoComplete?: string;
137
- readonly required?: boolean;
138
- readonly searchable?: boolean;
139
- readonly sortable?: boolean;
140
- readonly filterable?: boolean;
141
- readonly encrypted?: boolean;
142
- readonly sensitive?: boolean;
143
- readonly writeOnly?: boolean;
144
- readonly readAsInstruction?: boolean;
145
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
146
- readonly default?: string;
147
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
148
- readonly multiline?: boolean | {
149
- readonly rows?: number;
150
- };
151
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
71
+ readonly userAgent: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
152
72
  required: false;
153
73
  };
154
74
  }, "tenant">;
@@ -1,5 +1,7 @@
1
1
  import { access, i18nKey, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { SESSION_DETAIL_SCREEN_ID, SESSION_LIST_SCREEN_ID, SESSION_MINE_SCREEN_ID, SessionHandlers, SessionQueries, } from "./constants.js";
3
+ import { SESSION_DEVICE_CELL_COMPONENT } from "./web/constants.js";
4
+ const DEVICE_RENDERER = { react: { __component: SESSION_DEVICE_CELL_COMPONENT } };
3
5
  export const createSessionListScreen = (adminRoles = access.admin) => ({
4
6
  id: SESSION_LIST_SCREEN_ID,
5
7
  type: "projectionList",
@@ -20,6 +22,11 @@ export const createSessionListScreen = (adminRoles = access.admin) => ({
20
22
  refEntity: "user:user",
21
23
  refLabelField: "displayName",
22
24
  },
25
+ {
26
+ field: "device",
27
+ label: i18nKey("sessions.list.col.device"),
28
+ renderer: DEVICE_RENDERER,
29
+ },
23
30
  {
24
31
  field: "createdAt",
25
32
  label: i18nKey("sessions.list.col.createdAt"),
@@ -28,6 +35,7 @@ export const createSessionListScreen = (adminRoles = access.admin) => ({
28
35
  {
29
36
  field: "expiresAt",
30
37
  label: i18nKey("sessions.list.col.expiresAt"),
38
+ hideOnNarrow: true,
31
39
  renderer: { format: "timestamp" },
32
40
  },
33
41
  {
@@ -67,6 +75,7 @@ export const createSessionDetailScreen = (adminRoles = access.admin) => ({
67
75
  { field: "createdAt", renderer: { format: "timestamp" } },
68
76
  { field: "expiresAt", renderer: { format: "timestamp" } },
69
77
  { field: "revokedAt", renderer: { format: "timestamp" } },
78
+ { field: "device", renderer: DEVICE_RENDERER },
70
79
  "ip",
71
80
  "userAgent",
72
81
  ],
@@ -79,6 +88,7 @@ export const createSessionDetailScreen = (adminRoles = access.admin) => ({
79
88
  createdAt: i18nKey("sessions.detail.field.createdAt"),
80
89
  expiresAt: i18nKey("sessions.detail.field.expiresAt"),
81
90
  revokedAt: i18nKey("sessions.detail.field.revokedAt"),
91
+ device: i18nKey("sessions.list.col.device"),
82
92
  ip: i18nKey("sessions.detail.field.ip"),
83
93
  userAgent: i18nKey("sessions.detail.field.userAgent"),
84
94
  },
@@ -90,6 +100,17 @@ export const sessionMineScreen = {
90
100
  type: "projectionList",
91
101
  query: SessionQueries.mine,
92
102
  columns: [
103
+ { field: "device", label: i18nKey("sessions.mine.col.userAgent"), renderer: DEVICE_RENDERER },
104
+ { field: "ip", label: i18nKey("sessions.mine.col.ip") },
105
+ {
106
+ field: "current",
107
+ label: i18nKey("sessions.mine.col.current"),
108
+ renderer: {
109
+ format: "boolean",
110
+ trueLabel: i18nKey("sessions.mine.current.yes"),
111
+ falseLabel: i18nKey("sessions.mine.current.no"),
112
+ },
113
+ },
93
114
  {
94
115
  field: "createdAt",
95
116
  label: i18nKey("sessions.list.col.createdAt"),
@@ -98,15 +119,8 @@ export const sessionMineScreen = {
98
119
  {
99
120
  field: "expiresAt",
100
121
  label: i18nKey("sessions.list.col.expiresAt"),
101
- renderer: { format: "timestamp" },
102
- },
103
- { field: "ip", label: i18nKey("sessions.mine.col.ip") },
104
- { field: "userAgent", label: i18nKey("sessions.mine.col.userAgent") },
105
- {
106
- field: "current",
107
- label: i18nKey("sessions.mine.col.current"),
108
122
  hideOnNarrow: true,
109
- renderer: { format: "boolean" },
123
+ renderer: { format: "timestamp" },
110
124
  },
111
125
  ],
112
126
  rowActions: [
@@ -0,0 +1,6 @@
1
+ import { type TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
2
+ import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer/web";
3
+ export type SessionsClientOptions = {
4
+ readonly translations?: TranslationsByLocale;
5
+ };
6
+ export declare function sessionsClient(options?: SessionsClientOptions): ClientFeatureDefinition;
@@ -0,0 +1,11 @@
1
+ import { mergeTranslations } from "@cosmicdrift/kumiko-renderer";
2
+ import { SESSION_DEVICE_CELL_COMPONENT, SESSIONS_FEATURE } from "./constants.js";
3
+ import { defaultTranslations } from "./default-translations.js";
4
+ import { SessionDeviceCell } from "./session-device-cell.js";
5
+ export function sessionsClient(options) {
6
+ return {
7
+ name: SESSIONS_FEATURE,
8
+ translations: mergeTranslations(defaultTranslations, options?.translations ?? {}),
9
+ columnRenderers: { [SESSION_DEVICE_CELL_COMPONENT]: SessionDeviceCell },
10
+ };
11
+ }
@@ -0,0 +1,3 @@
1
+ export declare const SESSIONS_FEATURE: "sessions";
2
+ /** Column-renderer name shared by screens.ts and `sessionsClient()`. */
3
+ export declare const SESSION_DEVICE_CELL_COMPONENT: "session-device-cell";
@@ -0,0 +1,3 @@
1
+ export const SESSIONS_FEATURE = "sessions";
2
+ /** Column-renderer name shared by screens.ts and `sessionsClient()`. */
3
+ export const SESSION_DEVICE_CELL_COMPONENT = "session-device-cell";
@@ -0,0 +1,2 @@
1
+ import { type TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
2
+ export declare const defaultTranslations: TranslationsByLocale;
@@ -0,0 +1,3 @@
1
+ import { translationsByLocaleFromKeys, } from "@cosmicdrift/kumiko-renderer";
2
+ import { SESSIONS_I18N } from "./i18n.js";
3
+ export const defaultTranslations = translationsByLocaleFromKeys(SESSIONS_I18N);
@@ -19,7 +19,11 @@ export const SESSIONS_I18N = {
19
19
  "screen:my-sessions.title": { en: "Your sessions" },
20
20
  "sessions.mine.col.ip": { en: "IP address" },
21
21
  "sessions.mine.col.userAgent": { en: "Device" },
22
- "sessions.mine.col.current": { en: "This device" },
22
+ "sessions.mine.col.current": { en: "Session" },
23
+ "sessions.mine.current.yes": { en: "This session" },
24
+ "sessions.mine.current.no": { en: "-" },
25
+ "sessions.list.col.device": { en: "Device" },
26
+ "sessions.device.unknown": { en: "Unknown device" },
23
27
  "sessions.mine.revoke": { en: "Sign out" },
24
28
  "sessions.mine.revoke.confirm": { en: "Sign this device out?" },
25
29
  "sessions.mine.revokeAllOthers": { en: "Sign out all other devices" },
@@ -0,0 +1 @@
1
+ export { type SessionsClientOptions, sessionsClient } from "./client-plugin.tsx";
@@ -0,0 +1 @@
1
+ export { sessionsClient } from "./client-plugin.js";
@@ -0,0 +1,3 @@
1
+ import { type ColumnRendererProps } from "@cosmicdrift/kumiko-renderer";
2
+ import type { ReactNode } from "react";
3
+ export declare function SessionDeviceCell({ value }: ColumnRendererProps): ReactNode;
@@ -0,0 +1,16 @@
1
+ // Column renderer for the parsed `device` of a session row, registered via `sessionsClient()`.
2
+ import { useTranslation } from "@cosmicdrift/kumiko-renderer";
3
+ import { parsedDeviceSchema } from "./user-agent.js";
4
+ export function SessionDeviceCell({ value }) {
5
+ const t = useTranslation();
6
+ const parsed = parsedDeviceSchema.safeParse(value);
7
+ const device = parsed.success ? parsed.data : null;
8
+ if (device === null)
9
+ return t("sessions.device.unknown");
10
+ const browser = device.browser === null
11
+ ? null
12
+ : device.browserMajor === null
13
+ ? device.browser
14
+ : `${device.browser} ${device.browserMajor}`;
15
+ return [browser, device.os].filter((part) => part !== null).join(" · ");
16
+ }