@devxcrew/platform 0.1.2

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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.js +2 -0
  5. package/dist/modules/identity/identity.administration-migration.d.ts +2 -0
  6. package/dist/modules/identity/identity.administration-migration.js +75 -0
  7. package/dist/modules/identity/identity.administration-repository.d.ts +19 -0
  8. package/dist/modules/identity/identity.administration-repository.js +102 -0
  9. package/dist/modules/identity/identity.administration-schema.d.ts +113 -0
  10. package/dist/modules/identity/identity.administration-schema.js +121 -0
  11. package/dist/modules/identity/identity.administration-service.d.ts +97 -0
  12. package/dist/modules/identity/identity.administration-service.js +517 -0
  13. package/dist/modules/identity/identity.controller.d.ts +22 -0
  14. package/dist/modules/identity/identity.controller.js +278 -0
  15. package/dist/modules/identity/identity.error.d.ts +6 -0
  16. package/dist/modules/identity/identity.error.js +15 -0
  17. package/dist/modules/identity/identity.lifecycle-schema.d.ts +19 -0
  18. package/dist/modules/identity/identity.lifecycle-schema.js +29 -0
  19. package/dist/modules/identity/identity.lifecycle-service.d.ts +64 -0
  20. package/dist/modules/identity/identity.lifecycle-service.js +395 -0
  21. package/dist/modules/identity/identity.membership-policy.d.ts +3 -0
  22. package/dist/modules/identity/identity.membership-policy.js +27 -0
  23. package/dist/modules/identity/identity.migration.d.ts +2 -0
  24. package/dist/modules/identity/identity.migration.js +79 -0
  25. package/dist/modules/identity/identity.password.d.ts +3 -0
  26. package/dist/modules/identity/identity.password.js +21 -0
  27. package/dist/modules/identity/identity.permission-declarations-migration.d.ts +5 -0
  28. package/dist/modules/identity/identity.permission-declarations-migration.js +15 -0
  29. package/dist/modules/identity/identity.permission-declarations.d.ts +23 -0
  30. package/dist/modules/identity/identity.permission-declarations.js +57 -0
  31. package/dist/modules/identity/identity.permission-labels-migration.d.ts +5 -0
  32. package/dist/modules/identity/identity.permission-labels-migration.js +9 -0
  33. package/dist/modules/identity/identity.presentation-service.d.ts +23 -0
  34. package/dist/modules/identity/identity.presentation-service.js +51 -0
  35. package/dist/modules/identity/identity.provider.d.ts +78 -0
  36. package/dist/modules/identity/identity.provider.js +119 -0
  37. package/dist/modules/identity/identity.public-schemas.d.ts +4 -0
  38. package/dist/modules/identity/identity.public-schemas.js +3 -0
  39. package/dist/modules/identity/identity.repository.d.ts +40 -0
  40. package/dist/modules/identity/identity.repository.js +196 -0
  41. package/dist/modules/identity/identity.request-context.d.ts +2 -0
  42. package/dist/modules/identity/identity.request-context.js +10 -0
  43. package/dist/modules/identity/identity.roles-migration.d.ts +2 -0
  44. package/dist/modules/identity/identity.roles-migration.js +64 -0
  45. package/dist/modules/identity/identity.roles-service.d.ts +49 -0
  46. package/dist/modules/identity/identity.roles-service.js +327 -0
  47. package/dist/modules/identity/identity.routes.d.ts +22 -0
  48. package/dist/modules/identity/identity.routes.js +11 -0
  49. package/dist/modules/identity/identity.schema.d.ts +27 -0
  50. package/dist/modules/identity/identity.schema.js +34 -0
  51. package/dist/modules/identity/identity.seed.d.ts +3 -0
  52. package/dist/modules/identity/identity.seed.js +81 -0
  53. package/dist/modules/identity/identity.service.d.ts +32 -0
  54. package/dist/modules/identity/identity.service.js +83 -0
  55. package/dist/modules/identity/identity.types.d.ts +150 -0
  56. package/dist/modules/identity/identity.types.js +1 -0
  57. package/dist/modules/identity/index.d.ts +12 -0
  58. package/dist/modules/identity/index.js +9 -0
  59. package/package.json +62 -0
