@scryan7371/sdr-security 0.1.1

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 (47) hide show
  1. package/README.md +72 -0
  2. package/dist/api/access-policy.d.ts +15 -0
  3. package/dist/api/access-policy.js +41 -0
  4. package/dist/api/access-policy.test.d.ts +1 -0
  5. package/dist/api/access-policy.test.js +67 -0
  6. package/dist/api/contracts.d.ts +46 -0
  7. package/dist/api/contracts.js +4 -0
  8. package/dist/api/index.d.ts +5 -0
  9. package/dist/api/index.js +21 -0
  10. package/dist/api/migrations/1700000000001-add-refresh-tokens.d.ts +9 -0
  11. package/dist/api/migrations/1700000000001-add-refresh-tokens.js +40 -0
  12. package/dist/api/migrations/1739490000000-add-google-subject-to-user.d.ts +5 -0
  13. package/dist/api/migrations/1739490000000-add-google-subject-to-user.js +14 -0
  14. package/dist/api/migrations/1739500000000-create-security-identity.d.ts +9 -0
  15. package/dist/api/migrations/1739500000000-create-security-identity.js +68 -0
  16. package/dist/api/migrations/1739510000000-create-security-roles.d.ts +9 -0
  17. package/dist/api/migrations/1739510000000-create-security-roles.js +95 -0
  18. package/dist/api/migrations/index.d.ts +6 -0
  19. package/dist/api/migrations/index.js +17 -0
  20. package/dist/api/roles.d.ts +3 -0
  21. package/dist/api/roles.js +29 -0
  22. package/dist/api/roles.test.d.ts +1 -0
  23. package/dist/api/roles.test.js +19 -0
  24. package/dist/api/validation.d.ts +3 -0
  25. package/dist/api/validation.js +9 -0
  26. package/dist/app/client.d.ts +52 -0
  27. package/dist/app/client.js +78 -0
  28. package/dist/app/index.d.ts +1 -0
  29. package/dist/app/index.js +17 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +38 -0
  32. package/package.json +34 -0
  33. package/src/api/access-policy.test.ts +103 -0
  34. package/src/api/access-policy.ts +68 -0
  35. package/src/api/contracts.ts +47 -0
  36. package/src/api/index.ts +5 -0
  37. package/src/api/migrations/1700000000001-add-refresh-tokens.ts +47 -0
  38. package/src/api/migrations/1739490000000-add-google-subject-to-user.ts +12 -0
  39. package/src/api/migrations/1739500000000-create-security-identity.ts +95 -0
  40. package/src/api/migrations/1739510000000-create-security-roles.ts +122 -0
  41. package/src/api/migrations/index.ts +18 -0
  42. package/src/api/roles.test.ts +20 -0
  43. package/src/api/roles.ts +25 -0
  44. package/src/api/validation.ts +7 -0
  45. package/src/app/client.ts +131 -0
  46. package/src/app/index.ts +1 -0
  47. package/src/index.ts +2 -0
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # sdr-security
2
+
3
+ Reusable auth/security capability for API and app clients.
4
+
5
+ ## Surfaces
6
+
7
+ - `api`: shared auth types and input validation helpers.
8
+ - `app`: typed client for auth endpoints.
9
+
10
+ ## API Integration
11
+
12
+ Use shared helpers/types in your API controllers/services where useful:
13
+
14
+ - `sanitizeEmail`
15
+ - `isValidEmail`
16
+ - `isStrongPassword`
17
+ - `AuthResponse`, `RegisterResponse`, `SafeUser`
18
+
19
+ ## App Integration
20
+
21
+ Create one client per app session and reuse it across screens:
22
+
23
+ ```ts
24
+ import { app as sdrSecurity } from '@scryan7371/sdr-security';
25
+
26
+ const securityClient = sdrSecurity.createSecurityClient({
27
+ baseUrl,
28
+ getAccessToken: () => accessToken,
29
+ });
30
+ ```
31
+
32
+ Methods:
33
+
34
+ - `register`
35
+ - `login`
36
+ - `loginWithGoogle`
37
+ - `refresh`
38
+ - `revoke`
39
+ - `logout`
40
+ - `requestEmailVerification`
41
+ - `verifyEmail`
42
+ - `requestPhoneVerification`
43
+ - `verifyPhone`
44
+
45
+ ## Private Registry Publish (GitHub Packages)
46
+
47
+ 1. Set your token:
48
+
49
+ ```bash
50
+ export GITHUB_PACKAGES_TOKEN=ghp_xxx
51
+ ```
52
+
53
+ 2. Publish:
54
+
55
+ ```bash
56
+ npm publish
57
+ ```
58
+
59
+ ## Install From Any Environment
60
+
61
+ 1. Add auth to your consuming project's `.npmrc`:
62
+
63
+ ```ini
64
+ @scryan7371:registry=https://npm.pkg.github.com
65
+ //npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
66
+ ```
67
+
68
+ 2. Install a pinned version:
69
+
70
+ ```bash
71
+ npm install @scryan7371/sdr-security@0.1.0
72
+ ```
@@ -0,0 +1,15 @@
1
+ export type AccessPolicyUser = {
2
+ isActive: boolean;
3
+ emailVerifiedAt: string | Date | null;
4
+ phoneVerifiedAt?: string | Date | null;
5
+ adminApprovedAt: string | Date | null;
6
+ };
7
+ export type AccessPolicyOptions = {
8
+ requireEmailVerification?: boolean;
9
+ requirePhoneVerification?: boolean;
10
+ requireAdminApproval?: boolean;
11
+ requireActive?: boolean;
12
+ };
13
+ export type AccessBlockReason = "EMAIL_VERIFICATION_REQUIRED" | "PHONE_VERIFICATION_REQUIRED" | "ADMIN_APPROVAL_REQUIRED" | "ACCOUNT_DEACTIVATED";
14
+ export declare const getAuthBlockReason: (user: AccessPolicyUser, options?: AccessPolicyOptions) => AccessBlockReason | null;
15
+ export declare const accessBlockReasonToMessage: (reason: AccessBlockReason) => string;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.accessBlockReasonToMessage = exports.getAuthBlockReason = void 0;
4
+ const DEFAULT_OPTIONS = {
5
+ requireEmailVerification: true,
6
+ requirePhoneVerification: false,
7
+ requireAdminApproval: true,
8
+ requireActive: true,
9
+ };
10
+ const getAuthBlockReason = (user, options = {}) => {
11
+ const effective = { ...DEFAULT_OPTIONS, ...options };
12
+ if (effective.requireActive && !user.isActive) {
13
+ return "ACCOUNT_DEACTIVATED";
14
+ }
15
+ if (effective.requireEmailVerification && !user.emailVerifiedAt) {
16
+ return "EMAIL_VERIFICATION_REQUIRED";
17
+ }
18
+ if (effective.requirePhoneVerification && !user.phoneVerifiedAt) {
19
+ return "PHONE_VERIFICATION_REQUIRED";
20
+ }
21
+ if (effective.requireAdminApproval && !user.adminApprovedAt) {
22
+ return "ADMIN_APPROVAL_REQUIRED";
23
+ }
24
+ return null;
25
+ };
26
+ exports.getAuthBlockReason = getAuthBlockReason;
27
+ const accessBlockReasonToMessage = (reason) => {
28
+ switch (reason) {
29
+ case "EMAIL_VERIFICATION_REQUIRED":
30
+ return "Email verification required";
31
+ case "PHONE_VERIFICATION_REQUIRED":
32
+ return "Phone verification required";
33
+ case "ADMIN_APPROVAL_REQUIRED":
34
+ return "Admin approval required";
35
+ case "ACCOUNT_DEACTIVATED":
36
+ return "Account deactivated";
37
+ default:
38
+ return "Unauthorized";
39
+ }
40
+ };
41
+ exports.accessBlockReasonToMessage = accessBlockReasonToMessage;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const access_policy_1 = require("./access-policy");
5
+ const baseUser = {
6
+ isActive: true,
7
+ emailVerifiedAt: new Date(),
8
+ phoneVerifiedAt: null,
9
+ adminApprovedAt: new Date(),
10
+ };
11
+ (0, vitest_1.describe)("getAuthBlockReason", () => {
12
+ (0, vitest_1.it)("returns null for user meeting default requirements", () => {
13
+ (0, vitest_1.expect)((0, access_policy_1.getAuthBlockReason)(baseUser)).toBeNull();
14
+ });
15
+ (0, vitest_1.it)("blocks deactivated users first", () => {
16
+ (0, vitest_1.expect)((0, access_policy_1.getAuthBlockReason)({
17
+ ...baseUser,
18
+ isActive: false,
19
+ emailVerifiedAt: null,
20
+ adminApprovedAt: null,
21
+ })).toBe("ACCOUNT_DEACTIVATED");
22
+ });
23
+ (0, vitest_1.it)("blocks when email verification is required and missing", () => {
24
+ (0, vitest_1.expect)((0, access_policy_1.getAuthBlockReason)({
25
+ ...baseUser,
26
+ emailVerifiedAt: null,
27
+ })).toBe("EMAIL_VERIFICATION_REQUIRED");
28
+ });
29
+ (0, vitest_1.it)("does not block missing phone when phone verification is disabled", () => {
30
+ (0, vitest_1.expect)((0, access_policy_1.getAuthBlockReason)({
31
+ ...baseUser,
32
+ phoneVerifiedAt: null,
33
+ })).toBeNull();
34
+ });
35
+ (0, vitest_1.it)("blocks missing phone when phone verification is enabled", () => {
36
+ (0, vitest_1.expect)((0, access_policy_1.getAuthBlockReason)({
37
+ ...baseUser,
38
+ phoneVerifiedAt: null,
39
+ }, { requirePhoneVerification: true })).toBe("PHONE_VERIFICATION_REQUIRED");
40
+ });
41
+ (0, vitest_1.it)("blocks when admin approval is required and missing", () => {
42
+ (0, vitest_1.expect)((0, access_policy_1.getAuthBlockReason)({
43
+ ...baseUser,
44
+ adminApprovedAt: null,
45
+ })).toBe("ADMIN_APPROVAL_REQUIRED");
46
+ });
47
+ (0, vitest_1.it)("respects disabled flags", () => {
48
+ (0, vitest_1.expect)((0, access_policy_1.getAuthBlockReason)({
49
+ ...baseUser,
50
+ isActive: false,
51
+ emailVerifiedAt: null,
52
+ adminApprovedAt: null,
53
+ }, {
54
+ requireActive: false,
55
+ requireEmailVerification: false,
56
+ requireAdminApproval: false,
57
+ })).toBeNull();
58
+ });
59
+ });
60
+ (0, vitest_1.describe)("accessBlockReasonToMessage", () => {
61
+ (0, vitest_1.it)("maps each reason to expected message", () => {
62
+ (0, vitest_1.expect)((0, access_policy_1.accessBlockReasonToMessage)("EMAIL_VERIFICATION_REQUIRED")).toBe("Email verification required");
63
+ (0, vitest_1.expect)((0, access_policy_1.accessBlockReasonToMessage)("PHONE_VERIFICATION_REQUIRED")).toBe("Phone verification required");
64
+ (0, vitest_1.expect)((0, access_policy_1.accessBlockReasonToMessage)("ADMIN_APPROVAL_REQUIRED")).toBe("Admin approval required");
65
+ (0, vitest_1.expect)((0, access_policy_1.accessBlockReasonToMessage)("ACCOUNT_DEACTIVATED")).toBe("Account deactivated");
66
+ });
67
+ });
@@ -0,0 +1,46 @@
1
+ export declare const ADMIN_ROLE = "ADMIN";
2
+ export type UserRole = string;
3
+ export type SafeUser = {
4
+ id: string;
5
+ email: string;
6
+ firstName: string | null;
7
+ lastName: string | null;
8
+ phone: string | null;
9
+ roles: UserRole[];
10
+ emailVerifiedAt: string | Date | null;
11
+ phoneVerifiedAt: string | Date | null;
12
+ adminApprovedAt: string | Date | null;
13
+ isActive: boolean;
14
+ };
15
+ export type UserRolesResponse = {
16
+ userId: string;
17
+ roles: UserRole[];
18
+ };
19
+ export type RoleDefinition = {
20
+ role: UserRole;
21
+ description: string | null;
22
+ isSystem: boolean;
23
+ };
24
+ export type RoleCatalogResponse = {
25
+ roles: RoleDefinition[];
26
+ };
27
+ export type AuthResponse = {
28
+ accessToken: string;
29
+ accessTokenExpiresIn: string;
30
+ refreshToken: string;
31
+ refreshTokenExpiresAt: string | Date;
32
+ user: SafeUser;
33
+ };
34
+ export type RegisterResponse = {
35
+ success: true;
36
+ user: SafeUser;
37
+ debugToken?: string;
38
+ };
39
+ export type DebugTokenResponse = {
40
+ success: true;
41
+ debugToken?: string;
42
+ };
43
+ export type DebugCodeResponse = {
44
+ success: true;
45
+ debugCode?: string;
46
+ };
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ADMIN_ROLE = void 0;
4
+ exports.ADMIN_ROLE = "ADMIN";
@@ -0,0 +1,5 @@
1
+ export * from "./contracts";
2
+ export * from "./migrations";
3
+ export * from "./access-policy";
4
+ export * from "./validation";
5
+ export * from "./roles";
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./contracts"), exports);
18
+ __exportStar(require("./migrations"), exports);
19
+ __exportStar(require("./access-policy"), exports);
20
+ __exportStar(require("./validation"), exports);
21
+ __exportStar(require("./roles"), exports);
@@ -0,0 +1,9 @@
1
+ export declare class AddRefreshTokens1700000000001 {
2
+ name: string;
3
+ up(queryRunner: {
4
+ query: (sql: string) => Promise<unknown>;
5
+ }): Promise<void>;
6
+ down(queryRunner: {
7
+ query: (sql: string) => Promise<unknown>;
8
+ }): Promise<void>;
9
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AddRefreshTokens1700000000001 = void 0;
4
+ class AddRefreshTokens1700000000001 {
5
+ name = "AddRefreshTokens1700000000001";
6
+ async up(queryRunner) {
7
+ const userTableRef = getUserTableReference();
8
+ await queryRunner.query(`
9
+ CREATE TABLE "refresh_token" (
10
+ "id" varchar PRIMARY KEY NOT NULL,
11
+ "token_hash" varchar NOT NULL,
12
+ "expires_at" timestamptz NOT NULL,
13
+ "revoked_at" timestamptz,
14
+ "userId" varchar,
15
+ "created_at" timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP),
16
+ CONSTRAINT "FK_refresh_token_user" FOREIGN KEY ("userId") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE ON UPDATE NO ACTION
17
+ )
18
+ `);
19
+ await queryRunner.query(`CREATE INDEX "IDX_refresh_token_user" ON "refresh_token" ("userId")`);
20
+ }
21
+ async down(queryRunner) {
22
+ await queryRunner.query(`DROP INDEX "IDX_refresh_token_user"`);
23
+ await queryRunner.query(`DROP TABLE "refresh_token"`);
24
+ }
25
+ }
26
+ exports.AddRefreshTokens1700000000001 = AddRefreshTokens1700000000001;
27
+ const getUserTableReference = () => {
28
+ const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
29
+ const schema = process.env.USER_TABLE_SCHEMA
30
+ ? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "")
31
+ : "";
32
+ return schema ? `"${schema}"."${table}"` : `"${table}"`;
33
+ };
34
+ const getSafeIdentifier = (value, fallback) => {
35
+ const resolved = value?.trim() || fallback;
36
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
37
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
38
+ }
39
+ return resolved;
40
+ };
@@ -0,0 +1,5 @@
1
+ export declare class AddGoogleSubjectToUser1739490000000 {
2
+ name: string;
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AddGoogleSubjectToUser1739490000000 = void 0;
4
+ class AddGoogleSubjectToUser1739490000000 {
5
+ name = "AddGoogleSubjectToUser1739490000000";
6
+ // Legacy migration retained for backward compatibility with existing migration history.
7
+ async up() {
8
+ return;
9
+ }
10
+ async down() {
11
+ return;
12
+ }
13
+ }
14
+ exports.AddGoogleSubjectToUser1739490000000 = AddGoogleSubjectToUser1739490000000;
@@ -0,0 +1,9 @@
1
+ export declare class CreateSecurityIdentity1739500000000 {
2
+ name: string;
3
+ up(queryRunner: {
4
+ query: (sql: string, params?: unknown[]) => Promise<unknown>;
5
+ }): Promise<void>;
6
+ down(queryRunner: {
7
+ query: (sql: string) => Promise<unknown>;
8
+ }): Promise<void>;
9
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreateSecurityIdentity1739500000000 = void 0;
4
+ class CreateSecurityIdentity1739500000000 {
5
+ name = "CreateSecurityIdentity1739500000000";
6
+ async up(queryRunner) {
7
+ const userTable = getSafeIdentifier(process.env.USER_TABLE, "app_user");
8
+ const userSchema = getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public");
9
+ const userTableRef = `"${userSchema}"."${userTable}"`;
10
+ await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
11
+ await queryRunner.query(`
12
+ CREATE TABLE IF NOT EXISTS "security_identity" (
13
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
14
+ "user_id" varchar NOT NULL,
15
+ "provider" varchar NOT NULL,
16
+ "provider_subject" varchar NOT NULL,
17
+ "created_at" timestamptz NOT NULL DEFAULT now(),
18
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
19
+ CONSTRAINT "FK_security_identity_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
20
+ )
21
+ `);
22
+ await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_identity_provider_subject" ON "security_identity" ("provider", "provider_subject")`);
23
+ await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_identity_user_provider" ON "security_identity" ("user_id", "provider")`);
24
+ const hasGoogleSubjectColumn = (await queryRunner.query(`
25
+ SELECT 1
26
+ FROM information_schema.columns
27
+ WHERE table_schema = $1
28
+ AND table_name = $2
29
+ AND column_name = 'google_subject'
30
+ LIMIT 1
31
+ `, [userSchema, userTable]));
32
+ if (hasGoogleSubjectColumn.length > 0) {
33
+ await queryRunner.query(`
34
+ INSERT INTO "security_identity" (
35
+ "user_id",
36
+ "provider",
37
+ "provider_subject",
38
+ "created_at",
39
+ "updated_at"
40
+ )
41
+ SELECT
42
+ "id",
43
+ 'google',
44
+ "google_subject",
45
+ now(),
46
+ now()
47
+ FROM ${userTableRef}
48
+ WHERE "google_subject" IS NOT NULL
49
+ ON CONFLICT ("provider", "provider_subject") DO NOTHING
50
+ `);
51
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_app_user_google_subject"`);
52
+ await queryRunner.query(`ALTER TABLE ${userTableRef} DROP COLUMN IF EXISTS "google_subject"`);
53
+ }
54
+ }
55
+ async down(queryRunner) {
56
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_identity_user_provider"`);
57
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_identity_provider_subject"`);
58
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_identity"`);
59
+ }
60
+ }
61
+ exports.CreateSecurityIdentity1739500000000 = CreateSecurityIdentity1739500000000;
62
+ const getSafeIdentifier = (value, fallback) => {
63
+ const resolved = value?.trim() || fallback;
64
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
65
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
66
+ }
67
+ return resolved;
68
+ };
@@ -0,0 +1,9 @@
1
+ export declare class CreateSecurityRoles1739510000000 {
2
+ name: string;
3
+ up(queryRunner: {
4
+ query: (sql: string, params?: unknown[]) => Promise<unknown>;
5
+ }): Promise<void>;
6
+ down(queryRunner: {
7
+ query: (sql: string) => Promise<unknown>;
8
+ }): Promise<void>;
9
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreateSecurityRoles1739510000000 = void 0;
4
+ class CreateSecurityRoles1739510000000 {
5
+ name = "CreateSecurityRoles1739510000000";
6
+ async up(queryRunner) {
7
+ const userTable = getSafeIdentifier(process.env.USER_TABLE, "app_user");
8
+ const userSchema = getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public");
9
+ const userTableRef = `"${userSchema}"."${userTable}"`;
10
+ await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
11
+ await queryRunner.query(`
12
+ CREATE TABLE IF NOT EXISTS "security_role" (
13
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
14
+ "role_key" varchar NOT NULL,
15
+ "description" text,
16
+ "is_system" boolean NOT NULL DEFAULT false,
17
+ "created_at" timestamptz NOT NULL DEFAULT now(),
18
+ "updated_at" timestamptz NOT NULL DEFAULT now()
19
+ )
20
+ `);
21
+ await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_role_key" ON "security_role" ("role_key")`);
22
+ await queryRunner.query(`
23
+ CREATE TABLE IF NOT EXISTS "security_user_role" (
24
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
25
+ "user_id" varchar NOT NULL,
26
+ "role_id" uuid NOT NULL,
27
+ "created_at" timestamptz NOT NULL DEFAULT now(),
28
+ CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
29
+ CONSTRAINT "FK_security_user_role_role_id" FOREIGN KEY ("role_id") REFERENCES "security_role" ("id") ON DELETE CASCADE
30
+ )
31
+ `);
32
+ await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_user_role_user_role" ON "security_user_role" ("user_id", "role_id")`);
33
+ await queryRunner.query(`
34
+ INSERT INTO "security_role" ("role_key", "description", "is_system", "created_at", "updated_at")
35
+ VALUES ('ADMIN', 'Administrative access', true, now(), now())
36
+ ON CONFLICT ("role_key") DO NOTHING
37
+ `);
38
+ const hasRoleColumn = (await queryRunner.query(`
39
+ SELECT 1
40
+ FROM information_schema.columns
41
+ WHERE table_schema = $1
42
+ AND table_name = $2
43
+ AND column_name = 'role'
44
+ LIMIT 1
45
+ `, [userSchema, userTable]));
46
+ if (hasRoleColumn.length > 0) {
47
+ await queryRunner.query(`
48
+ INSERT INTO "security_role" ("role_key", "description", "is_system", "created_at", "updated_at")
49
+ SELECT DISTINCT
50
+ CASE
51
+ WHEN UPPER(TRIM("role")) = 'ADMINISTRATOR' THEN 'ADMIN'
52
+ ELSE UPPER(TRIM("role"))
53
+ END AS "role_key",
54
+ NULL,
55
+ false,
56
+ now(),
57
+ now()
58
+ FROM ${userTableRef}
59
+ WHERE "role" IS NOT NULL
60
+ AND LENGTH(TRIM("role")) > 0
61
+ ON CONFLICT ("role_key") DO NOTHING
62
+ `);
63
+ await queryRunner.query(`
64
+ INSERT INTO "security_user_role" ("user_id", "role_id", "created_at")
65
+ SELECT
66
+ u."id" AS "user_id",
67
+ r."id" AS "role_id",
68
+ now()
69
+ FROM ${userTableRef} u
70
+ INNER JOIN "security_role" r ON r."role_key" = CASE
71
+ WHEN UPPER(TRIM(u."role")) = 'ADMINISTRATOR' THEN 'ADMIN'
72
+ ELSE UPPER(TRIM(u."role"))
73
+ END
74
+ WHERE u."role" IS NOT NULL
75
+ AND LENGTH(TRIM(u."role")) > 0
76
+ ON CONFLICT ("user_id", "role_id") DO NOTHING
77
+ `);
78
+ await queryRunner.query(`ALTER TABLE ${userTableRef} DROP COLUMN IF EXISTS "role"`);
79
+ }
80
+ }
81
+ async down(queryRunner) {
82
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_user_role_user_role"`);
83
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_user_role"`);
84
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_role_key"`);
85
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_role"`);
86
+ }
87
+ }
88
+ exports.CreateSecurityRoles1739510000000 = CreateSecurityRoles1739510000000;
89
+ const getSafeIdentifier = (value, fallback) => {
90
+ const resolved = value?.trim() || fallback;
91
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
92
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
93
+ }
94
+ return resolved;
95
+ };
@@ -0,0 +1,6 @@
1
+ import { AddRefreshTokens1700000000001 } from "./1700000000001-add-refresh-tokens";
2
+ import { AddGoogleSubjectToUser1739490000000 } from "./1739490000000-add-google-subject-to-user";
3
+ import { CreateSecurityIdentity1739500000000 } from "./1739500000000-create-security-identity";
4
+ import { CreateSecurityRoles1739510000000 } from "./1739510000000-create-security-roles";
5
+ export declare const securityMigrations: (typeof AddRefreshTokens1700000000001)[];
6
+ export { AddRefreshTokens1700000000001, AddGoogleSubjectToUser1739490000000, CreateSecurityIdentity1739500000000, CreateSecurityRoles1739510000000, };
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreateSecurityRoles1739510000000 = exports.CreateSecurityIdentity1739500000000 = exports.AddGoogleSubjectToUser1739490000000 = exports.AddRefreshTokens1700000000001 = exports.securityMigrations = void 0;
4
+ const _1700000000001_add_refresh_tokens_1 = require("./1700000000001-add-refresh-tokens");
5
+ Object.defineProperty(exports, "AddRefreshTokens1700000000001", { enumerable: true, get: function () { return _1700000000001_add_refresh_tokens_1.AddRefreshTokens1700000000001; } });
6
+ const _1739490000000_add_google_subject_to_user_1 = require("./1739490000000-add-google-subject-to-user");
7
+ Object.defineProperty(exports, "AddGoogleSubjectToUser1739490000000", { enumerable: true, get: function () { return _1739490000000_add_google_subject_to_user_1.AddGoogleSubjectToUser1739490000000; } });
8
+ const _1739500000000_create_security_identity_1 = require("./1739500000000-create-security-identity");
9
+ Object.defineProperty(exports, "CreateSecurityIdentity1739500000000", { enumerable: true, get: function () { return _1739500000000_create_security_identity_1.CreateSecurityIdentity1739500000000; } });
10
+ const _1739510000000_create_security_roles_1 = require("./1739510000000-create-security-roles");
11
+ Object.defineProperty(exports, "CreateSecurityRoles1739510000000", { enumerable: true, get: function () { return _1739510000000_create_security_roles_1.CreateSecurityRoles1739510000000; } });
12
+ exports.securityMigrations = [
13
+ _1700000000001_add_refresh_tokens_1.AddRefreshTokens1700000000001,
14
+ _1739490000000_add_google_subject_to_user_1.AddGoogleSubjectToUser1739490000000,
15
+ _1739500000000_create_security_identity_1.CreateSecurityIdentity1739500000000,
16
+ _1739510000000_create_security_roles_1.CreateSecurityRoles1739510000000,
17
+ ];
@@ -0,0 +1,3 @@
1
+ export declare const normalizeRoleName: (value: string) => string;
2
+ export declare const hasRole: (roles: string[], role: string) => boolean;
3
+ export declare const isAdmin: (roles: string[]) => boolean;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAdmin = exports.hasRole = exports.normalizeRoleName = void 0;
4
+ const contracts_1 = require("./contracts");
5
+ const normalizeRoleName = (value) => {
6
+ const normalized = value.trim().toUpperCase().replace(/\s+/g, "_");
7
+ if (!normalized || !/^[A-Z][A-Z0-9_]*$/.test(normalized)) {
8
+ throw new Error("Invalid role name");
9
+ }
10
+ if (normalized === "ADMINISTRATOR") {
11
+ return contracts_1.ADMIN_ROLE;
12
+ }
13
+ return normalized;
14
+ };
15
+ exports.normalizeRoleName = normalizeRoleName;
16
+ const hasRole = (roles, role) => {
17
+ const normalizedRole = (0, exports.normalizeRoleName)(role);
18
+ return roles.some((assignedRole) => {
19
+ try {
20
+ return (0, exports.normalizeRoleName)(assignedRole) === normalizedRole;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ });
26
+ };
27
+ exports.hasRole = hasRole;
28
+ const isAdmin = (roles) => (0, exports.hasRole)(roles, contracts_1.ADMIN_ROLE);
29
+ exports.isAdmin = isAdmin;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const roles_1 = require("./roles");
5
+ (0, vitest_1.describe)("roles", () => {
6
+ (0, vitest_1.it)("normalizes role names", () => {
7
+ (0, vitest_1.expect)((0, roles_1.normalizeRoleName)("admin")).toBe("ADMIN");
8
+ (0, vitest_1.expect)((0, roles_1.normalizeRoleName)("case manager")).toBe("CASE_MANAGER");
9
+ (0, vitest_1.expect)((0, roles_1.normalizeRoleName)("ADMINISTRATOR")).toBe("ADMIN");
10
+ });
11
+ (0, vitest_1.it)("checks role membership", () => {
12
+ (0, vitest_1.expect)((0, roles_1.hasRole)(["ADMIN", "MEMBER"], "admin")).toBe(true);
13
+ (0, vitest_1.expect)((0, roles_1.hasRole)(["MEMBER"], "ADMIN")).toBe(false);
14
+ });
15
+ (0, vitest_1.it)("checks admin role", () => {
16
+ (0, vitest_1.expect)((0, roles_1.isAdmin)(["ADMIN"])).toBe(true);
17
+ (0, vitest_1.expect)((0, roles_1.isAdmin)(["MEMBER"])).toBe(false);
18
+ });
19
+ });
@@ -0,0 +1,3 @@
1
+ export declare const sanitizeEmail: (email: string) => string;
2
+ export declare const isValidEmail: (value: string) => boolean;
3
+ export declare const isStrongPassword: (value: string) => boolean;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isStrongPassword = exports.isValidEmail = exports.sanitizeEmail = void 0;
4
+ const sanitizeEmail = (email) => email.trim().toLowerCase();
5
+ exports.sanitizeEmail = sanitizeEmail;
6
+ const isValidEmail = (value) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value);
7
+ exports.isValidEmail = isValidEmail;
8
+ const isStrongPassword = (value) => /[A-Z]/.test(value) && /[a-z]/.test(value) && /\\d/.test(value);
9
+ exports.isStrongPassword = isStrongPassword;