@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.
- package/dist/audit/event-classification.d.ts +8 -0
- package/dist/audit/event-classification.js +69 -0
- package/dist/audit/feature.js +3 -3
- package/dist/audit/handlers/audit-events.d.ts +25 -3
- package/dist/audit/handlers/audit-events.js +41 -7
- package/dist/audit/handlers/details-all-tenants.query.d.ts +3 -0
- package/dist/audit/handlers/details-all-tenants.query.js +2 -5
- package/dist/audit/handlers/details.query.d.ts +3 -0
- package/dist/audit/handlers/details.query.js +5 -3
- package/dist/audit/handlers/list-all-tenants.query.d.ts +9 -2
- package/dist/audit/handlers/list-all-tenants.query.js +10 -8
- package/dist/audit/handlers/list.query.d.ts +8 -0
- package/dist/audit/handlers/list.query.js +12 -4
- package/dist/audit/screens.d.ts +37 -0
- package/dist/audit/screens.js +26 -4
- package/dist/audit/web/audit-event-cell.d.ts +4 -0
- package/dist/audit/web/audit-event-cell.js +20 -0
- package/dist/audit/web/audit-event-labels.d.ts +7 -0
- package/dist/audit/web/audit-event-labels.js +23 -0
- package/dist/audit/web/client-plugin.js +6 -1
- package/dist/audit/web/constants.d.ts +8 -0
- package/dist/audit/web/constants.js +7 -0
- package/dist/audit/web/i18n.js +48 -1
- package/dist/auth-email-password/feature.js +3 -2
- package/dist/auth-email-password/handlers/login-options.query.d.ts +5 -0
- package/dist/auth-email-password/handlers/login-options.query.js +17 -0
- package/dist/auth-email-password/index.d.ts +1 -1
- package/dist/auth-email-password/index.js +1 -1
- package/dist/auth-email-password/{auth-paths.d.ts → web/auth-paths.d.ts} +5 -3
- package/dist/auth-email-password/{auth-paths.js → web/auth-paths.js} +5 -3
- package/dist/auth-email-password/web/client-plugin.d.ts +5 -0
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/auth-email-password/web/constants.d.ts +1 -0
- package/dist/auth-email-password/web/constants.js +1 -0
- package/dist/auth-email-password/web/default-auth-pages-gate.d.ts +4 -0
- package/dist/auth-email-password/web/default-auth-pages-gate.js +28 -0
- package/dist/auth-email-password/web/login-screen.d.ts +13 -18
- package/dist/auth-email-password/web/login-screen.js +31 -19
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -126
- package/dist/billing-foundation/entities.d.ts +8 -168
- package/dist/billing-foundation/projection.d.ts +8 -168
- package/dist/cap-counter/entity.d.ts +1 -21
- package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -28
- package/dist/config/table.d.ts +6 -126
- package/dist/custom-fields/entity.d.ts +4 -84
- package/dist/data-retention/schema/tenant-retention-override.d.ts +6 -112
- package/dist/delivery/tables.d.ts +6 -126
- package/dist/document-ingest-foundation/entity.d.ts +6 -112
- package/dist/folders/entity.d.ts +8 -168
- package/dist/form-draft/entity.d.ts +2 -42
- package/dist/inbound-mail-foundation/entities.d.ts +25 -525
- package/dist/inbound-mail-foundation/projection.d.ts +20 -420
- package/dist/jobs/feature.js +22 -117
- package/dist/jobs/handlers/consumer.write.d.ts +37 -0
- package/dist/jobs/handlers/consumer.write.js +59 -0
- package/dist/jobs/handlers/consumers.query.d.ts +52 -0
- package/dist/jobs/handlers/consumers.query.js +114 -0
- package/dist/jobs/job-run-table.d.ts +6 -126
- package/dist/jobs/screens.d.ts +6 -0
- package/dist/jobs/screens.js +241 -0
- package/dist/jobs/web/constants.d.ts +20 -0
- package/dist/jobs/web/constants.js +21 -0
- package/dist/jobs/web/i18n.js +41 -0
- package/dist/ledger/entity.d.ts +13 -273
- package/dist/managed-pages/table.d.ts +12 -252
- package/dist/notes-history/entity.d.ts +14 -280
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -126
- package/dist/secrets/i18n.d.ts +0 -3
- package/dist/secrets/i18n.js +0 -3
- package/dist/secrets/table.d.ts +1 -21
- package/dist/sessions/constants.d.ts +2 -1
- package/dist/sessions/constants.js +2 -1
- package/dist/sessions/handlers/detail.query.d.ts +5 -0
- package/dist/sessions/handlers/detail.query.js +7 -3
- package/dist/sessions/handlers/list.query.d.ts +5 -0
- package/dist/sessions/handlers/list.query.js +16 -10
- package/dist/sessions/handlers/mine.query.d.ts +5 -0
- package/dist/sessions/handlers/mine.query.js +15 -9
- package/dist/sessions/schema/user-session.d.ts +4 -84
- package/dist/sessions/screens.js +22 -8
- package/dist/sessions/web/client-plugin.d.ts +6 -0
- package/dist/sessions/web/client-plugin.js +11 -0
- package/dist/sessions/web/constants.d.ts +3 -0
- package/dist/sessions/web/constants.js +3 -0
- package/dist/sessions/web/default-translations.d.ts +2 -0
- package/dist/sessions/web/default-translations.js +3 -0
- package/dist/sessions/web/i18n.js +5 -1
- package/dist/sessions/web/index.d.ts +1 -0
- package/dist/sessions/web/index.js +1 -0
- package/dist/sessions/web/session-device-cell.d.ts +3 -0
- package/dist/sessions/web/session-device-cell.js +16 -0
- package/dist/sessions/web/user-agent.d.ts +26 -0
- package/dist/sessions/web/user-agent.js +58 -0
- package/dist/tags/entity.d.ts +9 -189
- package/dist/template-resolver/feature.d.ts +1 -0
- package/dist/template-resolver/feature.js +2 -0
- package/dist/template-resolver/handlers/overview.query.d.ts +5 -0
- package/dist/template-resolver/handlers/overview.query.js +19 -2
- package/dist/template-resolver/handlers/tenant-override-counts.query.d.ts +17 -0
- package/dist/template-resolver/handlers/tenant-override-counts.query.js +35 -0
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +12 -0
- package/dist/template-resolver/handlers/tenant-override-selection.js +34 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.js +3 -29
- package/dist/template-resolver/table.d.ts +20 -350
- package/dist/template-resolver/user-content-table.d.ts +12 -238
- package/dist/template-resolver/web/qualified-names.d.ts +1 -0
- package/dist/template-resolver/web/qualified-names.js +1 -0
- package/dist/tenant/invitation-table.d.ts +6 -126
- package/dist/tenant/membership-table.d.ts +4 -84
- package/dist/tenant/schema/tenant.d.ts +6 -126
- package/dist/tier-engine/entity.d.ts +1 -21
- package/dist/tier-engine/tier-resolver.d.ts +1 -21
- package/dist/user/schema/user.d.ts +12 -252
- package/dist/user-data-rights/feature.js +33 -33
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -294
- package/dist/user-data-rights/schema/download-token.d.ts +8 -168
- package/dist/user-data-rights/schema/export-job.d.ts +8 -154
- package/dist/user-data-rights/web/i18n.js +4 -1
- package/dist/waitlist/entity.d.ts +16 -322
- package/package.json +17 -9
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +6 -0
- package/src/jobs/changes.json +6 -0
- package/src/sessions/changes.json +6 -0
- package/src/template-resolver/changes.json +6 -0
- 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: {
|
package/dist/secrets/i18n.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/secrets/i18n.js
CHANGED
|
@@ -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
|
};
|
package/dist/secrets/table.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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">;
|
package/dist/sessions/screens.js
CHANGED
|
@@ -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: "
|
|
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
|
+
}
|
|
@@ -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: "
|
|
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,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
|
+
}
|