@@ -0,0 +1,23 @@
1
+ import type { Kysely } from "kysely";
2
+ import type { IdentitySchema, Principal } from "./identity.types.js";
3
+ export declare class IdentityPresentationService {
4
+ private readonly db;
5
+ private readonly appName;
6
+ constructor(db: Kysely<IdentitySchema>, appName: string);
7
+ configuration(appId: string, requiresOrganizationId: boolean): Promise<{
8
+ displayName: string;
9
+ requiresOrganizationId: boolean;
10
+ }>;
11
+ presentation(actor: Principal): Promise<{
12
+ displayName: string;
13
+ organizationDisplayName: string;
14
+ locale: string;
15
+ timeZone: string;
16
+ }>;
17
+ organizationSettings(actor: Principal): Promise<{
18
+ displayName: string;
19
+ locale: string;
20
+ timeZone: string;
21
+ version: number;
22
+ }>;
23
+ }
@@ -0,0 +1,51 @@
1
+ export class IdentityPresentationService {
2
+ db;
3
+ appName;
4
+ constructor(db, appName) {
5
+ this.db = db;
6
+ this.appName = appName;
7
+ }
8
+ async configuration(appId, requiresOrganizationId) {
9
+ const app = await this.db
10
+ .selectFrom("identity_app_settings")
11
+ .select("display_name")
12
+ .where("app_id", "=", appId)
13
+ .executeTakeFirst();
14
+ return {
15
+ displayName: app?.display_name ?? this.appName,
16
+ requiresOrganizationId,
17
+ };
18
+ }
19
+ async presentation(actor) {
20
+ const app = await this.db
21
+ .selectFrom("identity_app_settings")
22
+ .select(["display_name", "locale", "time_zone"])
23
+ .where("app_id", "=", actor.appId)
24
+ .executeTakeFirst();
25
+ const organization = await this.db
26
+ .selectFrom("identity_settings")
27
+ .selectAll()
28
+ .where("tenant_id", "=", actor.tenant.id)
29
+ .executeTakeFirst();
30
+ return {
31
+ displayName: app?.display_name ?? this.appName,
32
+ organizationDisplayName: organization?.display_name ?? actor.tenant.name,
33
+ locale: organization?.locale ?? app?.locale ?? "en",
34
+ timeZone: organization?.time_zone ?? app?.time_zone ?? "UTC",
35
+ };
36
+ }
37
+ async organizationSettings(actor) {
38
+ const organization = await this.db
39
+ .selectFrom("identity_settings")
40
+ .select("version")
41
+ .where("tenant_id", "=", actor.tenant.id)
42
+ .executeTakeFirst();
43
+ const presentation = await this.presentation(actor);
44
+ return {
45
+ displayName: presentation.organizationDisplayName,
46
+ locale: presentation.locale,
47
+ timeZone: presentation.timeZone,
48
+ version: Number(organization?.version ?? 0),
49
+ };
50
+ }
51
+ }
@@ -0,0 +1,78 @@
1
+ import type { Kysely } from "kysely";
2
+ import type { IdentitySchema, IdentityProviderOptions } from "./identity.types.js";
3
+ import { type IdentityPermissionDeclaration } from "./identity.permission-declarations.js";
4
+ export declare function createIdentityProvider(database: Kysely<IdentitySchema>, environment: NodeJS.ProcessEnv, options?: IdentityProviderOptions): {
5
+ registerPermissions: (declaration: IdentityPermissionDeclaration) => Promise<{
6
+ owner: string;
7
+ permissionIds: string[];
8
+ }>;
9
+ handle: (request: import("node:http").IncomingMessage, response: import("node:http").ServerResponse, signal?: AbortSignal) => Promise<boolean>;
10
+ authenticate: (portal: import("./identity.types.js").Portal, token?: string) => Promise<import("./identity.types.js").Principal>;
11
+ authenticateRequest: (request: import("node:http").IncomingMessage, requestedPortal: import("./identity.types.js").Portal, signal?: AbortSignal) => Promise<import("./identity.types.js").Principal>;
12
+ requirePermission: (principal: import("./identity.types.js").Principal, permission: string) => void;
13
+ administration: {
14
+ list: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, query: import("./identity.administration-schema.js").IdentityListQuery) => Promise<{
15
+ data: Record<string, unknown>[];
16
+ meta: {
17
+ page: number;
18
+ perPage: number;
19
+ total: number;
20
+ lastPage: number;
21
+ };
22
+ }>;
23
+ show: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, id: string) => Promise<Record<string, unknown>>;
24
+ create: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, raw: unknown) => Promise<{
25
+ id: string;
26
+ name: string;
27
+ active: boolean;
28
+ version: number;
29
+ } | {
30
+ id: string;
31
+ userId: string;
32
+ tenantId: string;
33
+ roleId: string;
34
+ portal: import("./identity.types.js").Portal;
35
+ active: boolean;
36
+ version: number;
37
+ }>;
38
+ update: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, id: string, raw: unknown) => Promise<{
39
+ id: string;
40
+ userId: string;
41
+ tenantId: string;
42
+ roleId: string;
43
+ portal: import("./identity.types.js").Portal;
44
+ active: boolean;
45
+ version: number;
46
+ } | {
47
+ active: boolean;
48
+ version: number;
49
+ id: string;
50
+ name: string;
51
+ } | {
52
+ id: string;
53
+ permissionIds: string[];
54
+ version: number;
55
+ }>;
56
+ remove: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, id: string, expectedVersion?: number) => Promise<void>;
57
+ profile: (actor: import("./identity.types.js").Principal, raw?: unknown) => Promise<{
58
+ active: boolean;
59
+ version: number;
60
+ email: string;
61
+ id: string;
62
+ name: string;
63
+ }>;
64
+ settings: (actor: import("./identity.types.js").Principal, raw?: unknown) => Promise<{
65
+ displayName: string;
66
+ locale: string;
67
+ timeZone: string;
68
+ version: number;
69
+ }>;
70
+ presentation: (actor: import("./identity.types.js").Principal) => Promise<{
71
+ displayName: string;
72
+ organizationDisplayName: string;
73
+ locale: string;
74
+ timeZone: string;
75
+ }>;
76
+ };
77
+ verify(): Promise<void>;
78
+ };
@@ -0,0 +1,119 @@
1
+ import { z } from "zod";
2
+ import { IdentityRepository } from "./identity.repository.js";
3
+ import { IdentityService } from "./identity.service.js";
4
+ import { IdentityController } from "./identity.controller.js";
5
+ import { IdentityAdministrationService } from "./identity.administration-service.js";
6
+ import { IdentityLifecycleService } from "./identity.lifecycle-service.js";
7
+ import { identityKeySchema } from "./identity.schema.js";
8
+ import { registerIdentityPermissions } from "./identity.permission-declarations.js";
9
+ export function createIdentityProvider(database, environment, options = {}) {
10
+ const config = z
11
+ .object({
12
+ appId: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/),
13
+ origin: z.url(),
14
+ mode: z.enum(["single-client", "multi-tenant"]),
15
+ tenantId: identityKeySchema,
16
+ sessionSeconds: z.coerce.number().int().min(300).max(86400),
17
+ })
18
+ .parse({
19
+ appId: environment.APP_ID,
20
+ origin: environment.APP_URL,
21
+ mode: environment.IDENTITY_MODE ?? "single-client",
22
+ tenantId: environment.IDENTITY_TENANT_ID ?? "default",
23
+ sessionSeconds: environment.IDENTITY_SESSION_SECONDS ?? 28800,
24
+ });
25
+ const origin = new URL(config.origin);
26
+ if (origin.origin !== config.origin ||
27
+ !["http:", "https:"].includes(origin.protocol))
28
+ throw new Error("Identity requires a valid application origin.");
29
+ if (origin.protocol !== "https:" &&
30
+ !["127.0.0.1", "localhost", "[::1]"].includes(origin.hostname))
31
+ throw new Error("Identity requires HTTPS outside loopback development.");
32
+ const service = new IdentityService(new IdentityRepository(database), config);
33
+ const appName = z
34
+ .string()
35
+ .trim()
36
+ .min(1)
37
+ .max(100)
38
+ .parse(environment.APP_NAME || config.appId);
39
+ const administration = new IdentityAdministrationService(database, appName);
40
+ const lifecycle = new IdentityLifecycleService(database, config, options.delivery);
41
+ const controller = new IdentityController(service, config, administration, lifecycle);
42
+ return {
43
+ registerPermissions: (declaration) => registerIdentityPermissions(database, config.appId, declaration),
44
+ handle: controller.handle.bind(controller),
45
+ authenticate: service.authenticate.bind(service),
46
+ authenticateRequest: controller.authenticateRequest.bind(controller),
47
+ requirePermission: service.requirePermission.bind(service),
48
+ administration: {
49
+ list: administration.list.bind(administration),
50
+ show: administration.show.bind(administration),
51
+ create: administration.create.bind(administration),
52
+ update: administration.update.bind(administration),
53
+ remove: administration.remove.bind(administration),
54
+ profile: administration.profile.bind(administration),
55
+ settings: administration.settings.bind(administration),
56
+ presentation: administration.presentation.bind(administration),
57
+ },
58
+ async verify() {
59
+ await database.selectFrom("identity_permission_declarations").select(["permission_id", "label"]).limit(1).execute();
60
+ for (const declaration of options.permissions ?? [])
61
+ await registerIdentityPermissions(database, config.appId, declaration);
62
+ await database
63
+ .selectFrom("identity_roles")
64
+ .select("id")
65
+ .limit(1)
66
+ .execute();
67
+ await database
68
+ .selectFrom("identity_users")
69
+ .select("version")
70
+ .limit(1)
71
+ .execute();
72
+ await database
73
+ .selectFrom("identity_tenants")
74
+ .select("version")
75
+ .limit(1)
76
+ .execute();
77
+ await database
78
+ .selectFrom("identity_roles")
79
+ .select("version")
80
+ .limit(1)
81
+ .execute();
82
+ await database
83
+ .selectFrom("identity_settings")
84
+ .select("version")
85
+ .limit(1)
86
+ .execute();
87
+ await database
88
+ .selectFrom("identity_app_settings")
89
+ .select(["version", "session_seconds"])
90
+ .limit(1)
91
+ .execute();
92
+ await database
93
+ .selectFrom("identity_tokens")
94
+ .select(["delivered", "consumed_at"])
95
+ .limit(1)
96
+ .execute();
97
+ await database
98
+ .selectFrom("identity_audit_events")
99
+ .select("created_at")
100
+ .limit(1)
101
+ .execute();
102
+ await database
103
+ .selectFrom("identity_custom_roles")
104
+ .select(["app_id", "tenant_id", "version"])
105
+ .limit(1)
106
+ .execute();
107
+ await database
108
+ .selectFrom("identity_custom_role_permissions")
109
+ .select("permission_id")
110
+ .limit(1)
111
+ .execute();
112
+ await database
113
+ .selectFrom("identity_memberships")
114
+ .select(["custom_role_id", "active", "version"])
115
+ .limit(1)
116
+ .execute();
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./identity.administration-schema.js";
2
+ export * from "./identity.lifecycle-schema.js";
3
+ export { loginSchema, passwordSchema, portalSchema, } from "./identity.schema.js";
4
+ export type { IdentityPermissionCatalogEntry } from "./identity.types.js";
@@ -0,0 +1,3 @@
1
+ export * from "./identity.administration-schema.js";
2
+ export * from "./identity.lifecycle-schema.js";
3
+ export { loginSchema, passwordSchema, portalSchema, } from "./identity.schema.js";
@@ -0,0 +1,40 @@
1
+ import { type Kysely } from "kysely";
2
+ import type { IdentitySchema, Portal } from "./identity.types.js";
3
+ export declare class IdentityRepository {
4
+ private readonly db;
5
+ constructor(db: Kysely<IdentitySchema>);
6
+ user(email: string): Promise<{
7
+ active: number;
8
+ email: string;
9
+ id: string;
10
+ name: string;
11
+ password_hash: string;
12
+ version: number;
13
+ } | undefined>;
14
+ sessionSeconds(appId: string, fallback: number): Promise<number>;
15
+ principal(userId: string, tenantId: string, portal: Portal, appId: string): Promise<{
16
+ user: {
17
+ id: string;
18
+ email: string;
19
+ name: string;
20
+ };
21
+ tenant: {
22
+ id: string;
23
+ name: string;
24
+ };
25
+ permissions: string[];
26
+ } | null>;
27
+ session(tokenHash: string, appId: string, portal: Portal, now: string): Promise<{
28
+ app_id: string;
29
+ expires_at: string;
30
+ portal: Portal;
31
+ tenant_id: string;
32
+ token_hash: string;
33
+ user_id: string;
34
+ } | undefined>;
35
+ createSession(session: IdentitySchema["identity_sessions"], expectedHash: string, previous?: string): Promise<boolean>;
36
+ revoke(tokenHash: string, appId: string, portal: Portal): Promise<void>;
37
+ throttle(key: string): Promise<number>;
38
+ clearThrottle(key: string): Promise<void>;
39
+ changePassword(userId: string, expectedHash: string, passwordHash: string): Promise<boolean>;
40
+ }
@@ -0,0 +1,196 @@
1
+ import { filterDeclaredPermissions } from "./identity.permission-declarations.js";
2
+ import { sql } from "kysely";
3
+ import { checkIdentityRequest } from "./identity.request-context.js";
4
+ export class IdentityRepository {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ user(email) {
10
+ return this.db
11
+ .selectFrom("identity_users")
12
+ .selectAll()
13
+ .where("email", "=", email)
14
+ .executeTakeFirst();
15
+ }
16
+ async sessionSeconds(appId, fallback) {
17
+ const row = await this.db
18
+ .selectFrom("identity_app_settings")
19
+ .select("session_seconds")
20
+ .where("app_id", "=", appId)
21
+ .executeTakeFirst();
22
+ return Number(row?.session_seconds ?? fallback);
23
+ }
24
+ async principal(userId, tenantId, portal, appId) {
25
+ const member = await this.db
26
+ .selectFrom("identity_memberships as m")
27
+ .innerJoin("identity_users as u", "u.id", "m.user_id")
28
+ .innerJoin("identity_tenants as t", "t.id", "m.tenant_id")
29
+ .select([
30
+ "u.id",
31
+ "u.email",
32
+ "u.name",
33
+ "t.id as tenantId",
34
+ "t.name as tenantName",
35
+ "m.custom_role_id as customRoleId",
36
+ ])
37
+ .where("u.id", "=", userId)
38
+ .where("m.tenant_id", "=", tenantId)
39
+ .where("m.role_id", "=", portal)
40
+ .where("m.active", "=", 1)
41
+ .where("u.active", "=", 1)
42
+ .where("t.active", "=", 1)
43
+ .executeTakeFirst();
44
+ if (!member)
45
+ return null;
46
+ if (member.customRoleId) {
47
+ const role = await this.db
48
+ .selectFrom("identity_custom_roles")
49
+ .select("id")
50
+ .where("id", "=", member.customRoleId)
51
+ .where("app_id", "=", appId)
52
+ .where("tenant_id", "=", tenantId)
53
+ .where("active", "=", 1)
54
+ .executeTakeFirst();
55
+ if (!role || portal !== "user")
56
+ return null;
57
+ const permissions = await this.db
58
+ .selectFrom("identity_custom_role_permissions")
59
+ .select("permission_id")
60
+ .where("role_id", "=", role.id)
61
+ .execute();
62
+ return {
63
+ user: { id: member.id, email: member.email, name: member.name },
64
+ tenant: { id: member.tenantId, name: member.tenantName },
65
+ permissions: await filterDeclaredPermissions(this.db, appId, portal, permissions.map((row) => row.permission_id)),
66
+ };
67
+ }
68
+ const permissions = await this.db
69
+ .selectFrom("identity_role_permissions")
70
+ .select("permission_id")
71
+ .where("role_id", "=", portal)
72
+ .execute();
73
+ return {
74
+ user: { id: member.id, email: member.email, name: member.name },
75
+ tenant: { id: member.tenantId, name: member.tenantName },
76
+ permissions: await filterDeclaredPermissions(this.db, appId, portal, permissions.map((p) => p.permission_id)),
77
+ };
78
+ }
79
+ async session(tokenHash, appId, portal, now) {
80
+ return this.db
81
+ .selectFrom("identity_sessions")
82
+ .selectAll()
83
+ .where("token_hash", "=", tokenHash)
84
+ .where("app_id", "=", appId)
85
+ .where("portal", "=", portal)
86
+ .where("expires_at", ">", now)
87
+ .executeTakeFirst();
88
+ }
89
+ async createSession(session, expectedHash, previous) {
90
+ return this.db.transaction().execute(async (trx) => {
91
+ checkIdentityRequest();
92
+ const current = await trx
93
+ .selectFrom("identity_users")
94
+ .select("id")
95
+ .where("id", "=", session.user_id)
96
+ .where("password_hash", "=", expectedHash)
97
+ .where("active", "=", 1)
98
+ .executeTakeFirst();
99
+ if (!current)
100
+ return false;
101
+ const membership = await trx
102
+ .selectFrom("identity_memberships")
103
+ .select("custom_role_id")
104
+ .where("user_id", "=", session.user_id)
105
+ .where("tenant_id", "=", session.tenant_id)
106
+ .where("role_id", "=", session.portal)
107
+ .where("active", "=", 1)
108
+ .executeTakeFirst();
109
+ if (!membership)
110
+ return false;
111
+ if (membership.custom_role_id) {
112
+ const role = await trx
113
+ .selectFrom("identity_custom_roles")
114
+ .select("id")
115
+ .where("id", "=", membership.custom_role_id)
116
+ .where("app_id", "=", session.app_id)
117
+ .where("tenant_id", "=", session.tenant_id)
118
+ .where("active", "=", 1)
119
+ .executeTakeFirst();
120
+ if (!role)
121
+ return false;
122
+ }
123
+ await trx
124
+ .deleteFrom("identity_sessions")
125
+ .where("expires_at", "<=", new Date().toISOString())
126
+ .execute();
127
+ if (previous)
128
+ await trx
129
+ .deleteFrom("identity_sessions")
130
+ .where("token_hash", "=", previous)
131
+ .where("app_id", "=", session.app_id)
132
+ .where("portal", "=", session.portal)
133
+ .execute();
134
+ await trx.insertInto("identity_sessions").values(session).execute();
135
+ checkIdentityRequest();
136
+ return true;
137
+ });
138
+ }
139
+ async revoke(tokenHash, appId, portal) {
140
+ checkIdentityRequest();
141
+ await this.db
142
+ .deleteFrom("identity_sessions")
143
+ .where("token_hash", "=", tokenHash)
144
+ .where("app_id", "=", appId)
145
+ .where("portal", "=", portal)
146
+ .execute();
147
+ }
148
+ async throttle(key) {
149
+ const now = new Date();
150
+ return this.db.transaction().execute(async (trx) => {
151
+ await trx
152
+ .deleteFrom("identity_throttles")
153
+ .where("expires_at", "<=", now.toISOString())
154
+ .execute();
155
+ await trx
156
+ .insertInto("identity_throttles")
157
+ .values({
158
+ key,
159
+ attempts: 1,
160
+ expires_at: new Date(now.getTime() + 15 * 60 * 1000).toISOString(),
161
+ })
162
+ .onConflict((c) => c.column("key").doUpdateSet({ attempts: sql `attempts + 1` }))
163
+ .execute();
164
+ const row = await trx
165
+ .selectFrom("identity_throttles")
166
+ .select("attempts")
167
+ .where("key", "=", key)
168
+ .executeTakeFirstOrThrow();
169
+ return Number(row.attempts);
170
+ });
171
+ }
172
+ async clearThrottle(key) {
173
+ await this.db
174
+ .deleteFrom("identity_throttles")
175
+ .where("key", "=", key)
176
+ .execute();
177
+ }
178
+ async changePassword(userId, expectedHash, passwordHash) {
179
+ return this.db.transaction().execute(async (trx) => {
180
+ checkIdentityRequest();
181
+ const result = await trx
182
+ .updateTable("identity_users")
183
+ .set({ password_hash: passwordHash })
184
+ .where("id", "=", userId)
185
+ .where("password_hash", "=", expectedHash)
186
+ .executeTakeFirst();
187
+ if (result.numUpdatedRows !== 1n)
188
+ return false;
189
+ await trx
190
+ .deleteFrom("identity_sessions")
191
+ .where("user_id", "=", userId)
192
+ .execute();
193
+ return true;
194
+ });
195
+ }
196
+ }
@@ -0,0 +1,2 @@
1
+ export declare function runIdentityRequest<T>(signal: AbortSignal | undefined, work: () => T): T;
2
+ export declare function checkIdentityRequest(): void;
@@ -0,0 +1,10 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { IdentityError } from "./identity.error.js";
3
+ const requestSignals = new AsyncLocalStorage();
4
+ export function runIdentityRequest(signal, work) {
5
+ return signal ? requestSignals.run(signal, work) : work();
6
+ }
7
+ export function checkIdentityRequest() {
8
+ if (requestSignals.getStore()?.aborted)
9
+ throw new IdentityError(408, "The request expired. Try again.");
10
+ }
@@ -0,0 +1,2 @@
1
+ import type { Migration } from "kysely/migration";
2
+ export declare const identityRolesMigration: Migration;
@@ -0,0 +1,64 @@
1
+ export const identityRolesMigration = {
2
+ async up(db) {
3
+ await db.schema
4
+ .createTable("identity_custom_roles")
5
+ .addColumn("id", "text", (c) => c.primaryKey())
6
+ .addColumn("app_id", "text", (c) => c.notNull())
7
+ .addColumn("tenant_id", "text", (c) => c.notNull().references("identity_tenants.id"))
8
+ .addColumn("name", "text", (c) => c.notNull())
9
+ .addColumn("active", "integer", (c) => c.notNull().defaultTo(1))
10
+ .addColumn("version", "integer", (c) => c.notNull().defaultTo(0))
11
+ .addUniqueConstraint("identity_custom_role_name", [
12
+ "app_id",
13
+ "tenant_id",
14
+ "name",
15
+ ])
16
+ .execute();
17
+ await db.schema
18
+ .createTable("identity_custom_role_permissions")
19
+ .addColumn("role_id", "text", (c) => c.notNull().references("identity_custom_roles.id").onDelete("cascade"))
20
+ .addColumn("permission_id", "text", (c) => c.notNull().references("identity_permissions.id"))
21
+ .addPrimaryKeyConstraint("identity_custom_role_permission_pk", [
22
+ "role_id",
23
+ "permission_id",
24
+ ])
25
+ .execute();
26
+ await db.schema
27
+ .alterTable("identity_memberships")
28
+ .addColumn("custom_role_id", "text", (c) => c.references("identity_custom_roles.id"))
29
+ .execute();
30
+ await db.schema
31
+ .alterTable("identity_memberships")
32
+ .addColumn("active", "integer", (c) => c.notNull().defaultTo(1))
33
+ .execute();
34
+ await db.schema
35
+ .alterTable("identity_memberships")
36
+ .addColumn("version", "integer", (c) => c.notNull().defaultTo(0))
37
+ .execute();
38
+ },
39
+ async down(db) {
40
+ const customRole = await db
41
+ .selectFrom("identity_custom_roles")
42
+ .select("id")
43
+ .limit(1)
44
+ .executeTakeFirst();
45
+ const restrictedMembership = await db
46
+ .selectFrom("identity_memberships")
47
+ .select("user_id")
48
+ .where((eb) => eb.or([
49
+ eb("custom_role_id", "is not", null),
50
+ eb("active", "!=", 1),
51
+ ]))
52
+ .limit(1)
53
+ .executeTakeFirst();
54
+ if (customRole || restrictedMembership)
55
+ throw new Error("Role rollback would remove access restrictions. Restore a compatible database snapshot instead.");
56
+ for (const column of ["custom_role_id", "active", "version"])
57
+ await db.schema
58
+ .alterTable("identity_memberships")
59
+ .dropColumn(column)
60
+ .execute();
61
+ await db.schema.dropTable("identity_custom_role_permissions").execute();
62
+ await db.schema.dropTable("identity_custom_roles").execute();
63
+ },
64
+ };
@@ -0,0 +1,49 @@
1
+ import type { Kysely } from "kysely";
2
+ import type { IdentitySchema, Portal, Principal } from "./identity.types.js";
3
+ export declare class IdentityRolesService {
4
+ private readonly db;
5
+ constructor(db: Kysely<IdentitySchema>);
6
+ resolve(database: Kysely<IdentitySchema>, actor: Principal, roleId: string, tenantId: string): Promise<{
7
+ portal: Portal;
8
+ customRoleId: null;
9
+ } | {
10
+ portal: "user";
11
+ customRoleId: string;
12
+ }>;
13
+ create(actor: Principal, raw: unknown): Promise<{
14
+ id: `${string}-${string}-${string}-${string}-${string}`;
15
+ name: string;
16
+ tenantId: string;
17
+ portal: string;
18
+ system: boolean;
19
+ active: boolean;
20
+ version: number;
21
+ permissionIds: string[];
22
+ }>;
23
+ update(actor: Principal, id: string, raw: unknown): Promise<{
24
+ id: string;
25
+ name: string;
26
+ tenantId: string;
27
+ portal: string;
28
+ system: boolean;
29
+ active: boolean;
30
+ version: number;
31
+ permissionIds: string[];
32
+ }>;
33
+ updateMembership(actor: Principal, id: string, raw: unknown): Promise<{
34
+ id: string;
35
+ userId: string;
36
+ tenantId: string;
37
+ roleId: string;
38
+ portal: Portal;
39
+ active: boolean;
40
+ version: number;
41
+ }>;
42
+ removeMembership(actor: Principal, id: string, expectedVersion?: number): Promise<void>;
43
+ private manage;
44
+ private scope;
45
+ private permissions;
46
+ private uniqueName;
47
+ private replacePermissions;
48
+ private audit;
49
+ }